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, Eq)]
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 if let Some(t) = &mut self.transfer {
492 let mut out = Vec::new();
493 t.tick(Instant::now(), &mut out);
494 self.send(&out);
495 self.end_transfer(None);
496 }
497 self.check_login_prompt();
498 }
499
500 pub fn send(&mut self, data: &[u8]) {
502 if data.is_empty() {
503 return;
504 }
505 let encoded = match &self.telnet {
506 Some(t) => t.encode(data),
507 None => data.to_vec(),
508 };
509 self.write_raw(&encoded);
510 }
511
512 fn write_raw(&mut self, data: &[u8]) {
513 if let Some(w) = &mut self.writer
514 && let Err(e) = w.write_all(data)
515 {
516 self.error = Some(e.to_string());
517 self.hang_up();
518 }
519 }
520
521 pub fn hang_up(&mut self) {
522 self.answer_prompt(None);
523 if let Some(w) = self.writer.take() {
524 w.shutdown();
525 }
526 self.state = State::Closed;
527 self.stop_capture();
528 }
529
530 pub fn should_redial(&self) -> bool {
535 if self.state != State::Closed {
536 return false;
537 }
538 match self.connected_at {
539 None => self.error.as_deref().is_some_and(|e| e.contains("connecting to")),
540 Some(t) => t.elapsed() < BUSY_WINDOW && says_busy(&self.term.screen),
541 }
542 }
543
544 pub fn elapsed(&self) -> Duration {
546 self.connected_at.unwrap_or(self.started).elapsed()
547 }
548
549 pub fn download_dir(&self) -> &Path {
550 &self.config.download_dir
551 }
552
553 pub fn capture_path(&self) -> Option<&Path> {
554 self.capture.as_ref().map(|c| c.path.as_path())
555 }
556
557 pub fn start_capture(&mut self, path: PathBuf) -> Result<()> {
560 if let Some(dir) = path.parent() {
561 std::fs::create_dir_all(dir)?;
562 }
563 let f = File::options()
564 .create(true)
565 .append(true)
566 .open(&path)
567 .with_context(|| format!("opening {}", path.display()))?;
568 let kind = CaptureKind::for_path(&path);
569 self.capture = Some(Capture { path, file: BufWriter::new(f), kind, text: TextState::Ground });
570 Ok(())
571 }
572
573 pub fn stop_capture(&mut self) {
574 if let Some(mut c) = self.capture.take() {
575 let _ = c.file.flush();
576 }
577 }
578}
579
580#[derive(Clone, Copy, Debug, PartialEq, Eq)]
582pub enum CaptureKind {
583 Raw,
585 Text,
587}
588
589impl CaptureKind {
590 pub fn for_path(path: &Path) -> CaptureKind {
591 match path.extension().and_then(|e| e.to_str()) {
592 Some(e) if e.eq_ignore_ascii_case("txt") || e.eq_ignore_ascii_case("log") => CaptureKind::Text,
593 _ => CaptureKind::Raw,
594 }
595 }
596}
597
598#[derive(Clone, Copy, Debug, PartialEq, Eq)]
599enum TextState {
600 Ground,
601 Escape,
602 Csi,
603 String,
605}
606
607struct Capture {
608 path: PathBuf,
609 file: BufWriter<File>,
610 kind: CaptureKind,
611 text: TextState,
612}
613
614impl Capture {
615 fn write(&mut self, data: &[u8], encoding: crate::ansi::Encoding) {
616 if self.kind == CaptureKind::Raw {
617 let _ = self.file.write_all(data);
618 return;
619 }
620 let utf8 = encoding == crate::ansi::Encoding::Utf8;
621 let mut out = Vec::with_capacity(data.len());
622 for &b in data {
623 self.text = match (self.text, b) {
624 (_, 0x18 | 0x1A) => TextState::Ground,
625 (TextState::Ground, 0x1B) => TextState::Escape,
626 (TextState::Ground, b'\n' | b'\t') => {
627 out.push(b);
628 TextState::Ground
629 }
630 (TextState::Ground, 0x0C) => {
631 out.extend_from_slice(b"\n\n");
632 TextState::Ground
633 }
634 (TextState::Ground, 0x00..=0x1F | 0x7F) => TextState::Ground,
635 (TextState::Ground, _) if utf8 || b < 0x80 => {
636 out.push(b);
637 TextState::Ground
638 }
639 (TextState::Ground, _) => {
640 let mut buf = [0; 4];
641 out.extend_from_slice(crate::cp437::to_unicode(b).encode_utf8(&mut buf).as_bytes());
642 TextState::Ground
643 }
644 (TextState::Escape, b'[') => TextState::Csi,
645 (TextState::Escape, b']' | b'P' | b'X' | b'^' | b'_') => TextState::String,
646 (TextState::Escape, 0x20..=0x2F) => TextState::Escape,
647 (TextState::Escape, _) => TextState::Ground,
648 (TextState::Csi, 0x40..=0x7E) => TextState::Ground,
649 (TextState::Csi, _) => TextState::Csi,
650 (TextState::String, 0x07) => TextState::Ground,
651 (TextState::String, 0x1B) => TextState::Escape,
652 (TextState::String, _) => TextState::String,
653 };
654 }
655 let _ = self.file.write_all(&out);
656 }
657}
658
659impl Drop for Session {
660 fn drop(&mut self) {
661 self.hang_up();
662 }
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668 use crate::ansi::Encoding;
669
670 #[test]
671 fn text_captures_keep_only_the_words() {
672 let path = std::env::temp_dir().join(format!("badterm-capture-{}.txt", std::process::id()));
673 let _ = std::fs::remove_file(&path);
674 assert_eq!(CaptureKind::for_path(&path), CaptureKind::Text);
675 assert_eq!(CaptureKind::for_path(Path::new("x.ans")), CaptureKind::Raw);
676 let f = File::create(&path).unwrap();
677 let mut c =
678 Capture { path: path.clone(), file: BufWriter::new(f), kind: CaptureKind::Text, text: TextState::Ground };
679 c.write(b"\x1b[1;3", Encoding::Cp437);
681 c.write(b"1mHello\x1b]2;title\x07\r\n\xc4\xc4 bye\x07", Encoding::Cp437);
682 c.file.flush().unwrap();
683 assert_eq!(std::fs::read_to_string(&path).unwrap(), "Hello\n\u{2500}\u{2500} bye");
684 let _ = std::fs::remove_file(&path);
685 }
686}