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 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";
99
100const COMMIT: &str = "commit";
101const FLUSH_PACKETS: &str = "flush_packets";
102const WAIT: &str = "wait";
103const QUIT: &str = "quit";
104
105const YES: &str = "Yes";
106const NO: &str = "No";
107
108const ESC_TO_RET: &str = "ESC to return to actions";
109const STREAM_ID_PROMPT: &str = "stream ID:";
110const EMPTY_PICKS: &str = "empty picks next available ID";
111const AUTO_PICK: &str = "autopick StreamID";
112const PUSH_ID_PROMPT: &str = "push ID:";
113
114enum PromptOutcome {
115    Action(Action),
116    Repeat,
117    Commit,
118    Clear,
119}
120
121/// The main prompter interface and state management.
122pub struct Prompter {
123    host_port: String,
124    bidi_sid_alloc: StreamIdAllocator,
125    uni_sid_alloc: StreamIdAllocator,
126}
127
128impl Prompter {
129    /// Construct a prompter with the provided `config`.
130    pub fn with_config(config: &Config) -> Self {
131        CONNECTION_IDLE_TIMEOUT.with(|v| *v.borrow_mut() = config.idle_timeout);
132
133        Self {
134            host_port: config.host_port.clone(),
135            bidi_sid_alloc: StreamIdAllocator { id: 0 },
136            uni_sid_alloc: StreamIdAllocator { id: 2 },
137        }
138    }
139
140    fn handle_action(&mut self, action: &str) -> PromptOutcome {
141        let res = match action {
142            HEADERS |
143            HEADERS_NO_PSEUDO |
144            HEADERS_LITERAL |
145            HEADERS_NO_PSEUDO_LITERAL => {
146                let literal = action == HEADERS_LITERAL ||
147                    action == HEADERS_NO_PSEUDO_LITERAL;
148                let raw = action == HEADERS_NO_PSEUDO ||
149                    action == HEADERS_NO_PSEUDO_LITERAL;
150                headers::prompt_headers(
151                    &mut self.bidi_sid_alloc,
152                    &self.host_port,
153                    raw,
154                    literal,
155                )
156            },
157
158            DATA => prompt_data(),
159            SETTINGS => settings::prompt_settings(),
160            OPEN_UNI_STREAM =>
161                stream::prompt_open_uni_stream(&mut self.uni_sid_alloc),
162            RESET_STREAM => stream::prompt_reset_stream(),
163            STOP_SENDING => stream::prompt_stop_sending(),
164            GREASE => prompt_grease(),
165            EXTENSION => prompt_extension(),
166            GOAWAY => prompt_goaway(),
167            MAX_PUSH_ID => prompt_max_push_id(),
168            CANCEL_PUSH => prompt_cancel_push(),
169            PUSH_PROMISE => prompt_push_promise(),
170            PRIORITY_UPDATE => priority::prompt_priority(),
171            CONNECTION_CLOSE => prompt_connection_close(),
172            STREAM_BYTES => prompt_stream_bytes(),
173            FLUSH_PACKETS => return PromptOutcome::Action(Action::FlushPackets),
174            COMMIT => return PromptOutcome::Commit,
175            WAIT => prompt_wait(),
176            QUIT => return PromptOutcome::Clear,
177
178            _ => {
179                println!("error: unknown action {}", action);
180                return PromptOutcome::Repeat;
181            },
182        };
183
184        match res {
185            Ok(action) => PromptOutcome::Action(action),
186            Err(e) =>
187                if handle_action_loop_error(e) {
188                    PromptOutcome::Commit
189                } else {
190                    PromptOutcome::Repeat
191                },
192        }
193    }
194
195    /// Start the prompt loop.
196    ///
197    /// This continues to prompt for actions until a terminal choice is
198    /// made.
199    ///
200    /// Returns an ordered list of [Action]s, which may be empty.
201    pub fn prompt(&mut self) -> Vec<Action> {
202        let mut actions = vec![];
203
204        loop {
205            println!();
206
207            let action = match prompt_action() {
208                Ok(v) => v,
209                Err(inquire::InquireError::OperationCanceled) |
210                Err(inquire::InquireError::OperationInterrupted) =>
211                    return actions,
212                Err(e) => {
213                    println!("Unexpected error while determining action: {}", e);
214                    return actions;
215                },
216            };
217
218            match self.handle_action(&action) {
219                PromptOutcome::Action(action) => actions.push(action),
220                PromptOutcome::Repeat => continue,
221                PromptOutcome::Commit => return actions,
222                PromptOutcome::Clear => return vec![],
223            }
224        }
225    }
226}
227
228fn handle_action_loop_error(err: InquireError) -> bool {
229    match err {
230        inquire::InquireError::OperationCanceled |
231        inquire::InquireError::OperationInterrupted => false,
232
233        _ => {
234            println!("Unexpected error: {}", err);
235            true
236        },
237    }
238}
239
240fn prompt_action() -> InquireResult<String> {
241    let name = Text::new(
242        "Select an action to queue. `Commit` ends selection and flushes queue.",
243    )
244    .with_autocomplete(&action_suggester)
245    .with_page_size(18)
246    .prompt();
247
248    name
249}
250
251fn action_suggester(val: &str) -> SuggestionResult<Vec<String>> {
252    // TODO: make this an enum to automatically pick up new actions
253    let suggestions = [
254        HEADERS,
255        HEADERS_NO_PSEUDO,
256        HEADERS_LITERAL,
257        HEADERS_NO_PSEUDO_LITERAL,
258        DATA,
259        SETTINGS,
260        GOAWAY,
261        PRIORITY_UPDATE,
262        PUSH_PROMISE,
263        CANCEL_PUSH,
264        MAX_PUSH_ID,
265        GREASE,
266        EXTENSION,
267        OPEN_UNI_STREAM,
268        RESET_STREAM,
269        STOP_SENDING,
270        CONNECTION_CLOSE,
271        STREAM_BYTES,
272        FLUSH_PACKETS,
273        COMMIT,
274        WAIT,
275        QUIT,
276    ];
277
278    squish_suggester(&suggestions, val)
279}
280
281fn squish_suggester(
282    suggestions: &[&str], val: &str,
283) -> SuggestionResult<Vec<String>> {
284    let val_lower = val.to_lowercase();
285
286    Ok(suggestions
287        .iter()
288        .filter(|s| s.to_lowercase().contains(&val_lower))
289        .map(|s| String::from(*s))
290        .collect())
291}
292
293fn validate_varint(id: &str) -> SuggestionResult<Validation> {
294    let x = id.parse::<u64>();
295
296    match x {
297        Ok(v) =>
298            if v >= u64::pow(2, 62) {
299                return Ok(Validation::Invalid(ErrorMessage::Default));
300            },
301
302        Err(_) => {
303            return Ok(Validation::Invalid(ErrorMessage::Default));
304        },
305    }
306
307    Ok(Validation::Valid)
308}
309
310fn prompt_stream_id() -> InquireResult<u64> {
311    prompt_varint(STREAM_ID_PROMPT)
312}
313
314fn prompt_control_stream_id() -> InquireResult<u64> {
315    let id = Text::new(STREAM_ID_PROMPT)
316        .with_validator(h3::validate_varint)
317        .with_autocomplete(&control_stream_suggestor)
318        .with_help_message(ESC_TO_RET)
319        .prompt()?;
320
321    // id is already validated so unwrap always succeeds
322    Ok(id.parse::<u64>().unwrap())
323}
324
325fn prompt_varint(str: &str) -> InquireResult<u64> {
326    let id = Text::new(str)
327        .with_validator(h3::validate_varint)
328        .with_placeholder("Integer <= 2^62 -1")
329        .with_help_message(ESC_TO_RET)
330        .prompt()?;
331
332    // id is already validated so unwrap always succeeds
333    Ok(id.parse::<u64>().unwrap())
334}
335
336fn control_stream_suggestor(val: &str) -> SuggestionResult<Vec<String>> {
337    let suggestions = ["2"];
338
339    squish_suggester(&suggestions, val)
340}
341
342fn prompt_data() -> InquireResult<Action> {
343    let stream_id = h3::prompt_stream_id()?;
344
345    let payload = Text::new("payload:").prompt()?;
346
347    let fin_stream = prompt_fin_stream()?;
348
349    let action = Action::SendFrame {
350        stream_id,
351        fin_stream,
352        frame: quiche::h3::frame::Frame::Data {
353            payload: payload.into(),
354        },
355    };
356
357    Ok(action)
358}
359
360fn prompt_max_push_id() -> InquireResult<Action> {
361    let stream_id = h3::prompt_stream_id()?;
362    let push_id = h3::prompt_varint(PUSH_ID_PROMPT)?;
363
364    let fin_stream = prompt_fin_stream()?;
365
366    let action = Action::SendFrame {
367        stream_id,
368        fin_stream,
369        frame: quiche::h3::frame::Frame::MaxPushId { push_id },
370    };
371
372    Ok(action)
373}
374
375fn prompt_cancel_push() -> InquireResult<Action> {
376    let stream_id = h3::prompt_stream_id()?;
377    let push_id = h3::prompt_varint(PUSH_ID_PROMPT)?;
378
379    let fin_stream = prompt_fin_stream()?;
380
381    let action = Action::SendFrame {
382        stream_id,
383        fin_stream,
384        frame: quiche::h3::frame::Frame::CancelPush { push_id },
385    };
386
387    Ok(action)
388}
389
390fn prompt_goaway() -> InquireResult<Action> {
391    let stream_id = h3::prompt_stream_id()?;
392    let id = h3::prompt_varint("ID:")?;
393
394    let fin_stream = prompt_fin_stream()?;
395
396    let action = Action::SendFrame {
397        stream_id,
398        fin_stream,
399        frame: quiche::h3::frame::Frame::GoAway { id },
400    };
401
402    Ok(action)
403}
404
405fn prompt_grease() -> InquireResult<Action> {
406    let stream_id = h3::prompt_control_stream_id()?;
407    let raw_type = quiche::h3::grease_value();
408    let payload = Text::new("payload:")
409        .prompt()
410        .expect("An error happened when asking for payload, try again later.");
411
412    let fin_stream = prompt_fin_stream()?;
413
414    let action = Action::SendFrame {
415        stream_id,
416        fin_stream,
417        frame: quiche::h3::frame::Frame::Unknown {
418            raw_type,
419            payload: payload.into(),
420        },
421    };
422
423    Ok(action)
424}
425
426fn prompt_extension() -> InquireResult<Action> {
427    let stream_id = h3::prompt_control_stream_id()?;
428    let raw_type = h3::prompt_varint("frame type:")?;
429    let payload = Text::new("payload:")
430        .with_help_message(ESC_TO_RET)
431        .prompt()
432        .expect("An error happened when asking for payload, try again later.");
433
434    let fin_stream = prompt_fin_stream()?;
435
436    let action = Action::SendFrame {
437        stream_id,
438        fin_stream,
439        frame: quiche::h3::frame::Frame::Unknown {
440            raw_type,
441            payload: payload.into(),
442        },
443    };
444
445    Ok(action)
446}
447
448pub fn prompt_connection_close() -> InquireResult<Action> {
449    let (error_space, error_code) = errors::prompt_transport_or_app_error()?;
450    let reason = Text::new("reason phrase:")
451        .with_placeholder("optional reason phrase")
452        .prompt()
453        .unwrap_or_default();
454
455    Ok(Action::ConnectionClose {
456        error: ConnectionError {
457            is_app: matches!(error_space, ErrorSpace::ApplicationError),
458            error_code,
459            reason: reason.as_bytes().to_vec(),
460        },
461    })
462}
463
464pub fn prompt_stream_bytes() -> InquireResult<Action> {
465    let stream_id = h3::prompt_stream_id()?;
466    let bytes = Text::new("bytes:").prompt()?;
467    let fin_stream = prompt_fin_stream()?;
468
469    Ok(Action::StreamBytes {
470        stream_id,
471        fin_stream,
472        bytes: bytes.as_bytes().to_vec(),
473    })
474}
475
476fn validate_wait_period(period: &str) -> SuggestionResult<Validation> {
477    let x = period.parse::<u64>();
478
479    match x {
480        Ok(v) => {
481            let local_conn_timeout =
482                CONNECTION_IDLE_TIMEOUT.with(|v| *v.borrow());
483            if v >= local_conn_timeout {
484                return Ok(Validation::Invalid(ErrorMessage::Custom(format!(
485                    "wait time >= local connection idle timeout {}",
486                    local_conn_timeout
487                ))));
488            }
489        },
490
491        Err(_) => return Ok(Validation::Invalid(ErrorMessage::Default)),
492    }
493
494    Ok(Validation::Valid)
495}
496
497fn prompt_yes_no(msg: &str) -> InquireResult<bool> {
498    let res = Select::new(msg, vec![NO, YES]).prompt()?;
499
500    Ok(res == YES)
501}
502
503mod errors;
504mod headers;
505mod priority;
506mod settings;
507mod stream;
508mod wait;