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 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";
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
121pub struct Prompter {
123 host_port: String,
124 bidi_sid_alloc: StreamIdAllocator,
125 uni_sid_alloc: StreamIdAllocator,
126}
127
128impl Prompter {
129 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 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 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 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 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;