diff --git a/easytier-web/frontend/index.html b/easytier-web/frontend/index.html
index cc73f064..74fa0741 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 0b58d327..3b5dc2d3 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 f0ebf537..ced23276 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 5d9dbff2..e1b41962 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 9aeb2030..6a2e6d11 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 3838e030..27d86228 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 98d05291..7f0590d1 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)
}
}