use std::io::{Read, Write};
use std::net::TcpListener;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use badterm_core::session::SessionEvent;
use badterm_core::telnet::{DO, IAC, WILL, opt};
use badterm_core::transport::tls_pins_path;
use badterm_core::{Entry, Protocol};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
mod common;
use common::*;
#[test]
fn rlogin_sends_its_login_and_hides_the_reply() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let server = thread::spawn(move || {
let (mut sock, _) = listener.accept().unwrap();
sock.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let mut got = Vec::new();
let mut buf = [0u8; 256];
while got.iter().filter(|&&b| b == 0).count() < 4 {
let n = sock.read(&mut buf).unwrap();
assert!(n > 0, "client closed early; got {got:?}");
got.extend_from_slice(&buf[..n]);
}
sock.write_all(b"\0Welcome back\r\n").unwrap();
sock.write_all(&[IAC, DO, opt::ECHO]).unwrap();
thread::sleep(Duration::from_millis(300));
got
});
let entry = Entry {
host: "127.0.0.1".into(),
port,
protocol: Protocol::Rlogin,
username: "sysop".into(),
password: "secret".into(),
..Entry::default()
};
let (mut session, rx) = dial(entry, config("rlogin"));
let mut events = Vec::new();
pump_until(&mut session, &rx, &mut events, |s, _| row(s, 0) == "Welcome back");
let got = server.join().unwrap();
assert_eq!(got, b"\0secret\0sysop\0ansi/115200\0");
}
fn tls_bbs() -> (u16, thread::JoinHandle<Vec<u8>>) {
let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
let chain = vec![CertificateDer::from(cert.cert.der().to_vec())];
let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(cert.signing_key.serialize_der()));
let provider = Arc::new(rustls::crypto::ring::default_provider());
let config = rustls::ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.unwrap()
.with_no_client_auth()
.with_single_cert(chain, key)
.unwrap();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let server = thread::spawn(move || {
let (sock, _) = listener.accept().unwrap();
sock.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let conn = rustls::ServerConnection::new(Arc::new(config)).unwrap();
let mut tls = rustls::StreamOwned::new(conn, sock);
let mut got = Vec::new();
if tls.write_all(b"Secure BBS\r\n").and_then(|()| tls.write_all(&[IAC, DO, opt::TTYPE])).is_err() {
return got;
}
let _ = tls.flush();
let mut buf = [0u8; 256];
while !got.windows(3).any(|w| w == [IAC, WILL, opt::TTYPE]) {
match tls.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => got.extend_from_slice(&buf[..n]),
}
}
got
});
(port, server)
}
#[test]
fn telnet_over_tls_trusts_a_self_signed_certificate_once() {
let cfg = config("tls");
let (port, server) = tls_bbs();
let entry = Entry { host: "localhost".into(), port, protocol: Protocol::TelnetS, ..Entry::default() };
let (mut session, rx) = dial(entry.clone(), cfg.clone());
let mut events = Vec::new();
pump_until(&mut session, &rx, &mut events, |s, _| row(s, 0) == "Secure BBS" && server.is_finished());
let got = server.join().unwrap();
assert!(got.windows(3).any(|w| w == [IAC, WILL, opt::TTYPE]), "{got:?}");
let notice = events.iter().find_map(|e| match e {
SessionEvent::Notice(n) => Some(n.clone()),
_ => None,
});
assert!(notice.as_deref().is_some_and(|n| n.contains("self-signed")), "{events:?}");
let pins = std::fs::read_to_string(tls_pins_path(&cfg.known_hosts)).unwrap();
assert!(pins.starts_with(&format!("localhost:{port} sha256:")), "{pins}");
session.hang_up();
let (port2, server) = tls_bbs();
std::fs::write(tls_pins_path(&cfg.known_hosts), pins.replace(&format!(":{port} "), &format!(":{port2} "))).unwrap();
let (mut session, rx) = dial(Entry { port: port2, ..entry }, cfg);
let mut events = Vec::new();
pump_until(&mut session, &rx, &mut events, |s, _| s.error.is_some());
assert!(session.error.as_deref().unwrap().contains("CHANGED"), "{:?}", session.error);
let _ = server.join();
}
#[test]
fn auto_login_answers_the_prompts() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let server = thread::spawn(move || {
let (mut sock, _) = listener.accept().unwrap();
sock.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let mut got = Vec::new();
let mut buf = [0u8; 256];
let mut wait_for = |sock: &mut std::net::TcpStream, needle: &[u8], got: &mut Vec<u8>| {
while !got.windows(needle.len()).any(|w| w == needle) {
let n = sock.read(&mut buf).unwrap();
assert!(n > 0, "client closed early; got {got:?}");
got.extend_from_slice(&buf[..n]);
}
};
sock.write_all(b"Welcome!\r\nLast login:").unwrap();
thread::sleep(Duration::from_millis(50));
sock.write_all(b" yesterday\r\nLogin: ").unwrap();
wait_for(&mut sock, b"sysop\r", &mut got);
sock.write_all(b"\r\nPassword: ").unwrap();
wait_for(&mut sock, b"secret\r", &mut got);
sock.write_all(b"\r\nHello, sysop.\r\n").unwrap();
thread::sleep(Duration::from_millis(300));
got
});
let entry = Entry { username: "sysop".into(), password: "secret".into(), ..telnet_entry(port) };
let (mut session, rx) = dial(entry, config("autologin"));
let mut events = Vec::new();
pump_until(&mut session, &rx, &mut events, |s, _| (0..25).any(|y| row(s, y) == "Hello, sysop."));
let got = server.join().unwrap();
let text = String::from_utf8_lossy(&got);
assert_eq!(text.matches("sysop").count(), 1, "{got:?}");
assert!(text.find("sysop").unwrap() < text.find("secret").unwrap(), "{got:?}");
}
#[test]
fn busy_and_unreachable_calls_are_worth_redialing() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let server = thread::spawn(move || {
let (mut sock, _) = listener.accept().unwrap();
sock.write_all(b"Sorry, all nodes are busy. Please try again later.\r\n").unwrap();
});
let (mut session, rx) = dial(telnet_entry(port), config("busy"));
let mut events = Vec::new();
pump_until(&mut session, &rx, &mut events, |s, _| s.state == badterm_core::session::State::Closed);
server.join().unwrap();
assert!(session.should_redial());
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let server = thread::spawn(move || {
let (mut sock, _) = listener.accept().unwrap();
sock.write_all(b"Goodbye!\r\n").unwrap();
});
let (mut session, rx) = dial(telnet_entry(port), config("bye"));
pump_until(&mut session, &rx, &mut events, |s, _| s.state == badterm_core::session::State::Closed);
server.join().unwrap();
assert!(!session.should_redial());
let port = TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port();
let (mut session, rx) = dial(telnet_entry(port), config("down"));
pump_until(&mut session, &rx, &mut events, |s, _| s.state == badterm_core::session::State::Closed);
assert!(session.should_redial(), "{:?}", session.error);
}
#[test]
fn baud_rate_emulation_paces_the_screen() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let server = thread::spawn(move || {
let (mut sock, _) = listener.accept().unwrap();
sock.write_all(&[b'x'; 240]).unwrap();
thread::sleep(Duration::from_secs(3));
});
let entry = Entry { baud: 2400, ..telnet_entry(port) };
let (mut session, rx) = dial(entry, config("baud"));
let shown = |s: &badterm_core::Session| (0..25).map(|y| row(s, y).len()).sum::<usize>();
let mut events = Vec::new();
pump_until(&mut session, &rx, &mut events, |s, _| shown(s) > 0);
let start = std::time::Instant::now();
let mut halfway = None;
pump_until(&mut session, &rx, &mut events, |s, _| {
if halfway.is_none() && start.elapsed() >= Duration::from_millis(400) {
halfway = Some(shown(s));
}
shown(s) == 240
});
let took = start.elapsed();
assert!(took >= Duration::from_millis(800) && took < Duration::from_secs(3), "{took:?}");
let halfway = halfway.unwrap();
assert!((40..200).contains(&halfway), "{halfway} bytes shown after 0.4 s");
drop(server);
}