Skip to main content

vellaveto_http_proxy/proxy/
mod.rs

1// Copyright 2026 Paolo Vella
2// SPDX-License-Identifier: BUSL-1.1
3//
4// Use of this software is governed by the Business Source License
5// included in the LICENSE-BSL-1.1 file at the root of this repository.
6//
7// Change Date: Three years from the date of publication of this version.
8// Change License: MPL-2.0
9
10//! MCP Streamable HTTP reverse proxy.
11//!
12//! Implements the Streamable HTTP transport (MCP spec 2025-11-25) as a
13//! reverse proxy that intercepts tool calls, evaluates policies, and
14//! forwards allowed requests to an upstream MCP server.
15
16mod auth;
17pub mod call_chain;
18pub mod discovery;
19mod fallback;
20pub mod gateway;
21#[cfg(feature = "grpc")]
22pub mod grpc;
23mod handlers;
24mod helpers;
25mod inspection;
26pub mod origin;
27pub mod smart_fallback;
28#[cfg(test)]
29mod tests;
30pub mod trace_propagation;
31pub mod transport_health;
32mod upstream;
33pub mod websocket;
34
35pub use call_chain::PrivilegeEscalationCheck;
36pub use discovery::handle_transport_discovery;
37pub use handlers::{
38    handle_mcp_delete, handle_mcp_get, handle_mcp_post, handle_protected_resource_metadata,
39};
40pub use websocket::{handle_ws_upgrade, WebSocketConfig};
41
42use hmac::Hmac;
43use sha2::Sha256;
44use std::net::SocketAddr;
45use std::sync::Arc;
46use vellaveto_approval::ApprovalStore;
47use vellaveto_audit::AuditLogger;
48use vellaveto_config::ManifestConfig;
49use vellaveto_engine::PolicyEngine;
50use vellaveto_engine::{circuit_breaker::CircuitBreakerManager, deputy::DeputyValidator};
51use vellaveto_mcp::extension_registry::ExtensionRegistry;
52use vellaveto_mcp::inspection::InjectionScanner;
53use vellaveto_mcp::mediation::MediationConfig;
54use vellaveto_mcp::output_validation::OutputSchemaRegistry;
55use vellaveto_mcp::{
56    auth_level::AuthLevelTracker, sampling_detector::SamplingDetector,
57    schema_poisoning::SchemaLineageTracker, shadow_agent::ShadowAgentDetector,
58};
59use vellaveto_types::{Policy, SessionKeyScope, WorkloadIdentity};
60
61use crate::oauth::OAuthValidator;
62use crate::session::SessionStore;
63
64/// HMAC-SHA256 type alias for call chain signing (FIND-015).
65pub type HmacSha256 = Hmac<Sha256>;
66
67/// Query parameters for POST /mcp.
68#[derive(Debug, serde::Deserialize, Default)]
69#[serde(deny_unknown_fields)]
70pub struct McpQueryParams {
71    /// When true, include evaluation trace in the response.
72    #[serde(default)]
73    pub trace: bool,
74}
75
76/// Detached request-signature freshness policy applied before replay checks.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct DetachedSignatureFreshnessConfig {
79    pub max_age_secs: u64,
80    pub max_future_skew_secs: u64,
81}
82
83impl Default for DetachedSignatureFreshnessConfig {
84    fn default() -> Self {
85        Self {
86            max_age_secs: 600,
87            max_future_skew_secs: 300,
88        }
89    }
90}
91
92impl From<&vellaveto_config::AcisConfig> for DetachedSignatureFreshnessConfig {
93    fn from(acis: &vellaveto_config::AcisConfig) -> Self {
94        Self {
95            max_age_secs: acis.detached_request_signature_max_age_secs,
96            max_future_skew_secs: acis.detached_request_signature_max_future_skew_secs,
97        }
98    }
99}
100
101/// Trusted detached signer metadata used at verification time.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct TrustedRequestSigner {
104    pub public_key: [u8; 32],
105    pub session_key_scope: SessionKeyScope,
106    pub execution_is_ephemeral: bool,
107    pub workload_identity: Option<WorkloadIdentity>,
108}
109
110/// Shared state for the HTTP proxy handlers.
111#[derive(Clone)]
112pub struct ProxyState {
113    pub engine: Arc<PolicyEngine>,
114    pub policies: Arc<Vec<Policy>>,
115    pub audit: Arc<AuditLogger>,
116    pub sessions: Arc<SessionStore>,
117    pub upstream_url: String,
118    pub http_client: reqwest::Client,
119    /// OAuth 2.1 JWT validator. When `Some`, all MCP requests require a valid Bearer token.
120    pub oauth: Option<Arc<OAuthValidator>>,
121    /// Custom injection scanner. When `Some`, uses configured patterns instead of defaults.
122    pub injection_scanner: Option<Arc<InjectionScanner>>,
123    /// When true, injection scanning is completely disabled.
124    pub injection_disabled: bool,
125    /// When true, injection matches block the response instead of just logging (H4).
126    pub injection_blocking: bool,
127    /// API key for authenticating requests. None disables auth (--allow-anonymous).
128    pub api_key: Option<Arc<String>>,
129    /// Optional approval store for RequireApproval verdicts.
130    /// When set, creates pending approvals with approval_id in error response data.
131    pub approval_store: Option<Arc<ApprovalStore>>,
132    /// Optional manifest verification config. When set, tools/list responses
133    /// are verified against a pinned manifest per session.
134    pub manifest_config: Option<ManifestConfig>,
135    /// Allowed origins for CSRF / DNS rebinding protection. If non-empty,
136    /// Origin must be in the allowlist. If empty and the proxy is bound to a
137    /// loopback address, only localhost origins are accepted. If empty and
138    /// bound to a non-loopback address, falls back to same-origin check
139    /// (Origin host must match Host header).
140    /// Requests without an Origin header are always allowed (non-browser clients).
141    pub allowed_origins: Vec<String>,
142    /// The socket address the proxy is bound to. Used for automatic localhost
143    /// origin validation when `allowed_origins` is empty.
144    pub bind_addr: SocketAddr,
145    /// When true, re-serialize parsed JSON-RPC messages before forwarding to
146    /// upstream. This closes the TOCTOU gap where the proxy evaluates a parsed
147    /// representation but forwards original bytes that could differ (e.g., due to
148    /// duplicate keys or parser-specific handling). Duplicate keys are always
149    /// rejected regardless of this setting.
150    pub canonicalize: bool,
151    /// Output schema registry for structuredContent validation (MCP 2025-06-18).
152    pub output_schema_registry: Arc<OutputSchemaRegistry>,
153    /// When true, scan tool responses for secrets (DLP response scanning).
154    pub response_dlp_enabled: bool,
155    /// When true, block responses that contain detected secrets instead of just logging.
156    /// SECURITY (R18-DLP-BLOCK): Without this, DLP is log-only and secrets still reach the client.
157    pub response_dlp_blocking: bool,
158    /// Strict audit mode (FIND-CREATIVE-003): When true, audit logging failures
159    /// cause requests to be denied instead of proceeding without an audit trail.
160    /// This enforces non-repudiation guarantees — no unaudited security decisions
161    /// can occur. Default: false (backward compatible).
162    pub audit_strict_mode: bool,
163    /// Shared mediation-stage controls for provenance and semantic containment.
164    /// HTTP handlers keep their existing request scanners and use this config
165    /// for ACIS binding, provenance, and sink/lineage enforcement.
166    pub mediation_config: MediationConfig,
167    /// Trusted detached request-signature signers keyed by `RequestSignature.key_id`.
168    /// Used to promote detached per-request signatures from metadata to verified provenance.
169    pub trusted_request_signers: Arc<std::collections::HashMap<String, TrustedRequestSigner>>,
170    /// Freshness limits for verified detached request signatures.
171    pub detached_signature_freshness: DetachedSignatureFreshnessConfig,
172    /// Known legitimate tool names for squatting detection.
173    /// Built from DEFAULT_KNOWN_TOOLS + any config overrides.
174    pub known_tools: std::collections::HashSet<String>,
175    /// Elicitation interception configuration (MCP 2025-06-18).
176    /// Controls whether `elicitation/create` requests are allowed or blocked.
177    pub elicitation_config: vellaveto_config::ElicitationConfig,
178    /// Sampling request policy configuration.
179    /// Controls whether `sampling/createMessage` requests are allowed or blocked.
180    pub sampling_config: vellaveto_config::SamplingConfig,
181    /// Tool registry for tracking tool trust scores (P2.1).
182    /// None when tool registry is disabled.
183    pub tool_registry: Option<Arc<vellaveto_mcp::tool_registry::ToolRegistry>>,
184    /// HMAC-SHA256 key for signing and verifying X-Upstream-Agents call chain entries (FIND-015).
185    /// When `Some`, Vellaveto signs its own chain entries and verifies incoming ones.
186    /// When `None`, chain signing/verification is disabled (backward compatible).
187    pub call_chain_hmac_key: Option<[u8; 32]>,
188    /// When true, the `?trace=true` query parameter is honored and evaluation
189    /// traces are included in responses. When false (the default), trace output
190    /// is silently suppressed regardless of the client query parameter.
191    ///
192    /// SECURITY: Traces expose internal policy names, patterns, and constraint
193    /// configurations. Leaving this disabled prevents information leakage to
194    /// authenticated clients.
195    pub trace_enabled: bool,
196
197    // =========================================================================
198    // Phase 3.1 Security Managers
199    // =========================================================================
200    /// Circuit breaker for cascading failure prevention (OWASP ASI08).
201    /// When a tool fails repeatedly, the circuit opens and subsequent calls are rejected.
202    pub circuit_breaker: Option<Arc<CircuitBreakerManager>>,
203
204    /// Shadow agent detector for agent impersonation detection.
205    /// Tracks known agent fingerprints and alerts on impersonation attempts.
206    pub shadow_agent: Option<Arc<ShadowAgentDetector>>,
207
208    /// Deputy validator for confused deputy attack prevention (OWASP ASI02).
209    /// Tracks delegation chains and validates action permissions.
210    pub deputy: Option<Arc<DeputyValidator>>,
211
212    /// Schema lineage tracker for schema poisoning detection (OWASP ASI05).
213    /// Tracks tool schema changes and alerts on suspicious mutations.
214    pub schema_lineage: Option<Arc<SchemaLineageTracker>>,
215
216    /// Auth level tracker for step-up authentication.
217    /// Tracks session auth levels and enforces step-up requirements.
218    pub auth_level: Option<Arc<AuthLevelTracker>>,
219
220    /// Sampling detector for sampling attack prevention.
221    /// Tracks sampling request patterns and enforces rate limits.
222    pub sampling_detector: Option<Arc<SamplingDetector>>,
223
224    // =========================================================================
225    // Runtime Limits
226    // =========================================================================
227    /// Configurable runtime limits for memory bounds, timeouts, and chain lengths.
228    /// Provides operator control over previously hardcoded security constants.
229    pub limits: vellaveto_config::LimitsConfig,
230
231    // =========================================================================
232    // WebSocket Transport (Phase 17.1 — SEP-1288)
233    // =========================================================================
234    /// WebSocket transport configuration. When `Some`, the `/mcp/ws` endpoint
235    /// is active with the specified message size, idle timeout, and rate limit.
236    /// When `None`, WebSocket requests use default configuration.
237    pub ws_config: Option<WebSocketConfig>,
238
239    // =========================================================================
240    // Protocol Extensions (Phase 17.4)
241    // =========================================================================
242    /// Extension registry for `x-` prefixed protocol extensions.
243    /// When `Some`, extension method calls are routed to registered handlers
244    /// before falling back to upstream forwarding.
245    pub extension_registry: Option<Arc<ExtensionRegistry>>,
246
247    // =========================================================================
248    // Phase 18: Transport Discovery & Negotiation
249    // =========================================================================
250    /// Transport discovery and negotiation configuration.
251    pub transport_config: vellaveto_config::TransportConfig,
252
253    /// gRPC listen port, when gRPC transport is enabled.
254    /// Used by the discovery endpoint to advertise the gRPC endpoint.
255    pub grpc_port: Option<u16>,
256
257    // =========================================================================
258    // Phase 20: MCP Gateway Mode
259    // =========================================================================
260    /// Multi-backend gateway router. When `Some`, tool calls are routed to
261    /// different upstream MCP servers based on tool name prefix matching.
262    /// When `None`, all requests use `upstream_url` (single-server mode).
263    pub gateway: Option<Arc<gateway::GatewayRouter>>,
264
265    // =========================================================================
266    // Phase 21: Advanced Authorization (ABAC)
267    // =========================================================================
268    /// ABAC policy engine for Cedar-style permit/forbid evaluation.
269    /// When `Some`, refines Allow verdicts from the PolicyEngine.
270    /// When `None`, behavior is identical to pre-Phase 21.
271    pub abac_engine: Option<Arc<vellaveto_engine::abac::AbacEngine>>,
272    /// Least-agency tracker for permission usage monitoring.
273    /// When `Some`, records which permissions each agent actually uses.
274    pub least_agency: Option<Arc<vellaveto_engine::least_agency::LeastAgencyTracker>>,
275    /// Continuous authorization config for risk-based deny.
276    pub continuous_auth_config: Option<vellaveto_config::abac::ContinuousAuthConfig>,
277
278    // =========================================================================
279    // Phase 29: Cross-Transport Smart Fallback
280    // =========================================================================
281    /// Per-transport circuit breaker tracker. When `Some` and
282    /// `transport_config.cross_transport_fallback` is true, failed transports
283    /// trigger automatic fallback to the next transport in priority order.
284    pub transport_health: Option<Arc<transport_health::TransportHealthTracker>>,
285
286    // =========================================================================
287    // Phase 30: MCP 2025-11-25 Streamable HTTP
288    // =========================================================================
289    /// Streamable HTTP configuration for SSE resumability, strict tool name
290    /// validation, and retry directives.
291    pub streamable_http: vellaveto_config::StreamableHttpConfig,
292
293    // =========================================================================
294    // Phase 39: Agent Identity Federation
295    // =========================================================================
296    /// Federation resolver for cross-organization agent identity validation.
297    /// When `Some`, incoming JWTs are checked against federation trust anchors
298    /// before falling back to the local OAuth validator.
299    pub federation: Option<Arc<crate::federation::FederationResolver>>,
300
301    // =========================================================================
302    // Phase 34: Tool Discovery Service
303    // =========================================================================
304    /// Tool discovery engine for intent-based tool search.
305    /// When `Some`, the proxy intercepts `vv_discover` tool calls and returns
306    /// discovered tool schemas without forwarding to upstream.
307    #[cfg(feature = "discovery")]
308    pub discovery_engine: Option<std::sync::Arc<vellaveto_mcp::discovery::DiscoveryEngine>>,
309
310    // =========================================================================
311    // Phase 35.3: Model Projector
312    // =========================================================================
313    /// Model projector registry for cross-model tool schema translation.
314    /// When `Some`, tool schemas can be projected to model-specific formats.
315    #[cfg(feature = "projector")]
316    pub projector_registry: Option<std::sync::Arc<vellaveto_mcp::projector::ProjectorRegistry>>,
317}
318
319/// Per-request trust signal for forwarded-header handling.
320///
321/// FIND-R56-HTTP-002: `from_trusted_proxy` is `pub(crate)` to prevent external
322/// code from constructing a `TrustedProxyContext { from_trusted_proxy: true }`
323/// and injecting it as a spoofed trust signal. Use `is_from_trusted_proxy()`
324/// for read access from outside the crate.
325#[derive(Clone, Copy, Debug)]
326pub struct TrustedProxyContext {
327    pub(crate) from_trusted_proxy: bool,
328}
329
330impl TrustedProxyContext {
331    /// Create a new `TrustedProxyContext`.
332    pub fn new(from_trusted_proxy: bool) -> Self {
333        Self { from_trusted_proxy }
334    }
335
336    /// Returns whether the request came from a trusted reverse proxy.
337    pub fn is_from_trusted_proxy(&self) -> bool {
338        self.from_trusted_proxy
339    }
340}
341
342/// MCP Session ID header name.
343const MCP_SESSION_ID: &str = "mcp-session-id";
344
345/// MCP protocol version header (MCP 2025-06-18 spec requirement).
346const MCP_PROTOCOL_VERSION_HEADER: &str = "mcp-protocol-version";
347
348/// The protocol version value this proxy speaks.
349/// FIND-R56-HTTP-006: Renamed from `MCP_PROTOCOL_VERSION` to avoid confusion
350/// with `MCP_PROTOCOL_VERSION_HEADER` (the header name).
351const MCP_PROTOCOL_VERSION_VALUE: &str = "2025-11-25";
352
353/// Supported MCP protocol versions for incoming requests.
354/// The proxy accepts these versions for backwards compatibility.
355const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["2025-11-25", "2025-06-18", "2025-03-26"];
356
357/// Header for client transport preference negotiation (MCP June 2026).
358/// Clients may send a comma-separated list of preferred transports.
359/// Used in request handling when transport-preference-aware routing is active.
360const MCP_TRANSPORT_PREFERENCE_HEADER: &str = "mcp-transport-preference";
361
362/// OWASP ASI08: Header for tracking upstream agents in multi-hop MCP scenarios.
363/// Contains a JSON-encoded array of CallChainEntry objects from previous hops.
364/// This header is added by Vellaveto when forwarding requests downstream
365/// and read when receiving requests from upstream.
366pub const X_UPSTREAM_AGENTS: &str = "x-upstream-agents";
367
368/// SECURITY (FIND-R41-001): Allowlist of headers forwarded to upstream.
369/// Only these headers are forwarded to prevent leaking internal/sensitive
370/// headers (e.g., authorization, cookies) to upstream backends.
371/// Shared by `fallback.rs` and `smart_fallback.rs`.
372pub(crate) const FORWARDED_HEADERS: &[&str] = &[
373    "content-type",
374    "accept",
375    "user-agent",
376    "traceparent",
377    "tracestate",
378    "x-request-id",
379];
380
381/// Maximum response body size from upstream (16 MB).
382/// SECURITY (FIND-R41-004, FIND-R42-020): Prevents unbounded memory
383/// allocation from malicious upstream responses.
384/// Shared by `fallback.rs` and `smart_fallback.rs`.
385pub(crate) const MAX_RESPONSE_BODY_BYTES: usize = 16 * 1024 * 1024;
386
387/// SECURITY (R240-PROXY-1/2): Validate that an upstream URL uses HTTPS for non-local hosts.
388/// Only allows plaintext HTTP for localhost/127.0.0.1/[::1] (local development).
389/// Returns `Ok(())` if the URL is safe to use, `Err(reason)` if it should be rejected.
390pub(crate) fn validate_upstream_url_scheme(url: &str) -> Result<(), String> {
391    if let Some(scheme_end) = url.find("://") {
392        let scheme = &url[..scheme_end].to_lowercase();
393        if scheme == "http" || scheme == "ws" {
394            let after_scheme = &url[scheme_end + 3..];
395            let host_port = after_scheme.split('/').next().unwrap_or("");
396            // SECURITY (R240-IMP-008): Handle bracketed IPv6 addresses like [::1]:8080.
397            // split(':') on "[::1]:8080" gives "[" which is wrong.
398            let host_no_port = if host_port.starts_with('[') {
399                // IPv6 bracketed: extract up to and including ']'
400                host_port
401                    .split(']')
402                    .next()
403                    .map(|s| {
404                        let mut full = s.to_string();
405                        full.push(']');
406                        full
407                    })
408                    .unwrap_or_default()
409            } else {
410                host_port.split(':').next().unwrap_or("").to_string()
411            };
412            let is_local = host_no_port == "localhost"
413                || host_no_port == "127.0.0.1"
414                || host_no_port == "[::1]";
415            if !is_local {
416                return Err("HTTPS required for non-local upstream".to_string());
417            }
418        }
419    }
420    Ok(())
421}
422
423/// OWASP ASI07: Header for cryptographically attested agent identity.
424/// Contains a signed JWT with claims identifying the agent (issuer, subject, custom claims).
425/// Provides stronger identity guarantees than the simple agent_id string derived from OAuth.
426const X_AGENT_IDENTITY: &str = "x-agent-identity";
427
428/// Header carrying authenticated workload claims for transport provenance.
429/// The value is a base64url-encoded JSON object with allowlisted workload fields.
430const X_WORKLOAD_CLAIMS: &str = "x-workload-claims";
431
432/// Header carrying detached per-request signature metadata for provenance.
433/// The value is a base64url-encoded `RequestSignature` JSON object.
434const X_REQUEST_SIGNATURE: &str = "x-request-signature";