mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-06 17:59:11 +00:00
Supports customizing the API server address of the Web frontend through the --api-host parameter (#913)
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
<link rel="icon" type="image/png" href="/easytier.png" />
|
<link rel="icon" type="image/png" href="/easytier.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>EasyTier Dashboard</title>
|
<title>EasyTier Dashboard</title>
|
||||||
|
<script src="/api_meta.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
const defaultApiHost = 'https://config-server.easytier.cn';
|
|
||||||
|
|
||||||
interface ApiHost {
|
interface ApiHost {
|
||||||
value: string;
|
value: string;
|
||||||
usedAt: number;
|
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 => {
|
const isValidHttpUrl = (s: string): boolean => {
|
||||||
let url;
|
let url;
|
||||||
|
|
||||||
@@ -45,7 +52,7 @@ const saveApiHost = (host: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let hosts = cleanAndLoadApiHosts();
|
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 = hosts.filter((h) => h.value !== host);
|
||||||
hosts.push(newHost);
|
hosts.push(newHost);
|
||||||
localStorage.setItem('apiHosts', JSON.stringify(hosts));
|
localStorage.setItem('apiHosts', JSON.stringify(hosts));
|
||||||
@@ -61,4 +68,4 @@ const getInitialApiHost = (): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export {getInitialApiHost, cleanAndLoadApiHosts, saveApiHost}
|
export { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost }
|
||||||
@@ -3,9 +3,20 @@ import vue from '@vitejs/plugin-vue'
|
|||||||
// import { viteSingleFile } from "vite-plugin-singlefile"
|
// import { viteSingleFile } from "vite-plugin-singlefile"
|
||||||
|
|
||||||
const WEB_BASE_URL = process.env.WEB_BASE_URL || '';
|
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/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: WEB_BASE_URL,
|
base: WEB_BASE_URL,
|
||||||
plugins: [vue(),/* viteSingleFile() */],
|
plugins: [vue(),/* viteSingleFile() */],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: API_BASE_URL,
|
||||||
|
},
|
||||||
|
"/api_meta.js": {
|
||||||
|
target: API_BASE_URL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -28,3 +28,6 @@ cli:
|
|||||||
no_web:
|
no_web:
|
||||||
en: "Do not run the web dashboard server"
|
en: "Do not run the web dashboard server"
|
||||||
zh-CN: "不运行 web dashboard 服务器"
|
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 前端连接"
|
||||||
+42
-20
@@ -12,7 +12,9 @@ use easytier::{
|
|||||||
constants::EASYTIER_VERSION,
|
constants::EASYTIER_VERSION,
|
||||||
error::Error,
|
error::Error,
|
||||||
},
|
},
|
||||||
tunnel::{tcp::TcpTunnelListener, udp::UdpTunnelListener, websocket::WSTunnelListener, TunnelListener},
|
tunnel::{
|
||||||
|
tcp::TcpTunnelListener, udp::UdpTunnelListener, websocket::WSTunnelListener, TunnelListener,
|
||||||
|
},
|
||||||
utils::{init_logger, setup_panic_handler},
|
utils::{init_logger, setup_panic_handler},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,6 +91,13 @@ struct Cli {
|
|||||||
default_value = "false"
|
default_value = "false"
|
||||||
)]
|
)]
|
||||||
no_web: bool,
|
no_web: bool,
|
||||||
|
|
||||||
|
#[cfg(feature = "embed")]
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = t!("cli.api_host").to_string()
|
||||||
|
)]
|
||||||
|
api_host: Option<url::Url>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_listener_by_url(l: &url::Url) -> Result<Box<dyn TunnelListener>, Error> {
|
pub fn get_listener_by_url(l: &url::Url) -> Result<Box<dyn TunnelListener>, Error> {
|
||||||
@@ -137,36 +146,49 @@ async fn main() {
|
|||||||
let mgr = Arc::new(mgr);
|
let mgr = Arc::new(mgr);
|
||||||
|
|
||||||
#[cfg(feature = "embed")]
|
#[cfg(feature = "embed")]
|
||||||
let restful_also_serve_web = !cli.no_web
|
let (web_router_restful, web_router_static) = if cli.no_web {
|
||||||
&& (cli.web_server_port.is_none() || cli.web_server_port == Some(cli.api_server_port));
|
(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"))]
|
#[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(),
|
format!("0.0.0.0:{}", cli.api_server_port).parse().unwrap(),
|
||||||
mgr.clone(),
|
mgr.clone(),
|
||||||
db,
|
db,
|
||||||
restful_also_serve_web,
|
web_router_restful,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.start()
|
||||||
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
restful_server.start().await.unwrap();
|
|
||||||
|
|
||||||
#[cfg(feature = "embed")]
|
#[cfg(feature = "embed")]
|
||||||
let mut web_server = web::WebServer::new(
|
let _web_server_task = if let Some(web_router) = web_router_static {
|
||||||
format!("0.0.0.0:{}", cli.web_server_port.unwrap_or(0))
|
Some(
|
||||||
.parse()
|
web::WebServer::new(
|
||||||
|
format!("0.0.0.0:{}", cli.web_server_port.unwrap_or(0))
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
web_router,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.start()
|
||||||
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
.await
|
} else {
|
||||||
.unwrap();
|
None
|
||||||
|
};
|
||||||
#[cfg(feature = "embed")]
|
|
||||||
if !cli.no_web && !restful_also_serve_web {
|
|
||||||
web_server.start().await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::signal::ctrl_c().await.unwrap();
|
tokio::signal::ctrl_c().await.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,12 +39,11 @@ pub struct RestfulServer {
|
|||||||
client_mgr: Arc<ClientManager>,
|
client_mgr: Arc<ClientManager>,
|
||||||
db: Db,
|
db: Db,
|
||||||
|
|
||||||
serve_task: Option<ScopedTask<()>>,
|
// serve_task: Option<ScopedTask<()>>,
|
||||||
delete_task: Option<ScopedTask<tower_sessions::session_store::Result<()>>>,
|
// delete_task: Option<ScopedTask<tower_sessions::session_store::Result<()>>>,
|
||||||
|
|
||||||
network_api: NetworkApi,
|
network_api: NetworkApi,
|
||||||
|
|
||||||
enable_web_embed: bool,
|
web_router: Option<Router>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppStateInner = Arc<ClientManager>;
|
type AppStateInner = Arc<ClientManager>;
|
||||||
@@ -94,7 +93,7 @@ impl RestfulServer {
|
|||||||
bind_addr: SocketAddr,
|
bind_addr: SocketAddr,
|
||||||
client_mgr: Arc<ClientManager>,
|
client_mgr: Arc<ClientManager>,
|
||||||
db: Db,
|
db: Db,
|
||||||
enable_web_embed: bool,
|
web_router: Option<Router>,
|
||||||
) -> anyhow::Result<Self> {
|
) -> anyhow::Result<Self> {
|
||||||
assert!(client_mgr.is_running());
|
assert!(client_mgr.is_running());
|
||||||
|
|
||||||
@@ -104,10 +103,10 @@ impl RestfulServer {
|
|||||||
bind_addr,
|
bind_addr,
|
||||||
client_mgr,
|
client_mgr,
|
||||||
db,
|
db,
|
||||||
serve_task: None,
|
// serve_task: None,
|
||||||
delete_task: None,
|
// delete_task: None,
|
||||||
network_api,
|
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<tower_sessions::session_store::Result<()>>,
|
||||||
|
),
|
||||||
|
anyhow::Error,
|
||||||
|
> {
|
||||||
let listener = TcpListener::bind(self.bind_addr).await?;
|
let listener = TcpListener::bind(self.bind_addr).await?;
|
||||||
|
|
||||||
// Session layer.
|
// Session layer.
|
||||||
@@ -169,14 +176,13 @@ impl RestfulServer {
|
|||||||
let session_store = SqliteStore::new(self.db.inner());
|
let session_store = SqliteStore::new(self.db.inner());
|
||||||
session_store.migrate().await?;
|
session_store.migrate().await?;
|
||||||
|
|
||||||
self.delete_task.replace(
|
let delete_task: ScopedTask<tower_sessions::session_store::Result<()>> =
|
||||||
tokio::task::spawn(
|
tokio::task::spawn(
|
||||||
session_store
|
session_store
|
||||||
.clone()
|
.clone()
|
||||||
.continuously_delete_expired(tokio::time::Duration::from_secs(60)),
|
.continuously_delete_expired(tokio::time::Duration::from_secs(60)),
|
||||||
)
|
)
|
||||||
.into(),
|
.into();
|
||||||
);
|
|
||||||
|
|
||||||
// Generate a cryptographic key to sign the session cookie.
|
// Generate a cryptographic key to sign the session cookie.
|
||||||
let key = Key::generate();
|
let key = Key::generate();
|
||||||
@@ -216,19 +222,17 @@ impl RestfulServer {
|
|||||||
.layer(compression_layer);
|
.layer(compression_layer);
|
||||||
|
|
||||||
#[cfg(feature = "embed")]
|
#[cfg(feature = "embed")]
|
||||||
let app = if self.enable_web_embed {
|
let app = if let Some(web_router) = self.web_router.take() {
|
||||||
use axum_embed::ServeEmbed;
|
app.merge(web_router)
|
||||||
let service = ServeEmbed::<Assets>::new();
|
|
||||||
app.fallback_service(service)
|
|
||||||
} else {
|
} else {
|
||||||
app
|
app
|
||||||
};
|
};
|
||||||
|
|
||||||
let task = tokio::spawn(async move {
|
let serve_task: ScopedTask<()> = tokio::spawn(async move {
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
});
|
})
|
||||||
self.serve_task = Some(task.into());
|
.into();
|
||||||
|
|
||||||
Ok(())
|
Ok((serve_task, delete_task))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-10
@@ -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 easytier::common::scoped_task::ScopedTask;
|
||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use axum_embed::ServeEmbed;
|
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
/// Embed assets for web dashboard, build frontend first
|
/// Embed assets for web dashboard, build frontend first
|
||||||
@@ -10,30 +15,72 @@ use tokio::net::TcpListener;
|
|||||||
#[folder = "frontend/dist/"]
|
#[folder = "frontend/dist/"]
|
||||||
struct Assets;
|
struct Assets;
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
struct ApiMetaResponse {
|
||||||
|
api_host: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_api_meta(State(api_host): State<url::Url>) -> 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<url::Url>) -> Router {
|
||||||
|
let service = ServeEmbed::<Assets>::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 {
|
pub struct WebServer {
|
||||||
bind_addr: SocketAddr,
|
bind_addr: SocketAddr,
|
||||||
|
router: Router,
|
||||||
serve_task: Option<ScopedTask<()>>,
|
serve_task: Option<ScopedTask<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebServer {
|
impl WebServer {
|
||||||
pub async fn new(bind_addr: SocketAddr) -> anyhow::Result<Self> {
|
pub async fn new(bind_addr: SocketAddr, router: Router) -> anyhow::Result<Self> {
|
||||||
Ok(WebServer {
|
Ok(WebServer {
|
||||||
bind_addr,
|
bind_addr,
|
||||||
|
router,
|
||||||
serve_task: None,
|
serve_task: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(&mut self) -> Result<(), anyhow::Error> {
|
pub async fn start(self) -> Result<ScopedTask<()>, anyhow::Error> {
|
||||||
let listener = TcpListener::bind(self.bind_addr).await?;
|
let listener = TcpListener::bind(self.bind_addr).await?;
|
||||||
let service = ServeEmbed::<Assets>::new();
|
let app = self.router;
|
||||||
let app = Router::new().fallback_service(service);
|
|
||||||
|
|
||||||
let task = tokio::spawn(async move {
|
let task = tokio::spawn(async move {
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
});
|
})
|
||||||
|
.into();
|
||||||
|
|
||||||
self.serve_task = Some(task.into());
|
Ok(task)
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user