use std::time::{Duration, Instant};
use crate::dialer::{Entry, Protocol};
const WINDOW: Duration = Duration::from_secs(120);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Step {
User(String),
Password(String),
}
#[derive(Debug)]
pub struct AutoLogin {
user: String,
password: String,
automatic: bool,
sent_user: bool,
sent_password: bool,
until: Option<Instant>,
}
impl AutoLogin {
pub fn new(entry: &Entry) -> AutoLogin {
AutoLogin {
user: entry.username.clone(),
password: entry.password.clone(),
automatic: entry.auto_login && (entry.protocol.is_telnet() || entry.protocol == Protocol::Raw),
sent_user: false,
sent_password: false,
until: None,
}
}
pub fn connected(&mut self) {
self.until = Some(Instant::now() + WINDOW);
}
pub fn answer(&mut self, prompt: &str) -> Option<Step> {
if !self.automatic || self.until.is_none_or(|t| Instant::now() > t) {
return None;
}
if !self.sent_user && !self.user.is_empty() && is_user_prompt(prompt) {
return self.next_step();
}
if self.sent_user && !self.sent_password && !self.password.is_empty() && is_password_prompt(prompt) {
return self.next_step();
}
None
}
pub fn next_step(&mut self) -> Option<Step> {
if !self.sent_user && !self.user.is_empty() {
self.sent_user = true;
return Some(Step::User(self.user.clone()));
}
self.sent_user = true;
if !self.sent_password && !self.password.is_empty() {
self.sent_password = true;
return Some(Step::Password(self.password.clone()));
}
None
}
pub fn has_login(&self) -> bool {
!self.user.is_empty() || !self.password.is_empty()
}
}
fn prompt_text(prompt: &str) -> Option<String> {
let p = prompt.trim_end();
let p = p.strip_suffix([':', '?', '>'])?;
Some(p.trim().to_ascii_lowercase())
}
fn is_user_prompt(prompt: &str) -> bool {
let Some(p) = prompt_text(prompt) else { return false };
if p.contains("password") || p.contains("pass word") {
return false;
}
let words = ["login", "user", "username", "name", "handle", "alias", "nn", "logon", "callsign"];
let last: Vec<&str> = p.split(|c: char| !c.is_ascii_alphanumeric()).filter(|w| !w.is_empty()).collect();
let tail = &last[last.len().saturating_sub(4)..];
tail.iter().any(|w| words.contains(w)) && !tail.contains(&"real") && !tail.contains(&"first")
}
fn is_password_prompt(prompt: &str) -> bool {
let Some(p) = prompt_text(prompt) else { return false };
p.ends_with("password") || p.ends_with("pw") || p.ends_with("passwd") || p.ends_with("pass")
}
#[cfg(test)]
mod tests {
use super::*;
fn login(user: &str, password: &str) -> AutoLogin {
let mut a = AutoLogin::new(&Entry { username: user.into(), password: password.into(), ..Entry::default() });
a.connected();
a
}
#[test]
fn prompts() {
for p in ["Login: ", "Username:", "Enter your user name or 'NEW':", "Handle? ", "NN: ", "Alias>", "logon:"] {
assert!(is_user_prompt(p), "{p}");
}
for p in ["Password: ", "PW: ", "Enter your password:", "passwd:"] {
assert!(is_password_prompt(p), "{p}");
assert!(!is_user_prompt(p), "{p}");
}
for p in ["Press any key", "Login", "Your real name:", "First name?", "Choose a password for your new user:"] {
assert!(!is_user_prompt(p), "{p}");
}
assert!(!is_password_prompt("Choose a password for your new user name:"));
}
#[test]
fn answers_in_order_once() {
let mut a = login("sysop", "secret");
assert_eq!(a.answer("Password:"), None);
assert_eq!(a.answer("Login: "), Some(Step::User("sysop".into())));
assert_eq!(a.answer("Login: "), None);
assert_eq!(a.answer("Password: "), Some(Step::Password("secret".into())));
assert_eq!(a.answer("Password: "), None);
assert_eq!(a.next_step(), None);
}
#[test]
fn manual_steps_and_settings() {
let mut a = login("sysop", "secret");
assert_eq!(a.next_step(), Some(Step::User("sysop".into())));
assert_eq!(a.next_step(), Some(Step::Password("secret".into())));
assert_eq!(a.next_step(), None);
let mut a = AutoLogin::new(&Entry { username: "u".into(), ..Entry::default() });
assert_eq!(a.answer("Login:"), None);
let off = Entry { username: "u".into(), auto_login: false, ..Entry::default() };
let mut a = AutoLogin::new(&off);
a.connected();
assert_eq!(a.answer("Login:"), None);
let ssh = Entry { username: "u".into(), protocol: Protocol::Ssh, ..Entry::default() };
let mut a = AutoLogin::new(&ssh);
a.connected();
assert_eq!(a.answer("Login:"), None);
assert!(a.has_login());
}
}