1use std::collections::VecDeque;
5use std::fs::File;
6use std::io::{BufWriter, Write};
7use std::path::{Path, PathBuf};
8use std::time::{Duration, Instant};
9
10use anyhow::{Context, Result};
11
12use crate::ansi::{TermEvent, Terminal};
13use crate::autologin::{AutoLogin, Step};
14use crate::dialer::Entry;
15use crate::screen::Screen;
16use crate::telnet::Telnet;
17use crate::transfer::{
18 self, Download, Kermit, Outcome, TrailerFilter, Transfer, Upload, XyProtocol, XyReceive, XySend,
19};
20pub use crate::transport::{AuthPrompt, ConnectOptions, NetEvent, PromptKind, Writer, spawn_connection};
21
22const PROMPT_QUIET: Duration = Duration::from_millis(250);
25
26const BUSY_WINDOW: Duration = Duration::from_secs(30);
28
29fn says_busy(screen: &Screen) -> bool {
31 const WORDS: [&str; 6] = ["busy", "no free node", "all nodes", "all lines", "try again later", "too many users"];
32 (0..screen.rows()).any(|y| {
33 let line = Screen::row_text(screen.row(y)).to_ascii_lowercase();
34 WORDS.iter().any(|w| line.contains(w))
35 })
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum State {
40 Connecting,
41 Connected,
42 Closed,
43}
44
45#[derive(Clone, Debug)]
47pub struct SessionConfig {
48 pub scrollback: usize,
49 pub known_hosts: PathBuf,
50 pub download_dir: PathBuf,
51 pub ssh_agent: bool,
53 pub ssh_key_dir: Option<PathBuf>,
55 pub modem: crate::modem::ModemConfig,
57}
58
59#[derive(Clone, Debug, PartialEq)]
61pub enum SessionEvent {
62 Connected,
64 Term(TermEvent),
65 Notice(String),
66 UploadRequested,
70 AuthPrompt,
73 TransferStarted,
74 TransferFinished(Outcome),
75}
76
77pub struct Session {
78 pub entry: Entry,
79 pub term: Terminal,
80 pub state: State,
81 pub error: Option<String>,
83 config: SessionConfig,
84 telnet: Option<Telnet>,
85 writer: Option<Writer>,
86 capture: Option<Capture>,
87 transfer: Option<Transfer>,
88 upload_wait: Option<Vec<u8>>,
91 auth_prompt: Option<AuthPrompt>,
92 login: AutoLogin,
93 quiet_since: Option<Instant>,
95 paced: VecDeque<u8>,
98 pace_credit: f64,
99 pace_at: Instant,
100 scan_tail: Vec<u8>,
103 trailer: Option<TrailerFilter>,
105 started: Instant,
106 connected_at: Option<Instant>,
107 events: Vec<SessionEvent>,
108}
109
110impl Session {
111 pub fn new(entry: Entry, cols: usize, rows: usize, config: SessionConfig) -> Session {
112 let login = AutoLogin::new(&entry);
113 let mut term = Terminal::new(cols, rows, config.scrollback);
114 term.encoding = entry.encoding;
115 term.screen.base_font = entry.font;
116 term.screen.base_ice = entry.ice.unwrap_or(false);
117 term.screen.ice_colors = term.screen.base_ice;
118 term.screen.look.fonts[0] = entry.font;
119 term.start_emulation();
121 let telnet = entry.protocol.is_telnet().then(|| Telnet::new(&entry.term_type, cols as u16, rows as u16));
122 Session {
123 entry,
124 term,
125 state: State::Connecting,
126 error: None,
127 config,
128 telnet,
129 writer: None,
130 capture: None,
131 transfer: None,
132 upload_wait: None,
133 auth_prompt: None,
134 login,
135 quiet_since: None,
136 paced: VecDeque::new(),
137 pace_credit: 0.0,
138 pace_at: Instant::now(),
139 scan_tail: Vec::new(),
140 trailer: None,
141 started: Instant::now(),
142 connected_at: None,
143 events: Vec::new(),
144 }
145 }
146
147 pub fn connect<F>(&self, sink: F)
150 where
151 F: Fn(NetEvent) + Send + 'static,
152 {
153 let opts = ConnectOptions {
154 cols: self.term.screen.cols() as u16,
155 rows: self.term.screen.rows() as u16,
156 known_hosts: self.config.known_hosts.clone(),
157 ssh_agent: self.config.ssh_agent,
158 ssh_key_dir: self.config.ssh_key_dir.clone(),
159 modem: self.config.modem.clone(),
160 };
161 spawn_connection(&self.entry, &opts, sink);
162 }
163
164 pub fn handle(&mut self, ev: NetEvent) {
165 match ev {
166 NetEvent::Connected(w) => {
167 self.writer = Some(w);
168 self.state = State::Connected;
169 self.connected_at = Some(Instant::now());
170 self.login.connected();
171 self.events.push(SessionEvent::Connected);
172 }
173 NetEvent::Data(bytes) => self.receive(&bytes),
174 NetEvent::Notice(msg) => self.events.push(SessionEvent::Notice(msg)),
175 NetEvent::Prompt(p) => {
176 self.auth_prompt = Some(p);
177 self.events.push(SessionEvent::AuthPrompt);
178 }
179 NetEvent::Closed(err) => {
180 self.auth_prompt = None;
181 self.state = State::Closed;
182 self.error = err;
183 self.writer = None;
184 self.end_transfer(Some(Outcome::Failed("the connection closed".into())));
185 }
186 }
187 }
188
189 fn receive(&mut self, bytes: &[u8]) {
190 let data = match &mut self.telnet {
191 Some(t) => {
192 let (mut data, mut reply) = (Vec::with_capacity(bytes.len()), Vec::new());
193 t.receive(bytes, &mut data, &mut reply);
194 if !reply.is_empty() {
195 self.write_raw(&reply);
196 }
197 data
198 }
199 None => bytes.to_vec(),
200 };
201 if self.entry.baud == 0 || self.transfer.is_some() && self.paced.is_empty() {
203 self.process(data);
204 self.quiet_since = Some(Instant::now());
205 } else {
206 if self.paced.is_empty() {
207 self.pace_at = Instant::now();
208 }
209 self.paced.extend(data);
210 self.pump();
211 }
212 }
213
214 pub fn pacing(&self) -> bool {
217 !self.paced.is_empty()
218 }
219
220 pub fn pump(&mut self) {
223 if self.paced.is_empty() {
224 return;
225 }
226 let now = Instant::now();
227 self.pace_credit += now.duration_since(self.pace_at).as_secs_f64() * self.entry.baud as f64 / 10.0;
228 self.pace_at = now;
229 let n = (self.pace_credit as usize).min(self.paced.len());
230 if n == 0 {
231 return;
232 }
233 self.pace_credit -= n as f64;
234 let chunk: Vec<u8> = self.paced.drain(..n).collect();
235 self.process(chunk);
236 if self.transfer.is_some() {
238 let rest: Vec<u8> = self.paced.drain(..).collect();
239 self.process(rest);
240 }
241 if self.paced.is_empty() {
242 self.pace_credit = 0.0;
243 }
244 self.quiet_since = Some(now);
245 }
246
247 fn check_login_prompt(&mut self) {
250 let Some(t) = self.quiet_since else { return };
251 if t.elapsed() < PROMPT_QUIET || self.transfer.is_some() || self.upload_wait.is_some() {
252 return;
253 }
254 self.quiet_since = None;
255 let screen = &self.term.screen;
256 let (x, y) = screen.cursor();
257 let row = screen.row(y);
258 let prompt = Screen::row_text(&row[..x.min(row.len())]);
259 if let Some(step) = self.login.answer(&prompt) {
260 self.send_login_step(step);
261 }
262 }
263
264 pub fn send_login(&mut self) -> bool {
267 match self.login.next_step() {
268 Some(step) => {
269 self.send_login_step(step);
270 true
271 }
272 None => false,
273 }
274 }
275
276 pub fn has_login(&self) -> bool {
278 self.login.has_login()
279 }
280
281 fn send_login_step(&mut self, step: Step) {
282 let (text, what) = match step {
283 Step::User(u) => (u, "user name"),
284 Step::Password(p) => (p, "password"),
285 };
286 let bytes = self.term.encoding.encode_text(&format!("{text}\r"));
287 self.send(&bytes);
288 self.events.push(SessionEvent::Notice(format!("Sent your {what}")));
289 }
290
291 fn process(&mut self, mut data: Vec<u8>) {
294 if let Some(f) = &mut self.trailer
295 && !f.strip(&mut data)
296 {
297 self.trailer = None;
298 }
299 if let Some(t) = &mut self.transfer {
300 let mut out = Vec::new();
301 t.feed(&data, &mut out);
302 self.send(&out);
303 self.end_transfer(None);
304 return;
305 }
306 if let Some(buf) = &mut self.upload_wait {
307 buf.extend_from_slice(&data);
308 return;
309 }
310
311 let mut scan = std::mem::take(&mut self.scan_tail);
312 let already_shown = scan.len();
313 scan.extend_from_slice(&data);
314 if let Some(i) = transfer::find(&scan, transfer::ZRQINIT) {
315 self.show(&scan[already_shown.min(i)..i]);
316 match Download::new(self.config.download_dir.clone()) {
317 Ok(d) => {
318 self.transfer = Some(Transfer::Download(d));
319 self.events.push(SessionEvent::TransferStarted);
320 self.process(scan[i..].to_vec());
321 }
322 Err(e) => self.events.push(SessionEvent::Notice(format!("Cannot start download: {e:#}"))),
323 }
324 return;
325 }
326 if let Some(i) = transfer::find(&scan, transfer::ZRINIT) {
327 self.show(&scan[already_shown.min(i)..i]);
328 self.upload_wait = Some(scan[i..].to_vec());
329 self.events.push(SessionEvent::UploadRequested);
330 return;
331 }
332 if let Some(i) = crate::kermit::find_send_init(&scan) {
334 self.show(&scan[already_shown.min(i)..i]);
335 self.transfer = Some(Transfer::Kermit(Kermit::receive(self.config.download_dir.clone())));
336 self.events.push(SessionEvent::TransferStarted);
337 self.process(scan[i..].to_vec());
338 return;
339 }
340 let keep = scan.len().min(transfer::ZRQINIT.len() - 1);
341 self.scan_tail = scan[scan.len() - keep..].to_vec();
342 self.show(&data);
343 }
344
345 fn show(&mut self, data: &[u8]) {
346 if data.is_empty() {
347 return;
348 }
349 if let Some(c) = &mut self.capture {
350 c.write(data, self.term.encoding);
351 }
352 self.term.feed(data);
353 let replies = self.term.take_replies();
354 if !replies.is_empty() {
355 self.send(&replies);
356 }
357 self.events.extend(self.term.take_events().into_iter().map(SessionEvent::Term));
358 }
359
360 fn end_transfer(&mut self, force: Option<Outcome>) {
363 let done = self.transfer.as_ref().is_some_and(Transfer::is_finished);
364 if !done && force.is_none() {
365 return;
366 }
367 let Some(t) = self.transfer.take() else { return };
368 let (outcome, leftover) = t.finish();
369 let outcome = if done { outcome } else { force.unwrap_or(outcome) };
370 self.events.push(SessionEvent::TransferFinished(outcome));
371 self.scan_tail.clear();
372 self.trailer = Some(TrailerFilter::new());
373 self.process(leftover);
374 }
375
376 pub fn take_events(&mut self) -> Vec<SessionEvent> {
377 std::mem::take(&mut self.events)
378 }
379
380 pub fn transfer(&self) -> Option<&Transfer> {
381 self.transfer.as_ref()
382 }
383
384 pub fn auth_prompt(&self) -> Option<&AuthPrompt> {
386 self.auth_prompt.as_ref()
387 }
388
389 pub fn answer_prompt(&mut self, answer: Option<String>) {
391 if let Some(p) = self.auth_prompt.take() {
392 p.answer(answer);
393 }
394 }
395
396 pub fn upload_requested(&self) -> bool {
398 self.upload_wait.is_some()
399 }
400
401 pub fn start_upload(&mut self, paths: Vec<PathBuf>) -> Result<()> {
404 if let Some(t) = &mut self.transfer {
405 for p in paths {
406 t.enqueue(p)?;
407 }
408 return Ok(());
409 }
410 let up = Upload::new(paths)?;
411 self.transfer = Some(Transfer::Upload(up));
412 self.events.push(SessionEvent::TransferStarted);
413 let waiting = self.upload_wait.take().unwrap_or_default();
415 self.process(waiting);
416 Ok(())
417 }
418
419 pub fn start_xy_upload(&mut self, proto: XyProtocol, paths: Vec<PathBuf>) -> Result<()> {
421 if self.transfer.is_some() {
422 anyhow::bail!("a transfer is already in progress");
423 }
424 let up = XySend::new(proto, paths)?;
425 self.upload_wait = None;
426 self.transfer = Some(Transfer::XyUpload(up));
427 self.events.push(SessionEvent::TransferStarted);
428 Ok(())
429 }
430
431 pub fn start_xy_download(&mut self, proto: XyProtocol, name: Option<String>) -> Result<()> {
434 if self.transfer.is_some() {
435 anyhow::bail!("a transfer is already in progress");
436 }
437 let mut t = Transfer::XyDownload(XyReceive::new(proto, self.config.download_dir.clone(), name));
438 let mut out = Vec::new();
439 t.tick(Instant::now(), &mut out);
440 self.transfer = Some(t);
441 self.send(&out);
442 self.events.push(SessionEvent::TransferStarted);
443 Ok(())
444 }
445
446 pub fn start_kermit_upload(&mut self, paths: Vec<PathBuf>) -> Result<()> {
448 if self.transfer.is_some() {
449 anyhow::bail!("a transfer is already in progress");
450 }
451 let mut t = Transfer::Kermit(Kermit::send(paths)?);
452 let mut out = Vec::new();
453 t.start(&mut out);
454 self.upload_wait = None;
455 self.transfer = Some(t);
456 self.send(&out);
457 self.events.push(SessionEvent::TransferStarted);
458 Ok(())
459 }
460
461 pub fn start_kermit_download(&mut self) -> Result<()> {
464 if self.transfer.is_some() {
465 anyhow::bail!("a transfer is already in progress");
466 }
467 self.transfer = Some(Transfer::Kermit(Kermit::receive(self.config.download_dir.clone())));
468 self.events.push(SessionEvent::TransferStarted);
469 Ok(())
470 }
471
472 pub fn decline_upload(&mut self) {
474 if self.upload_wait.take().is_some() {
475 self.send(transfer::CANCEL);
476 }
477 }
478
479 pub fn cancel_transfer(&mut self) {
480 if let Some(t) = &mut self.transfer {
481 let mut out = Vec::new();
482 t.cancel(&mut out);
483 self.send(&out);
484 self.end_transfer(None);
485 }
486 }
487
488 pub fn tick(&mut self) {
490 self.pump();
491 self.term.poll(Instant::now());
493 let replies = self.term.take_replies();
494 if !replies.is_empty() {
495 self.send(&replies);
496 }
497 self.events.extend(self.term.take_events().into_iter().map(SessionEvent::Term));
498 if let Some(t) = &mut self.transfer {
499 let mut out = Vec::new();
500 t.tick(Instant::now(), &mut out);
501 self.send(&out);
502 self.end_transfer(None);
503 }
504 self.check_login_prompt();
505 }
506
507 pub fn next_tick(&self) -> Option<Instant> {
509 self.term.next_poll()
510 }
511
512 pub fn send(&mut self, data: &[u8]) {
514 if data.is_empty() {
515 return;
516 }
517 let encoded = match &self.telnet {
518 Some(t) => t.encode(data),
519 None => data.to_vec(),
520 };
521 self.write_raw(&encoded);
522 }
523
524 fn write_raw(&mut self, data: &[u8]) {
525 if let Some(w) = &mut self.writer
526 && let Err(e) = w.write_all(data)
527 {
528 self.error = Some(e.to_string());
529 self.hang_up();
530 }
531 }
532
533 pub fn hang_up(&mut self) {
534 self.answer_prompt(None);
535 if let Some(w) = self.writer.take() {
536 w.shutdown();
537 }
538 self.state = State::Closed;
539 self.stop_capture();
540 }
541
542 pub fn should_redial(&self) -> bool {
547 if self.state != State::Closed {
548 return false;
549 }
550 match self.connected_at {
551 None => self.error.as_deref().is_some_and(|e| e.contains("connecting to")),
552 Some(t) => t.elapsed() < BUSY_WINDOW && says_busy(&self.term.screen),
553 }
554 }
555
556 pub fn elapsed(&self) -> Duration {
558 self.connected_at.unwrap_or(self.started).elapsed()
559 }
560
561 pub fn download_dir(&self) -> &Path {
562 &self.config.download_dir
563 }
564
565 pub fn capture_path(&self) -> Option<&Path> {
566 self.capture.as_ref().map(|c| c.path.as_path())
567 }
568
569 pub fn start_capture(&mut self, path: PathBuf) -> Result<()> {
572 if let Some(dir) = path.parent() {
573 std::fs::create_dir_all(dir)?;
574 }
575 let f = File::options()
576 .create(true)
577 .append(true)
578 .open(&path)
579 .with_context(|| format!("opening {}", path.display()))?;
580 let kind = CaptureKind::for_path(&path);
581 self.capture = Some(Capture { path, file: BufWriter::new(f), kind, text: TextState::Ground });
582 Ok(())
583 }
584
585 pub fn stop_capture(&mut self) {
586 if let Some(mut c) = self.capture.take() {
587 let _ = c.file.flush();
588 }
589 }
590}
591
592#[derive(Clone, Copy, Debug, PartialEq, Eq)]
594pub enum CaptureKind {
595 Raw,
597 Text,
599}
600
601impl CaptureKind {
602 pub fn for_path(path: &Path) -> CaptureKind {
603 match path.extension().and_then(|e| e.to_str()) {
604 Some(e) if e.eq_ignore_ascii_case("txt") || e.eq_ignore_ascii_case("log") => CaptureKind::Text,
605 _ => CaptureKind::Raw,
606 }
607 }
608}
609
610#[derive(Clone, Copy, Debug, PartialEq, Eq)]
611enum TextState {
612 Ground,
613 Escape,
614 Csi,
615 String,
617}
618
619struct Capture {
620 path: PathBuf,
621 file: BufWriter<File>,
622 kind: CaptureKind,
623 text: TextState,
624}
625
626impl Capture {
627 fn write(&mut self, data: &[u8], encoding: crate::ansi::Encoding) {
628 if self.kind == CaptureKind::Raw {
629 let _ = self.file.write_all(data);
630 return;
631 }
632 let utf8 = encoding == crate::ansi::Encoding::Utf8;
633 let mut out = Vec::with_capacity(data.len());
634 for &b in data {
635 self.text = match (self.text, b) {
636 (_, 0x18 | 0x1A) => TextState::Ground,
637 (TextState::Ground, 0x1B) => TextState::Escape,
638 (TextState::Ground, b'\n' | b'\t') => {
639 out.push(b);
640 TextState::Ground
641 }
642 (TextState::Ground, 0x0C) => {
643 out.extend_from_slice(b"\n\n");
644 TextState::Ground
645 }
646 (TextState::Ground, 0x00..=0x1F | 0x7F) => TextState::Ground,
647 (TextState::Ground, _) if utf8 || b < 0x80 => {
648 out.push(b);
649 TextState::Ground
650 }
651 (TextState::Ground, _) => {
652 let mut buf = [0; 4];
653 out.extend_from_slice(crate::cp437::to_unicode(b).encode_utf8(&mut buf).as_bytes());
654 TextState::Ground
655 }
656 (TextState::Escape, b'[') => TextState::Csi,
657 (TextState::Escape, b']' | b'P' | b'X' | b'^' | b'_') => TextState::String,
658 (TextState::Escape, 0x20..=0x2F) => TextState::Escape,
659 (TextState::Escape, _) => TextState::Ground,
660 (TextState::Csi, 0x40..=0x7E) => TextState::Ground,
661 (TextState::Csi, _) => TextState::Csi,
662 (TextState::String, 0x07) => TextState::Ground,
663 (TextState::String, 0x1B) => TextState::Escape,
664 (TextState::String, _) => TextState::String,
665 };
666 }
667 let _ = self.file.write_all(&out);
668 }
669}
670
671impl Drop for Session {
672 fn drop(&mut self) {
673 self.hang_up();
674 }
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680 use crate::ansi::Encoding;
681
682 #[test]
683 fn text_captures_keep_only_the_words() {
684 let path = std::env::temp_dir().join(format!("badterm-capture-{}.txt", std::process::id()));
685 let _ = std::fs::remove_file(&path);
686 assert_eq!(CaptureKind::for_path(&path), CaptureKind::Text);
687 assert_eq!(CaptureKind::for_path(Path::new("x.ans")), CaptureKind::Raw);
688 let f = File::create(&path).unwrap();
689 let mut c =
690 Capture { path: path.clone(), file: BufWriter::new(f), kind: CaptureKind::Text, text: TextState::Ground };
691 c.write(b"\x1b[1;3", Encoding::Cp437);
693 c.write(b"1mHello\x1b]2;title\x07\r\n\xc4\xc4 bye\x07", Encoding::Cp437);
694 c.file.flush().unwrap();
695 assert_eq!(std::fs::read_to_string(&path).unwrap(), "Hello\n\u{2500}\u{2500} bye");
696 let _ = std::fs::remove_file(&path);
697 }
698}