refactor(proxy): 简化 HMAC 认证、移除明文 HTTP 代理并优化 setup 流程

- HMAC 签名移除 node_id,仅使用 timestamp,消除重注册时的认证竞态问题
- 删除 plain HTTP forward proxy(plain.rs),仅保留 CONNECT 隧道和 delegate,非支持方法返回 405
- 提取共享 BoxBody 类型和 empty_box_body() 到 proxy/mod.rs
- CLI 解析重构为 clap 原生 subcommand,启用 subcommand_negates_reqs
- setup 向导返回 SetupOutcome 枚举,支持保存后自动启动 proxy
- setup TUI 增加未保存变更的退出确认(pending_quit)
- validate_target 改为 async,使用 tokio::net::lookup_host 避免阻塞 DNS
- 显式初始化 rustls ring CryptoProvider
- ConfigFile 新增 inject_env_override() 用于 setup 后重载配置
- Python 端 HMAC 签名同步移除 node_id,缓存时间桶从 120s 调整为 240s
This commit is contained in:
fawney19
2026-02-08 16:10:13 +08:00
parent f07cae540e
commit 183e7c60e1
15 changed files with 261 additions and 357 deletions

View File

@@ -2,4 +2,4 @@ pub(crate) mod service;
mod tui;
pub(crate) mod upgrade;
pub use self::tui::run;
pub use self::tui::{run, SetupOutcome};

View File

@@ -93,9 +93,11 @@ pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
eprintln!();
eprintln!(" Commands:");
eprintln!(" sudo systemctl status {} # status", SERVICE_NAME);
eprintln!(" sudo systemctl restart {} # restart", SERVICE_NAME);
eprintln!(" sudo journalctl -u {} -f # logs", SERVICE_NAME);
eprintln!(" aether-proxy status # service status");
eprintln!(" aether-proxy logs # tail logs");
eprintln!(" sudo aether-proxy restart # restart");
eprintln!(" sudo aether-proxy stop # stop");
eprintln!(" sudo aether-proxy uninstall # remove service");
eprintln!();
Ok(())

View File

@@ -21,6 +21,16 @@ use ratatui::Terminal;
use crate::config::ConfigFile;
/// Outcome of the setup wizard, returned to the caller.
pub enum SetupOutcome {
/// Config saved; systemd service installed and started.
ServiceInstalled,
/// Config saved; no service — caller should start the proxy directly.
ReadyToRun(PathBuf),
/// User quit without saving.
Cancelled,
}
/// Column width reserved for the field label (chars).
const LABEL_WIDTH: usize = 22;
@@ -63,6 +73,7 @@ struct App {
message: Option<(String, Instant, bool)>, // (text, when, is_error)
scroll_offset: usize,
saved_once: bool,
pending_quit: bool, // true after first q/Esc with unsaved changes
}
impl App {
@@ -148,6 +159,7 @@ impl App {
message: None,
scroll_offset: 0,
saved_once: false,
pending_quit: false,
}
}
@@ -235,9 +247,9 @@ impl App {
/// Returns `true` when the app should exit.
fn handle_key(&mut self, key: KeyEvent) -> bool {
// Expire old messages
// Expire old messages (but keep quit-confirmation messages alive)
if let Some((_, when, _)) = &self.message {
if when.elapsed() > Duration::from_secs(4) {
if !self.pending_quit && when.elapsed() > Duration::from_secs(4) {
self.message = None;
}
}
@@ -252,8 +264,29 @@ impl App {
}
fn handle_normal(&mut self, key: KeyEvent) -> bool {
// ── Quit handling (with unsaved-changes confirmation) ─────────
let is_quit_key = matches!(key.code, KeyCode::Char('q') | KeyCode::Esc);
if is_quit_key {
if !self.modified || self.pending_quit {
return true;
}
self.pending_quit = true;
self.message = Some((
"unsaved changes! q again to discard, ^S to save".into(),
Instant::now(),
true,
));
return false;
}
// Any other key cancels the pending quit
if self.pending_quit {
self.pending_quit = false;
self.message = None;
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => return true,
KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if let Err(e) = self.save() {
self.message = Some((format!("error: {}", e), Instant::now(), true));
@@ -564,7 +597,7 @@ fn render_footer(f: &mut Frame, app: &App, area: Rect) {
// ── Entry point ──────────────────────────────────────────────────────────────
pub fn run(config_path: PathBuf) -> anyhow::Result<()> {
pub fn run(config_path: PathBuf) -> anyhow::Result<SetupOutcome> {
// Setup terminal
terminal::enable_raw_mode()?;
let mut stdout = io::stdout();
@@ -584,46 +617,42 @@ pub fn run(config_path: PathBuf) -> anyhow::Result<()> {
result?;
// Post-TUI message
if app.saved_once {
eprintln!();
eprintln!(" Config saved to {}", config_path.display());
eprintln!();
// ── Post-TUI: decide outcome ─────────────────────────────────────
let wants_service = app
.fields
.iter()
.find(|f| f.key == "install_service")
.map(|f| f.value == "true")
.unwrap_or(false);
if !app.saved_once {
return Ok(SetupOutcome::Cancelled);
}
if wants_service {
match super::service::install_service(&config_path) {
Ok(()) => {}
Err(e) => {
eprintln!(" Service install failed: {}", e);
eprintln!();
}
eprintln!();
eprintln!(" Config saved to {}", config_path.display());
eprintln!();
let wants_service = app
.fields
.iter()
.find(|f| f.key == "install_service")
.map(|f| f.value == "true")
.unwrap_or(false);
if wants_service {
match super::service::install_service(&config_path) {
Ok(()) => return Ok(SetupOutcome::ServiceInstalled),
Err(e) => {
eprintln!(" Service install failed: {}", e);
eprintln!(" Starting proxy directly instead.\n");
}
} else {
// Uninstall service if it was previously installed
if super::service::is_installed() {
if let Err(e) = super::service::uninstall_service() {
eprintln!(" Service uninstall failed: {}", e);
eprintln!();
}
}
} else {
// Uninstall service if it was previously installed but toggled off
if super::service::is_installed() {
if let Err(e) = super::service::uninstall_service() {
eprintln!(" Service uninstall failed: {}", e);
eprintln!();
}
eprintln!(" Run with:");
eprintln!(
" aether-proxy (auto-reads {})",
config_path.display()
);
eprintln!();
}
}
Ok(())
Ok(SetupOutcome::ReadyToRun(config_path))
}
fn event_loop(