h3i/prompts/h3/
mod.rs

1// Copyright (C) 2024, Cloudflare, Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//
11//     * Redistributions in binary form must reproduce the above copyright
12//       notice, this list of conditions and the following disclaimer in the
13//       documentation and/or other materials provided with the distribution.
14//
15// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27//! A collection of interactive CLI prompts for HTTP/3 based on [inquire].
28
29use inquire::error::CustomUserError;
30use inquire::error::InquireResult;
31use inquire::validator::ErrorMessage;
32use inquire::validator::Validation;
33use inquire::InquireError;
34use inquire::Select;
35use inquire::Text;
36use qlog::events::quic::ErrorSpace;
37use quiche::ConnectionError;
38
39use crate::actions::h3::Action;
40use crate::config::Config;
41use crate::prompts::h3;
42use crate::prompts::h3::headers::prompt_push_promise;
43use crate::StreamIdAllocator;
44
45use std::cell::RefCell;
46
47use crate::quiche;
48
49use self::stream::prompt_fin_stream;
50use self::wait::prompt_wait;
51
52/// An error indicating that the provided buffer is not big enough.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum Error {
55    InternalError,
56    BufferTooShort,
57}
58
59impl std::convert::From<octets::BufferTooShortError> for Error {
60    fn from(_err: octets::BufferTooShortError) -> Self {
61        Error::BufferTooShort
62    }
63}
64
65/// A specialized [`Result`] type for prompt operations.
66///
67/// [`Result`]: https://doc.rust-lang.org/std/result/enum.Result.html
68pub type Result<T> = std::result::Result<T, Error>;
69
70/// A specialized [`Result`] type for internal prompt suggestion.
71///
72/// [`Result`]: https://doc.rust-lang.org/std/result/enum.Result.html
73type SuggestionResult<T> = std::result::Result<T, CustomUserError>;
74
75/// A tuple of stream ID and quiche HTTP/3 frame.
76pub type PromptedFrame = (u64, quiche::h3::frame::Frame);
77
78thread_local! {static CONNECTION_IDLE_TIMEOUT: RefCell<u64> = const { RefCell::new(0) }}
79
80// TODO(erittenhouse): exploring generating prompts at compile-time
81const HEADERS: &str = "headers";
82const HEADERS_NO_PSEUDO: &str = "headers_no_pseudo";
83const HEADERS_LITERAL: &str = "headers_literal";
84const HEADERS_NO_PSEUDO_LITERAL: &str = "headers_no_pseudo_literal";
85const DATA: &str = "data";
86const SETTINGS: &str = "settings";
87const PUSH_PROMISE: &str = "push_promise";
88const CANCEL_PUSH: &str = "cancel_push";
89const GOAWAY: &str = "goaway";
90const MAX_PUSH_ID: &str = "max_push_id";
91const PRIORITY_UPDATE: &str = "priority_update";
92const GREASE: &str = "grease";
93const EXTENSION: &str = "extension_frame";
94const OPEN_UNI_STREAM: &str = "open_uni_stream";
95const RESET_STREAM: &str = "reset_stream";
96const STOP_SENDING: &str = "stop_sending";
97const CONNECTION_CLOSE: &str = "connection_close";
98const STREAM_BYTES: &str = "stream_bytes";
99const DATAGRAM_QUARTER_STREAM_ID: &str = "datagram_quarter_stream_id";
100const DATAGRAM_RAW_PAYLOAD: &str = "datagram_raw_payload";
101
102const COMMIT: &str = "commit";
103const FLUSH_PACKETS: &str = "flush_packets";
104const WAIT: &str = "wait";
105const QUIT: &str = "quit";
106
107const YES: &str = "Yes";
108const NO: &str = "No";
109
110const ESC_TO_RET: &str = "ESC to return to actions";
111const STREAM_ID_PROMPT: &str = "stream ID:";
112const EMPTY_PICKS: &str = "empty picks next available ID";
113const AUTO_PICK: &str = "autopick StreamID";
114const PUSH_ID_PROMPT: &str = "push ID:";
115
116enum PromptOutcome {
117    Action(Action),
118    Repeat,
119    Commit,
120    Clear,
121}
122
123/// The main prompter interface and state management.
124pub struct Prompter {
125    host_port: String,
126    bidi_sid_alloc: StreamIdAllocator,
127    uni_sid_alloc: StreamIdAllocator,
128}
129
130impl Prompter {
131    /// Construct a prompter with the provided `config`.
132    pub fn with_config(config: &Config) -> Self {
133        CONNECTION_IDLE_TIMEOUT.with(|v| *v.borrow_mut() = config.idle_timeout);
134
135        Self {
136            host_port: config.host_port.clone(),
137            bidi_sid_alloc: StreamIdAllocator { id: 0 },
138            uni_sid_alloc: StreamIdAllocator { id: 2 },
139        }
140    }
141
142    fn handle_action(&mut self, action: &str) -> PromptOutcome {
143        let res = match action {
144            HEADERS |
145            HEADERS_NO_PSEUDO |
146            HEADERS_LITERAL |
147            HEADERS_NO_PSEUDO_LITERAL => {
148                let literal = action == HEADERS_LITERAL ||
149                    action == HEADERS_NO_PSEUDO_LITERAL;
150                let raw = action == HEADERS_NO_PSEUDO ||
151                    action == HEADERS_NO_PSEUDO_LITERAL;
152                headers::prompt_headers(
153                    &mut self.bidi_sid_alloc,
154                    &self.host_port,
155                    raw,
156                    literal,
157                )
158            },
159
160            DATA => prompt_data(),
161            SETTINGS => settings::prompt_settings(),
162            OPEN_UNI_STREAM =>
163                stream::prompt_open_uni_stream(&mut self.uni_sid_alloc),
164            RESET_STREAM => stream::prompt_reset_stream(),
165            STOP_SENDING => stream::prompt_stop_sending(),
166            GREASE => prompt_grease(),
167            EXTENSION => prompt_extension(),
168            GOAWAY => prompt_goaway(),
169            MAX_PUSH_ID => prompt_max_push_id(),
170            CANCEL_PUSH => prompt_cancel_push(),
171            PUSH_PROMISE => prompt_push_promise(),
172            PRIORITY_UPDATE => priority::prompt_priority(),
173            CONNECTION_CLOSE => prompt_connection_close(),
174            STREAM_BYTES => prompt_stream_bytes(),
175            DATAGRAM_QUARTER_STREAM_ID | DATAGRAM_RAW_PAYLOAD =>
176                prompt_send_datagram(action == DATAGRAM_QUARTER_STREAM_ID),
177            FLUSH_PACKETS => return PromptOutcome::Action(Action::FlushPackets),
178            COMMIT => return PromptOutcome::Commit,
179            WAIT => prompt_wait(),
180            QUIT => return PromptOutcome::Clear,
181
182            _ => {
183                println!("error: unknown action {action}");
184                return PromptOutcome::Repeat;
185            },
186        };
187
188        match res {
189            Ok(action) => PromptOutcome::Action(action),
190            Err(e) =>
191                if handle_action_loop_error(e) {
192                    PromptOutcome::Commit
193                } else {
194                    PromptOutcome::Repeat
195                },
196        }
197    }
198
199    /// Start the prompt loop.
200    ///
201    /// This continues to prompt for actions until a terminal choice is
202    /// made.
203    ///
204    /// Returns an ordered list of [Action]s, which may be empty.
205    pub fn prompt(&mut self) -> Vec<Action> {
206        let mut actions = vec![];
207
208        loop {
209            println!();
210
211            let action = match prompt_action() {
212                Ok(v) => v,
213                Err(inquire::InquireError::OperationCanceled) |
214                Err(inquire::InquireError::OperationInterrupted) =>
215                    return actions,
216                Err(e) => {
217                    println!("Unexpected error while determining action: {e}");
218                    return actions;
219                },
220            };
221
222            match self.handle_action(&action) {
223                PromptOutcome::Action(action) => actions.push(action),
224                PromptOutcome::Repeat => continue,
225                PromptOutcome::Commit => return actions,
226                PromptOutcome::Clear => return vec![],
227            }
228        }
229    }
230}
231
232fn handle_action_loop_error(err: InquireError) -> bool {
233    match err {
234        inquire::InquireError::OperationCanceled |
235        inquire::InquireError::OperationInterrupted => false,
236
237        _ => {
238            println!("Unexpected error: {err}");
239            true
240        },
241    }
242}
243
244fn prompt_action() -> InquireResult<String> {
245    let name = Text::new(
246        "Select an action to queue. `Commit` ends selection and flushes queue.",
247    )
248    .with_autocomplete(&action_suggester)
249    .with_page_size(18)
250    .prompt();
251
252    name
253}
254
255fn action_suggester(val: &str) -> SuggestionResult<Vec<String>> {
256    // TODO: make this an enum to automatically pick up new actions
257    let suggestions = [
258        HEADERS,
259        HEADERS_NO_PSEUDO,
260        HEADERS_LITERAL,
261        HEADERS_NO_PSEUDO_LITERAL,
262        DATA,
263        SETTINGS,
264        GOAWAY,
265        PRIORITY_UPDATE,
266        PUSH_PROMISE,
267        CANCEL_PUSH,
268        MAX_PUSH_ID,
269        GREASE,
270        EXTENSION,
271        OPEN_UNI_STREAM,
272        RESET_STREAM,
273        STOP_SENDING,
274        CONNECTION_CLOSE,
275        STREAM_BYTES,
276        DATAGRAM_QUARTER_STREAM_ID,
277        DATAGRAM_RAW_PAYLOAD,
278        FLUSH_PACKETS,
279        COMMIT,
280        WAIT,
281        QUIT,
282    ];
283
284    squish_suggester(&suggestions, val)
285}
286
287fn squish_suggester(
288    suggestions: &[&str], val: &str,
289) -> SuggestionResult<Vec<String>> {
290    let val_lower = val.to_lowercase();
291
292    Ok(suggestions
293        .iter()
294        .filter(|s| s.to_lowercase().contains(&val_lower))
295        .map(|s| String::from(*s))
296        .collect())
297}
298
299fn validate_varint(id: &str) -> SuggestionResult<Validation> {
300    let x = id.parse::<u64>();
301
302    match x {
303        Ok(v) =>
304            if v >= u64::pow(2, 62) {
305                return Ok(Validation::Invalid(ErrorMessage::Default));
306            },
307
308        Err(_) => {
309            return Ok(Validation::Invalid(ErrorMessage::Default));
310        },
311    }
312
313    Ok(Validation::Valid)
314}
315
316fn prompt_stream_id() -> InquireResult<u64> {
317    prompt_varint(STREAM_ID_PROMPT)
318}
319
320fn prompt_control_stream_id() -> InquireResult<u64> {
321    let id = Text::new(STREAM_ID_PROMPT)
322        .with_validator(h3::validate_varint)
323        .with_autocomplete(&control_stream_suggestor)
324        .with_help_message(ESC_TO_RET)
325        .prompt()?;
326
327    // id is already validated so unwrap always succeeds
328    Ok(id.parse::<u64>().unwrap())
329}
330
331fn prompt_varint(str: &str) -> InquireResult<u64> {
332    let id = Text::new(str)
333        .with_validator(h3::validate_varint)
334        .with_placeholder("Integer <= 2^62 -1")
335        .with_help_message(ESC_TO_RET)
336        .prompt()?;
337
338    // id is already validated so unwrap always succeeds
339    Ok(id.parse::<u64>().unwrap())
340}
341
342fn control_stream_suggestor(val: &str) -> SuggestionResult<Vec<String>> {
343    let suggestions = ["2"];
344
345    squish_suggester(&suggestions, val)
346}
347
348fn prompt_data() -> InquireResult<Action> {
349    let stream_id = h3::prompt_stream_id()?;
350
351    let payload = Text::new("payload:").prompt()?;
352
353    let fin_stream = prompt_fin_stream()?;
354
355    let action = Action::SendFrame {
356        stream_id,
357        fin_stream,
358        frame: quiche::h3::frame::Frame::Data {
359            payload: payload.into(),
360        },
361    };
362
363    Ok(action)
364}
365
366fn prompt_max_push_id() -> InquireResult<Action> {
367    let stream_id = h3::prompt_stream_id()?;
368    let push_id = h3::prompt_varint(PUSH_ID_PROMPT)?;
369
370    let fin_stream = prompt_fin_stream()?;
371
372    let action = Action::SendFrame {
373        stream_id,
374        fin_stream,
375        frame: quiche::h3::frame::Frame::MaxPushId { push_id },
376    };
377
378    Ok(action)
379}
380
381fn prompt_cancel_push() -> InquireResult<Action> {
382    let stream_id = h3::prompt_stream_id()?;
383    let push_id = h3::prompt_varint(PUSH_ID_PROMPT)?;
384
385    let fin_stream = prompt_fin_stream()?;
386
387    let action = Action::SendFrame {
388        stream_id,
389        fin_stream,
390        frame: quiche::h3::frame::Frame::CancelPush { push_id },
391    };
392
393    Ok(action)
394}
395
396fn prompt_goaway() -> InquireResult<Action> {
397    let stream_id = h3::prompt_stream_id()?;
398    let id = h3::prompt_varint("ID:")?;
399
400    let fin_stream = prompt_fin_stream()?;
401
402    let action = Action::SendFrame {
403        stream_id,
404        fin_stream,
405        frame: quiche::h3::frame::Frame::GoAway { id },
406    };
407
408    Ok(action)
409}
410
411fn prompt_grease() -> InquireResult<Action> {
412    let stream_id = h3::prompt_control_stream_id()?;
413    let raw_type = quiche::h3::grease_value();
414    let payload = Text::new("payload:")
415        .prompt()
416        .expect("An error happened when asking for payload, try again later.");
417
418    let fin_stream = prompt_fin_stream()?;
419
420    let action = Action::SendFrame {
421        stream_id,
422        fin_stream,
423        frame: quiche::h3::frame::Frame::Unknown {
424            raw_type,
425            payload: payload.into(),
426        },
427    };
428
429    Ok(action)
430}
431
432fn prompt_extension() -> InquireResult<Action> {
433    let stream_id = h3::prompt_control_stream_id()?;
434    let raw_type = h3::prompt_varint("frame type:")?;
435    let payload = Text::new("payload:")
436        .with_help_message(ESC_TO_RET)
437        .prompt()
438        .expect("An error happened when asking for payload, try again later.");
439
440    let fin_stream = prompt_fin_stream()?;
441
442    let action = Action::SendFrame {
443        stream_id,
444        fin_stream,
445        frame: quiche::h3::frame::Frame::Unknown {
446            raw_type,
447            payload: payload.into(),
448        },
449    };
450
451    Ok(action)
452}
453
454pub fn prompt_connection_close() -> InquireResult<Action> {
455    let (error_space, error_code) = errors::prompt_transport_or_app_error()?;
456    let reason = Text::new("reason phrase:")
457        .with_placeholder("optional reason phrase")
458        .prompt()
459        .unwrap_or_default();
460
461    Ok(Action::ConnectionClose {
462        error: ConnectionError {
463            is_app: matches!(error_space, ErrorSpace::ApplicationError),
464            error_code,
465            reason: reason.as_bytes().to_vec(),
466        },
467    })
468}
469
470pub fn prompt_stream_bytes() -> InquireResult<Action> {
471    let stream_id = h3::prompt_stream_id()?;
472    let bytes = Text::new("bytes:").prompt()?;
473    let fin_stream = prompt_fin_stream()?;
474
475    Ok(Action::StreamBytes {
476        stream_id,
477        fin_stream,
478        bytes: bytes.as_bytes().to_vec(),
479    })
480}
481
482pub fn prompt_send_datagram(with_quarter_stream: bool) -> InquireResult<Action> {
483    if with_quarter_stream {
484        let stream_id = h3::prompt_varint("stream ID to be quartered:")?;
485        // https://www.rfc-editor.org/rfc/rfc9297#name-http-3-datagrams
486        let quarter_stream_id = stream_id / 4;
487
488        let payload = Text::new("payload bytes:").prompt()?;
489
490        let len = octets::varint_len(quarter_stream_id) + payload.len();
491        let mut d = vec![0; len];
492        let mut b = octets::OctetsMut::with_slice(&mut d);
493        b.put_varint(quarter_stream_id).unwrap();
494        b.put_bytes(payload.as_bytes()).unwrap();
495        Ok(Action::SendDatagram { payload: d })
496    } else {
497        let payload_str = Text::new("payload bytes:").prompt()?;
498        Ok(Action::SendDatagram {
499            payload: payload_str.as_bytes().to_owned(),
500        })
501    }
502}
503
504fn validate_wait_period(period: &str) -> SuggestionResult<Validation> {
505    let x = period.parse::<u64>();
506
507    match x {
508        Ok(v) => {
509            let local_conn_timeout =
510                CONNECTION_IDLE_TIMEOUT.with(|v| *v.borrow());
511            if v >= local_conn_timeout {
512                return Ok(Validation::Invalid(ErrorMessage::Custom(format!(
513                    "wait time >= local connection idle timeout {local_conn_timeout}"
514                ))));
515            }
516        },
517
518        Err(_) => return Ok(Validation::Invalid(ErrorMessage::Default)),
519    }
520
521    Ok(Validation::Valid)
522}
523
524fn prompt_yes_no(msg: &str) -> InquireResult<bool> {
525    let res = Select::new(msg, vec![NO, YES]).prompt()?;
526
527    Ok(res == YES)
528}
529
530mod errors;
531mod headers;
532mod priority;
533mod settings;
534mod stream;
535mod wait;