commit e6148f6d50fe57a40595df03fb730f371d8d02f5 Author: Andrew Pamment Date: Wed Nov 15 11:07:08 2023 +1000 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4c7d07b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "doorlib" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anes = "0.1.6" + +[target.'cfg(target_family = "unix")'.dependencies] +termios = "0.3.3" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f3e8c8d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,360 @@ +#[cfg(target_family = "unix")] +use termios::{cfmakeraw, tcsetattr, Termios, TCSANOW}; + +use anes::{esc, ResetAttributes}; +use std::env; +use std::fs::read_to_string; +use std::io; +use std::io::{Read, Write}; +use std::net::TcpStream; +use std::process; +use std::thread; +use std::time::{Duration, Instant}; + +type Result = std::result::Result; + +#[derive(Debug)] +enum ConnType { + Local, + Serial, + Telnet(i64), +} + +#[derive(Debug)] +#[allow(dead_code)] +pub struct UserInfo { + typ: ConnType, + bbsid: String, + record: i32, + realname: String, + alias: String, + seclevel: i32, + timeleft: u64, + emulation: i32, + node: i32, + timeout: Instant, + starttime: Instant, +} + +pub trait Conn { + fn write(&mut self, data: &[u8]) -> Result<()>; + + fn write_str(&mut self, s: &str) -> Result<()> { + self.write(s.as_bytes()) + } + + fn write_ln(&mut self, line: &str) -> Result<()> { + self.write_str(line)?; + self.write_str("\r\n") + } + + fn read_byte(&mut self) -> Result; + + fn info(&self) -> &UserInfo; +} + +struct LocalUser { + info: UserInfo, + #[cfg(target_family = "unix")] + termios: Termios, +} + +impl LocalUser { + fn new(info: UserInfo) -> LocalUser { + #[cfg(target_family = "unix")] + { + let termios = Termios::from_fd(0).expect("terminal mode"); + let mut raw = termios; + cfmakeraw(&mut raw); + tcsetattr(0, TCSANOW, &raw).expect("terminal raw mode"); + LocalUser { info, termios } + } + #[cfg(target_family = "windows")] + LocalUser { info } + } +} + +impl Conn for LocalUser { + fn write(&mut self, data: &[u8]) -> Result<()> { + #[cfg(target_family = "unix")] + { + io::stdout().write_all(data)?; + io::stdout().flush() + } + #[cfg(target_family = "windows")] + Ok(()) + } + + fn read_byte(&mut self) -> Result { + let mut byte = 0u8; + #[cfg(target_family = "unix")] + { + io::stdin() + .lock() + .read_exact(std::slice::from_mut(&mut byte))?; + } + Ok(byte) + } + + fn info(&self) -> &UserInfo { + &self.info + } +} + +impl Drop for LocalUser { + fn drop(&mut self) { + #[cfg(target_family = "unix")] + tcsetattr(0, TCSANOW, &self.termios).expect("reset terminal"); + } +} + +#[derive(Debug)] +struct NetUser { + info: UserInfo, + stream: TcpStream, +} + +impl NetUser { + fn read1(&mut self) -> Result { + let mut rx_byte: u8 = 0; + loop { + match self.stream.read(std::slice::from_mut(&mut rx_byte)) { + Ok(_) => { + self.info.timeout = Instant::now(); + return Ok(rx_byte); + } + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + self.maybe_timeout(); + self.maybe_out_of_time(); + thread::sleep(Duration::from_millis(100)); + } + Err(e) => return Err(e), + } + } + } + + fn maybe_timeout(&mut self) { + const TIME_OUT: u64 = 300; + if self.info.timeout.elapsed().as_secs() >= TIME_OUT { + let e = esc!("[1;31m"); + self.write_ln(format!("\r\n\r\n{e}Timeout!{ResetAttributes}").as_str()) + .expect("write"); + process::exit(0); + } + } + + fn maybe_out_of_time(&mut self) { + let start_time = self.info.starttime; + let time_left = self.info.timeleft; + if start_time.elapsed().as_secs() >= time_left { + let e = esc!("[1;31m"); + self.write_ln(format!("\r\n\r\n{e}You're out of time!{ResetAttributes}\r\n").as_str()) + .expect("write"); + process::exit(0); + } + } +} + +impl Conn for NetUser { + fn write(&mut self, data: &[u8]) -> Result<()> { + self.stream.write_all(data)?; + Ok(()) + } + + fn read_byte(&mut self) -> Result { + let mut state = 0; + loop { + let rx_byte = self.read1()?; + state = match (state, rx_byte) { + (0, 255) => 1, + (0, _) => return Ok(rx_byte), + (1, 255) => { + self.stream.write_all(std::slice::from_ref(&rx_byte))?; + 0 + } + (1, 250) => 3, + (1, _) => 2, + (2, _) => 0, + (3, 240) => 0, + _ => state, + }; + } + } + + fn info(&self) -> &UserInfo { + &self.info + } +} + +pub struct User { + typ: bool, + netuser: Option, + localuser: Option, +} + +impl Conn for User { + fn write(&mut self, data: &[u8]) -> Result<()> { + match self.typ { + true => { + self.netuser.as_mut().unwrap().write(data)?; + return Ok(()); + } + false => { + self.localuser.as_mut().unwrap().write(data)?; + return Ok(()); + } + }; + } + + fn read_byte(&mut self) -> Result { + match self.typ { + true => return self.netuser.as_mut().unwrap().read_byte(), + false => return self.localuser.as_mut().unwrap().read_byte(), + }; + } + + fn info(&self) -> &UserInfo { + match self.typ { + true => return &self.netuser.as_ref().unwrap().info, + false => return &self.localuser.as_ref().unwrap().info, + }; + } +} + +pub fn display_file(user: &mut User, path: &str) -> Result<()> { + if let Ok(file) = std::fs::read(path) { + user.write(&file)?; + user.write_str(ResetAttributes.to_string().as_str())?; + } + Ok(()) +} + +pub fn read_string(user: &mut impl Conn, len: usize) -> Result { + const BACKSPACE: [u8; 3] = *b"\x08\x20\x08"; + + user.write_str(esc!("[s"))?; + user.write_str(esc!("[1;37;45m"))?; + for _i in 1..len { + user.write_str(" ")?; + } + user.write_str(esc!("[u"))?; + + let mut received: Vec = vec![]; + while received.len() != len { + let ch = user.read_byte()?; + match ch { + 0 | 10 => {} + 13 => break, + 127 | 8 => { + if !received.is_empty() { + user.write(&BACKSPACE)?; + received.truncate(received.len() - 1); + } + } + _ => { + user.write(std::slice::from_ref(&ch))?; + received.extend_from_slice(&[ch]); + } + } + } + let s = std::str::from_utf8(&received).unwrap().to_string(); + user.write_ln(ResetAttributes.to_string().as_str())?; + Ok(s) +} + +fn read_lines(filename: &str) -> Result> { + let s = read_to_string(filename)?; + Ok(s.lines().map(str::to_owned).collect()) +} + +fn read_trimmed_lines(path: &str) -> Result> { + let lines = read_lines(path)?; + Ok(lines.into_iter().map(|s| s.trim().to_owned()).collect()) +} + +fn read_door32(path: &str) -> Result { + let lines = read_trimmed_lines(path)?; + let typ = match lines[0].parse::().expect("i32") { + 0 => ConnType::Local, + 1 => ConnType::Serial, + 2 => ConnType::Telnet(lines[1].parse().expect("door32.sys socket")), + _ => ConnType::Local, + }; + let info = UserInfo { + typ, + bbsid: lines[3].clone(), + record: lines[4].parse().expect("door32.sys record"), + realname: lines[5].clone(), + alias: lines[6].clone(), + seclevel: lines[7].parse().expect("door32.sys seclevel"), + timeleft: lines[8].parse().expect("door32.sys timeleft"), + emulation: lines[9].parse().expect("door32.sys emulation"), + node: lines[10].parse().expect("door32.sys node"), + timeout: Instant::now(), + starttime: Instant::now(), + }; + Ok(info) +} + +fn convert_socket(sock: i64) -> Result { + #[cfg(target_family = "unix")] + let stream = unsafe { + use std::os::fd::FromRawFd; + TcpStream::from_raw_fd(sock as i32) + }; + #[cfg(target_family = "windows")] + let stream = unsafe { + use std::os::windows::io::FromRawSocket; + drop(std::net::TcpListener::bind("255.255.255.255:0")); // hack to get WSAStartup to fire + TcpStream::from_raw_socket(sock as u64) + }; + stream.set_nonblocking(true)?; + Ok(stream) +} + +pub fn init() -> Result { + let args = env::args().collect::>(); + if args.len() < 2 || 3 < args.len() { + panic!("Usage: door.exe door32.sys [socket]"); + } + let info = read_door32(&args[1]).expect("read door32.sys"); + let fd = if args.len() == 3 { + let fd = args[2].parse().expect("Socket is an integer"); + if fd > 0 { + fd + } else if let ConnType::Telnet(fd) = info.typ { + fd + } else { + 0 + } + } else { + if let ConnType::Telnet(fd) = info.typ { + fd + } else { + 0 + } + }; + match info.typ { + ConnType::Serial => panic!("UART not supported"), + ConnType::Local => { + let user = LocalUser::new(info); + + let u = User { + typ: false, + netuser: None, + localuser: Some(user), + }; + return Ok(u); + } + ConnType::Telnet(_) => { + let stream = convert_socket(fd).expect("tcp stream"); + let user = NetUser { info, stream }; + let u = User { + typ: true, + netuser: Some(user), + localuser: None, + }; + return Ok(u); + } + }; +}