From b469f8197a19c2f35bcfe311329007145f777375 Mon Sep 17 00:00:00 2001 From: Mg Pig Date: Mon, 2 Jun 2025 06:46:12 +0800 Subject: [PATCH] Supports customizing the API server address of the Web frontend through the --api-host parameter (#913) --- easytier-web/frontend/index.html | 1 + easytier-web/frontend/src/modules/api-host.ts | 15 +++-- easytier-web/frontend/vite.config.ts | 11 +++ easytier-web/locales/app.yml | 5 +- easytier-web/src/main.rs | 62 +++++++++++------ easytier-web/src/restful/mod.rs | 44 ++++++------ easytier-web/src/web/mod.rs | 67 ++++++++++++++++--- 7 files changed, 150 insertions(+), 55 deletions(-) diff --git a/easytier-web/frontend/index.html b/easytier-web/frontend/index.html index cc73f06..74fa074 100644 --- a/easytier-web/frontend/index.html +++ b/easytier-web/frontend/index.html @@ -5,6 +5,7 @@ EasyTier Dashboard +
diff --git a/easytier-web/frontend/src/modules/api-host.ts b/easytier-web/frontend/src/modules/api-host.ts index 0b58d32..3b5dc2d 100644 --- a/easytier-web/frontend/src/modules/api-host.ts +++ b/easytier-web/frontend/src/modules/api-host.ts @@ -1,10 +1,17 @@ -const defaultApiHost = 'https://config-server.easytier.cn'; - interface ApiHost { value: string; usedAt: number; } +let apiMeta: { + api_host: string; +} | undefined = (window as any).apiMeta; + +// remove trailing slashes from the URL +const cleanUrl = (url: string) => url.replace(/\/+$/, ''); + +const defaultApiHost = cleanUrl(apiMeta?.api_host ?? `${location.origin}${location.pathname}`); + const isValidHttpUrl = (s: string): boolean => { let url; @@ -45,7 +52,7 @@ const saveApiHost = (host: string) => { } let hosts = cleanAndLoadApiHosts(); - const newHost: ApiHost = {value: host, usedAt: Date.now()}; + const newHost: ApiHost = { value: host, usedAt: Date.now() }; hosts = hosts.filter((h) => h.value !== host); hosts.push(newHost); localStorage.setItem('apiHosts', JSON.stringify(hosts)); @@ -61,4 +68,4 @@ const getInitialApiHost = (): string => { } }; -export {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost} \ No newline at end of file +export { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } \ No newline at end of file diff --git a/easytier-web/frontend/vite.config.ts b/easytier-web/frontend/vite.config.ts index f0ebf53..ced2327 100644 --- a/easytier-web/frontend/vite.config.ts +++ b/easytier-web/frontend/vite.config.ts @@ -3,9 +3,20 @@ import vue from '@vitejs/plugin-vue' // import { viteSingleFile } from "vite-plugin-singlefile" const WEB_BASE_URL = process.env.WEB_BASE_URL || ''; +const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:11211'; // https://vite.dev/config/ export default defineConfig({ base: WEB_BASE_URL, plugins: [vue(),/* viteSingleFile() */], + server: { + proxy: { + "/api": { + target: API_BASE_URL, + }, + "/api_meta.js": { + target: API_BASE_URL, + }, + } + } }) diff --git a/easytier-web/locales/app.yml b/easytier-web/locales/app.yml index 5d9dbff..e1b4196 100644 --- a/easytier-web/locales/app.yml +++ b/easytier-web/locales/app.yml @@ -27,4 +27,7 @@ cli: zh-CN: "web dashboard 服务器的监听端口, 默认为与 api 服务器端口相同" no_web: en: "Do not run the web dashboard server" - zh-CN: "不运行 web dashboard 服务器" \ No newline at end of file + zh-CN: "不运行 web dashboard 服务器" + api_host: + en: "The URL of the API server, used by the web frontend to connect to" + zh-CN: "API 服务器的 URL,用于 web 前端连接" \ No newline at end of file diff --git a/easytier-web/src/main.rs b/easytier-web/src/main.rs index 9aeb203..6a2e6d1 100644 --- a/easytier-web/src/main.rs +++ b/easytier-web/src/main.rs @@ -12,7 +12,9 @@ use easytier::{ constants::EASYTIER_VERSION, error::Error, }, - tunnel::{tcp::TcpTunnelListener, udp::UdpTunnelListener, websocket::WSTunnelListener, TunnelListener}, + tunnel::{ + tcp::TcpTunnelListener, udp::UdpTunnelListener, websocket::WSTunnelListener, TunnelListener, + }, utils::{init_logger, setup_panic_handler}, }; @@ -89,6 +91,13 @@ struct Cli { default_value = "false" )] no_web: bool, + + #[cfg(feature = "embed")] + #[arg( + long, + help = t!("cli.api_host").to_string() + )] + api_host: Option, } pub fn get_listener_by_url(l: &url::Url) -> Result, Error> { @@ -137,36 +146,49 @@ async fn main() { let mgr = Arc::new(mgr); #[cfg(feature = "embed")] - let restful_also_serve_web = !cli.no_web - && (cli.web_server_port.is_none() || cli.web_server_port == Some(cli.api_server_port)); - + let (web_router_restful, web_router_static) = if cli.no_web { + (None, None) + } else { + let web_router = web::build_router(cli.api_host.clone()); + if cli.web_server_port.is_none() || cli.web_server_port == Some(cli.api_server_port) { + (Some(web_router), None) + } else { + (None, Some(web_router)) + } + }; #[cfg(not(feature = "embed"))] - let restful_also_serve_web = false; + let web_router_restful = None; - let mut restful_server = restful::RestfulServer::new( + let _restful_server_tasks = restful::RestfulServer::new( format!("0.0.0.0:{}", cli.api_server_port).parse().unwrap(), mgr.clone(), db, - restful_also_serve_web, + web_router_restful, ) .await + .unwrap() + .start() + .await .unwrap(); - restful_server.start().await.unwrap(); - #[cfg(feature = "embed")] - let mut web_server = web::WebServer::new( - format!("0.0.0.0:{}", cli.web_server_port.unwrap_or(0)) - .parse() + let _web_server_task = if let Some(web_router) = web_router_static { + Some( + web::WebServer::new( + format!("0.0.0.0:{}", cli.web_server_port.unwrap_or(0)) + .parse() + .unwrap(), + web_router, + ) + .await + .unwrap() + .start() + .await .unwrap(), - ) - .await - .unwrap(); - - #[cfg(feature = "embed")] - if !cli.no_web && !restful_also_serve_web { - web_server.start().await.unwrap(); - } + ) + } else { + None + }; tokio::signal::ctrl_c().await.unwrap(); } diff --git a/easytier-web/src/restful/mod.rs b/easytier-web/src/restful/mod.rs index 3838e03..27d8622 100644 --- a/easytier-web/src/restful/mod.rs +++ b/easytier-web/src/restful/mod.rs @@ -39,12 +39,11 @@ pub struct RestfulServer { client_mgr: Arc, db: Db, - serve_task: Option>, - delete_task: Option>>, - + // serve_task: Option>, + // delete_task: Option>>, network_api: NetworkApi, - enable_web_embed: bool, + web_router: Option, } type AppStateInner = Arc; @@ -94,7 +93,7 @@ impl RestfulServer { bind_addr: SocketAddr, client_mgr: Arc, db: Db, - enable_web_embed: bool, + web_router: Option, ) -> anyhow::Result { assert!(client_mgr.is_running()); @@ -104,10 +103,10 @@ impl RestfulServer { bind_addr, client_mgr, db, - serve_task: None, - delete_task: None, + // serve_task: None, + // delete_task: None, network_api, - enable_web_embed, + web_router, }) } @@ -159,7 +158,15 @@ impl RestfulServer { } } - pub async fn start(&mut self) -> Result<(), anyhow::Error> { + pub async fn start( + mut self, + ) -> Result< + ( + ScopedTask<()>, + ScopedTask>, + ), + anyhow::Error, + > { let listener = TcpListener::bind(self.bind_addr).await?; // Session layer. @@ -169,14 +176,13 @@ impl RestfulServer { let session_store = SqliteStore::new(self.db.inner()); session_store.migrate().await?; - self.delete_task.replace( + let delete_task: ScopedTask> = tokio::task::spawn( session_store .clone() .continuously_delete_expired(tokio::time::Duration::from_secs(60)), ) - .into(), - ); + .into(); // Generate a cryptographic key to sign the session cookie. let key = Key::generate(); @@ -216,19 +222,17 @@ impl RestfulServer { .layer(compression_layer); #[cfg(feature = "embed")] - let app = if self.enable_web_embed { - use axum_embed::ServeEmbed; - let service = ServeEmbed::::new(); - app.fallback_service(service) + let app = if let Some(web_router) = self.web_router.take() { + app.merge(web_router) } else { app }; - let task = tokio::spawn(async move { + let serve_task: ScopedTask<()> = tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); - }); - self.serve_task = Some(task.into()); + }) + .into(); - Ok(()) + Ok((serve_task, delete_task)) } } diff --git a/easytier-web/src/web/mod.rs b/easytier-web/src/web/mod.rs index 98d0529..7f0590d 100644 --- a/easytier-web/src/web/mod.rs +++ b/easytier-web/src/web/mod.rs @@ -1,8 +1,13 @@ -use axum::Router; +use axum::{ + extract::State, + http::header, + response::{IntoResponse, Response}, + routing, Router, +}; +use axum_embed::ServeEmbed; use easytier::common::scoped_task::ScopedTask; use rust_embed::RustEmbed; use std::net::SocketAddr; -use axum_embed::ServeEmbed; use tokio::net::TcpListener; /// Embed assets for web dashboard, build frontend first @@ -10,30 +15,72 @@ use tokio::net::TcpListener; #[folder = "frontend/dist/"] struct Assets; +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct ApiMetaResponse { + api_host: String, +} + +async fn handle_api_meta(State(api_host): State) -> impl IntoResponse { + Response::builder() + .header( + header::CONTENT_TYPE, + "application/javascript; charset=utf-8", + ) + .header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate") + .header(header::PRAGMA, "no-cache") + .header(header::EXPIRES, "0") + .body(format!( + "window.apiMeta = {}", + serde_json::to_string(&ApiMetaResponse { + api_host: api_host.to_string() + }) + .unwrap(), + )) + .unwrap() +} + +pub fn build_router(api_host: Option) -> Router { + let service = ServeEmbed::::new(); + let router = Router::new(); + + let router = if let Some(api_host) = api_host { + let sub_router = Router::new() + .route("/api_meta.js", routing::get(handle_api_meta)) + .with_state(api_host); + router.merge(sub_router) + } else { + router + }; + + let router = router.fallback_service(service); + + router +} + pub struct WebServer { bind_addr: SocketAddr, + router: Router, serve_task: Option>, } impl WebServer { - pub async fn new(bind_addr: SocketAddr) -> anyhow::Result { + pub async fn new(bind_addr: SocketAddr, router: Router) -> anyhow::Result { Ok(WebServer { bind_addr, + router, serve_task: None, }) } - pub async fn start(&mut self) -> Result<(), anyhow::Error> { + pub async fn start(self) -> Result, anyhow::Error> { let listener = TcpListener::bind(self.bind_addr).await?; - let service = ServeEmbed::::new(); - let app = Router::new().fallback_service(service); + let app = self.router; let task = tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); - }); + }) + .into(); - self.serve_task = Some(task.into()); - - Ok(()) + Ok(task) } }