Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 145 additions & 1 deletion src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::connection_manager::ConnectionManager;
use crate::ftp_client::FtpConfig;
use crate::os_detect::{self, OsInfo};
use crate::proxy::{ProxyConfig, ProxyType};
use crate::sftp_client::{FileEntry, FileEntryType, SftpAuthMethod, SftpConfig};
use crate::ssh::{AuthMethod, SshConfig};
use base64::Engine as _;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tauri::State;
Expand All @@ -18,6 +18,18 @@ pub struct ConnectRequest {
pub password: Option<String>,
pub key_path: Option<String>,
pub passphrase: Option<String>,
/// Advanced SSH options — `Option` so legacy callers that omit them keep
/// the previous defaults (compression on, keepalive 60 s / 3).
pub compression: Option<bool>,
pub keepalive_enabled: Option<bool>,
pub keepalive_interval: Option<u64>,
pub keepalive_max: Option<u32>,
/// Proxy options — ignored when `proxy_type` is "none" or missing.
pub proxy_type: Option<String>,
pub proxy_host: Option<String>,
pub proxy_port: Option<u16>,
pub proxy_username: Option<String>,
pub proxy_password: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -58,6 +70,21 @@ pub async fn ssh_connect(
request: ConnectRequest,
state: State<'_, Arc<ConnectionManager>>,
) -> Result<CommandResponse, String> {
let proxy = build_proxy(&request)?;

// Keepalive defaults match the connection dialog UI: enabled at 60 s / 3.
let keepalive_enabled = request.keepalive_enabled.unwrap_or(true);
let keepalive_interval = if keepalive_enabled {
Some(request.keepalive_interval.unwrap_or(60))
} else {
None
};
let keepalive_max = if keepalive_enabled {
Some(request.keepalive_max.unwrap_or(3))
} else {
None
};

let auth_method = match request.auth_method.as_str() {
"password" => AuthMethod::Password {
password: request.password.ok_or("Password required")?,
Expand All @@ -74,6 +101,10 @@ pub async fn ssh_connect(
port: request.port,
username: request.username,
auth_method,
compression: request.compression.unwrap_or(true),
keepalive_interval,
keepalive_max,
proxy,
};

match state
Expand All @@ -93,6 +124,34 @@ pub async fn ssh_connect(
}
}

/// Map the proxy request fields into a `ProxyConfig`, or `None` when the
/// connection should go direct.
fn build_proxy(request: &ConnectRequest) -> Result<Option<ProxyConfig>, String> {
match request.proxy_type.as_deref() {
None | Some("none") | Some("") => Ok(None),
Some(kind) => {
let host = request
.proxy_host
.clone()
.filter(|h| !h.trim().is_empty())
.ok_or("Proxy host is required")?;
let proxy_type = match kind {
"http" => ProxyType::Http,
"socks4" => ProxyType::Socks4,
"socks5" => ProxyType::Socks5,
other => return Err(format!("Invalid proxy type: {other}")),
};
Ok(Some(ProxyConfig {
proxy_type,
host,
port: request.proxy_port.unwrap_or(8080),
username: request.proxy_username.clone(),
password: request.proxy_password.clone(),
}))
}
}
}

#[tauri::command]
pub async fn ssh_cancel_connect(
connection_id: String,
Expand Down Expand Up @@ -3327,3 +3386,88 @@ mod local_fs_tests {
assert_eq!(s, "2024-01-01 00:00:00");
}
}

#[cfg(test)]
mod proxy_config_tests {
use super::*;

fn request(proxy_type: Option<&str>) -> ConnectRequest {
ConnectRequest {
connection_id: "c1".to_string(),
host: "example.com".to_string(),
port: 22,
username: "root".to_string(),
auth_method: "password".to_string(),
password: Some("pw".to_string()),
key_path: None,
passphrase: None,
compression: None,
keepalive_enabled: None,
keepalive_interval: None,
keepalive_max: None,
proxy_type: proxy_type.map(|s| s.to_string()),
proxy_host: None,
proxy_port: None,
proxy_username: None,
proxy_password: None,
}
}

#[test]
fn no_proxy_when_type_none_or_none_type() {
assert!(build_proxy(&request(None)).unwrap().is_none());
assert!(build_proxy(&request(Some("none"))).unwrap().is_none());
assert!(build_proxy(&request(Some(""))).unwrap().is_none());
}

/// Request with a proxy host set, so type/host validation reaches the type.
fn request_with_host(proxy_type: &str) -> ConnectRequest {
let mut req = request(Some(proxy_type));
req.proxy_host = Some("proxy.local".to_string());
req
}

#[test]
fn maps_supported_proxy_types() {
let http = build_proxy(&request_with_host("http")).unwrap().unwrap();
assert_eq!(http.proxy_type, ProxyType::Http);
let socks5 = build_proxy(&request_with_host("socks5")).unwrap().unwrap();
assert_eq!(socks5.proxy_type, ProxyType::Socks5);
let socks4 = build_proxy(&request_with_host("socks4")).unwrap().unwrap();
assert_eq!(socks4.proxy_type, ProxyType::Socks4);
}

#[test]
fn requires_proxy_host() {
let err = build_proxy(&request(Some("http"))).unwrap_err();
assert!(err.contains("Proxy host is required"));
}

#[test]
fn rejects_unknown_proxy_type() {
let err = build_proxy(&request_with_host("ftp")).unwrap_err();
assert!(err.contains("Invalid proxy type"));
}

#[test]
fn carries_proxy_credentials_and_port() {
let mut req = request(Some("socks5"));
req.proxy_host = Some("proxy.local".to_string());
req.proxy_port = Some(1080);
req.proxy_username = Some("user".to_string());
req.proxy_password = Some("pass".to_string());
let cfg = build_proxy(&req).unwrap().unwrap();
assert_eq!(cfg.host, "proxy.local");
assert_eq!(cfg.port, 1080);
assert_eq!(cfg.username.as_deref(), Some("user"));
assert_eq!(cfg.password.as_deref(), Some("pass"));
}

#[test]
fn defaults_proxy_port_to_8080() {
let mut req = request(Some("http"));
req.proxy_host = Some("proxy.local".to_string());
let cfg = build_proxy(&req).unwrap().unwrap();
assert_eq!(cfg.port, 8080);
}
}
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod desktop_protocol;
mod ftp_client;
mod ls_parser;
mod os_detect;
mod proxy;
mod rdp_client;
mod sftp_client;
mod ssh;
Expand Down
Loading
Loading