feat: add HTTPS support for admin panel

This commit is contained in:
lxien
2026-11-09 22:23:32 +08:00
parent 2447bc0fd1
commit ed02ac4e8a
12 changed files with 187 additions and 22 deletions
Generated
+1
View File
@@ -4199,6 +4199,7 @@ dependencies = [
"rustls",
"serde",
"tokio",
"tokio-rustls",
"tracing",
"tracing-subscriber",
"uuid",
+4
View File
@@ -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"
+2 -2
View File
@@ -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;
+37
View File
@@ -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 {
+3 -2
View File
@@ -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,
+16
View File
@@ -142,6 +142,22 @@ fn load_ca_roots(ca_path: &str) -> Result<RootCertStore> {
Ok(roots)
}
pub fn load_server_cert_key(cert_file: &str, key_file: &str) -> Result<Arc<ServerConfig>> {
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,
+19 -7
View File
@@ -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
:::
@@ -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
:::
+3
View File
@@ -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",
+30
View File
@@ -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
}
}
+1
View File
@@ -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 }
+52 -4
View File
@@ -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<Service>, 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<TcpStream>;
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::Addr> {
self.listener.local_addr()
}
}
#[derive(Clone)]
pub struct DashState {
pub svc: Arc<Service>,