mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-09-22 08:01:00 +00:00
feat: add global proxy settings and startup env reader (resolves #53)
This commit is contained in:
Generated
+1
@@ -355,6 +355,7 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-socks",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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: "保存代理设置"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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"))?;
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"))?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user