Merge pull request #5186 from menloresearch/fix/headers-validations-for-local-api-server
Fix: headers validations for local api server
This commit is contained in:
commit
30acc6f493
@ -347,8 +347,10 @@ pub async fn start_server(
|
|||||||
host: String,
|
host: String,
|
||||||
port: u16,
|
port: u16,
|
||||||
prefix: String,
|
prefix: String,
|
||||||
|
trusted_hosts: Vec<String>,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
server::start_server(host, port, prefix, app_token(app.state()).unwrap())
|
let auth_token = app.state::<AppState>().app_token.clone().unwrap_or_default();
|
||||||
|
server::start_server(host, port, prefix, auth_token, trusted_hosts)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
|||||||
@ -19,6 +19,7 @@ struct ProxyConfig {
|
|||||||
upstream: String,
|
upstream: String,
|
||||||
prefix: String,
|
prefix: String,
|
||||||
auth_token: String,
|
auth_token: String,
|
||||||
|
trusted_hosts: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes a prefix from a path, ensuring proper formatting
|
/// Removes a prefix from a path, ensuring proper formatting
|
||||||
@ -69,6 +70,22 @@ async fn proxy_request(
|
|||||||
let original_path = req.uri().path();
|
let original_path = req.uri().path();
|
||||||
let path = get_destination_path(original_path, &config.prefix);
|
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
|
// Block access to /configs endpoint
|
||||||
if path.contains("/configs") {
|
if path.contains("/configs") {
|
||||||
return Ok(Response::builder()
|
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
|
/// Starts the proxy server
|
||||||
pub async fn start_server(
|
pub async fn start_server(
|
||||||
host: String,
|
host: String,
|
||||||
port: u16,
|
port: u16,
|
||||||
prefix: String,
|
prefix: String,
|
||||||
auth_token: String,
|
auth_token: String,
|
||||||
|
trusted_hosts: Vec<String>,
|
||||||
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
// Check if server is already running
|
// Check if server is already running
|
||||||
let mut handle_guard = SERVER_HANDLE.lock().await;
|
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(),
|
upstream: "http://127.0.0.1:39291".to_string(),
|
||||||
prefix,
|
prefix,
|
||||||
auth_token,
|
auth_token,
|
||||||
|
trusted_hosts,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create HTTP client
|
// Create HTTP client
|
||||||
|
|||||||
43
web-app/src/containers/TrustedHostsInput.tsx
Normal file
43
web-app/src/containers/TrustedHostsInput.tsx
Normal file
@ -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<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
className="w-full h-8 text-sm"
|
||||||
|
placeholder="Enter trusted hosts"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -21,6 +21,11 @@ type LocalApiServerState = {
|
|||||||
// Verbose server logs
|
// Verbose server logs
|
||||||
verboseLogs: boolean
|
verboseLogs: boolean
|
||||||
setVerboseLogs: (value: boolean) => void
|
setVerboseLogs: (value: boolean) => void
|
||||||
|
// Trusted hosts
|
||||||
|
trustedHosts: string[]
|
||||||
|
addTrustedHost: (host: string) => void
|
||||||
|
removeTrustedHost: (host: string) => void
|
||||||
|
setTrustedHosts: (hosts: string[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useLocalApiServer = create<LocalApiServerState>()(
|
export const useLocalApiServer = create<LocalApiServerState>()(
|
||||||
@ -38,6 +43,16 @@ export const useLocalApiServer = create<LocalApiServerState>()(
|
|||||||
setCorsEnabled: (value) => set({ corsEnabled: value }),
|
setCorsEnabled: (value) => set({ corsEnabled: value }),
|
||||||
verboseLogs: true,
|
verboseLogs: true,
|
||||||
setVerboseLogs: (value) => set({ verboseLogs: value }),
|
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,
|
name: localStorageKey.settingLocalApiServer,
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { ServerHostSwitcher } from '@/containers/ServerHostSwitcher'
|
import { ServerHostSwitcher } from '@/containers/ServerHostSwitcher'
|
||||||
import { PortInput } from '@/containers/PortInput'
|
import { PortInput } from '@/containers/PortInput'
|
||||||
import { ApiPrefixInput } from '@/containers/ApiPrefixInput'
|
import { ApiPrefixInput } from '@/containers/ApiPrefixInput'
|
||||||
|
import { TrustedHostsInput } from '@/containers/TrustedHostsInput'
|
||||||
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
|
import { useLocalApiServer } from '@/hooks/useLocalApiServer'
|
||||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||||
import { useAppState } from '@/hooks/useAppState'
|
import { useAppState } from '@/hooks/useAppState'
|
||||||
@ -31,6 +32,7 @@ function LocalAPIServer() {
|
|||||||
serverHost,
|
serverHost,
|
||||||
serverPort,
|
serverPort,
|
||||||
apiPrefix,
|
apiPrefix,
|
||||||
|
trustedHosts,
|
||||||
} = useLocalApiServer()
|
} = useLocalApiServer()
|
||||||
|
|
||||||
const { serverStatus, setServerStatus } = useAppState()
|
const { serverStatus, setServerStatus } = useAppState()
|
||||||
@ -43,6 +45,7 @@ function LocalAPIServer() {
|
|||||||
host: serverHost,
|
host: serverHost,
|
||||||
port: serverPort,
|
port: serverPort,
|
||||||
prefix: apiPrefix,
|
prefix: apiPrefix,
|
||||||
|
trustedHosts,
|
||||||
isCorsEnabled: corsEnabled,
|
isCorsEnabled: corsEnabled,
|
||||||
isVerboseEnabled: verboseLogs,
|
isVerboseEnabled: verboseLogs,
|
||||||
})
|
})
|
||||||
@ -179,6 +182,14 @@ function LocalAPIServer() {
|
|||||||
)}
|
)}
|
||||||
actions={<ApiPrefixInput />}
|
actions={<ApiPrefixInput />}
|
||||||
/>
|
/>
|
||||||
|
<CardItem
|
||||||
|
title="Trusted Hosts"
|
||||||
|
description="Add trusted hosts that can access the API server"
|
||||||
|
className={cn(
|
||||||
|
isServerRunning && 'opacity-50 pointer-events-none'
|
||||||
|
)}
|
||||||
|
actions={<TrustedHostsInput />}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Advanced Settings */}
|
{/* Advanced Settings */}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user