diff --git a/src-tauri/src/core/cmd.rs b/src-tauri/src/core/cmd.rs index 37b89dc7c..4023329d9 100644 --- a/src-tauri/src/core/cmd.rs +++ b/src-tauri/src/core/cmd.rs @@ -347,8 +347,10 @@ pub async fn start_server( host: String, port: u16, prefix: String, + trusted_hosts: Vec, ) -> Result { - server::start_server(host, port, prefix, app_token(app.state()).unwrap()) + let auth_token = app.state::().app_token.clone().unwrap_or_default(); + server::start_server(host, port, prefix, auth_token, trusted_hosts) .await .map_err(|e| e.to_string())?; Ok(true) diff --git a/src-tauri/src/core/server.rs b/src-tauri/src/core/server.rs index 042ecf135..bf13a2073 100644 --- a/src-tauri/src/core/server.rs +++ b/src-tauri/src/core/server.rs @@ -19,6 +19,7 @@ struct ProxyConfig { upstream: String, prefix: String, auth_token: String, + trusted_hosts: Vec, } /// Removes a prefix from a path, ensuring proper formatting @@ -69,6 +70,22 @@ async fn proxy_request( let original_path = req.uri().path(); let path = get_destination_path(original_path, &config.prefix); + // Verify Host header + if let Some(host) = req.headers().get(hyper::header::HOST) { + let host_str = host.to_str().unwrap_or(""); + if !is_valid_host(host_str, &config.trusted_hosts) { + return Ok(Response::builder() + .status(StatusCode::FORBIDDEN) + .body(Body::from("Invalid host header")) + .unwrap()); + } + } else { + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from("Missing host header")) + .unwrap()); + } + // Block access to /configs endpoint if path.contains("/configs") { return Ok(Response::builder() @@ -129,12 +146,29 @@ async fn proxy_request( } } +// Validates if the host header is allowed +fn is_valid_host(host: &str, trusted_hosts: &[String]) -> bool { + if host.is_empty() { + return false; + } + + let host_without_port = if host.starts_with('[') { host.split(']').next().unwrap_or(host).trim_start_matches('[') } else { host.split(':').next().unwrap_or(host) }; + let default_valid_hosts = ["localhost", "127.0.0.1"]; + + if default_valid_hosts.iter().any(|&valid| host_without_port.to_lowercase() == valid.to_lowercase()) { + return true; + } + + trusted_hosts.iter().any(|valid| host_without_port.to_lowercase() == valid.to_lowercase()) +} + /// Starts the proxy server pub async fn start_server( host: String, port: u16, prefix: String, auth_token: String, + trusted_hosts: Vec, ) -> Result> { // Check if server is already running let mut handle_guard = SERVER_HANDLE.lock().await; @@ -152,6 +186,7 @@ pub async fn start_server( upstream: "http://127.0.0.1:39291".to_string(), prefix, auth_token, + trusted_hosts, }; // Create HTTP client diff --git a/web-app/src/containers/TrustedHostsInput.tsx b/web-app/src/containers/TrustedHostsInput.tsx new file mode 100644 index 000000000..67ca19e7a --- /dev/null +++ b/web-app/src/containers/TrustedHostsInput.tsx @@ -0,0 +1,43 @@ +import { Input } from '@/components/ui/input' +import { useLocalApiServer } from '@/hooks/useLocalApiServer' +import { useState, useEffect } from 'react' + +export function TrustedHostsInput() { + const { trustedHosts, setTrustedHosts } = useLocalApiServer() + const [inputValue, setInputValue] = useState(trustedHosts.join(', ')) + + // Update input value when trustedHosts changes externally + useEffect(() => { + setInputValue(trustedHosts.join(', ')) + }, [trustedHosts]) + + const handleChange = (e: React.ChangeEvent) => { + const value = e.target.value + setInputValue(value) + } + + const handleBlur = () => { + // Split by comma and clean up each host + const hosts = inputValue + .split(',') + .map((host) => host.trim()) + .filter((host) => host.length > 0) + + // Remove duplicates + const uniqueHosts = [...new Set(hosts)] + + setTrustedHosts(uniqueHosts) + setInputValue(uniqueHosts.join(', ')) + } + + return ( + + ) +} diff --git a/web-app/src/hooks/useLocalApiServer.ts b/web-app/src/hooks/useLocalApiServer.ts index d6e8615d1..0ddf64835 100644 --- a/web-app/src/hooks/useLocalApiServer.ts +++ b/web-app/src/hooks/useLocalApiServer.ts @@ -21,6 +21,11 @@ type LocalApiServerState = { // Verbose server logs verboseLogs: boolean setVerboseLogs: (value: boolean) => void + // Trusted hosts + trustedHosts: string[] + addTrustedHost: (host: string) => void + removeTrustedHost: (host: string) => void + setTrustedHosts: (hosts: string[]) => void } export const useLocalApiServer = create()( @@ -38,6 +43,16 @@ export const useLocalApiServer = create()( setCorsEnabled: (value) => set({ corsEnabled: value }), verboseLogs: true, setVerboseLogs: (value) => set({ verboseLogs: value }), + trustedHosts: [], + addTrustedHost: (host) => + set((state) => ({ + trustedHosts: [...state.trustedHosts, host], + })), + removeTrustedHost: (host) => + set((state) => ({ + trustedHosts: state.trustedHosts.filter((h) => h !== host), + })), + setTrustedHosts: (hosts) => set({ trustedHosts: hosts }), }), { name: localStorageKey.settingLocalApiServer, diff --git a/web-app/src/routes/settings/local-api-server.tsx b/web-app/src/routes/settings/local-api-server.tsx index d6a13ac36..3f935b333 100644 --- a/web-app/src/routes/settings/local-api-server.tsx +++ b/web-app/src/routes/settings/local-api-server.tsx @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next' import { ServerHostSwitcher } from '@/containers/ServerHostSwitcher' import { PortInput } from '@/containers/PortInput' import { ApiPrefixInput } from '@/containers/ApiPrefixInput' +import { TrustedHostsInput } from '@/containers/TrustedHostsInput' import { useLocalApiServer } from '@/hooks/useLocalApiServer' import { WebviewWindow } from '@tauri-apps/api/webviewWindow' import { useAppState } from '@/hooks/useAppState' @@ -31,6 +32,7 @@ function LocalAPIServer() { serverHost, serverPort, apiPrefix, + trustedHosts, } = useLocalApiServer() const { serverStatus, setServerStatus } = useAppState() @@ -43,6 +45,7 @@ function LocalAPIServer() { host: serverHost, port: serverPort, prefix: apiPrefix, + trustedHosts, isCorsEnabled: corsEnabled, isVerboseEnabled: verboseLogs, }) @@ -179,6 +182,14 @@ function LocalAPIServer() { )} actions={} /> + } + /> {/* Advanced Settings */}