From 3b93e29c3e1487736aeb211d8f3e1ef37013af6a Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 27 Mar 2025 21:21:30 +0700 Subject: [PATCH] refactor: proxy server and clean up --- src-tauri/Cargo.toml | 4 + src-tauri/src/core/cmd.rs | 36 +++- src-tauri/src/core/mod.rs | 2 + src-tauri/src/core/server.rs | 201 ++++++++++++++++++ src-tauri/src/core/setup.rs | 4 +- src-tauri/src/core/state.rs | 13 ++ src-tauri/src/lib.rs | 29 +-- .../LocalServerLeftPanel/index.tsx | 2 +- 8 files changed, 261 insertions(+), 30 deletions(-) create mode 100644 src-tauri/src/core/server.rs create mode 100644 src-tauri/src/core/state.rs diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2772625b7..dc482f57f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,3 +29,7 @@ tar = "0.4" rand = "0.8" tauri-plugin-http = { version = "2", features = ["unsafe-headers"] } tauri-plugin-store = "2" +hyper = { version = "0.14", features = ["server"] } +reqwest = { version = "0.11", features = ["json"] } +tokio = { version = "1", features = ["full"] } +tracing = "0.1.41" diff --git a/src-tauri/src/core/cmd.rs b/src-tauri/src/core/cmd.rs index 8539c54f8..656fc8806 100644 --- a/src-tauri/src/core/cmd.rs +++ b/src-tauri/src/core/cmd.rs @@ -1,11 +1,8 @@ -use std::fs; -use tauri::AppHandle; - use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use tauri::Manager; +use std::{fs, path::PathBuf}; +use tauri::{AppHandle, Manager, State}; -use super::setup; +use super::{server, setup, state::AppState}; const CONFIGURATION_FILE_NAME: &str = "settings.json"; @@ -248,3 +245,30 @@ pub fn get_active_extensions(app: AppHandle) -> Vec { pub fn get_user_home_path(app: AppHandle) -> String { return get_app_configurations(app.clone()).data_folder; } + +#[tauri::command] +pub fn app_token(state: State<'_, AppState>) -> Option { + state.app_token.clone() +} + +#[tauri::command] +pub async fn start_server( + app: AppHandle, + host: String, + port: u16, + prefix: String, +) -> Result { + server::start_server( + host, + port, + prefix, + app_token(app.state()).unwrap(), + ).await.map_err(|e| e.to_string())?; + Ok(true) +} + +#[tauri::command] +pub async fn stop_server() -> Result<(), String> { + server::stop_server().await.map_err(|e| e.to_string())?; + Ok(()) +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 7ad92a077..5ef113aa4 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,3 +1,5 @@ pub mod cmd; pub mod fs; pub mod setup; +pub mod state; +pub mod server; \ No newline at end of file diff --git a/src-tauri/src/core/server.rs b/src-tauri/src/core/server.rs new file mode 100644 index 000000000..67e802564 --- /dev/null +++ b/src-tauri/src/core/server.rs @@ -0,0 +1,201 @@ +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Method, Request, Response, Server, StatusCode}; +use reqwest::Client; +use std::convert::Infallible; +use std::net::SocketAddr; +use std::sync::LazyLock; +use tokio::sync::Mutex; +use tokio::task::JoinHandle; +use tracing::{debug, error, info}; + +/// Server handle type for managing the proxy server lifecycle +type ServerHandle = JoinHandle>>; + +/// Global singleton for the current server instance +static SERVER_HANDLE: LazyLock>> = LazyLock::new(|| Mutex::new(None)); + +/// Configuration for the proxy server +#[derive(Clone)] +struct ProxyConfig { + upstream: String, + prefix: String, + auth_token: String, +} + +/// Removes a prefix from a path, ensuring proper formatting +fn remove_prefix(path: &str, prefix: &str) -> String { + debug!("Processing path: {}, removing prefix: {}", path, prefix); + + if !prefix.is_empty() && path.starts_with(prefix) { + let result = path[prefix.len()..].to_string(); + if result.is_empty() { + "/".to_string() + } else { + result + } + } else { + path.to_string() + } +} + +/// Determines the final destination path based on the original request path +fn get_destination_path(original_path: &str, prefix: &str) -> String { + let removed_prefix_path = remove_prefix(original_path, prefix); + + println!("Removed prefix path: {}", removed_prefix_path); + // Special paths don't need the /v1 prefix + if !original_path.contains(prefix) || removed_prefix_path.contains("/healthz") || removed_prefix_path.contains("/process") { + original_path.to_string() + } else { + format!("/v1{}", removed_prefix_path) + } +} + +/// Creates the full upstream URL for the proxied request +fn build_upstream_url(upstream: &str, path: &str) -> String { + let upstream_clean = upstream.trim_end_matches('/'); + let path_clean = path.trim_start_matches('/'); + + format!("{}/{}", upstream_clean, path_clean) +} + +/// Handles the proxy request logic +async fn proxy_request( + req: Request, + client: Client, + config: ProxyConfig, +) -> Result, hyper::Error> { + let original_path = req.uri().path(); + let path = get_destination_path(original_path, &config.prefix); + + // Block access to /configs endpoint + if path.contains("/configs") { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("Not Found")) + .unwrap()); + } + + // Build the outbound request + let upstream_url = build_upstream_url(&config.upstream, &path); + debug!("Proxying request to: {}", upstream_url); + + let mut outbound_req = client.request(req.method().clone(), &upstream_url); + + // Copy original headers + for (name, value) in req.headers() { + if name != hyper::header::HOST { // Skip host header + outbound_req = outbound_req.header(name, value); + } + } + + // Add authorization header + outbound_req = outbound_req.header("Authorization", format!("Bearer {}", config.auth_token)); + + // Send the request and handle the response + match outbound_req.body(req.into_body()).send().await { + Ok(response) => { + let status = response.status(); + debug!("Received response with status: {}", status); + + let mut builder = Response::builder().status(status); + + // Copy response headers + for (name, value) in response.headers() { + builder = builder.header(name, value); + } + + // Read response body + match response.bytes().await { + Ok(bytes) => Ok(builder.body(Body::from(bytes)).unwrap()), + Err(e) => { + error!("Failed to read response body: {}", e); + Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("Error reading upstream response")) + .unwrap()) + } + } + } + Err(e) => { + error!("Proxy request failed: {}", e); + Ok(Response::builder() + .status(StatusCode::BAD_GATEWAY) + .body(Body::from(format!("Upstream error: {}", e))) + .unwrap()) + } + } +} + +/// Starts the proxy server +pub async fn start_server( + host: String, + port: u16, + prefix: String, + auth_token: String, +) -> Result> { + // Check if server is already running + let mut handle_guard = SERVER_HANDLE.lock().await; + if handle_guard.is_some() { + return Err("Server is already running".into()); + } + + // Create server address + let addr: SocketAddr = format!("{}:{}", host, port) + .parse() + .map_err(|e| format!("Invalid address: {}", e))?; + + // Configure proxy settings + let config = ProxyConfig { + upstream: "http://127.0.0.1:39291".to_string(), + prefix, + auth_token, + }; + + // Create HTTP client + let client = Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build()?; + + // Create service handler + let make_svc = make_service_fn(move |_conn| { + let client = client.clone(); + let config = config.clone(); + + async move { + Ok::<_, Infallible>(service_fn(move |req| { + proxy_request(req, client.clone(), config.clone()) + })) + } + }); + + // Create and start the server + let server = Server::bind(&addr).serve(make_svc); + info!("Proxy server started on http://{}", addr); + + // Spawn server task + let server_handle = tokio::spawn(async move { + if let Err(e) = server.await { + error!("Server error: {}", e); + return Err(Box::new(e) as Box); + } + Ok(()) + }); + + *handle_guard = Some(server_handle); + Ok(true) +} + +/// Stops the currently running proxy server +pub async fn stop_server() -> Result<(), Box> { + let mut handle_guard = SERVER_HANDLE.lock().await; + + if let Some(handle) = handle_guard.take() { + handle.abort(); + info!("Proxy server stopped"); + } else { + debug!("No server was running"); + } + + Ok(()) +} diff --git a/src-tauri/src/core/setup.rs b/src-tauri/src/core/setup.rs index 7411a286c..8e9e03d2b 100644 --- a/src-tauri/src/core/setup.rs +++ b/src-tauri/src/core/setup.rs @@ -11,9 +11,7 @@ use tauri_plugin_shell::process::CommandEvent; use tauri_plugin_shell::ShellExt; use tauri_plugin_store::StoreExt; -use crate::AppState; - -use super::cmd::get_jan_extensions_path; +use super::{cmd::get_jan_extensions_path, state::AppState}; pub fn install_extensions(app: tauri::AppHandle, force: bool) -> Result<(), String> { let store = app.store("store.json").expect("Store not initialized"); diff --git a/src-tauri/src/core/state.rs b/src-tauri/src/core/state.rs new file mode 100644 index 000000000..8654c74b5 --- /dev/null +++ b/src-tauri/src/core/state.rs @@ -0,0 +1,13 @@ +use rand::{distributions::Alphanumeric, Rng}; + +pub struct AppState { + pub app_token: Option, +} + +pub fn generate_app_token() -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect() +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 46aaeb298..e7f94e3dd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,24 +1,10 @@ mod core; -use core::setup::{self, setup_engine_binaries, setup_sidecar}; +use core::{ + setup::{self, setup_engine_binaries, setup_sidecar}, + state::{generate_app_token, AppState}, +}; -use rand::{distributions::Alphanumeric, Rng}; -use tauri::{command, Emitter, State}; -struct AppState { - app_token: Option, -} - -#[command] -fn app_token(state: State<'_, AppState>) -> Option { - state.app_token.clone() -} - -fn generate_app_token() -> String { - rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(32) - .map(char::from) - .collect() -} +use tauri::Emitter; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { @@ -45,7 +31,10 @@ pub fn run() { core::cmd::open_app_directory, core::cmd::open_file_explorer, core::cmd::install_extensions, - app_token, + core::cmd::read_theme, + core::cmd::app_token, + core::cmd::start_server, + core::cmd::stop_server, ]) .manage(AppState { app_token: Some(generate_app_token()), diff --git a/web/screens/LocalServer/LocalServerLeftPanel/index.tsx b/web/screens/LocalServer/LocalServerLeftPanel/index.tsx index 99c2d7488..660ff305b 100644 --- a/web/screens/LocalServer/LocalServerLeftPanel/index.tsx +++ b/web/screens/LocalServer/LocalServerLeftPanel/index.tsx @@ -89,7 +89,7 @@ const LocalServerLeftPanel = () => { setIsLoading(true) const isStarted = await window.core?.api?.startServer({ host, - port, + port: parseInt(port), prefix, isCorsEnabled, isVerboseEnabled,