refactor: proxy server and clean up
This commit is contained in:
parent
a85a98f295
commit
e9d1731781
@ -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"
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
pub mod cmd;
|
||||
pub mod fs;
|
||||
pub mod setup;
|
||||
pub mod state;
|
||||
pub mod server;
|
||||
201
src-tauri/src/core/server.rs
Normal file
201
src-tauri/src/core/server.rs
Normal 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(())
|
||||
}
|
||||
@ -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");
|
||||
|
||||
13
src-tauri/src/core/state.rs
Normal file
13
src-tauri/src/core/state.rs
Normal 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()
|
||||
}
|
||||
@ -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()),
|
||||
|
||||
@ -89,7 +89,7 @@ const LocalServerLeftPanel = () => {
|
||||
setIsLoading(true)
|
||||
const isStarted = await window.core?.api?.startServer({
|
||||
host,
|
||||
port,
|
||||
port: parseInt(port),
|
||||
prefix,
|
||||
isCorsEnabled,
|
||||
isVerboseEnabled,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user