diff --git a/Cargo.lock b/Cargo.lock index fcda609..534bf65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4199,6 +4199,7 @@ dependencies = [ "rustls", "serde", "tokio", + "tokio-rustls", "tracing", "tracing-subscriber", "uuid", diff --git a/conf/orbien-server-full.toml b/conf/orbien-server-full.toml index 1fd086e..20ac196 100644 --- a/conf/orbien-server-full.toml +++ b/conf/orbien-server-full.toml @@ -40,6 +40,10 @@ port = 8020 user = "admin" password = "123456" +# [dashboard.tls] +# certFile = "./certs/dashboard.crt" +# keyFile = "./certs/dashboard.key" + [log] file = "./orbien-server.log" level = "info" diff --git a/core/src/config/mod.rs b/core/src/config/mod.rs index 157d1db..012e0c5 100644 --- a/core/src/config/mod.rs +++ b/core/src/config/mod.rs @@ -10,8 +10,8 @@ pub use client::{ pub use env::{contains_env_placeholders, expand_env_placeholders}; pub use log::LogConfig; pub use server::{ - parse_host_port, DashboardConfig, QuicConfig, ServerConfig, ServerTlsConfig, - ServerTransportConfig, + parse_host_port, DashboardConfig, DashboardTlsConfig, QuicConfig, ServerConfig, + ServerTlsConfig, ServerTransportConfig, }; use anyhow::Context; diff --git a/core/src/config/server.rs b/core/src/config/server.rs index c914ebb..6e0a224 100644 --- a/core/src/config/server.rs +++ b/core/src/config/server.rs @@ -132,6 +132,38 @@ pub struct DashboardConfig { pub user: String, #[serde(default)] pub password: String, + + #[serde(default)] + pub tls: DashboardTlsConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct DashboardTlsConfig { + #[serde(default, rename = "certFile", alias = "cert_file")] + pub cert_file: String, + #[serde(default, rename = "keyFile", alias = "key_file")] + pub key_file: String, +} + +impl DashboardTlsConfig { + pub fn enabled(&self) -> bool { + !self.cert_file.trim().is_empty() && !self.key_file.trim().is_empty() + } + + pub fn validate(&self) -> anyhow::Result<()> { + let cert = self.cert_file.trim(); + let key = self.key_file.trim(); + match (cert.is_empty(), key.is_empty()) { + (true, true) => Ok(()), + (false, false) => Ok(()), + (false, true) => anyhow::bail!( + "dashboard.tls.keyFile is required when dashboard.tls.certFile is set" + ), + (true, false) => anyhow::bail!( + "dashboard.tls.certFile is required when dashboard.tls.keyFile is set" + ), + } + } } impl DashboardConfig { @@ -156,6 +188,7 @@ impl DashboardConfig { "dashboard.user and dashboard.password are required when dashboard.port > 0" ); } + self.tls.validate()?; Ok(()) } } @@ -358,6 +391,10 @@ impl ServerConfig { tls.cert_file = super::resolve_maybe_relative(base, &tls.cert_file); tls.key_file = super::resolve_maybe_relative(base, &tls.key_file); tls.trusted_ca_file = super::resolve_maybe_relative(base, &tls.trusted_ca_file); + + let dash_tls = &mut self.dashboard.tls; + dash_tls.cert_file = super::resolve_maybe_relative(base, &dash_tls.cert_file); + dash_tls.key_file = super::resolve_maybe_relative(base, &dash_tls.key_file); } pub fn from_defaults() -> Self { diff --git a/core/src/transport/mod.rs b/core/src/transport/mod.rs index 2b3efa8..0fcb4cb 100644 --- a/core/src/transport/mod.rs +++ b/core/src/transport/mod.rs @@ -10,8 +10,9 @@ pub use quic::{build_client_endpoint, build_server_endpoint, quic_bi, QuicBiStre pub use stream::{boxed_stream, AsyncStream, DynStream}; pub use tls::{ check_and_enable_tls, client_crypto_from_tls_files, client_crypto_insecure, client_enable_tls, - generate_self_signed_cert, install_ring_provider, load_pem_cert_key, new_client_tls_config, - new_server_tls_config, server_crypto, server_crypto_from_tls_files, ALPN_ORBIEN, + generate_self_signed_cert, install_ring_provider, load_pem_cert_key, load_server_cert_key, + new_client_tls_config, new_server_tls_config, server_crypto, server_crypto_from_tls_files, + ALPN_ORBIEN, }; pub use websocket::{ accept_websocket, dial_websocket, is_websocket_http_request, normalize_ws_path, WsByteStream, diff --git a/core/src/transport/tls.rs b/core/src/transport/tls.rs index cd0071c..9e92dd4 100644 --- a/core/src/transport/tls.rs +++ b/core/src/transport/tls.rs @@ -142,6 +142,22 @@ fn load_ca_roots(ca_path: &str) -> Result { Ok(roots) } +pub fn load_server_cert_key(cert_file: &str, key_file: &str) -> Result> { + install_ring_provider()?; + let cert = cert_file.trim(); + let key = key_file.trim(); + if cert.is_empty() || key.is_empty() { + bail!("certFile and keyFile are required"); + } + let (certs, key) = load_pem_cert_key(cert, key)?; + let mut cfg = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, key) + .context("build rustls ServerConfig")?; + cfg.alpn_protocols = vec![b"http/1.1".to_vec()]; + Ok(Arc::new(cfg)) +} + pub fn new_server_tls_config( cert_file: &str, key_file: &str, diff --git a/docs/docs/features/dashboard.md b/docs/docs/features/dashboard.md index 41bd04b..2ece3ad 100644 --- a/docs/docs/features/dashboard.md +++ b/docs/docs/features/dashboard.md @@ -22,9 +22,14 @@ addr = "0.0.0.0" port = 8020 user = "admin" password = "123456" + +# 可选 +# [dashboard.tls] +# certFile = "./certs/dashboard.crt" +# keyFile = "./certs/dashboard.key" ``` -浏览器访问 `http://SERVER_IP:8020`,在弹出框输入用户名和密码登录。 +浏览器访问 `http://SERVER_IP:8020`(启用 `[dashboard.tls]` 后为 `https://`)。 :::tip `addr` 默认为 `127.0.0.1`(仅本机),需远程访问时设为 `0.0.0.0` @@ -32,9 +37,16 @@ password = "123456" ## 参数 -| 参数 | 必填 | 默认值 | 说明 | -|-----------------------|----|-------------|----------------------| -| `dashboard.addr` | 否 | `127.0.0.1` | 监听地址;远程访问需 `0.0.0.0` | -| `dashboard.port` | 是 | `0` | 监听端口;`0` 表示关闭 | -| `dashboard.user` | 是 | | 登录用户名 | -| `dashboard.password` | 是 | | 登录密码 | +| 参数 | 必填 | 默认值 | 说明 | +|--------------------------|------|-------------|--------------------------------| +| `dashboard.addr` | 否 | `127.0.0.1` | 监听地址;远程访问需 `0.0.0.0` | +| `dashboard.port` | 是 | `0` | 监听端口;`0` 表示关闭 | +| `dashboard.user` | 是 | | 登录用户名 | +| `dashboard.password` | 是 | | 登录密码 | +| `dashboard.tls.certFile` | 否 | | PEM 证书路径 | +| `dashboard.tls.keyFile` | 否 | | PEM 私钥路径 | + +:::info[说明] + +- keyFile 和 certFile 必须同时配置才会开启 HTTPS +::: \ No newline at end of file diff --git a/docs/i18n/en/docusaurus-plugin-content-docs/current/features/dashboard.md b/docs/i18n/en/docusaurus-plugin-content-docs/current/features/dashboard.md index c606cc7..b31f05c 100644 --- a/docs/i18n/en/docusaurus-plugin-content-docs/current/features/dashboard.md +++ b/docs/i18n/en/docusaurus-plugin-content-docs/current/features/dashboard.md @@ -21,9 +21,14 @@ addr = "0.0.0.0" port = 8020 user = "admin" password = "123456" + +# Optional +# [dashboard.tls] +# certFile = "./certs/dashboard.crt" +# keyFile = "./certs/dashboard.key" ``` -Open `http://SERVER_IP:8020` in a browser and sign in with username and password in the login dialog. +Open `http://SERVER_IP:8020` in a browser (use `https://` when `[dashboard.tls]` is enabled). :::tip `addr` defaults to `127.0.0.1` (localhost only). Set it to `0.0.0.0` for remote access. @@ -31,9 +36,16 @@ Open `http://SERVER_IP:8020` in a browser and sign in with username and password ## Parameters -| Parameter | Required | Default | Description | -|-------------------------|----------|---------------|----------------------------------------------| -| `dashboard.addr` | No | `127.0.0.1` | Listen address; use `0.0.0.0` for remote access | -| `dashboard.port` | Yes | `0` | Listen port; `0` disables the dashboard | -| `dashboard.user` | Yes | | Login username | -| `dashboard.password` | Yes | | Login password | +| Parameter | Required | Default | Description | +|--------------------------|----------|-------------|------------------------------------------------| +| `dashboard.addr` | No | `127.0.0.1` | Listen address; use `0.0.0.0` for remote access | +| `dashboard.port` | Yes | `0` | Listen port; `0` disables the dashboard | +| `dashboard.user` | Yes | | Username | +| `dashboard.password` | Yes | | Password | +| `dashboard.tls.certFile` | No | | Path to the PEM certificate | +| `dashboard.tls.keyFile` | No | | Path to the PEM private key | + +:::info[Note] + +- Both `certFile` and `keyFile` must be set to enable HTTPS +::: diff --git a/docs/package.json b/docs/package.json index 26808cc..8dcff9d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -6,8 +6,11 @@ "docusaurus": "docusaurus", "fetch-release-manifest": "node scripts/fetch-release-manifest.mjs", "prebuild": "node scripts/fetch-release-manifest.mjs", + "prestart": "node scripts/fetch-release-manifest.mjs", + "prestart:en": "node scripts/fetch-release-manifest.mjs", "start": "docusaurus start --no-open", "start:en": "docusaurus start --locale en --no-open", + "predev": "node scripts/fetch-release-manifest.mjs", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "predeploy": "node scripts/fetch-release-manifest.mjs", diff --git a/docs/src/data/release-manifest.json b/docs/src/data/release-manifest.json new file mode 100644 index 0000000..67e0aa9 --- /dev/null +++ b/docs/src/data/release-manifest.json @@ -0,0 +1,30 @@ +{ + "version": "3.9.0", + "fetchedAt": "2026-11-09T14:14:31.910Z", + "assets": { + "orbien-desktop_3.9.0_darwin_amd64.dmg": 14728180, + "orbien-desktop_3.9.0_darwin_arm64.dmg": 13716115, + "orbien-desktop_3.9.0_linux_amd64.deb": 11985208, + "orbien-desktop_3.9.0_linux_arm64.deb": 11605068, + "orbien-desktop_3.9.0_windows_amd64.zip": 12136382, + "orbien-desktop_3.9.0_windows_arm64.zip": 11662935, + "orbien-server_3.9.0_darwin_amd64.tar.gz": 2776015, + "orbien-server_3.9.0_darwin_arm64.tar.gz": 2548570, + "orbien-server_3.9.0_freebsd_amd64.tar.gz": 2881813, + "orbien-server_3.9.0_linux_amd64_gnu.tar.gz": 2916874, + "orbien-server_3.9.0_linux_amd64_musl.tar.gz": 2293429, + "orbien-server_3.9.0_linux_arm64_gnu.tar.gz": 2753719, + "orbien-server_3.9.0_linux_arm64_musl.tar.gz": 2013390, + "orbien-server_3.9.0_windows_amd64.tar.gz": 2686107, + "orbien-server_3.9.0_windows_arm64.tar.gz": 2538893, + "orbien_3.9.0_darwin_amd64.tar.gz": 2446430, + "orbien_3.9.0_darwin_arm64.tar.gz": 2208569, + "orbien_3.9.0_freebsd_amd64.tar.gz": 2552659, + "orbien_3.9.0_linux_amd64_gnu.tar.gz": 2591259, + "orbien_3.9.0_linux_amd64_musl.tar.gz": 1967497, + "orbien_3.9.0_linux_arm64_gnu.tar.gz": 2420371, + "orbien_3.9.0_linux_arm64_musl.tar.gz": 1688071, + "orbien_3.9.0_windows_amd64.tar.gz": 2358926, + "orbien_3.9.0_windows_arm64.tar.gz": 2206299 + } +} diff --git a/server/Cargo.toml b/server/Cargo.toml index 0c19513..7546ebe 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -22,6 +22,7 @@ quinn = { workspace = true } kcp-tokio = { workspace = true } httparse = { workspace = true } rustls = { workspace = true } +tokio-rustls = { workspace = true } base64 = { workspace = true } axum = { workspace = true } rust-embed = { workspace = true } diff --git a/server/src/dashboard/mod.rs b/server/src/dashboard/mod.rs index 05eee58..d8a5120 100644 --- a/server/src/dashboard/mod.rs +++ b/server/src/dashboard/mod.rs @@ -5,24 +5,72 @@ mod snapshot; use crate::service::Service; use anyhow::Result; use axum::middleware; +use axum::serve::Listener; use orbien_core::config::DashboardConfig; +use orbien_core::transport::load_server_cert_key; +use std::io; +use std::net::SocketAddr; use std::sync::Arc; -use tokio::net::TcpListener; +use tokio::net::{TcpListener, TcpStream}; +use tokio_rustls::server::TlsStream; +use tokio_rustls::TlsAcceptor; pub async fn run(svc: Arc, cfg: DashboardConfig) -> Result<()> { let addr = format!("{}:{}", cfg.addr, cfg.port); let listener = TcpListener::bind(&addr).await?; - tracing::info!(%addr, user = %cfg.user, "dashboard listening"); - let state = Arc::new(DashState { svc, cfg }); + let state = Arc::new(DashState { + svc, + cfg: cfg.clone(), + }); let app = routes::router(state.clone()) .layer(middleware::from_fn_with_state(state, routes::basic_auth)) .into_make_service(); - axum::serve(listener, app).await?; + if cfg.tls.enabled() { + let tls_cfg = load_server_cert_key(&cfg.tls.cert_file, &cfg.tls.key_file)?; + let acceptor = TlsAcceptor::from(tls_cfg); + tracing::info!(%addr, user = %cfg.user, "dashboard listening (https)"); + axum::serve(TlsListener { listener, acceptor }, app).await?; + } else { + tracing::info!(%addr, user = %cfg.user, "dashboard listening"); + axum::serve(listener, app).await?; + } Ok(()) } +struct TlsListener { + listener: TcpListener, + acceptor: TlsAcceptor, +} + +impl Listener for TlsListener { + type Io = TlsStream; + type Addr = SocketAddr; + + async fn accept(&mut self) -> (Self::Io, Self::Addr) { + loop { + let (tcp, peer) = match self.listener.accept().await { + Ok(tup) => tup, + Err(e) => { + tracing::debug!(error = %e, "dashboard accept failed"); + continue; + } + }; + match self.acceptor.accept(tcp).await { + Ok(tls) => return (tls, peer), + Err(e) => { + tracing::debug!(%peer, error = %e, "dashboard TLS handshake failed"); + } + } + } + } + + fn local_addr(&self) -> io::Result { + self.listener.local_addr() + } +} + #[derive(Clone)] pub struct DashState { pub svc: Arc,