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::{self, Download, Outcome, TrailerFilter, Transfer, Upload, XyProtocol, XyReceive, XySend};
18pub use crate::transport::{AuthPrompt, ConnectOptions, NetEvent, PromptKind, Writer, spawn_connection};
19
20const PROMPT_QUIET: Duration = Duration::from_millis(250);
23
24const BUSY_WINDOW: Duration = Duration::from_secs(30);
26
27fn says_busy(screen: &Screen) -> bool {
29 const WORDS: [&str; 6] = ["busy", "no free node", "all nodes", "all lines", "try again later", "too many users"];
30 (0..screen.rows()).any(|y| {
31 let line = Screen::row_text(screen.row(y)).to_ascii_lowercase();
32 WORDS.iter().any(|w| line.contains(w))
33 })
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum State {
38 Connecting,
39 Connected,
40 Closed,
41}
42
43#[derive(Clone, Debug)]
45pub struct SessionConfig {
46 pub scrollback: usize,
47 pub known_hosts: PathBuf,
48 pub download_dir: PathBuf,
49 pub ssh_agent: bool,
51 pub ssh_key_dir: Option<PathBuf>,
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
57pub enum SessionEvent {
58 Connected,
60 Term(TermEvent),
61 Notice(String),
62 UploadRequested,
66 AuthPrompt,
69 TransferStarted,
70 TransferFinished(Outcome),
71}
72
73pub struct Session {
74 pub entry: Entry,
75 pub term: Terminal,
76 pub state: State,
77 pub error: Option<String>,
79 config: SessionConfig,
80 telnet: Option<Telnet>,
81 writer: Option<Writer>,
82 capture: Option<(PathBuf, BufWriter<File>)>,
83 transfer: Option<Transfer>,
84 upload_wait: Option<Vec<u8>>,
87 auth_prompt: Option<AuthPrompt>,
88 login: AutoLogin,
89 quiet_since: Option<Instant>,
91 paced: VecDeque<u8>,
94 pace_credit: f64,
95 pace_at: Instant,
96 scan_tail: Vec<u8>,
99 trailer: Option<TrailerFilter>,
101 started: Instant,
102 connected_at: Option<Instant>,
103 events: Vec<SessionEvent>,
104}
105
106impl Session {
107 pub fn new(entry: Entry, cols: usize, rows: usize, config: SessionConfig) -> Session {
108 let login = AutoLogin::new(&entry);
109 let mut term = Terminal::new(cols, rows, config.scrollback);
110 term.encoding = entry.encoding;
111 term.screen.base_font = entry.font;
112 term.screen.base_ice = entry.ice.unwrap_or(false);
113 term.screen.ice_colors = term.screen.base_ice;
114 term.screen.look.fonts[0] = entry.font;
115 term.start_emulation();
117 let telnet = entry.protocol.is_telnet().then(|| Telnet::new(&entry.term_type, cols as u16, rows as u16));
118 Session {
119 entry,
120 term,
121 state: State::Connecting,
122 error: None,
123 config,
124 telnet,
125 writer: None,
126 capture: None,
127 transfer: None,
128 upload_wait: None,
129 auth_prompt: None,
130 login,
131 quiet_since: None,
132 paced: VecDeque::new(),
133 pace_credit: 0.0,
134 pace_at: Instant::now(),
135 scan_tail: Vec::new(),
136 trailer: None,
137 started: Instant::now(),
138 connected_at: None,
139 events: Vec::new(),
140 }
141 }
142
143 pub fn connect<F>(&self, sink: F)
146 where
147 F: Fn(NetEvent) + Send + 'static,
148 {
149 let opts = ConnectOptions {
150 cols: self.term.screen.cols() as u16,
151 rows: self.term.screen.rows() as u16,
152 known_hosts: self.config.known_hosts.clone(),
153 ssh_agent: self.config.ssh_agent,
154 ssh_key_dir: self.config.ssh_key_dir.clone(),
155 };
156 spawn_connection(&self.entry, &opts, sink);
157 }
158
159 pub fn handle(&mut self, ev: NetEvent) {
160 match ev {
161 NetEvent::Connected(w) => {
162 self.writer = Some(w);
163 self.state = State::Connected;
164 self.connected_at = Some(Instant::now());
165 self.login.connected();
166 self.events.push(SessionEvent::Connected);
167 }
168 NetEvent::Data(bytes) => self.receive(&bytes),
169 NetEvent::Notice(msg) => self.events.push(SessionEvent::Notice(msg)),
170 NetEvent::Prompt(p) => {
171 self.auth_prompt = Some(p);
172 self.events.push(SessionEvent::AuthPrompt);
173 }
174 NetEvent::Closed(err) => {
175 self.auth_prompt = None;
176 self.state = State::Closed;
177 self.error = err;
178 self.writer = None;
179 self.end_transfer(Some(Outcome::Failed("the connection closed".into())));
180 }
181 }
182 }
183
184 fn receive(&mut self, bytes: &[u8]) {
185 let data = match &mut self.telnet {
186 Some(t) => {
187 let (mut data, mut reply) = (Vec::with_capacity(bytes.len()), Vec::new());
188 t.receive(bytes, &mut data, &mut reply);
189 if !reply.is_empty() {
190 self.write_raw(&reply);
191 }
192 data
193 }
194 None => bytes.to_vec(),
195 };
196 if self.entry.baud == 0 || self.transfer.is_some() && self.paced.is_empty() {
198 self.process(data);
199 self.quiet_since = Some(Instant::now());
200 } else {
201 if self.paced.is_empty() {
202 self.pace_at = Instant::now();
203 }
204 self.paced.extend(data);
205 self.pump();
206 }
207 }
208
209 pub fn pacing(&self) -> bool {
212 !self.paced.is_empty()
213 }
214
215 pub fn pump(&mut self) {
218 if self.paced.is_empty() {
219 return;
220 }
221 let now = Instant::now();
222 self.pace_credit += now.duration_since(self.pace_at).as_secs_f64() * self.entry.baud as f64 / 10.0;
223 self.pace_at = now;
224 let n = (self.pace_credit as usize).min(self.paced.len());
225 if n == 0 {
226 return;
227 }
228 self.pace_credit -= n as f64;
229 let chunk: Vec<u8> = self.paced.drain(..n).collect();
230 self.process(chunk);
231 if self.transfer.is_some() {
233 let rest: Vec<u8> = self.paced.drain(..).collect();
234 self.process(rest);
235 }
236 if self.paced.is_empty() {
237 self.pace_credit = 0.0;
238 }
239 self.quiet_since = Some(now);
240 }
241
242 fn check_login_prompt(&mut self) {
245 let Some(t) = self.quiet_since else { return };
246 if t.elapsed() < PROMPT_QUIET || self.transfer.is_some() || self.upload_wait.is_some() {
247 return;
248 }
249 self.quiet_since = None;
250 let screen = &self.term.screen;
251 let (x, y) = screen.cursor();
252 let row = screen.row(y);
253 let prompt = Screen::row_text(&row[..x.min(row.len())]);
254 if let Some(step) = self.login.answer(&prompt) {
255 self.send_login_step(step);
256 }
257 }
258
259 pub fn send_login(&mut self) -> bool {
262 match self.login.next_step() {
263 Some(step) => {
264 self.send_login_step(step);
265 true
266 }
267 None => false,
268 }
269 }
270
271 pub fn has_login(&self) -> bool {
273 self.login.has_login()
274 }
275
276 fn send_login_step(&mut self, step: Step) {
277 let (text, what) = match step {
278 Step::User(u) => (u, "user name"),
279 Step::Password(p) => (p, "password"),
280 };
281 let bytes = self.term.encoding.encode_text(&format!("{text}\r"));
282 self.send(&bytes);
283 self.events.push(SessionEvent::Notice(format!("Sent your {what}")));
284 }
285
286 fn process(&mut self, mut data: Vec<u8>) {
289 if let Some(f) = &mut self.trailer
290 && !f.strip(&mut data)
291 {
292 self.trailer = None;
293 }
294 if let Some(t) = &mut self.transfer {
295 let mut out = Vec::new();
296 t.feed(&data, &mut out);
297 self.send(&out);
298 self.end_transfer(None);
299 return;
300 }
301 if let Some(buf) = &mut self.upload_wait {
302 buf.extend_from_slice(&data);
303 return;
304 }
305
306 let mut scan = std::mem::take(&mut self.scan_tail);
307 let already_shown = scan.len();
308 scan.extend_from_slice(&data);
309 if let Some(i) = transfer::find(&scan, transfer::ZRQINIT) {
310 self.show(&scan[already_shown.min(i)..i]);
311 match Download::new(self.config.download_dir.clone()) {
312 Ok(d) => {
313 self.transfer = Some(Transfer::Download(d));
314 self.events.push(SessionEvent::TransferStarted);
315 self.process(scan[i..].to_vec());
316 }
317 Err(e) => self.events.push(SessionEvent::Notice(format!("Cannot start download: {e:#}"))),
318 }
319 return;
320 }
321 if let Some(i) = transfer::find(&scan, transfer::ZRINIT) {
322 self.show(&scan[already_shown.min(i)..i]);
323 self.upload_wait = Some(scan[i..].to_vec());
324 self.events.push(SessionEvent::UploadRequested);
325 return;
326 }
327 let keep = scan.len().min(transfer::ZRQINIT.len() - 1);
328 self.scan_tail = scan[scan.len() - keep..].to_vec();
329 self.show(&data);
330 }
331
332 fn show(&mut self, data: &[u8]) {
333 if data.is_empty() {
334 return;
335 }
336 if let Some((_, f)) = &mut self.capture {
337 let _ = f.write_all(data);
338 }
339 self.term.feed(data);
340 let replies = self.term.take_replies();
341 if !replies.is_empty() {
342 self.send(&replies);
343 }
344 self.events.extend(self.term.take_events().into_iter().map(SessionEvent::Term));
345 }
346
347 fn end_transfer(&mut self, force: Option<Outcome>) {
350 let done = self.transfer.as_ref().is_some_and(Transfer::is_finished);
351 if !done && force.is_none() {
352 return;
353 }
354 let Some(t) = self.transfer.take() else { return };
355 let (outcome, leftover) = t.finish();
356 let outcome = if done { outcome } else { force.unwrap_or(outcome) };
357 self.events.push(SessionEvent::TransferFinished(outcome));
358 self.scan_tail.clear();
359 self.trailer = Some(TrailerFilter::new());
360 self.process(leftover);
361 }
362
363 pub fn take_events(&mut self) -> Vec<SessionEvent> {
364 std::mem::take(&mut self.events)
365 }
366
367 pub fn transfer(&self) -> Option<&Transfer> {
368 self.transfer.as_ref()
369 }
370
371 pub fn auth_prompt(&self) -> Option<&AuthPrompt> {
373 self.auth_prompt.as_ref()
374 }
375
376 pub fn answer_prompt(&mut self, answer: Option<String>) {
378 if let Some(p) = self.auth_prompt.take() {
379 p.answer(answer);
380 }
381 }
382
383 pub fn upload_requested(&self) -> bool {
385 self.upload_wait.is_some()
386 }
387
388 pub fn start_upload(&mut self, paths: Vec<PathBuf>) -> Result<()> {
391 if let Some(t) = &mut self.transfer {
392 for p in paths {
393 t.enqueue(p)?;
394 }
395 return Ok(());
396 }
397 let up = Upload::new(paths)?;
398 self.transfer = Some(Transfer::Upload(up));
399 self.events.push(SessionEvent::TransferStarted);
400 let waiting = self.upload_wait.take().unwrap_or_default();
402 self.process(waiting);
403 Ok(())
404 }
405
406 pub fn start_xy_upload(&mut self, proto: XyProtocol, paths: Vec<PathBuf>) -> Result<()> {
408 if self.transfer.is_some() {
409 anyhow::bail!("a transfer is already in progress");
410 }
411 let up = XySend::new(proto, paths)?;
412 self.upload_wait = None;
413 self.transfer = Some(Transfer::XyUpload(up));
414 self.events.push(SessionEvent::TransferStarted);
415 Ok(())
416 }
417
418 pub fn start_xy_download(&mut self, proto: XyProtocol, name: Option<String>) -> Result<()> {
421 if self.transfer.is_some() {
422 anyhow::bail!("a transfer is already in progress");
423 }
424 let mut t = Transfer::XyDownload(XyReceive::new(proto, self.config.download_dir.clone(), name));
425 let mut out = Vec::new();
426 t.tick(Instant::now(), &mut out);
427 self.transfer = Some(t);
428 self.send(&out);
429 self.events.push(SessionEvent::TransferStarted);
430 Ok(())
431 }
432
433 pub fn decline_upload(&mut self) {
435 if self.upload_wait.take().is_some() {
436 self.send(transfer::CANCEL);
437 }
438 }
439
440 pub fn cancel_transfer(&mut self) {
441 if let Some(t) = &mut self.transfer {
442 let mut out = Vec::new();
443 t.cancel(&mut out);
444 self.send(&out);
445 self.end_transfer(None);
446 }
447 }
448
449 pub fn tick(&mut self) {
451 self.pump();
452 if let Some(t) = &mut self.transfer {
453 let mut out = Vec::new();
454 t.tick(Instant::now(), &mut out);
455 self.send(&out);
456 self.end_transfer(None);
457 }
458 self.check_login_prompt();
459 }
460
461 pub fn send(&mut self, data: &[u8]) {
463 if data.is_empty() {
464 return;
465 }
466 let encoded = match &self.telnet {
467 Some(t) => t.encode(data),
468 None => data.to_vec(),
469 };
470 self.write_raw(&encoded);
471 }
472
473 fn write_raw(&mut self, data: &[u8]) {
474 if let Some(w) = &mut self.writer
475 && let Err(e) = w.write_all(data)
476 {
477 self.error = Some(e.to_string());
478 self.hang_up();
479 }
480 }
481
482 pub fn hang_up(&mut self) {
483 self.answer_prompt(None);
484 if let Some(w) = self.writer.take() {
485 w.shutdown();
486 }
487 self.state = State::Closed;
488 self.stop_capture();
489 }
490
491 pub fn should_redial(&self) -> bool {
496 if self.state != State::Closed {
497 return false;
498 }
499 match self.connected_at {
500 None => self.error.as_deref().is_some_and(|e| e.contains("connecting to")),
501 Some(t) => t.elapsed() < BUSY_WINDOW && says_busy(&self.term.screen),
502 }
503 }
504
505 pub fn elapsed(&self) -> Duration {
507 self.connected_at.unwrap_or(self.started).elapsed()
508 }
509
510 pub fn download_dir(&self) -> &Path {
511 &self.config.download_dir
512 }
513
514 pub fn capture_path(&self) -> Option<&Path> {
515 self.capture.as_ref().map(|(p, _)| p.as_path())
516 }
517
518 pub fn start_capture(&mut self, path: PathBuf) -> Result<()> {
520 if let Some(dir) = path.parent() {
521 std::fs::create_dir_all(dir)?;
522 }
523 let f = File::options()
524 .create(true)
525 .append(true)
526 .open(&path)
527 .with_context(|| format!("opening {}", path.display()))?;
528 self.capture = Some((path, BufWriter::new(f)));
529 Ok(())
530 }
531
532 pub fn stop_capture(&mut self) {
533 if let Some((_, mut f)) = self.capture.take() {
534 let _ = f.flush();
535 }
536 }
537}
538
539impl Drop for Session {
540 fn drop(&mut self) {
541 self.hang_up();
542 }
543}