badterm_core/
autologin.rs1use std::time::{Duration, Instant};
13
14use crate::dialer::{Entry, Protocol};
15
16const WINDOW: Duration = Duration::from_secs(120);
18
19#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum Step {
22 User(String),
23 Password(String),
24}
25
26#[derive(Debug)]
27pub struct AutoLogin {
28 user: String,
29 password: String,
30 automatic: bool,
32 sent_user: bool,
33 sent_password: bool,
34 until: Option<Instant>,
35}
36
37impl AutoLogin {
38 pub fn new(entry: &Entry) -> AutoLogin {
39 AutoLogin {
40 user: entry.username.clone(),
41 password: entry.password.clone(),
42 automatic: entry.auto_login && (entry.protocol.is_telnet() || entry.protocol == Protocol::Raw),
44 sent_user: false,
45 sent_password: false,
46 until: None,
47 }
48 }
49
50 pub fn connected(&mut self) {
52 self.until = Some(Instant::now() + WINDOW);
53 }
54
55 pub fn answer(&mut self, prompt: &str) -> Option<Step> {
57 if !self.automatic || self.until.is_none_or(|t| Instant::now() > t) {
58 return None;
59 }
60 if !self.sent_user && !self.user.is_empty() && is_user_prompt(prompt) {
61 return self.next_step();
62 }
63 if self.sent_user && !self.sent_password && !self.password.is_empty() && is_password_prompt(prompt) {
64 return self.next_step();
65 }
66 None
67 }
68
69 pub fn next_step(&mut self) -> Option<Step> {
72 if !self.sent_user && !self.user.is_empty() {
73 self.sent_user = true;
74 return Some(Step::User(self.user.clone()));
75 }
76 self.sent_user = true;
77 if !self.sent_password && !self.password.is_empty() {
78 self.sent_password = true;
79 return Some(Step::Password(self.password.clone()));
80 }
81 None
82 }
83
84 pub fn has_login(&self) -> bool {
86 !self.user.is_empty() || !self.password.is_empty()
87 }
88}
89
90fn prompt_text(prompt: &str) -> Option<String> {
93 let p = prompt.trim_end();
94 let p = p.strip_suffix([':', '?', '>'])?;
95 Some(p.trim().to_ascii_lowercase())
96}
97
98fn is_user_prompt(prompt: &str) -> bool {
99 let Some(p) = prompt_text(prompt) else { return false };
100 if p.contains("password") || p.contains("pass word") {
101 return false;
102 }
103 let words = ["login", "user", "username", "name", "handle", "alias", "nn", "logon", "callsign"];
106 let last: Vec<&str> = p.split(|c: char| !c.is_ascii_alphanumeric()).filter(|w| !w.is_empty()).collect();
107 let tail = &last[last.len().saturating_sub(4)..];
108 tail.iter().any(|w| words.contains(w)) && !tail.contains(&"real") && !tail.contains(&"first")
109}
110
111fn is_password_prompt(prompt: &str) -> bool {
112 let Some(p) = prompt_text(prompt) else { return false };
113 p.ends_with("password") || p.ends_with("pw") || p.ends_with("passwd") || p.ends_with("pass")
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 fn login(user: &str, password: &str) -> AutoLogin {
121 let mut a = AutoLogin::new(&Entry { username: user.into(), password: password.into(), ..Entry::default() });
122 a.connected();
123 a
124 }
125
126 #[test]
127 fn prompts() {
128 for p in ["Login: ", "Username:", "Enter your user name or 'NEW':", "Handle? ", "NN: ", "Alias>", "logon:"] {
129 assert!(is_user_prompt(p), "{p}");
130 }
131 for p in ["Password: ", "PW: ", "Enter your password:", "passwd:"] {
132 assert!(is_password_prompt(p), "{p}");
133 assert!(!is_user_prompt(p), "{p}");
134 }
135 for p in ["Press any key", "Login", "Your real name:", "First name?", "Choose a password for your new user:"] {
136 assert!(!is_user_prompt(p), "{p}");
137 }
138 assert!(!is_password_prompt("Choose a password for your new user name:"));
139 }
140
141 #[test]
142 fn answers_in_order_once() {
143 let mut a = login("sysop", "secret");
144 assert_eq!(a.answer("Password:"), None);
146 assert_eq!(a.answer("Login: "), Some(Step::User("sysop".into())));
147 assert_eq!(a.answer("Login: "), None);
148 assert_eq!(a.answer("Password: "), Some(Step::Password("secret".into())));
149 assert_eq!(a.answer("Password: "), None);
150 assert_eq!(a.next_step(), None);
151 }
152
153 #[test]
154 fn manual_steps_and_settings() {
155 let mut a = login("sysop", "secret");
156 assert_eq!(a.next_step(), Some(Step::User("sysop".into())));
157 assert_eq!(a.next_step(), Some(Step::Password("secret".into())));
158 assert_eq!(a.next_step(), None);
159
160 let mut a = AutoLogin::new(&Entry { username: "u".into(), ..Entry::default() });
162 assert_eq!(a.answer("Login:"), None);
163 let off = Entry { username: "u".into(), auto_login: false, ..Entry::default() };
164 let mut a = AutoLogin::new(&off);
165 a.connected();
166 assert_eq!(a.answer("Login:"), None);
167 let ssh = Entry { username: "u".into(), protocol: Protocol::Ssh, ..Entry::default() };
168 let mut a = AutoLogin::new(&ssh);
169 a.connected();
170 assert_eq!(a.answer("Login:"), None);
171 assert!(a.has_login());
172 }
173}