1use 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#[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
65pub type Result<T> = std::result::Result<T, Error>;
69
70type SuggestionResult<T> = std::result::Result<T, CustomUserError>;
74
75pub type PromptedFrame = (u64, quiche::h3::frame::Frame);
77
78thread_local! {static CONNECTION_IDLE_TIMEOUT: RefCell<u64> = const { RefCell::new(0) }}
79
80const 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
123pub struct Prompter {
125 host_port: String,
126 bidi_sid_alloc: StreamIdAllocator,
127 uni_sid_alloc: StreamIdAllocator,
128}
129
130impl Prompter {
131 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 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 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 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 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 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;