Skip to main content

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(Box<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 =>
178                return PromptOutcome::Action(Box::new(Action::FlushPackets)),
179            COMMIT => return PromptOutcome::Commit,
180            WAIT => prompt_wait(),
181            QUIT => return PromptOutcome::Clear,
182
183            _ => {
184                println!("error: unknown action {action}");
185                return PromptOutcome::Repeat;
186            },
187        };
188
189        match res {
190            Ok(action) => PromptOutcome::Action(Box::new(action)),
191            Err(e) =>
192                if handle_action_loop_error(e) {
193                    PromptOutcome::Commit
194                } else {
195                    PromptOutcome::Repeat
196                },
197        }
198    }
199
200    /// Start the prompt loop.
201    ///
202    /// This continues to prompt for actions until a terminal choice is
203    /// made.
204    ///
205    /// Returns an ordered list of [Action]s, which may be empty.
206    pub fn prompt(&mut self) -> Vec<Action> {
207        let mut actions = vec![];
208
209        loop {
210            println!();
211
212            let action = match prompt_action() {
213                Ok(v) => v,
214                Err(inquire::InquireError::OperationCanceled) |
215                Err(inquire::InquireError::OperationInterrupted) =>
216                    return actions,
217                Err(e) => {
218                    println!("Unexpected error while determining action: {e}");
219                    return actions;
220                },
221            };
222
223            match self.handle_action(&action) {
224                PromptOutcome::Action(action) => actions.push(*action),
225                PromptOutcome::Repeat => continue,
226                PromptOutcome::Commit => return actions,
227                PromptOutcome::Clear => return vec![],
228            }
229        }
230    }
231}
232
233fn handle_action_loop_error(err: InquireError) -> bool {
234    match err {
235        inquire::InquireError::OperationCanceled |
236        inquire::InquireError::OperationInterrupted => false,
237
238        _ => {
239            println!("Unexpected error: {err}");
240            true
241        },
242    }
243}
244
245fn prompt_action() -> InquireResult<String> {
246    let name = Text::new(
247        "Select an action to queue. `Commit` ends selection and flushes queue.",
248    )
249    .with_autocomplete(&action_suggester)
250    .with_page_size(18)
251    .prompt();
252
253    name
254}
255
256fn action_suggester(val: &str) -> SuggestionResult<Vec<String>> {
257    // TODO: make this an enum to automatically pick up new actions
258    let suggestions = [
259        HEADERS,
260        HEADERS_NO_PSEUDO,
261        HEADERS_LITERAL,
262        HEADERS_NO_PSEUDO_LITERAL,
263        DATA,
264        SETTINGS,
265        GOAWAY,
266        PRIORITY_UPDATE,
267        PUSH_PROMISE,
268        CANCEL_PUSH,
269        MAX_PUSH_ID,
270        GREASE,
271        EXTENSION,
272        OPEN_UNI_STREAM,
273        RESET_STREAM,
274        STOP_SENDING,
275        CONNECTION_CLOSE,
276        STREAM_BYTES,
277        DATAGRAM_QUARTER_STREAM_ID,
278        DATAGRAM_RAW_PAYLOAD,
279        FLUSH_PACKETS,
280        COMMIT,
281        WAIT,
282        QUIT,
283    ];
284
285    squish_suggester(&suggestions, val)
286}
287
288fn squish_suggester(
289    suggestions: &[&str], val: &str,
290) -> SuggestionResult<Vec<String>> {
291    let val_lower = val.to_lowercase();
292
293    Ok(suggestions
294        .iter()
295        .filter(|s| s.to_lowercase().contains(&val_lower))
296        .map(|s| String::from(*s))
297        .collect())
298}
299
300fn validate_varint(id: &str) -> SuggestionResult<Validation> {
301    let x = id.parse::<u64>();
302
303    match x {
304        Ok(v) =>
305            if v >= u64::pow(2, 62) {
306                return Ok(Validation::Invalid(ErrorMessage::Default));
307            },
308
309        Err(_) => {
310            return Ok(Validation::Invalid(ErrorMessage::Default));
311        },
312    }
313
314    Ok(Validation::Valid)
315}
316
317fn prompt_stream_id() -> InquireResult<u64> {
318    prompt_varint(STREAM_ID_PROMPT)
319}
320
321fn prompt_control_stream_id() -> InquireResult<u64> {
322    let id = Text::new(STREAM_ID_PROMPT)
323        .with_validator(h3::validate_varint)
324        .with_autocomplete(&control_stream_suggestor)
325        .with_help_message(ESC_TO_RET)
326        .prompt()?;
327
328    // id is already validated so unwrap always succeeds
329    Ok(id.parse::<u64>().unwrap())
330}
331
332fn prompt_varint(str: &str) -> InquireResult<u64> {
333    let id = Text::new(str)
334        .with_validator(h3::validate_varint)
335        .with_placeholder("Integer <= 2^62 -1")
336        .with_help_message(ESC_TO_RET)
337        .prompt()?;
338
339    // id is already validated so unwrap always succeeds
340    Ok(id.parse::<u64>().unwrap())
341}
342
343fn control_stream_suggestor(val: &str) -> SuggestionResult<Vec<String>> {
344    let suggestions = ["2"];
345
346    squish_suggester(&suggestions, val)
347}
348
349fn prompt_data() -> InquireResult<Action> {
350    let stream_id = h3::prompt_stream_id()?;
351
352    let payload = Text::new("payload:").prompt()?;
353
354    let fin_stream = prompt_fin_stream()?;
355
356    let action = Action::SendFrame {
357        stream_id,
358        fin_stream,
359        frame: quiche::h3::frame::Frame::Data {
360            payload: payload.into(),
361        },
362        expected_result: Default::default(),
363    };
364
365    Ok(action)
366}
367
368fn prompt_max_push_id() -> InquireResult<Action> {
369    let stream_id = h3::prompt_stream_id()?;
370    let push_id = h3::prompt_varint(PUSH_ID_PROMPT)?;
371
372    let fin_stream = prompt_fin_stream()?;
373
374    let action = Action::SendFrame {
375        stream_id,
376        fin_stream,
377        frame: quiche::h3::frame::Frame::MaxPushId { push_id },
378        expected_result: Default::default(),
379    };
380
381    Ok(action)
382}
383
384fn prompt_cancel_push() -> InquireResult<Action> {
385    let stream_id = h3::prompt_stream_id()?;
386    let push_id = h3::prompt_varint(PUSH_ID_PROMPT)?;
387
388    let fin_stream = prompt_fin_stream()?;
389
390    let action = Action::SendFrame {
391        stream_id,
392        fin_stream,
393        frame: quiche::h3::frame::Frame::CancelPush { push_id },
394        expected_result: Default::default(),
395    };
396
397    Ok(action)
398}
399
400fn prompt_goaway() -> InquireResult<Action> {
401    let stream_id = h3::prompt_stream_id()?;
402    let id = h3::prompt_varint("ID:")?;
403
404    let fin_stream = prompt_fin_stream()?;
405
406    let action = Action::SendFrame {
407        stream_id,
408        fin_stream,
409        frame: quiche::h3::frame::Frame::GoAway { id },
410        expected_result: Default::default(),
411    };
412
413    Ok(action)
414}
415
416fn prompt_grease() -> InquireResult<Action> {
417    let stream_id = h3::prompt_control_stream_id()?;
418    let raw_type = quiche::h3::grease_value();
419    let payload = Text::new("payload:")
420        .prompt()
421        .expect("An error happened when asking for payload, try again later.");
422
423    let fin_stream = prompt_fin_stream()?;
424
425    let action = Action::SendFrame {
426        stream_id,
427        fin_stream,
428        frame: quiche::h3::frame::Frame::Unknown {
429            raw_type,
430            payload: payload.into(),
431        },
432        expected_result: Default::default(),
433    };
434
435    Ok(action)
436}
437
438fn prompt_extension() -> InquireResult<Action> {
439    let stream_id = h3::prompt_control_stream_id()?;
440    let raw_type = h3::prompt_varint("frame type:")?;
441    let payload = Text::new("payload:")
442        .with_help_message(ESC_TO_RET)
443        .prompt()
444        .expect("An error happened when asking for payload, try again later.");
445
446    let fin_stream = prompt_fin_stream()?;
447
448    let action = Action::SendFrame {
449        stream_id,
450        fin_stream,
451        frame: quiche::h3::frame::Frame::Unknown {
452            raw_type,
453            payload: payload.into(),
454        },
455        expected_result: Default::default(),
456    };
457
458    Ok(action)
459}
460
461pub fn prompt_connection_close() -> InquireResult<Action> {
462    let (error_space, error_code) = errors::prompt_transport_or_app_error()?;
463    let reason = Text::new("reason phrase:")
464        .with_placeholder("optional reason phrase")
465        .prompt()
466        .unwrap_or_default();
467
468    Ok(Action::ConnectionClose {
469        error: ConnectionError {
470            is_app: matches!(error_space, ErrorSpace::Application),
471            error_code,
472            reason: reason.as_bytes().to_vec(),
473        },
474    })
475}
476
477pub fn prompt_stream_bytes() -> InquireResult<Action> {
478    let stream_id = h3::prompt_stream_id()?;
479    let bytes = Text::new("bytes:").prompt()?;
480    let fin_stream = prompt_fin_stream()?;
481
482    Ok(Action::StreamBytes {
483        stream_id,
484        fin_stream,
485        bytes: bytes.as_bytes().to_vec(),
486        expected_result: Default::default(),
487    })
488}
489
490pub fn prompt_send_datagram(with_quarter_stream: bool) -> InquireResult<Action> {
491    if with_quarter_stream {
492        let stream_id = h3::prompt_varint("stream ID to be quartered:")?;
493        // https://www.rfc-editor.org/rfc/rfc9297#name-http-3-datagrams
494        let quarter_stream_id = stream_id / 4;
495
496        let payload = Text::new("payload bytes:").prompt()?;
497
498        let len = octets::varint_len(quarter_stream_id) + payload.len();
499        let mut d = vec![0; len];
500        let mut b = octets::OctetsMut::with_slice(&mut d);
501        b.put_varint(quarter_stream_id).unwrap();
502        b.put_bytes(payload.as_bytes()).unwrap();
503        Ok(Action::SendDatagram { payload: d })
504    } else {
505        let payload_str = Text::new("payload bytes:").prompt()?;
506        Ok(Action::SendDatagram {
507            payload: payload_str.as_bytes().to_owned(),
508        })
509    }
510}
511
512fn validate_wait_period(period: &str) -> SuggestionResult<Validation> {
513    let x = period.parse::<u64>();
514
515    match x {
516        Ok(v) => {
517            let local_conn_timeout =
518                CONNECTION_IDLE_TIMEOUT.with(|v| *v.borrow());
519            if v >= local_conn_timeout {
520                return Ok(Validation::Invalid(ErrorMessage::Custom(format!(
521                    "wait time >= local connection idle timeout {local_conn_timeout}"
522                ))));
523            }
524        },
525
526        Err(_) => return Ok(Validation::Invalid(ErrorMessage::Default)),
527    }
528
529    Ok(Validation::Valid)
530}
531
532fn prompt_yes_no(msg: &str) -> InquireResult<bool> {
533    let res = Select::new(msg, vec![NO, YES]).prompt()?;
534
535    Ok(res == YES)
536}
537
538mod errors;
539mod headers;
540mod priority;
541mod settings;
542mod stream;
543mod wait;