feat: add global proxy settings and startup env reader (resolves #53)

This commit is contained in:
TomZz
2026-06-25 03:20:33 +08:00
parent bfc7c9ac83
commit f6d9cd00e7
11 changed files with 627 additions and 3 deletions
Generated
+1
View File
@@ -355,6 +355,7 @@ dependencies = [
"thiserror 1.0.69",
"time",
"tokio",
"tokio-socks",
"tracing",
"tracing-appender",
"tracing-subscriber",
+1
View File
@@ -9,6 +9,7 @@ rust-version = "1.85.0"
[dependencies]
anyhow = "1"
tokio-socks = "0.5"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "fcf32feacb367b75ec84dd40f041e4fd411d3cc1" }
async-trait = "0.1"
chrono = { version = "0.4", features = ["serde"] }
+18
View File
@@ -62,6 +62,12 @@ add_ssh: "+ ssh"
open_local_or_ssh: "Open a local terminal or pick an SSH session."
open_session: "Open Session"
settings: "Settings"
proxy: "Proxy"
proxy_none: "Follow Global"
proxy_host: "Proxy Host"
proxy_port: "Proxy Port"
proxy_user: "Proxy Username (optional)"
proxy_password: "Proxy Password (optional)"
ui_font_size: "UI Font Size"
terminal_font_size: "Terminal Font Size"
system_default: "System Default"
@@ -191,3 +197,15 @@ sync_upload_complete: "Configuration uploaded"
sync_download_complete: "Configuration downloaded"
sync_failed: "Synchronization failed"
keyboard_interactive: "Keyboard Interactive"
settings_proxy: "Proxy"
enable_proxy: "Enable Proxy"
read_env_proxy: "Read environment variables on startup"
read_env_proxy_desc: "If enabled, the global proxy settings below will be overridden by environment variables on startup."
global_proxy_settings: "Global Proxy Configuration"
global_proxy_type: "Proxy Type"
global_proxy_host: "Proxy Host"
global_proxy_port: "Proxy Port"
global_proxy_user: "Proxy Username"
global_proxy_password: "Proxy Password"
save_proxy: "Save Proxy"
+18
View File
@@ -63,6 +63,12 @@ add_ssh: "+ SSH"
open_local_or_ssh: "打开本地终端或选择一个 SSH 会话。"
open_session: "打开会话"
settings: "设置"
proxy: "代理"
proxy_none: "跟随全局"
proxy_host: "代理服务器地址"
proxy_port: "端口"
proxy_user: "用户名 (可选)"
proxy_password: "密码 (可选)"
ui_font_size: "界面字体大小"
terminal_font_size: "终端字体大小"
system_default: "系统默认"
@@ -193,3 +199,15 @@ sync_upload_complete: "配置上传完成"
sync_download_complete: "配置下载完成"
sync_failed: "同步失败"
keyboard_interactive: "键盘交互"
settings_proxy: "代理设置"
enable_proxy: "启用代理功能"
read_env_proxy: "启动时读取环境变量"
read_env_proxy_desc: "若开启此项,启动时将读取环境变量,下方配置的全局代理设置将失效。"
global_proxy_settings: "全局代理配置"
global_proxy_type: "代理类型"
global_proxy_host: "代理地址"
global_proxy_port: "代理端口"
global_proxy_user: "代理用户名"
global_proxy_password: "代理密码"
save_proxy: "保存代理设置"
+177
View File
@@ -36,6 +36,10 @@ impl Ashell {
let key_path_input = self.key_path_input.clone();
let key_inline_input = self.key_inline_input.clone();
let passphrase_input = self.passphrase_input.clone();
let proxy_host_input = self.proxy_host_input.clone();
let proxy_port_input = self.proxy_port_input.clone();
let proxy_user_input = self.proxy_user_input.clone();
let proxy_password_input = self.proxy_password_input.clone();
window.open_dialog(cx, move |dialog: Dialog, _window, _cx| {
dialog
@@ -61,12 +65,18 @@ impl Ashell {
let key_path_input = key_path_input.clone();
let key_inline_input = key_inline_input.clone();
let passphrase_input = passphrase_input.clone();
let proxy_host_input = proxy_host_input.clone();
let proxy_port_input = proxy_port_input.clone();
let proxy_user_input = proxy_user_input.clone();
let proxy_password_input = proxy_password_input.clone();
move |content, window, cx| {
let method = view.read(cx).ssh_auth_method;
let is_password = method == AuthMethod::Password;
let is_key = method == AuthMethod::Key;
let is_kb = method == AuthMethod::KeyboardInteractive;
let is_editing = view.read(cx).editing_session_id.is_some();
let proxy_type = view.read(cx).ssh_proxy_type.clone();
let show_proxy_fields = proxy_type != "none";
content.child(
v_flex()
.gap_3()
@@ -170,6 +180,60 @@ impl Ashell {
.child(Input::new(&key_inline_input).h(px(128.)).tab_index(5))
.child(Input::new(&passphrase_input).mask_toggle().tab_index(6))
})
.child(
div().text_sm().font_weight(FontWeight::BOLD).child(t!("proxy").to_string())
)
.child(
h_flex()
.gap_2()
.child(
Button::new("proxy-none")
.label(t!("proxy_none").to_string())
.when(proxy_type == "none", |button| button.primary())
.on_click(window.listener_for(
&view,
|this, _, _, cx| {
this.set_ssh_proxy_type("none".to_string(), cx)
},
)),
)
.child(
Button::new("proxy-socks5")
.label("SOCKS5")
.when(proxy_type == "socks5", |button| button.primary())
.on_click(window.listener_for(
&view,
|this, _, _, cx| {
this.set_ssh_proxy_type("socks5".to_string(), cx)
},
)),
)
.child(
Button::new("proxy-http")
.label("HTTP")
.when(proxy_type == "http", |button| button.primary())
.on_click(window.listener_for(
&view,
|this, _, _, cx| {
this.set_ssh_proxy_type("http".to_string(), cx)
},
)),
)
)
.when(show_proxy_fields, |this| {
this.child(
h_flex()
.gap_2()
.child(Input::new(&proxy_host_input).flex_1())
.child(Input::new(&proxy_port_input).w(px(96.)))
)
.child(
h_flex()
.gap_2()
.child(Input::new(&proxy_user_input).flex_1())
.child(Input::new(&proxy_password_input).flex_1())
)
})
.child(
h_flex()
.justify_end()
@@ -1817,6 +1881,119 @@ impl Ashell {
}))
)
)
.page(
SettingPage::new(t!("settings_proxy").to_string())
.icon(IconName::Network)
.group(
SettingGroup::new()
.title(t!("settings_proxy").to_string())
.item(
SettingItem::new(
t!("enable_proxy").to_string(),
SettingField::render({
let view = view.clone();
move |_, window, cx| {
Switch::new("use-proxy")
.small()
.checked(view.read(cx).config.use_proxy())
.on_click(window.listener_for(&view, |this, checked, _, cx| {
this.config.set_use_proxy(*checked);
let _ = this.config.save();
cx.notify();
}))
.into_any_element()
}
})
)
)
.item(
SettingItem::new(
t!("read_env_proxy").to_string(),
SettingField::render({
let view = view.clone();
move |_, window, cx| {
Switch::new("read-env-proxy")
.small()
.checked(view.read(cx).config.read_env_proxy())
.on_click(window.listener_for(&view, |this, checked, _, cx| {
this.config.set_read_env_proxy(*checked);
let _ = this.config.save();
cx.notify();
}))
.into_any_element()
}
})
).description(t!("read_env_proxy_desc").to_string())
)
.item(SettingItem::render({
let view = view.clone();
let global_proxy_host_input = view.read(cx).global_proxy_host_input.clone();
let global_proxy_port_input = view.read(cx).global_proxy_port_input.clone();
let global_proxy_user_input = view.read(cx).global_proxy_user_input.clone();
let global_proxy_password_input = view.read(cx).global_proxy_password_input.clone();
move |_, window, cx| {
let proxy_type = view.read(cx).global_proxy_type.clone();
v_flex()
.w_full()
.gap_3()
.child(div().text_sm().font_weight(FontWeight::BOLD).child(t!("global_proxy_settings").to_string()))
.child(
h_flex()
.gap_2()
.child(
Button::new("global-proxy-type-socks5")
.small()
.label("SOCKS5")
.when(proxy_type == "socks5", |b| b.primary())
.on_click(window.listener_for(&view, |this, _, _, cx| {
this.global_proxy_type = "socks5".to_string();
cx.notify();
}))
)
.child(
Button::new("global-proxy-type-http")
.small()
.label("HTTP")
.when(proxy_type == "http", |b| b.primary())
.on_click(window.listener_for(&view, |this, _, _, cx| {
this.global_proxy_type = "http".to_string();
cx.notify();
}))
)
)
.child(v_flex().gap_1().child(div().text_sm().child(t!("global_proxy_host").to_string())).child(Input::new(&global_proxy_host_input).w_full()))
.child(v_flex().gap_1().child(div().text_sm().child(t!("global_proxy_port").to_string())).child(Input::new(&global_proxy_port_input).w_full()))
.child(v_flex().gap_1().child(div().text_sm().child(t!("global_proxy_user").to_string())).child(Input::new(&global_proxy_user_input).w_full()))
.child(v_flex().gap_1().child(div().text_sm().child(t!("global_proxy_password").to_string())).child(Input::new(&global_proxy_password_input).w_full()))
.child(
Button::new("save-global-proxy")
.small()
.primary()
.label(t!("save_proxy").to_string())
.on_click(window.listener_for(&view, |this, _, _, cx| {
let host = this.global_proxy_host_input.read(cx).value().trim().to_string();
let port_str = this.global_proxy_port_input.read(cx).value();
let port = port_str.trim().parse::<u16>().ok();
let user = this.global_proxy_user_input.read(cx).value().trim().to_string();
let password = this.global_proxy_password_input.read(cx).value().to_string();
if host.is_empty() || port.is_none() {
return;
}
this.config.set_global_proxy_type(this.global_proxy_type.clone());
this.config.set_global_proxy_host(host);
this.config.set_global_proxy_port(port);
this.config.set_global_proxy_user(user);
this.config.set_global_proxy_password(password);
let _ = this.config.save();
cx.notify();
}))
)
}
}))
)
)
.page({
let mut page = SettingPage::new(t!("settings_key_bindings").to_string())
.icon(IconName::SquareTerminal)
+49
View File
@@ -211,6 +211,16 @@ pub(crate) struct Ashell {
pub(crate) key_path_input: Entity<InputState>,
pub(crate) key_inline_input: Entity<InputState>,
pub(crate) passphrase_input: Entity<InputState>,
pub(crate) ssh_proxy_type: String,
pub(crate) proxy_host_input: Entity<InputState>,
pub(crate) proxy_port_input: Entity<InputState>,
pub(crate) proxy_user_input: Entity<InputState>,
pub(crate) proxy_password_input: Entity<InputState>,
pub(crate) global_proxy_type: String,
pub(crate) global_proxy_host_input: Entity<InputState>,
pub(crate) global_proxy_port_input: Entity<InputState>,
pub(crate) global_proxy_user_input: Entity<InputState>,
pub(crate) global_proxy_password_input: Entity<InputState>,
pub(crate) sync_endpoint_input: Entity<InputState>,
pub(crate) sync_username_input: Entity<InputState>,
pub(crate) sync_webdav_password_input: Entity<InputState>,
@@ -377,6 +387,10 @@ impl Ashell {
.placeholder("SSH private key passphrase (optional)")
.masked(true)
});
let proxy_host_input = cx.new(|cx| InputState::new(window, cx).placeholder(t!("proxy_host").to_string()));
let proxy_port_input = cx.new(|cx| InputState::new(window, cx).placeholder(t!("proxy_port").to_string()));
let proxy_user_input = cx.new(|cx| InputState::new(window, cx).placeholder(t!("proxy_user").to_string()));
let proxy_password_input = cx.new(|cx| InputState::new(window, cx).placeholder(t!("proxy_password").to_string()).masked(true));
let sftp_path_input = cx.new(|cx| InputState::new(window, cx).default_value("/"));
let sftp_new_folder_input =
cx.new(|cx| InputState::new(window, cx).placeholder(t!("new_folder").to_string()));
@@ -387,6 +401,27 @@ impl Ashell {
tracing::warn!("failed to load config: {err:#}");
ConfigStore::in_memory()
});
let global_proxy_host_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder(t!("proxy_host").to_string())
.default_value(config.global_proxy_host())
});
let global_proxy_port_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder(t!("proxy_port").to_string())
.default_value(config.global_proxy_port().map(|p| p.to_string()).unwrap_or_default())
});
let global_proxy_user_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder(t!("proxy_user").to_string())
.default_value(config.global_proxy_user())
});
let global_proxy_password_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder(t!("proxy_password").to_string())
.masked(true)
.default_value(config.global_proxy_password())
});
let sync_endpoint_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("https://dav.example.com/ashell/")
@@ -450,6 +485,10 @@ impl Ashell {
cx.subscribe_in(&key_path_input, window, Self::on_input_event),
cx.subscribe_in(&key_inline_input, window, Self::on_input_event),
cx.subscribe_in(&passphrase_input, window, Self::on_input_event),
cx.subscribe_in(&proxy_host_input, window, Self::on_input_event),
cx.subscribe_in(&proxy_port_input, window, Self::on_input_event),
cx.subscribe_in(&proxy_user_input, window, Self::on_input_event),
cx.subscribe_in(&proxy_password_input, window, Self::on_input_event),
cx.subscribe_in(&sftp_path_input, window, Self::on_input_event),
cx.subscribe_in(&sftp_new_folder_input, window, Self::on_input_event),
cx.subscribe_in(&search_input, window, Self::on_input_event),
@@ -524,6 +563,16 @@ impl Ashell {
key_path_input,
key_inline_input,
passphrase_input,
ssh_proxy_type: "none".to_string(),
proxy_host_input,
proxy_port_input,
proxy_user_input,
proxy_password_input,
global_proxy_type: config.global_proxy_type().to_string(),
global_proxy_host_input,
global_proxy_port_input,
global_proxy_user_input,
global_proxy_password_input,
sync_endpoint_input,
sync_username_input,
sync_webdav_password_input,
+50
View File
@@ -162,6 +162,12 @@ pub(crate) fn sync_macos_launch_environment() {
| "HOMEBREW_PREFIX"
| "HOMEBREW_CELLAR"
| "HOMEBREW_REPOSITORY"
| "HTTP_PROXY"
| "HTTPS_PROXY"
| "ALL_PROXY"
| "http_proxy"
| "https_proxy"
| "all_proxy"
) || key.starts_with("LC_");
if should_import {
@@ -172,12 +178,56 @@ pub(crate) fn sync_macos_launch_environment() {
}
}
fn read_proxy_from_env() -> Option<(String, String, Option<u16>, String, String)> {
let vars = ["ALL_PROXY", "all_proxy", "HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
for var in vars {
if let Ok(val) = std::env::var(var) {
if val.is_empty() {
continue;
}
if let Ok(url) = reqwest::Url::parse(&val) {
let scheme = url.scheme();
let proxy_type = match scheme {
"socks5" | "socks5h" => "socks5".to_string(),
"http" | "https" => "http".to_string(),
_ => "socks5".to_string(),
};
let host = url.host_str().unwrap_or("").to_string();
let port = url.port();
let user = url.username().to_string();
let password = url.password().unwrap_or("").to_string();
return Some((proxy_type, host, port, user, password));
}
}
}
None
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn sync_macos_launch_environment() {}
pub(crate) fn open_main_window(cx: &mut App) {
let config = ConfigStore::load().unwrap_or_else(|_| ConfigStore::in_memory());
let _ = crate::session::config::ENV_PROXY.get_or_init(|| {
read_proxy_from_env().map(|(proxy_type, host, port, user, password)| {
tracing::info!(
"[proxy] Loaded proxy configuration from environment: type={}, host={}, port={:?}, user={}",
proxy_type,
host,
port,
user
);
crate::session::config::EnvProxy {
proxy_type,
host,
port,
user,
pass: password,
}
})
});
let mut window_options = WindowOptions::default();
if config.title_bar_style() == crate::session::config::TitleBarStyle::Integrated {
+9 -2
View File
@@ -265,11 +265,18 @@ async fn connect_and_authenticate(
addr,
session.user
);
let status_text = if let Some((ptype, phost, pport)) = crate::session::config::active_proxy(session) {
let pport_val = pport.unwrap_or_else(|| if ptype == "http" { 8080 } else { 1080 });
format!("connecting to {addr} via {} proxy {}:{}", ptype.to_uppercase(), phost, pport_val)
} else {
format!("opening tcp connection to {addr}")
};
let _ = events.send(BackendEvent::Status {
tab_id: tab_id.to_string(),
text: format!("opening tcp connection to {addr}"),
text: status_text,
});
let mut handle = client::connect(config, addr.as_str(), ClientHandler)
let stream = crate::session::config::connect_proxy(session).await?;
let mut handle = client::connect_stream(config, stream, ClientHandler)
.await
.with_context(|| format!("connect {addr} failed"))?;
+260
View File
@@ -31,6 +31,16 @@ pub struct Session {
pub passphrase: String,
#[serde(default)]
pub last_used: Option<String>,
#[serde(default)]
pub proxy_type: String, // "none", "socks5", "http"
#[serde(default)]
pub proxy_host: String,
#[serde(default)]
pub proxy_port: Option<u16>,
#[serde(default)]
pub proxy_user: String,
#[serde(default)]
pub proxy_password: String,
}
impl Session {
@@ -48,6 +58,11 @@ impl Session {
private_key_inline: String::new(),
passphrase: String::new(),
last_used: None,
proxy_type: "none".to_string(),
proxy_host: String::new(),
proxy_port: None,
proxy_user: String::new(),
proxy_password: String::new(),
}
}
@@ -72,6 +87,11 @@ impl Session {
private_key_inline,
passphrase,
last_used: None,
proxy_type: "none".to_string(),
proxy_host: String::new(),
proxy_port: None,
proxy_user: String::new(),
proxy_password: String::new(),
}
}
@@ -93,6 +113,11 @@ impl Session {
private_key_inline: String::new(),
passphrase: String::new(),
last_used: None,
proxy_type: "none".to_string(),
proxy_host: String::new(),
proxy_port: None,
proxy_user: String::new(),
proxy_password: String::new(),
}
}
}
@@ -206,6 +231,28 @@ pub struct ConfigFile {
pub sync_s3_bucket: String,
#[serde(default = "default_s3_object_key")]
pub sync_s3_object_key: String,
#[serde(default)]
pub use_proxy: bool,
#[serde(default = "default_read_env_proxy")]
pub read_env_proxy: bool,
#[serde(default = "default_global_proxy_type")]
pub global_proxy_type: String,
#[serde(default)]
pub global_proxy_host: String,
#[serde(default)]
pub global_proxy_port: Option<u16>,
#[serde(default)]
pub global_proxy_user: String,
#[serde(default)]
pub global_proxy_password: String,
}
fn default_read_env_proxy() -> bool {
true
}
fn default_global_proxy_type() -> String {
"socks5".to_string()
}
fn default_monitoring_position() -> String {
@@ -282,6 +329,13 @@ impl Default for ConfigFile {
sync_s3_region: default_s3_region(),
sync_s3_bucket: String::new(),
sync_s3_object_key: default_s3_object_key(),
use_proxy: false,
read_env_proxy: true,
global_proxy_type: default_global_proxy_type(),
global_proxy_host: String::new(),
global_proxy_port: None,
global_proxy_user: String::new(),
global_proxy_password: String::new(),
}
}
}
@@ -624,6 +678,49 @@ impl ConfigStore {
self.cache.cursor_style = style;
}
pub fn use_proxy(&self) -> bool {
self.cache.use_proxy
}
pub fn set_use_proxy(&mut self, val: bool) {
self.cache.use_proxy = val;
}
pub fn read_env_proxy(&self) -> bool {
self.cache.read_env_proxy
}
pub fn set_read_env_proxy(&mut self, val: bool) {
self.cache.read_env_proxy = val;
}
pub fn global_proxy_type(&self) -> &str {
&self.cache.global_proxy_type
}
pub fn set_global_proxy_type(&mut self, val: String) {
self.cache.global_proxy_type = val;
}
pub fn global_proxy_host(&self) -> &str {
&self.cache.global_proxy_host
}
pub fn set_global_proxy_host(&mut self, val: String) {
self.cache.global_proxy_host = val;
}
pub fn global_proxy_port(&self) -> Option<u16> {
self.cache.global_proxy_port
}
pub fn set_global_proxy_port(&mut self, val: Option<u16>) {
self.cache.global_proxy_port = val;
}
pub fn global_proxy_user(&self) -> &str {
&self.cache.global_proxy_user
}
pub fn set_global_proxy_user(&mut self, val: String) {
self.cache.global_proxy_user = val;
}
pub fn global_proxy_password(&self) -> &str {
&self.cache.global_proxy_password
}
pub fn set_global_proxy_password(&mut self, val: String) {
self.cache.global_proxy_password = val;
}
pub fn show_hidden_files(&self) -> bool {
self.cache.show_hidden_files
}
@@ -684,3 +781,166 @@ impl ConfigStore {
Ok(())
}
}
pub trait ProxyStream: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + Sync + 'static {}
impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + Sync + 'static> ProxyStream for T {}
use std::sync::OnceLock;
#[derive(Debug, Clone)]
pub struct EnvProxy {
pub proxy_type: String,
pub host: String,
pub port: Option<u16>,
pub user: String,
pub pass: String,
}
pub static ENV_PROXY: OnceLock<Option<EnvProxy>> = OnceLock::new();
pub async fn connect_proxy(session: &Session) -> Result<Box<dyn ProxyStream>> {
let target_host = &session.host;
let target_port = session.port;
let config = ConfigStore::load().unwrap_or_else(|_| ConfigStore::in_memory());
let (proxy_type, proxy_host, proxy_port, proxy_user, proxy_password) = {
if !session.proxy_type.is_empty() && session.proxy_type != "none" {
(
session.proxy_type.clone(),
session.proxy_host.clone(),
session.proxy_port,
session.proxy_user.clone(),
session.proxy_password.clone(),
)
} else if config.cache.read_env_proxy && ENV_PROXY.get().and_then(|opt| opt.as_ref()).is_some() {
let env_p = ENV_PROXY.get().and_then(|opt| opt.as_ref()).unwrap();
(
env_p.proxy_type.clone(),
env_p.host.clone(),
env_p.port,
env_p.user.clone(),
env_p.pass.clone(),
)
} else if config.cache.use_proxy {
(
config.cache.global_proxy_type.clone(),
config.cache.global_proxy_host.clone(),
config.cache.global_proxy_port,
config.cache.global_proxy_user.clone(),
config.cache.global_proxy_password.clone(),
)
} else {
("none".to_string(), String::new(), None, String::new(), String::new())
}
};
if proxy_type != "none" && (proxy_host.is_empty() || proxy_port.is_none()) {
let addr = format!("{}:{}", target_host, target_port);
let stream = tokio::net::TcpStream::connect(&addr).await?;
return Ok(Box::new(stream));
}
match proxy_type.as_str() {
"socks5" | "socks5h" => {
let proxy_port = proxy_port.unwrap_or(1080);
let proxy_addr = format!("{}:{}", proxy_host, proxy_port);
if !proxy_user.is_empty() {
let stream = tokio_socks::tcp::Socks5Stream::connect_with_password(
proxy_addr.as_str(),
(target_host.as_str(), target_port),
&proxy_user,
&proxy_password,
)
.await
.map_err(|e| anyhow::anyhow!("SOCKS5 proxy connection failed: {}", e))?;
Ok(Box::new(stream))
} else {
let stream = tokio_socks::tcp::Socks5Stream::connect(
proxy_addr.as_str(),
(target_host.as_str(), target_port),
)
.await
.map_err(|e| anyhow::anyhow!("SOCKS5 proxy connection failed: {}", e))?;
Ok(Box::new(stream))
}
}
"http" => {
let proxy_port = proxy_port.unwrap_or(8080);
let proxy_addr = format!("{}:{}", proxy_host, proxy_port);
use tokio::io::AsyncWriteExt;
let mut stream = tokio::net::TcpStream::connect(&proxy_addr)
.await
.map_err(|e| anyhow::anyhow!("HTTP proxy connection failed: {}", e))?;
let mut request = format!(
"CONNECT {}:{} HTTP/1.1\r\nHost: {}:{}\r\n",
target_host, target_port, target_host, target_port
);
if !proxy_user.is_empty() {
use base64::Engine as _;
let auth = format!("{}:{}", proxy_user, proxy_password);
let encoded = base64::engine::general_purpose::STANDARD.encode(auth);
request.push_str(&format!("Proxy-Authorization: Basic {}\r\n", encoded));
}
request.push_str("\r\n");
stream.write_all(request.as_bytes()).await?;
let mut response = [0u8; 1024];
let n = tokio::io::AsyncReadExt::read(&mut stream, &mut response).await?;
let resp_str = String::from_utf8_lossy(&response[..n]);
if !resp_str.contains("200") && !resp_str.contains("established") {
return Err(anyhow::anyhow!("HTTP proxy CONNECT failed: {}", resp_str));
}
Ok(Box::new(stream))
}
_ => {
let addr = format!("{}:{}", target_host, target_port);
let stream = tokio::net::TcpStream::connect(&addr).await?;
Ok(Box::new(stream))
}
}
}
pub fn active_proxy(session: &Session) -> Option<(String, String, Option<u16>)> {
let config = ConfigStore::load().unwrap_or_else(|_| ConfigStore::in_memory());
let (proxy_type, proxy_host, proxy_port, _, _) = {
if !session.proxy_type.is_empty() && session.proxy_type != "none" {
(
session.proxy_type.clone(),
session.proxy_host.clone(),
session.proxy_port,
session.proxy_user.clone(),
session.proxy_password.clone(),
)
} else if config.cache.read_env_proxy && ENV_PROXY.get().and_then(|opt| opt.as_ref()).is_some() {
let env_p = ENV_PROXY.get().and_then(|opt| opt.as_ref()).unwrap();
(
env_p.proxy_type.clone(),
env_p.host.clone(),
env_p.port,
env_p.user.clone(),
env_p.pass.clone(),
)
} else if config.cache.use_proxy {
(
config.cache.global_proxy_type.clone(),
config.cache.global_proxy_host.clone(),
config.cache.global_proxy_port,
config.cache.global_proxy_user.clone(),
config.cache.global_proxy_password.clone(),
)
} else {
("none".to_string(), String::new(), None, String::new(), String::new())
}
};
if proxy_type != "none" && !proxy_host.is_empty() && proxy_port.is_some() {
Some((proxy_type, proxy_host, proxy_port))
} else {
None
}
}
+42
View File
@@ -79,6 +79,17 @@ impl Ashell {
return;
}
if self.ssh_proxy_type != "none" {
let proxy_host = self.proxy_host_input.read(cx).value().trim().to_string();
let proxy_port_str = self.proxy_port_input.read(cx).value().trim().to_string();
let proxy_port = proxy_port_str.parse::<u16>().ok();
if proxy_host.is_empty() || proxy_port.is_none() {
self.status = "Proxy host and port are required".into();
cx.notify();
return;
}
}
let name = if session_name.is_empty() {
host.clone()
} else {
@@ -107,6 +118,17 @@ impl Ashell {
session.id = id;
}
session.last_used = existing_last_used;
session.proxy_type = self.ssh_proxy_type.clone();
session.proxy_host = self.proxy_host_input.read(cx).value().trim().to_string();
session.proxy_port = self
.proxy_port_input
.read(cx)
.value()
.trim()
.parse::<u16>()
.ok();
session.proxy_user = self.proxy_user_input.read(cx).value().trim().to_string();
session.proxy_password = self.proxy_password_input.read(cx).value().to_string();
self.config.upsert(session.clone());
if let Err(err) = self.config.save() {
tracing::warn!("failed to save config: {err:#}");
@@ -139,6 +161,11 @@ impl Ashell {
Self::set_input_value(&self.key_path_input, "", window, cx);
Self::set_input_value(&self.key_inline_input, "", window, cx);
Self::set_input_value(&self.passphrase_input, "", window, cx);
self.ssh_proxy_type = "none".to_string();
Self::set_input_value(&self.proxy_host_input, "", window, cx);
Self::set_input_value(&self.proxy_port_input, "", window, cx);
Self::set_input_value(&self.proxy_user_input, "", window, cx);
Self::set_input_value(&self.proxy_password_input, "", window, cx);
}
pub(crate) fn load_session_into_form(
@@ -172,6 +199,16 @@ impl Ashell {
window,
cx,
);
self.ssh_proxy_type = session.proxy_type.clone();
Self::set_input_value(&self.proxy_host_input, session.proxy_host.clone(), window, cx);
Self::set_input_value(
&self.proxy_port_input,
session.proxy_port.map(|p| p.to_string()).unwrap_or_default(),
window,
cx,
);
Self::set_input_value(&self.proxy_user_input, session.proxy_user.clone(), window, cx);
Self::set_input_value(&self.proxy_password_input, session.proxy_password.clone(), window, cx);
}
pub(crate) fn pick_ssh_key_path(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -326,6 +363,11 @@ impl Ashell {
cx.notify();
}
pub(crate) fn set_ssh_proxy_type(&mut self, proxy_type: String, cx: &mut Context<Self>) {
self.ssh_proxy_type = proxy_type;
cx.notify();
}
pub(crate) fn connect_saved_session(&mut self, session_id: String, cx: &mut Context<Self>) {
tracing::info!(
"[ui] user clicked to connect saved session '{}'",
+2 -1
View File
@@ -827,7 +827,8 @@ async fn connect_and_authenticate(
..Default::default()
});
let addr = format!("{}:{}", session.host, session.port);
let mut handle = client::connect(config, addr.as_str(), SftpClientHandler)
let stream = crate::session::config::connect_proxy(session).await?;
let mut handle = client::connect_stream(config, stream, SftpClientHandler)
.await
.with_context(|| format!("connect {addr} failed"))?;