refactor: proxy server and clean up

This commit is contained in:
Louis 2025-03-27 21:21:30 +07:00
parent a85a98f295
commit e9d1731781
No known key found for this signature in database
GPG Key ID: 44FA9F4D33C37DE2
8 changed files with 261 additions and 30 deletions

View File

@ -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"

View File

@ -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<serde_json::Value> {
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<String> {
state.app_token.clone()
}
#[tauri::command]
pub async fn start_server(
app: AppHandle,
host: String,
port: u16,
prefix: String,
) -> Result<bool, String> {
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(())
}

View File

@ -1,3 +1,5 @@
pub mod cmd;
pub mod fs;
pub mod setup;
pub mod state;
pub mod server;

View File

@ -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<Result<(), Box<dyn std::error::Error + Send + Sync>>>;
/// Global singleton for the current server instance
static SERVER_HANDLE: LazyLock<Mutex<Option<ServerHandle>>> = 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<Body>,
client: Client,
config: ProxyConfig,
) -> Result<Response<Body>, 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<bool, Box<dyn std::error::Error + Send + Sync>> {
// 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<dyn std::error::Error + Send + Sync>);
}
Ok(())
});
*handle_guard = Some(server_handle);
Ok(true)
}
/// Stops the currently running proxy server
pub async fn stop_server() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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(())
}

View File

@ -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");

View File

@ -0,0 +1,13 @@
use rand::{distributions::Alphanumeric, Rng};
pub struct AppState {
pub app_token: Option<String>,
}
pub fn generate_app_token() -> String {
rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(32)
.map(char::from)
.collect()
}

View File

@ -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<String>,
}
#[command]
fn app_token(state: State<'_, AppState>) -> Option<String> {
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()),

View File

@ -89,7 +89,7 @@ const LocalServerLeftPanel = () => {
setIsLoading(true)
const isStarted = await window.core?.api?.startServer({
host,
port,
port: parseInt(port),
prefix,
isCorsEnabled,
isVerboseEnabled,