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(Box<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 =>
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 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 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 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 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 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;