starfire / src /secure_api.rs
system's picture
system HF Staff
Deploy native Starfire runtime from befef3b (part 2)
661a2e7 verified
Raw
History Blame Contribute Delete
40.2 kB
//! Token-free bounded HTTP boundary.
//!
//! Local loopback API use may retain the operator runtime. Any non-loopback or
//! explicitly hosted deployment is fail-closed unless it uses the PUB-1
//! request-ephemeral mode. Public mode exposes only `/`, `/health`, and
//! `POST /chat`; every chat runs in a fresh child process and temporary data
//! directory that is destroyed after one response.
use anyhow::{anyhow, Context, Result};
use serde::Deserialize;
use serde_json::{json, Value};
use star::Runtime;
use std::collections::{hash_map::DefaultHasher, HashMap};
use std::fs;
use std::hash::{Hash, Hasher};
use std::io::{Read, Write};
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tracing::{info, warn};
pub const MAX_REQUEST_BODY_BYTES: usize = 256 * 1024;
const DEFAULT_RATE_LIMIT_PER_MINUTE: u32 = 120;
const RATE_WINDOW_SECONDS: u64 = 60;
const MAX_TRACKED_CLIENTS: usize = 4_096;
const PUBLIC_HISTORY_LIMIT: usize = 24;
const PUBLIC_HISTORY_ITEM_BYTES: usize = 8 * 1024;
const PUBLIC_MESSAGE_BYTES: usize = 16 * 1024;
const PUBLIC_WORKER_TIMEOUT_SECONDS: u64 = 90;
const PUBLIC_WORKER_OUTPUT_BYTES: u64 = 1024 * 1024;
static PUBLIC_REQUEST_COUNTER: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BoundaryMode {
LocalShared,
PublicRequestEphemeral,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProxyPolicy {
DirectPeer,
ForwardedForLast,
}
#[derive(Debug, Clone)]
struct SecurityConfig {
rate_limit_per_minute: u32,
mode: BoundaryMode,
proxy_policy: ProxyPolicy,
rate_probe_enabled: bool,
}
impl SecurityConfig {
fn from_env(host: &str) -> Result<Self> {
let rate_limit_per_minute = std::env::var("STARFIRE_RATE_LIMIT_PER_MINUTE")
.ok()
.and_then(|value| value.parse::<u32>().ok())
.filter(|value| (1..=10_000).contains(value))
.unwrap_or(DEFAULT_RATE_LIMIT_PER_MINUTE);
let requested_mode = std::env::var("STARFIRE_PUBLIC_RUNTIME_MODE").unwrap_or_default();
let hosted = env_truthy("STARFIRE_HOSTED_MODE");
let loopback = host_is_loopback(host);
let mode = if requested_mode == "request_ephemeral" {
BoundaryMode::PublicRequestEphemeral
} else if !requested_mode.is_empty() {
return Err(anyhow!(
"unsupported STARFIRE_PUBLIC_RUNTIME_MODE: {}",
requested_mode
));
} else if hosted {
// STARFIRE_HOSTED_MODE is the established deployment declaration.
// Hosted mode must fail safe into request-ephemeral public isolation,
// never into the legacy shared local runtime.
BoundaryMode::PublicRequestEphemeral
} else if !loopback {
return Err(anyhow!(
"non-loopback binding requires STARFIRE_PUBLIC_RUNTIME_MODE=request_ephemeral"
));
} else {
BoundaryMode::LocalShared
};
let proxy_policy = match mode {
BoundaryMode::LocalShared => ProxyPolicy::DirectPeer,
BoundaryMode::PublicRequestEphemeral => {
match std::env::var("STARFIRE_TRUSTED_PROXY_HEADERS").as_deref() {
Ok("render") => ProxyPolicy::ForwardedForLast,
_ if hosted => ProxyPolicy::ForwardedForLast,
_ => {
return Err(anyhow!(
"non-hosted public mode requires STARFIRE_TRUSTED_PROXY_HEADERS=render"
))
}
}
}
};
Self::from_values(
rate_limit_per_minute,
mode,
proxy_policy,
hosted || env_truthy("STARFIRE_RATE_LIMIT_PROBE"),
)
}
fn from_values(
rate_limit_per_minute: u32,
mode: BoundaryMode,
proxy_policy: ProxyPolicy,
rate_probe_enabled: bool,
) -> Result<Self> {
if rate_limit_per_minute == 0 {
return Err(anyhow!("request rate limit must be greater than zero"));
}
Ok(Self {
rate_limit_per_minute,
mode,
proxy_policy,
rate_probe_enabled,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RoutePolicy {
Health,
Preflight,
PublicRoot,
PublicChat,
ForwardLocal,
NotFound,
}
fn route_policy(mode: BoundaryMode, method: &str, path: &str) -> RoutePolicy {
if method == "OPTIONS" {
return RoutePolicy::Preflight;
}
if method == "GET" && path == "/health" {
return RoutePolicy::Health;
}
match mode {
BoundaryMode::PublicRequestEphemeral => match (method, path) {
("GET", "/") => RoutePolicy::PublicRoot,
("POST", "/chat") => RoutePolicy::PublicChat,
_ => RoutePolicy::NotFound,
},
BoundaryMode::LocalShared => RoutePolicy::ForwardLocal,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum BoundaryError {
PayloadTooLarge,
RateLimited { retry_after: u64 },
BadRequest,
WorkerTimeout,
Internal,
}
impl BoundaryError {
fn response(self) -> ApiResponse {
match self {
Self::PayloadTooLarge => ApiResponse::error(
413,
"payload_too_large",
"Request body exceeds the 262144-byte limit",
),
Self::RateLimited { retry_after } => {
ApiResponse::error(429, "rate_limited", "Too many requests")
.with_header("Retry-After", retry_after.to_string())
}
Self::BadRequest => {
ApiResponse::error(400, "bad_request", "Malformed or incomplete JSON request")
}
Self::WorkerTimeout => {
ApiResponse::error(503, "worker_timeout", "Temporary public session timed out")
}
Self::Internal => ApiResponse::error(500, "internal_error", "Internal server error"),
}
}
}
#[derive(Debug, Clone)]
struct ApiResponse {
status: u16,
body: String,
content_type: &'static str,
headers: Vec<(&'static str, String)>,
}
impl ApiResponse {
fn new(status: u16, body: String) -> Self {
Self {
status,
body,
content_type: "application/json; charset=utf-8",
headers: Vec::new(),
}
}
fn html(status: u16, body: String) -> Self {
Self {
status,
body,
content_type: "text/html; charset=utf-8",
headers: Vec::new(),
}
}
fn error(status: u16, code: &str, message: &str) -> Self {
Self::new(
status,
json!({
"error": message,
"code": code,
})
.to_string(),
)
}
fn with_header(mut self, name: &'static str, value: impl Into<String>) -> Self {
self.headers.push((name, value.into()));
self
}
}
#[derive(Debug, Clone, Copy)]
struct ClientWindow {
started_at: u64,
count: u32,
}
#[derive(Debug)]
struct RateLimiter {
limit: u32,
window_seconds: u64,
clients: HashMap<String, ClientWindow>,
}
impl RateLimiter {
fn new(limit: u32) -> Self {
Self {
limit,
window_seconds: RATE_WINDOW_SECONDS,
clients: HashMap::new(),
}
}
fn check(&mut self, client: &str) -> std::result::Result<(), BoundaryError> {
self.check_at(client, unix_seconds())
}
fn check_at(&mut self, client: &str, now: u64) -> std::result::Result<(), BoundaryError> {
self.clients.retain(|_, window| {
now.saturating_sub(window.started_at) < self.window_seconds.saturating_mul(2)
});
if !self.clients.contains_key(client) && self.clients.len() >= MAX_TRACKED_CLIENTS {
let oldest = self
.clients
.iter()
.min_by(|(left_key, left), (right_key, right)| {
left.started_at
.cmp(&right.started_at)
.then_with(|| left_key.cmp(right_key))
})
.map(|(key, _)| key.clone());
if let Some(oldest) = oldest {
self.clients.remove(&oldest);
}
}
let window = self
.clients
.entry(client.to_owned())
.or_insert(ClientWindow {
started_at: now,
count: 0,
});
let elapsed = now.saturating_sub(window.started_at);
if elapsed >= self.window_seconds {
*window = ClientWindow {
started_at: now,
count: 0,
};
}
if window.count >= self.limit {
let retry_after = self
.window_seconds
.saturating_sub(now.saturating_sub(window.started_at))
.max(1);
return Err(BoundaryError::RateLimited { retry_after });
}
window.count = window.count.saturating_add(1);
Ok(())
}
}
#[derive(Debug, Clone)]
struct ClientIdentity {
key: String,
source: &'static str,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct PublicChatRequest {
message: String,
#[serde(default)]
history: Vec<PublicHistoryItem>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct PublicHistoryItem {
role: String,
content: String,
}
pub fn start(host: &str, port: u16, data_dir: &Path) -> Result<()> {
let config = SecurityConfig::from_env(host)?;
let backend_port = match config.mode {
BoundaryMode::LocalShared => {
let runtime = Runtime::new(data_dir)?;
let runtime = Arc::new(Mutex::new(runtime));
let port = backend_port(port);
let backend_data_dir = data_dir.to_path_buf();
thread::Builder::new()
.name("starfire-local-backend".to_owned())
.spawn(move || {
if let Err(error) = start_backend(runtime, port, &backend_data_dir) {
warn!("Starfire loopback backend exited: {}", error);
}
})
.context("spawn Starfire loopback backend")?;
wait_for_backend(port)?;
Some(port)
}
BoundaryMode::PublicRequestEphemeral => None,
};
let address = format!("{}:{}", host, port);
let server = tiny_http::Server::http(&address)
.map_err(|error| anyhow!("Starfire public API server error: {}", error))?;
let mut rate_limiter = RateLimiter::new(config.rate_limit_per_minute);
let probe_salt = probe_salt();
info!(
"Starfire API ready at http://{} ({:?}; operator persistence exposed: false in public mode)",
address, config.mode
);
for request in server.incoming_requests() {
if let Err(error) = handle_request(
request,
backend_port,
data_dir,
&config,
&mut rate_limiter,
probe_salt,
) {
warn!("Starfire request transport failure: {}", error);
}
}
Ok(())
}
#[cfg(feature = "starfire-live")]
fn start_backend(runtime: Arc<Mutex<Runtime>>, port: u16, data_dir: &Path) -> Result<()> {
crate::live_api::start(runtime, "127.0.0.1", port, data_dir)
}
#[cfg(not(feature = "starfire-live"))]
fn start_backend(runtime: Arc<Mutex<Runtime>>, port: u16, _data_dir: &Path) -> Result<()> {
star::api::start(runtime, "127.0.0.1", port)
}
fn backend_port(external_port: u16) -> u16 {
if let Some(parsed) = std::env::var("STARFIRE_SECURE_BACKEND_PORT")
.ok()
.and_then(|value| value.parse::<u16>().ok())
.filter(|parsed| *parsed != external_port)
{
return parsed;
}
external_port.checked_add(2).unwrap_or(18_082)
}
fn wait_for_backend(port: u16) -> Result<()> {
let url = format!("http://127.0.0.1:{}/health", port);
for _ in 0..400 {
if ureq::get(&url).call().is_ok() {
return Ok(());
}
thread::sleep(Duration::from_millis(50));
}
Err(anyhow!(
"Starfire loopback backend did not become ready at {}",
url
))
}
fn handle_request(
mut request: tiny_http::Request,
backend_port: Option<u16>,
_operator_data_dir: &Path,
config: &SecurityConfig,
rate_limiter: &mut RateLimiter,
probe_salt: u64,
) -> Result<()> {
let method = request.method().as_str().to_owned();
let path = request.url().to_owned();
let policy = route_policy(config.mode, &method, &path);
if policy == RoutePolicy::Preflight {
return respond(request, ApiResponse::new(204, String::new()));
}
if policy == RoutePolicy::Health {
let response = match config.mode {
BoundaryMode::PublicRequestEphemeral => public_health_response(),
BoundaryMode::LocalShared => forward(
"GET",
"/health",
&[],
backend_port.ok_or_else(|| anyhow!("local backend port missing"))?,
),
};
return respond(request, response);
}
let identity = client_identity(&request, config.proxy_policy);
let probe_requested =
config.rate_probe_enabled && request_header(&request, "X-Starfire-Rate-Probe") == Some("1");
if let Err(error) = rate_limiter.check(&identity.key) {
let response =
attach_probe_headers(error.response(), probe_requested, &identity, probe_salt);
return respond(request, response);
}
if policy == RoutePolicy::NotFound {
let response = attach_probe_headers(
ApiResponse::new(404, json!({ "error": "Not found" }).to_string()),
probe_requested,
&identity,
probe_salt,
);
return respond(request, response);
}
if content_length(&request).is_some_and(|length| length > MAX_REQUEST_BODY_BYTES) {
let response = attach_probe_headers(
BoundaryError::PayloadTooLarge.response(),
probe_requested,
&identity,
probe_salt,
);
return respond(request, response);
}
let mut body = Vec::new();
request
.as_reader()
.take((MAX_REQUEST_BODY_BYTES + 1) as u64)
.read_to_end(&mut body)
.context("read bounded request body")?;
if body.len() > MAX_REQUEST_BODY_BYTES {
let response = attach_probe_headers(
BoundaryError::PayloadTooLarge.response(),
probe_requested,
&identity,
probe_salt,
);
return respond(request, response);
}
if validate_json_request(config.mode, &method, &path, &body).is_err() {
let response = attach_probe_headers(
BoundaryError::BadRequest.response(),
probe_requested,
&identity,
probe_salt,
);
return respond(request, response);
}
let response = match policy {
RoutePolicy::PublicRoot => public_root_response(),
RoutePolicy::PublicChat => run_public_chat_worker(&body),
RoutePolicy::ForwardLocal => forward(
&method,
&path,
&body,
backend_port.ok_or_else(|| anyhow!("local backend port missing"))?,
),
RoutePolicy::Health | RoutePolicy::Preflight | RoutePolicy::NotFound => {
BoundaryError::Internal.response()
}
};
let response = attach_probe_headers(response, probe_requested, &identity, probe_salt);
respond(request, response)
}
fn public_health_response() -> ApiResponse {
ApiResponse::new(
200,
json!({
"status": "ok",
"mode": "public_request_ephemeral",
"state_scope": "single_request",
"persistent": false,
"operator_state_access": false,
"sensitive_routes_exposed": false,
"ei_collection_enabled": false,
"live_learning_enabled": false
})
.to_string(),
)
}
fn public_root_response() -> ApiResponse {
ApiResponse::html(200, PUBLIC_CHAT_UI.to_owned()).with_header(
"Content-Security-Policy",
"default-src 'self'; connect-src 'self'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'self'",
)
}
const PUBLIC_CHAT_UI: &str = r#"<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Starfire</title>
<style>
:root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, sans-serif; background: #10111a; color: #f4f0ff; }
* { box-sizing: border-box; } body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px; }
main { width: min(760px, 100%); min-height: min(760px, calc(100vh - 48px)); display: flex; flex-direction: column; background: #181a27; border: 1px solid #333650; border-radius: 20px; box-shadow: 0 18px 70px #0008; overflow: hidden; }
header { padding: 24px 26px 18px; background: linear-gradient(120deg, #31255d, #1b2141); border-bottom: 1px solid #45456d; }
h1 { margin: 0; font-size: 1.45rem; } header p { margin: 7px 0 0; color: #cbc9e6; font-size: .92rem; }
#conversation { flex: 1; overflow-y: auto; padding: 22px; display: flex; flex-direction: column; gap: 14px; }
.message { max-width: 88%; white-space: pre-wrap; overflow-wrap: anywhere; padding: 12px 14px; border-radius: 14px; line-height: 1.45; }
.assistant { align-self: flex-start; background: #292c42; border-bottom-left-radius: 4px; }
.user { align-self: flex-end; background: #6f4ed6; border-bottom-right-radius: 4px; }
.status { align-self: flex-start; color: #b8b8ce; font-size: .88rem; padding: 4px; }
form { display: flex; gap: 10px; padding: 16px; border-top: 1px solid #333650; background: #141622; }
textarea { flex: 1; resize: vertical; min-height: 48px; max-height: 160px; padding: 12px; border: 1px solid #4b4e6d; border-radius: 11px; background: #22243a; color: inherit; font: inherit; }
button { border: 0; border-radius: 11px; padding: 0 18px; background: #8a67ff; color: #fff; font: inherit; font-weight: 650; cursor: pointer; }
button:disabled, textarea:disabled { opacity: .6; cursor: wait; } .secondary { background: transparent; color: #cbc9e6; border: 1px solid #4b4e6d; }
footer { padding: 0 18px 14px; color: #9898b7; font-size: .78rem; } @media (max-width: 560px) { body { padding: 0; } main { min-height: 100vh; border-radius: 0; border: 0; } form { flex-wrap: wrap; } form button { min-height: 42px; } }
</style>
</head>
<body>
<main>
<header><h1>Starfire</h1><p>Public, request-ephemeral conversation. Nothing is saved between requests.</p></header>
<section id="conversation" aria-live="polite"><div class="message assistant">Hi — what would you like to explore?</div></section>
<form id="chat-form"><textarea id="message" aria-label="Message" placeholder="Write a message…" required maxlength="16384"></textarea><button id="send" type="submit">Send</button><button id="clear" class="secondary" type="button">Clear</button></form>
<footer>Temporary browser history is kept only in this tab to provide conversational context.</footer>
</main>
<script>
(() => {
const conversation = document.getElementById('conversation');
const form = document.getElementById('chat-form');
const input = document.getElementById('message');
const send = document.getElementById('send');
const clear = document.getElementById('clear');
let history = [];
function add(role, text, kind = 'message') { const item = document.createElement('div'); item.className = `${kind} ${role}`; item.textContent = text; conversation.append(item); conversation.scrollTop = conversation.scrollHeight; return item; }
form.addEventListener('submit', async (event) => {
event.preventDefault(); const message = input.value.trim(); if (!message) return;
add('user', message); history.push({ role: 'user', content: message }); input.value = ''; input.disabled = true; send.disabled = true;
const pending = add('status', 'Starfire is thinking…', 'status');
try {
const response = await fetch('/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, history: history.slice(0, -1) }) });
const payload = await response.json(); if (!response.ok || typeof payload.response !== 'string') throw new Error(payload.error || 'The request could not be completed.');
pending.remove(); add('assistant', payload.response); history.push({ role: 'assistant', content: payload.response });
} catch (error) { pending.remove(); add('status', error instanceof Error ? error.message : 'The request could not be completed.', 'status'); history.pop(); }
finally { input.disabled = false; send.disabled = false; input.focus(); }
});
clear.addEventListener('click', () => { history = []; conversation.replaceChildren(); add('assistant', 'Conversation cleared. What would you like to explore?'); input.focus(); });
})();
</script>
</body>
</html>"#;
fn run_public_chat_worker(body: &[u8]) -> ApiResponse {
let session = match EphemeralDirectory::create() {
Ok(session) => session,
Err(error) => {
warn!("create public ephemeral directory: {}", error);
return BoundaryError::Internal.response();
}
};
let request_id = session.request_id().to_owned();
let executable = match std::env::current_exe() {
Ok(executable) => executable,
Err(error) => {
warn!("resolve public worker executable: {}", error);
return BoundaryError::Internal.response();
}
};
let mut child = match Command::new(executable)
.arg("--data-dir")
.arg(session.path())
.arg("public-chat-once")
.env("STARFIRE_HOSTED_MODE", "false")
.env_remove("STARFIRE_PUBLIC_RUNTIME_MODE")
.env_remove("STARFIRE_TRUSTED_PROXY_HEADERS")
.env_remove("STARFIRE_RATE_LIMIT_PROBE")
.env("STARFIRE_PUBLIC_REQUEST_ID", &request_id)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
{
Ok(child) => child,
Err(error) => {
warn!("spawn public chat worker: {}", error);
return BoundaryError::Internal.response();
}
};
if let Some(mut stdin) = child.stdin.take() {
if let Err(error) = stdin.write_all(body) {
let _ = child.kill();
let _ = child.wait();
warn!("write public chat worker request: {}", error);
return BoundaryError::Internal.response();
}
}
let deadline = Instant::now() + Duration::from_secs(PUBLIC_WORKER_TIMEOUT_SECONDS);
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break status,
Ok(None) if Instant::now() < deadline => {
thread::sleep(Duration::from_millis(20));
}
Ok(None) => {
let _ = child.kill();
let _ = child.wait();
return BoundaryError::WorkerTimeout.response();
}
Err(error) => {
let _ = child.kill();
let _ = child.wait();
warn!("wait for public chat worker: {}", error);
return BoundaryError::Internal.response();
}
}
};
let mut output = Vec::new();
if let Some(stdout) = child.stdout.take() {
let mut bounded = stdout.take(PUBLIC_WORKER_OUTPUT_BYTES + 1);
if let Err(error) = bounded.read_to_end(&mut output) {
warn!("read public chat worker response: {}", error);
return BoundaryError::Internal.response();
}
}
if !status.success() || output.len() as u64 > PUBLIC_WORKER_OUTPUT_BYTES {
warn!(
"public chat worker failed: status={}, output_bytes={}",
status,
output.len()
);
return BoundaryError::Internal.response();
}
let value = match worker_json_value(&output) {
Some(value) => value,
None => {
warn!("public chat worker returned no valid JSON payload");
return BoundaryError::Internal.response();
}
};
let valid = value.get("response").and_then(Value::as_str).is_some()
&& value
.get("public_session")
.and_then(|session| session.get("request_id"))
.and_then(Value::as_str)
== Some(request_id.as_str())
&& value
.pointer("/public_session/persistence")
.and_then(Value::as_bool)
== Some(false)
&& value
.pointer("/public_session/operator_state_access")
.and_then(Value::as_bool)
== Some(false);
if !valid {
warn!("public chat worker returned an invalid authority envelope");
return BoundaryError::Internal.response();
}
ApiResponse::new(200, value.to_string())
}
fn worker_json_value(output: &[u8]) -> Option<Value> {
output
.split(|byte| *byte == b'\n')
.rev()
.find_map(|line| serde_json::from_slice::<Value>(line).ok())
}
struct EphemeralDirectory {
path: PathBuf,
request_id: String,
}
impl EphemeralDirectory {
fn create() -> Result<Self> {
let counter = PUBLIC_REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let request_id = format!("public-{}-{}-{}", std::process::id(), unix_nanos(), counter);
let root = std::env::temp_dir().join("starfire-public-requests");
fs::create_dir_all(&root)
.with_context(|| format!("create public request root {}", root.display()))?;
let path = root.join(&request_id);
fs::create_dir(&path)
.with_context(|| format!("create public request directory {}", path.display()))?;
Ok(Self { path, request_id })
}
fn path(&self) -> &Path {
&self.path
}
fn request_id(&self) -> &str {
&self.request_id
}
}
impl Drop for EphemeralDirectory {
fn drop(&mut self) {
if let Err(error) = fs::remove_dir_all(&self.path) {
warn!(
"remove public request directory {}: {}",
self.path.display(),
error
);
}
}
}
fn forward(method: &str, path: &str, body: &[u8], port: u16) -> ApiResponse {
let url = format!("http://127.0.0.1:{}{}", port, path);
let forwarded = ureq::request(method, &url).set("Content-Type", "application/json");
let result = if body.is_empty() {
forwarded.call()
} else {
forwarded.send_bytes(body)
};
match result {
Ok(response) => {
let status = response.status();
match response.into_string() {
Ok(body) => map_backend_response(status, body),
Err(_) => BoundaryError::Internal.response(),
}
}
Err(ureq::Error::Status(status, response)) => {
let body = response
.into_string()
.unwrap_or_else(|_| json!({ "error": "Backend request failed" }).to_string());
map_backend_response(status, body)
}
Err(ureq::Error::Transport(_)) => BoundaryError::Internal.response(),
}
}
fn map_backend_response(status: u16, body: String) -> ApiResponse {
if status == 404 {
return ApiResponse::new(404, json!({ "error": "Not found" }).to_string());
}
if status >= 500 {
return BoundaryError::Internal.response();
}
if status >= 400 {
return ApiResponse::new(status, body);
}
let backend_error = serde_json::from_str::<Value>(&body).ok().and_then(|value| {
value
.get("error")
.and_then(Value::as_str)
.map(str::to_owned)
});
match backend_error.as_deref() {
Some(error) if error.starts_with("Invalid request:") => {
BoundaryError::BadRequest.response()
}
Some(error)
if error.starts_with("Lock poisoned:")
|| error.starts_with("Chat error:")
|| error.starts_with("Serialization:") =>
{
BoundaryError::Internal.response()
}
_ => ApiResponse::new(status, body),
}
}
fn validate_json_request(
mode: BoundaryMode,
method: &str,
path: &str,
body: &[u8],
) -> std::result::Result<(), BoundaryError> {
if method != "POST" {
return Ok(());
}
if mode == BoundaryMode::PublicRequestEphemeral && path == "/chat" {
let request: PublicChatRequest =
serde_json::from_slice(body).map_err(|_| BoundaryError::BadRequest)?;
if request.message.trim().is_empty() || request.message.len() > PUBLIC_MESSAGE_BYTES {
return Err(BoundaryError::BadRequest);
}
if request.history.len() > PUBLIC_HISTORY_LIMIT {
return Err(BoundaryError::BadRequest);
}
for item in request.history {
if !matches!(item.role.as_str(), "user" | "assistant")
|| item.content.trim().is_empty()
|| item.content.len() > PUBLIC_HISTORY_ITEM_BYTES
{
return Err(BoundaryError::BadRequest);
}
}
return Ok(());
}
let required_field = match path {
"/chat" => Some("message"),
"/reason" => Some("query"),
"/remember" => Some("topic"),
_ => return Ok(()),
};
let value: Value = serde_json::from_slice(body).map_err(|_| BoundaryError::BadRequest)?;
let object = value.as_object().ok_or(BoundaryError::BadRequest)?;
if let Some(field) = required_field {
let valid = object
.get(field)
.and_then(Value::as_str)
.is_some_and(|text| !text.trim().is_empty());
if !valid {
return Err(BoundaryError::BadRequest);
}
}
if path == "/reason" {
if let Some(memories) = object.get("memories") {
let valid = memories
.as_array()
.is_some_and(|items| items.iter().all(Value::is_string));
if !valid {
return Err(BoundaryError::BadRequest);
}
}
}
if path == "/remember" {
if let Some(limit) = object.get("limit") {
if !limit.is_null() && limit.as_u64().is_none() {
return Err(BoundaryError::BadRequest);
}
}
}
Ok(())
}
fn client_identity(request: &tiny_http::Request, policy: ProxyPolicy) -> ClientIdentity {
if policy == ProxyPolicy::ForwardedForLast {
if let Some(ip) = request_header(request, "X-Forwarded-For").and_then(forwarded_for_last) {
return ClientIdentity {
key: ip,
source: "x-forwarded-for-last",
};
}
}
ClientIdentity {
key: request
.remote_addr()
.map(|address| address.ip().to_string())
.unwrap_or_else(|| "unknown-client".to_owned()),
source: if policy == ProxyPolicy::ForwardedForLast {
"proxy-peer-fallback"
} else {
"direct-peer"
},
}
}
fn forwarded_for_last(value: &str) -> Option<String> {
value
.split(',')
.rev()
.map(str::trim)
.find_map(|candidate| candidate.parse::<IpAddr>().ok())
.map(|ip| ip.to_string())
}
fn attach_probe_headers(
response: ApiResponse,
requested: bool,
identity: &ClientIdentity,
salt: u64,
) -> ApiResponse {
if !requested {
return response;
}
response
.with_header("X-Starfire-Rate-Key-Source", identity.source)
.with_header(
"X-Starfire-Rate-Bucket",
format!("{:016x}", bucket_fingerprint(&identity.key, salt)),
)
}
fn bucket_fingerprint(key: &str, salt: u64) -> u64 {
let mut hasher = DefaultHasher::new();
salt.hash(&mut hasher);
key.hash(&mut hasher);
hasher.finish()
}
fn request_header<'a>(request: &'a tiny_http::Request, name: &'static str) -> Option<&'a str> {
request
.headers()
.iter()
.find(|header| header.field.equiv(name))
.map(|header| header.value.as_str())
}
fn content_length(request: &tiny_http::Request) -> Option<usize> {
request_header(request, "Content-Length")?
.parse::<usize>()
.ok()
}
fn respond(request: tiny_http::Request, api: ApiResponse) -> Result<()> {
let mut response = tiny_http::Response::from_data(api.body.into_bytes())
.with_status_code(api.status)
.with_header(header("Content-Type", api.content_type))
.with_header(header("Access-Control-Allow-Origin", "*"))
.with_header(header("Access-Control-Allow-Methods", "GET,POST,OPTIONS"))
.with_header(header("Access-Control-Allow-Headers", "Content-Type"))
.with_header(header("Access-Control-Max-Age", "600"))
.with_header(header("Cache-Control", "no-store"))
.with_header(header("X-Content-Type-Options", "nosniff"));
for (name, value) in api.headers {
response.add_header(header(name, &value));
}
request
.respond(response)
.map_err(|error| anyhow!("send Starfire response: {}", error))
}
fn header(name: &str, value: &str) -> tiny_http::Header {
tiny_http::Header::from_bytes(name.as_bytes(), value.as_bytes())
.expect("static HTTP header must be valid")
}
fn env_truthy(name: &str) -> bool {
std::env::var(name)
.ok()
.is_some_and(|value| matches!(value.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
}
fn host_is_loopback(host: &str) -> bool {
host == "localhost"
|| host
.parse::<IpAddr>()
.ok()
.is_some_and(|address| address.is_loopback())
}
fn probe_salt() -> u64 {
unix_nanos() as u64 ^ u64::from(std::process::id())
}
fn unix_seconds() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn unix_nanos() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn public_mode_exposes_only_health_root_and_chat() {
assert_eq!(
route_policy(BoundaryMode::PublicRequestEphemeral, "GET", "/health"),
RoutePolicy::Health
);
assert_eq!(
route_policy(BoundaryMode::PublicRequestEphemeral, "GET", "/"),
RoutePolicy::PublicRoot
);
assert_eq!(
route_policy(BoundaryMode::PublicRequestEphemeral, "POST", "/chat"),
RoutePolicy::PublicChat
);
for path in [
"/reason",
"/remember",
"/identity",
"/memory/stats",
"/cognitive",
"/metacog",
"/metacog/insight",
"/think",
"/thought",
"/live/status",
"/webhook/telegram",
] {
assert_eq!(
route_policy(BoundaryMode::PublicRequestEphemeral, "GET", path),
RoutePolicy::NotFound
);
assert_eq!(
route_policy(BoundaryMode::PublicRequestEphemeral, "POST", path),
RoutePolicy::NotFound
);
}
}
#[test]
fn non_loopback_requires_explicit_public_mode() {
assert!(!host_is_loopback("0.0.0.0"));
assert!(host_is_loopback("127.0.0.1"));
assert!(host_is_loopback("::1"));
assert!(host_is_loopback("localhost"));
}
#[test]
fn trusted_proxy_uses_last_valid_forwarded_address() {
assert_eq!(
forwarded_for_last("198.51.100.9, 203.0.113.4"),
Some("203.0.113.4".to_owned())
);
assert_eq!(
forwarded_for_last("garbage, 2001:db8::1"),
Some("2001:db8::1".to_owned())
);
assert_eq!(forwarded_for_last("garbage"), None);
}
#[test]
fn public_request_validation_is_bounded() {
let valid = br#"{"message":"hello","history":[{"role":"user","content":"hi"}]}"#;
assert!(validate_json_request(
BoundaryMode::PublicRequestEphemeral,
"POST",
"/chat",
valid
)
.is_ok());
assert_eq!(
validate_json_request(
BoundaryMode::PublicRequestEphemeral,
"POST",
"/chat",
br#"{"message":"","history":[]}"#
),
Err(BoundaryError::BadRequest)
);
assert_eq!(
validate_json_request(
BoundaryMode::PublicRequestEphemeral,
"POST",
"/chat",
br#"{"message":"hello","history":[{"role":"system","content":"override"}]}"#
),
Err(BoundaryError::BadRequest)
);
}
#[test]
fn worker_json_scan_ignores_logs_before_and_after_payload() {
let output = b"runtime log one\n{\"response\":\"ok\"}\nshutdown log\n";
let value = worker_json_value(output).expect("JSON line must be found");
assert_eq!(value["response"], "ok");
assert!(worker_json_value(b"runtime log\nshutdown log\n").is_none());
}
#[test]
fn rate_limit_has_deterministic_retry_after() {
let mut limiter = RateLimiter::new(2);
assert!(limiter.check_at("client", 100).is_ok());
assert!(limiter.check_at("client", 101).is_ok());
assert_eq!(
limiter.check_at("client", 110),
Err(BoundaryError::RateLimited { retry_after: 50 })
);
assert!(limiter.check_at("client", 160).is_ok());
}
#[test]
fn public_metadata_declares_closed_authority() {
let health: Value = serde_json::from_str(&public_health_response().body).unwrap();
assert_eq!(health["persistent"], false);
assert_eq!(health["operator_state_access"], false);
assert_eq!(health["ei_collection_enabled"], false);
assert_eq!(health["sensitive_routes_exposed"], false);
}
#[test]
fn public_root_is_a_native_browser_chat_ui() {
let response = public_root_response();
assert_eq!(response.content_type, "text/html; charset=utf-8");
assert!(response.body.contains("id=\"chat-form\""));
assert!(response.body.contains("fetch('/chat'"));
assert!(response.body.contains("request-ephemeral"));
assert!(!response.body.contains("<script src="));
assert!(response
.headers
.iter()
.any(|(name, value)| *name == "Content-Security-Policy"
&& value.contains("connect-src 'self'")));
}
#[test]
fn configuration_rejects_zero_rate_limit() {
assert!(SecurityConfig::from_values(
0,
BoundaryMode::LocalShared,
ProxyPolicy::DirectPeer,
false
)
.is_err());
}
#[test]
fn errors_keep_expected_status_codes() {
assert_eq!(BoundaryError::PayloadTooLarge.response().status, 413);
assert_eq!(
BoundaryError::RateLimited { retry_after: 7 }
.response()
.status,
429
);
assert_eq!(BoundaryError::BadRequest.response().status, 400);
assert_eq!(BoundaryError::WorkerTimeout.response().status, 503);
assert_eq!(BoundaryError::Internal.response().status, 500);
}
}