refactor: split the session manager into modules
sessions.rs (1 730 lines) becomes sessions/ with mod.rs (types, session lifecycle, I/O, recording, tests), credentials.rs (secret resolution, prompts, host-key checks), protocols.rs (SSH, GitHub, IRC, VNC, RDP connects and SSH tunnels), local.rs (local shells, login scripts) and extras.rs (per-protocol accessors). Code moves unchanged; helpers that crossed a module boundary became pub(super).
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
//! Per-protocol connect: SSH, GitHub, IRC, VNC and RDP, and the SSH tunnels they can use.
|
||||
|
||||
use super::*;
|
||||
|
||||
impl Manager {
|
||||
// --- per-protocol connect -----------------------------------------------------------
|
||||
|
||||
pub(super) fn connect_ssh(self: &Arc<Self>, id: &str, epoch: u64, p: &Profile, prompt: Prompt) -> Result<Client> {
|
||||
let mut p = p.clone();
|
||||
let sc = p.ssh.get_or_insert_with(SshConfig::defaults).clone();
|
||||
let password = self.secret_fn(id, &p.id, SecretKind::Password, sc.secret_mode, prompt.clone());
|
||||
let passphrase = self.secret_fn(id, &p.id, SecretKind::Passphrase, sc.key_secret_mode, prompt);
|
||||
let c = SshClient::new(&p, self.terminal_callbacks(id, epoch));
|
||||
let stored = |k: SecretKind| self.secrets.get(&k.key(&p.id)).unwrap_or_default();
|
||||
c.set_stored_secrets(stored(SecretKind::Totp), stored(SecretKind::ProxyPassword));
|
||||
c.connect(self.host_key_check(id, sc.host_key_policy), Some(password), Some(passphrase))?;
|
||||
for pf in sc.port_forwards.iter().filter(|f| f.enabled) {
|
||||
match c.start_forward(pf.clone()) {
|
||||
Ok(()) => self.emit(Event::ForwardStarted { session: id.into(), forward: pf.clone() }),
|
||||
Err(e) => self.emit(Event::ForwardError { session: id.into(), forward_id: pf.id.clone(), label: pf.label.clone(), error: e.to_string() }),
|
||||
}
|
||||
}
|
||||
self.start_login_scripts(id, epoch, sc.login_scripts.clone());
|
||||
Ok(Client::Ssh(c))
|
||||
}
|
||||
|
||||
/// An SSH connection used only to tunnel another protocol (IRC, VNC).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn tunnel(
|
||||
self: &Arc<Self>,
|
||||
id: &str,
|
||||
profile_id: &str,
|
||||
host: &str,
|
||||
port: i64,
|
||||
user: &str,
|
||||
method: &str,
|
||||
key: &str,
|
||||
agent: bool,
|
||||
mode: SecretMode,
|
||||
kind: &str,
|
||||
prompt: Prompt,
|
||||
) -> Result<Arc<SshClient>> {
|
||||
let mut tp = Profile::new(Protocol::Ssh);
|
||||
tp.host = host.into();
|
||||
tp.port = if port == 0 { 22 } else { port };
|
||||
tp.username = user.into();
|
||||
let c = tp.ssh.as_mut().unwrap();
|
||||
c.auth_methods = vec![if method.is_empty() { "publickey".into() } else { method.into() }];
|
||||
c.private_key_path = key.into();
|
||||
c.agent_auth = agent;
|
||||
let (pw_kind, pp_kind) = match kind {
|
||||
"vnc" => (SecretKind::VncSshPassword, SecretKind::VncSshPassphrase),
|
||||
"rdp" => (SecretKind::RdpSshPassword, SecretKind::RdpSshPassphrase),
|
||||
_ => (SecretKind::IrcSshPassword, SecretKind::IrcSshPassphrase),
|
||||
};
|
||||
let password = (method == "password").then(|| self.secret_fn(id, profile_id, pw_kind, mode, prompt.clone()));
|
||||
let passphrase = self.secret_fn(id, profile_id, pp_kind, mode, prompt);
|
||||
let t = SshClient::new(&tp, Callbacks::noop());
|
||||
t.connect_tunnel_only(self.host_key_check(id, HostKeyPolicy::Ask), password, Some(passphrase))?;
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
pub(super) fn connect_github(self: &Arc<Self>, id: &str, epoch: u64, p: &Profile, prompt: Prompt) -> Result<Client> {
|
||||
let gh = p.github.clone().ok_or_else(|| format!("GitHub configuration missing on profile {:?}", p.id))?;
|
||||
let token = self.secret_fn(id, &p.id, SecretKind::GithubToken, gh.secret_mode, prompt.clone())().map_err(|e| format!("obtaining GitHub token: {e}"))?;
|
||||
if token.is_empty() {
|
||||
return err("GitHub token cannot be empty".into());
|
||||
}
|
||||
let mut client = github::Client::new(&gh.base_url, &token);
|
||||
let login = match client.validate_token() {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
let msg = e.to_string().to_lowercase();
|
||||
let auth = ["token validation failed", "authentication failed", "http 401", "http 403"].iter().any(|k| msg.contains(k));
|
||||
let retry = auth.then(|| prompt("GitHub token did not work. Please enter a valid token:").ok()).flatten().filter(|t| !t.is_empty());
|
||||
let Some(t) = retry else { return err(format!("GitHub authentication failed: {e}")) };
|
||||
if gh.secret_mode == SecretMode::Saved {
|
||||
let _ = self.with(id, |s| s.session_secrets.lock().pending.insert(SecretKind::GithubToken.key(&p.id), Zeroizing::new(t.clone())));
|
||||
}
|
||||
client = github::Client::new(&gh.base_url, &t);
|
||||
client.validate_token().map_err(|e| format!("GitHub authentication failed: {e}"))?
|
||||
}
|
||||
};
|
||||
let client = Arc::new(client);
|
||||
if !self.is_current(id, epoch) {
|
||||
return err(format!("session {id:?} was closed while connecting"));
|
||||
}
|
||||
let fetch = {
|
||||
let (me, sid, client) = (self.clone(), id.to_string(), client.clone());
|
||||
move |status: &str| {
|
||||
me.emit(Event::GithubStatus { session: sid.clone(), status: status.into() });
|
||||
let (me2, sid2) = (me.clone(), sid.clone());
|
||||
client.fetch_repos_with_progress(move |done, total| {
|
||||
if total > 0 {
|
||||
me2.emit(Event::GithubStatus { session: sid2.clone(), status: format!("Loading commit info ({done}/{total})") });
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
let repos = fetch("Loading repositories...").map_err(|e| format!("fetching repositories: {e}"))?;
|
||||
self.emit(Event::GithubRepos { session: id.into(), login: login.clone(), repos });
|
||||
let interval = Duration::from_secs(if gh.refresh_interval_secs < 60 { 300 } else { gh.refresh_interval_secs as u64 });
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let (me, sid, stop2) = (self.clone(), id.to_string(), stop.clone());
|
||||
std::thread::spawn(move || {
|
||||
loop {
|
||||
let deadline = std::time::Instant::now() + interval;
|
||||
while std::time::Instant::now() < deadline {
|
||||
if stop2.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
match fetch("Refreshing repositories...") {
|
||||
Ok(repos) => me.emit(Event::GithubRepos { session: sid.clone(), login: login.clone(), repos }),
|
||||
Err(e) => me.emit(Event::GithubError { session: sid.clone(), error: e.to_string() }),
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(Client::Github(client, stop))
|
||||
}
|
||||
|
||||
pub(super) fn connect_irc(self: &Arc<Self>, id: &str, epoch: u64, p: &Profile, prompt: Prompt) -> Result<Client> {
|
||||
let ic = p.irc.clone().ok_or("IRC configuration missing")?;
|
||||
let server_pass = if ic.server_password_required {
|
||||
self.secret_fn(id, &p.id, SecretKind::IrcServerPassword, ic.server_password_mode, prompt.clone())()
|
||||
.map_err(|e| format!("IRC server password: {e}"))?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let ns_pass = if ic.nick_serv_identify {
|
||||
self.secret_fn(id, &p.id, SecretKind::IrcNickServPassword, ic.nick_serv_mode, prompt.clone())().map_err(|e| format!("NickServ password: {e}"))?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let mut rooms = ic.auto_join_rooms.clone();
|
||||
if !p.id.is_empty() {
|
||||
let last = self.store.irc_last_rooms(&p.id).map_err(|e| format!("loading remembered IRC rooms: {e}"))?;
|
||||
rooms = crate::storage::dedup_rooms(&[&rooms, &last]);
|
||||
}
|
||||
let tunnel = if ic.ssh_tunnel {
|
||||
Some(self.tunnel(
|
||||
id,
|
||||
&p.id,
|
||||
&ic.ssh_host,
|
||||
ic.ssh_port,
|
||||
&ic.ssh_username,
|
||||
&ic.ssh_auth_method,
|
||||
&ic.ssh_private_key,
|
||||
ic.ssh_agent_auth,
|
||||
ic.ssh_secret_mode,
|
||||
"irc",
|
||||
prompt,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let cb = self.terminal_callbacks(id, epoch);
|
||||
let (me, sid) = (self.clone(), id.to_string());
|
||||
let on_event: irc::EventFn = Arc::new(move |e| me.emit(Event::Irc { session: sid.clone(), event: e }));
|
||||
let (me, sid, pid) = (self.clone(), id.to_string(), p.id.clone());
|
||||
let on_rooms: irc::RoomsChangedFn = Arc::new(move |rooms: Vec<String>| {
|
||||
if pid.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Err(e) = me.store.save_irc_last_rooms(&pid, &rooms) {
|
||||
me.emit(Event::Irc {
|
||||
session: sid.clone(),
|
||||
event: irc::Event {
|
||||
kind: "system".into(),
|
||||
target: irc::SERVER_TARGET.into(),
|
||||
text: format!("Could not remember open IRC rooms: {e}"),
|
||||
timestamp: chrono::Utc::now().timestamp_millis(),
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
let connector: Option<irc::Connector> = tunnel.clone().map(|t| {
|
||||
let (host, port) = (p.host.clone(), p.port as u16);
|
||||
Arc::new(move || {
|
||||
let s = t.open_tunnel(&host, port)?;
|
||||
s.set_read_timeout(Some(Duration::from_millis(50)))?;
|
||||
Ok(Box::new(s) as Box<dyn irc::ReadWrite>)
|
||||
}) as irc::Connector
|
||||
});
|
||||
let cfg = irc::Config {
|
||||
server: p.host.clone(),
|
||||
port: p.port as u16,
|
||||
nick: ic.nick.clone(),
|
||||
user: if p.username.is_empty() { ic.nick.clone() } else { p.username.clone() },
|
||||
real_name: ic.real_name.clone(),
|
||||
use_tls: ic.use_tls,
|
||||
server_pass,
|
||||
auto_join_rooms: rooms,
|
||||
nickserv_identify: ic.nick_serv_identify,
|
||||
nickserv_account: ic.nick_serv_account.clone(),
|
||||
nickserv_password: ns_pass,
|
||||
connector,
|
||||
connect_timeout: Duration::from_secs(p.connect_timeout.max(1) as u64),
|
||||
on_event,
|
||||
on_error: cb.on_error,
|
||||
on_close: cb.on_close,
|
||||
on_rooms_changed: on_rooms,
|
||||
};
|
||||
let c = irc::Client::new(cfg);
|
||||
if let Err(e) = c.connect() {
|
||||
if let Some(t) = tunnel {
|
||||
t.close();
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
Ok(Client::Irc(c, tunnel))
|
||||
}
|
||||
|
||||
pub(super) fn connect_vnc(self: &Arc<Self>, id: &str, epoch: u64, p: &Profile, prompt: Prompt) -> Result<Client> {
|
||||
let vc = p.vnc.clone().ok_or("VNC configuration missing")?;
|
||||
let host = if vc.vnc_host.is_empty() { p.host.clone() } else { vc.vnc_host.clone() };
|
||||
let port = if vc.vnc_port == 0 { 5900 } else { vc.vnc_port } as u16;
|
||||
let tunnel = if vc.ssh_tunnel {
|
||||
Some(self.tunnel(
|
||||
id,
|
||||
&p.id,
|
||||
&vc.ssh_host,
|
||||
vc.ssh_port,
|
||||
&vc.ssh_username,
|
||||
&vc.ssh_auth_method,
|
||||
&vc.ssh_private_key,
|
||||
vc.ssh_agent_auth,
|
||||
vc.ssh_secret_mode,
|
||||
"vnc",
|
||||
prompt.clone(),
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let password = self.secret_fn(id, &p.id, SecretKind::VncPassword, vc.secret_mode, prompt);
|
||||
let cb = self.terminal_callbacks(id, epoch);
|
||||
let (me, sid) = (self.clone(), id.to_string());
|
||||
let (me2, sid2) = (self.clone(), id.to_string());
|
||||
let clip: clients::vnc::ClipboardFn = Arc::new(move |text| me2.emit(Event::VncClipboard { session: sid2.clone(), text }));
|
||||
let c = connect_vnc_client(
|
||||
&host,
|
||||
port,
|
||||
&vc,
|
||||
p.connect_timeout,
|
||||
password,
|
||||
tunnel.clone(),
|
||||
cb,
|
||||
Arc::new(move || me.emit(Event::DesktopFrame { session: sid.clone() })),
|
||||
clip,
|
||||
);
|
||||
match c {
|
||||
Ok(c) => {
|
||||
self.emit(Event::DesktopReady { session: id.into() });
|
||||
Ok(Client::Vnc(c, tunnel))
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(t) = tunnel {
|
||||
t.close();
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn connect_rdp(self: &Arc<Self>, id: &str, epoch: u64, p: &Profile, prompt: Prompt) -> Result<Client> {
|
||||
let rc = p.rdp.clone().unwrap_or_else(RdpConfig::defaults);
|
||||
let port = if p.port == 0 { DEFAULT_RDP_PORT } else { p.port as u16 };
|
||||
let tunnel = if rc.ssh_tunnel {
|
||||
Some(self.tunnel(
|
||||
id,
|
||||
&p.id,
|
||||
&rc.ssh_host,
|
||||
rc.ssh_port,
|
||||
&rc.ssh_username,
|
||||
&rc.ssh_auth_method,
|
||||
&rc.ssh_private_key,
|
||||
rc.ssh_agent_auth,
|
||||
rc.ssh_secret_mode,
|
||||
"rdp",
|
||||
prompt.clone(),
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let r = self.rdp_client(id, epoch, p, &rc, port, tunnel.clone(), prompt);
|
||||
match r {
|
||||
Ok(c) => {
|
||||
self.emit(Event::DesktopReady { session: id.into() });
|
||||
Ok(Client::Rdp(c, tunnel))
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(t) = tunnel {
|
||||
t.close();
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn rdp_client(
|
||||
self: &Arc<Self>,
|
||||
id: &str,
|
||||
epoch: u64,
|
||||
p: &Profile,
|
||||
rc: &RdpConfig,
|
||||
port: u16,
|
||||
tunnel: Option<Arc<SshClient>>,
|
||||
prompt: Prompt,
|
||||
) -> Result<Arc<clients::rdp::Client>> {
|
||||
let timeout = Duration::from_secs(p.connect_timeout.max(1) as u64);
|
||||
let host = p.host.clone();
|
||||
let dial: clients::rdp::Dial = match tunnel {
|
||||
Some(t) => {
|
||||
let h = host.clone();
|
||||
Arc::new(move || t.open_tunnel(&h, port))
|
||||
}
|
||||
None => {
|
||||
let proxy = clients::proxy::Proxy {
|
||||
kind: rc.proxy_type.clone(),
|
||||
host: rc.proxy_host.clone(),
|
||||
port: rc.proxy_port as u16,
|
||||
username: rc.proxy_username.clone(),
|
||||
password: self.secrets.get(&SecretKind::ProxyPassword.key(&p.id)).unwrap_or_default(),
|
||||
};
|
||||
let h = host.clone();
|
||||
Arc::new(move || proxy.connect(&h, port, timeout))
|
||||
}
|
||||
};
|
||||
let check = self.host_key_check(id, rc.host_key_policy);
|
||||
let h = host.clone();
|
||||
let check_cert: clients::rdp::CertCheck = Arc::new(move |fp: &str| {
|
||||
check(&HostKeyInfo { host: h.clone(), port, key_type: "rdp-tls".into(), fingerprint: fp.into(), public_key_b64: String::new() })
|
||||
});
|
||||
// A 0×0 configured size follows the tab, which reports its pixel size like a terminal.
|
||||
let (width, height) = match (rc.width, rc.height) {
|
||||
(w, h) if w > 0 && h > 0 => (w as u16, h as u16),
|
||||
_ => self.with(id, |s| s.pending_resize).ok().flatten().unwrap_or((1280, 800)),
|
||||
};
|
||||
let password = self.secret_fn(id, &p.id, SecretKind::RdpPassword, rc.secret_mode, prompt)()?;
|
||||
let cb = self.terminal_callbacks(id, epoch);
|
||||
let (me, sid) = (self.clone(), id.to_string());
|
||||
let c = clients::rdp::Client::new(clients::rdp::Config {
|
||||
host,
|
||||
username: p.username.clone(),
|
||||
domain: rc.domain.clone(),
|
||||
password,
|
||||
width,
|
||||
height,
|
||||
allow_tls_only: rc.allow_tls_only,
|
||||
dial,
|
||||
check_cert,
|
||||
connect_timeout: timeout,
|
||||
on_frame: Arc::new(move || me.emit(Event::DesktopFrame { session: sid.clone() })),
|
||||
on_error: cb.on_error,
|
||||
on_close: cb.on_close,
|
||||
});
|
||||
c.connect()?;
|
||||
Ok(c)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds and connects a VNC client from a profile's settings, optionally through an SSH tunnel.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn connect_vnc_client(
|
||||
host: &str,
|
||||
port: u16,
|
||||
vc: &VncConfig,
|
||||
timeout: i64,
|
||||
password: SecretFn,
|
||||
tunnel: Option<Arc<SshClient>>,
|
||||
cb: Callbacks,
|
||||
on_frame: Arc<dyn Fn() + Send + Sync>,
|
||||
on_clipboard: clients::vnc::ClipboardFn,
|
||||
) -> Result<Arc<clients::vnc::Client>> {
|
||||
let connector: Option<irc::Connector> = tunnel.map(|t| {
|
||||
let host = host.to_string();
|
||||
Arc::new(move || {
|
||||
let s = t.open_tunnel(&host, port)?;
|
||||
s.set_read_timeout(Some(Duration::from_millis(50)))?;
|
||||
Ok(Box::new(s) as Box<dyn irc::ReadWrite>)
|
||||
}) as irc::Connector
|
||||
});
|
||||
let mut cfg = clients::vnc::Config {
|
||||
host: host.into(),
|
||||
port,
|
||||
password: None,
|
||||
shared: vc.shared_session,
|
||||
view_only: vc.view_only,
|
||||
quality: vc.quality.clamp(0, 9) as u8,
|
||||
connector,
|
||||
connect_timeout: Duration::from_secs(timeout.max(1) as u64),
|
||||
on_frame,
|
||||
on_clipboard,
|
||||
on_error: cb.on_error,
|
||||
on_close: cb.on_close,
|
||||
..Default::default()
|
||||
};
|
||||
let c = clients::vnc::Client::new(cfg.clone());
|
||||
match c.connect() {
|
||||
Ok(()) => Ok(c),
|
||||
// Ask for the password only when the server wants one.
|
||||
Err(e) if e.to_string().to_lowercase().contains("password") => {
|
||||
cfg.password = Some(password()?);
|
||||
let c = clients::vnc::Client::new(cfg);
|
||||
c.connect()?;
|
||||
Ok(c)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user