use crate::audit;
use crate::config::InjectMode;
use crate::credential::{CredentialStore, LoadedCredential};
use crate::error::{ProxyError, Result};
use crate::filter::ProxyFilter;
use crate::forward::{self, AuditCtx, UpstreamScheme, UpstreamSpec, UpstreamStrategy};
use crate::route::RouteStore;
use crate::token;
use std::net::SocketAddr;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use tokio_rustls::TlsConnector;
use tracing::{debug, warn};
use zeroize::Zeroizing;
const MAX_REQUEST_BODY: usize = 16 * 1024 * 1024;
fn auth_mechanism_for_inject_mode(mode: &InjectMode) -> nono::undo::NetworkAuditAuthMechanism {
match mode {
InjectMode::Header | InjectMode::BasicAuth => {
nono::undo::NetworkAuditAuthMechanism::PhantomHeader
}
InjectMode::UrlPath => nono::undo::NetworkAuditAuthMechanism::PhantomPath,
InjectMode::QueryParam => nono::undo::NetworkAuditAuthMechanism::PhantomQuery,
}
}
fn audit_injection_mode_for_inject_mode(
mode: &InjectMode,
) -> nono::undo::NetworkAuditInjectionMode {
match mode {
InjectMode::Header => nono::undo::NetworkAuditInjectionMode::Header,
InjectMode::UrlPath => nono::undo::NetworkAuditInjectionMode::UrlPath,
InjectMode::QueryParam => nono::undo::NetworkAuditInjectionMode::QueryParam,
InjectMode::BasicAuth => nono::undo::NetworkAuditInjectionMode::BasicAuth,
}
}
fn proxy_auth_event_ctx<'a>(route_id: &'a str) -> audit::EventContext<'a> {
audit::EventContext {
route_id: Some(route_id),
auth_mechanism: Some(nono::undo::NetworkAuditAuthMechanism::ProxyAuthorization),
..audit::EventContext::default()
}
}
fn managed_credential_event_ctx<'a>(
route_id: &'a str,
proxy_mode: &InjectMode,
inject_mode: nono::undo::NetworkAuditInjectionMode,
) -> audit::EventContext<'a> {
audit::EventContext {
route_id: Some(route_id),
auth_mechanism: Some(auth_mechanism_for_inject_mode(proxy_mode)),
managed_credential_active: Some(true),
injection_mode: Some(inject_mode),
..audit::EventContext::default()
}
}
pub struct ReverseProxyCtx<'a> {
pub route_store: &'a RouteStore,
pub credential_store: &'a CredentialStore,
pub session_token: &'a Zeroizing<String>,
pub filter: &'a ProxyFilter,
pub tls_connector: &'a TlsConnector,
pub audit_log: Option<&'a audit::SharedAuditLog>,
}
pub async fn handle_reverse_proxy(
first_line: &str,
stream: &mut TcpStream,
remaining_header: &[u8],
ctx: &ReverseProxyCtx<'_>,
buffered_body: &[u8],
) -> Result<()> {
let (method, path, version) = parse_request_line(first_line)?;
debug!("Reverse proxy: {} {}", method, path);
let (service, upstream_path) = parse_service_prefix(&path)?;
let route = ctx
.route_store
.get(&service)
.ok_or_else(|| ProxyError::UnknownService {
prefix: service.clone(),
})?;
let static_cred = ctx.credential_store.get(&service);
let oauth2_route = ctx.credential_store.get_oauth2(&service);
let managed_ctx = static_cred.map(|cred| {
managed_credential_event_ctx(
&service,
&cred.proxy_inject_mode,
audit_injection_mode_for_inject_mode(&cred.inject_mode),
)
});
let oauth2_ctx = oauth2_route.map(|_| audit::EventContext {
route_id: Some(&service),
auth_mechanism: Some(nono::undo::NetworkAuditAuthMechanism::PhantomHeader),
managed_credential_active: Some(true),
injection_mode: Some(nono::undo::NetworkAuditInjectionMode::OAuth2),
..audit::EventContext::default()
});
let route_ctx = managed_ctx
.clone()
.or_else(|| oauth2_ctx.clone())
.unwrap_or_else(|| audit::EventContext {
route_id: Some(&service),
managed_credential_active: Some(false),
..audit::EventContext::default()
});
if route.missing_managed_credential(static_cred.is_some(), oauth2_route.is_some()) {
let reason = format!(
"managed credential unavailable for service '{}': route is configured for proxy-supplied auth",
service
);
warn!("{}", reason);
let deny_ctx = audit::EventContext {
route_id: Some(&service),
auth_mechanism: route.managed_auth_mechanism.clone(),
auth_outcome: Some(nono::undo::NetworkAuditAuthOutcome::Failed),
managed_credential_active: Some(false),
injection_mode: route.managed_injection_mode.clone(),
denial_category: Some(
nono::undo::NetworkAuditDenialCategory::ManagedCredentialUnavailable,
),
};
audit::log_denied(
ctx.audit_log,
audit::ProxyMode::Reverse,
&deny_ctx,
&service,
0,
&reason,
);
send_error(stream, 503, "Service Unavailable").await?;
return Ok(());
}
if !route.endpoint_rules.is_allowed(&method, &upstream_path) {
let reason = format!(
"endpoint denied: {} {} on service '{}'",
method, upstream_path, service
);
warn!("{}", reason);
let deny_ctx = audit::EventContext {
denial_category: Some(nono::undo::NetworkAuditDenialCategory::EndpointPolicy),
..route_ctx.clone()
};
audit::log_denied(
ctx.audit_log,
audit::ProxyMode::Reverse,
&deny_ctx,
&service,
0,
&reason,
);
send_error(stream, 403, "Forbidden").await?;
return Ok(());
}
if let Some(oauth2_route) = oauth2_route {
return handle_oauth2_credential(
oauth2_route,
route,
&service,
&upstream_path,
&method,
&version,
stream,
remaining_header,
buffered_body,
ctx,
)
.await;
}
let cred = static_cred;
if let Some(cred) = cred {
if let Err(e) = validate_phantom_token_for_mode(
&cred.proxy_inject_mode,
remaining_header,
&upstream_path,
&cred.proxy_header_name,
cred.proxy_path_pattern.as_deref(),
cred.proxy_query_param_name.as_deref(),
ctx.session_token,
) {
let deny_ctx = audit::EventContext {
auth_outcome: Some(nono::undo::NetworkAuditAuthOutcome::Failed),
denial_category: Some(nono::undo::NetworkAuditDenialCategory::AuthenticationFailed),
..managed_ctx.clone().unwrap_or_else(|| route_ctx.clone())
};
audit::log_denied(
ctx.audit_log,
audit::ProxyMode::Reverse,
&deny_ctx,
&service,
0,
&e.to_string(),
);
send_error(stream, 401, "Unauthorized").await?;
return Ok(());
}
} else if let Err(e) = token::validate_proxy_auth(remaining_header, ctx.session_token) {
let deny_ctx = audit::EventContext {
auth_outcome: Some(nono::undo::NetworkAuditAuthOutcome::Failed),
denial_category: Some(nono::undo::NetworkAuditDenialCategory::AuthenticationFailed),
..proxy_auth_event_ctx(&service)
};
audit::log_denied(
ctx.audit_log,
audit::ProxyMode::Reverse,
&deny_ctx,
&service,
0,
&e.to_string(),
);
send_error(stream, 407, "Proxy Authentication Required").await?;
return Ok(());
}
let transformed_path = if let Some(cred) = cred {
let cleaned_path = strip_proxy_artifacts(
&upstream_path,
&cred.proxy_inject_mode,
&cred.inject_mode,
cred.proxy_path_pattern.as_deref(),
cred.proxy_query_param_name.as_deref(),
);
transform_path_for_mode(
&cred.inject_mode,
&cleaned_path,
cred.path_pattern.as_deref(),
cred.path_replacement.as_deref(),
cred.query_param_name.as_deref(),
&cred.raw_credential,
)?
} else {
upstream_path.clone()
};
let upstream_url = format!(
"{}{}",
route.upstream.trim_end_matches('/'),
transformed_path
);
debug!("Forwarding to upstream: {} {}", method, upstream_url);
let (upstream_scheme, upstream_host, upstream_port, upstream_path_full) =
parse_upstream_url(&upstream_url)?;
let check = ctx.filter.check_host(&upstream_host, upstream_port).await?;
if !check.result.is_allowed() {
let reason = check.result.reason();
warn!("Upstream host denied by filter: {}", reason);
send_error(stream, 403, "Forbidden").await?;
let deny_ctx = audit::EventContext {
denial_category: Some(nono::undo::NetworkAuditDenialCategory::HostDenied),
..route_ctx.clone()
};
audit::log_denied(
ctx.audit_log,
audit::ProxyMode::Reverse,
&deny_ctx,
&service,
0,
&reason,
);
return Ok(());
}
if let Err(reason) =
validate_http_upstream_target(upstream_scheme, &upstream_host, &check.resolved_addrs)
{
warn!("{}", reason);
send_error(stream, 502, "Bad Gateway").await?;
let deny_ctx = audit::EventContext {
denial_category: Some(nono::undo::NetworkAuditDenialCategory::UpstreamConnectFailed),
..route_ctx.clone()
};
audit::log_denied(
ctx.audit_log,
audit::ProxyMode::Reverse,
&deny_ctx,
&service,
0,
&reason,
);
return Ok(());
}
let success_ctx = if let Some(ctx) = managed_ctx.clone() {
audit::EventContext {
auth_outcome: Some(nono::undo::NetworkAuditAuthOutcome::Succeeded),
..ctx
}
} else if oauth2_ctx.is_some() {
audit::EventContext {
auth_outcome: Some(nono::undo::NetworkAuditAuthOutcome::Succeeded),
..oauth2_ctx.clone().unwrap_or_default()
}
} else {
audit::EventContext {
auth_outcome: Some(nono::undo::NetworkAuditAuthOutcome::Succeeded),
managed_credential_active: Some(false),
..proxy_auth_event_ctx(&service)
}
};
let strip_header = cred.map(|c| c.proxy_header_name.as_str()).unwrap_or("");
let filtered_headers = filter_headers(remaining_header, strip_header);
let content_length = extract_content_length(remaining_header);
let body = match read_request_body(stream, content_length, buffered_body).await? {
Some(body) => body,
None => return Ok(()),
};
let upstream_authority = format_host_header(upstream_scheme, &upstream_host, upstream_port);
let mut request = Zeroizing::new(format!(
"{} {} {}\r\nHost: {}\r\n",
method, upstream_path_full, version, upstream_authority
));
if let Some(cred) = cred {
inject_credential_for_mode(cred, &mut request);
}
let auth_header_lower = cred.map(|c| c.header_name.to_lowercase());
for (name, value) in &filtered_headers {
if let (Some(cred), Some(header_lower)) = (cred, auth_header_lower.as_ref()) {
if matches!(cred.inject_mode, InjectMode::Header | InjectMode::BasicAuth)
&& name.to_lowercase() == *header_lower
{
continue;
}
}
request.push_str(&format!("{}: {}\r\n", name, value));
}
request.push_str("Connection: close\r\n");
if !body.is_empty() {
request.push_str(&format!("Content-Length: {}\r\n", body.len()));
}
request.push_str("\r\n");
let connector = route.tls_connector.as_ref().unwrap_or(ctx.tls_connector);
let upstream_spec = UpstreamSpec {
scheme: upstream_scheme,
host: &upstream_host,
port: upstream_port,
strategy: UpstreamStrategy::Direct {
resolved_addrs: &check.resolved_addrs,
},
tls_connector: connector,
};
let audit_ctx = AuditCtx {
log: ctx.audit_log,
mode: audit::ProxyMode::Reverse,
event_ctx: success_ctx.clone(),
target: &service,
method: &method,
path: &upstream_path,
};
if let Err(e) =
forward::forward_request(stream, request.as_bytes(), &body, upstream_spec, audit_ctx).await
{
warn!("Upstream connection failed: {}", e);
send_error(stream, 502, "Bad Gateway").await?;
let deny_ctx = audit::EventContext {
denial_category: Some(nono::undo::NetworkAuditDenialCategory::UpstreamConnectFailed),
..success_ctx.clone()
};
audit::log_denied(
ctx.audit_log,
audit::ProxyMode::Reverse,
&deny_ctx,
&service,
0,
&e.to_string(),
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn handle_oauth2_credential(
oauth2_route: &crate::credential::OAuth2Route,
route: &crate::route::LoadedRoute,
service: &str,
upstream_path: &str,
method: &str,
version: &str,
stream: &mut TcpStream,
remaining_header: &[u8],
buffered_body: &[u8],
ctx: &ReverseProxyCtx<'_>,
) -> Result<()> {
let access_token = oauth2_route.cache.get_or_refresh().await;
if let Err(e) = validate_phantom_token(remaining_header, "Authorization", ctx.session_token) {
let deny_ctx = audit::EventContext {
route_id: Some(service),
auth_mechanism: Some(nono::undo::NetworkAuditAuthMechanism::PhantomHeader),
auth_outcome: Some(nono::undo::NetworkAuditAuthOutcome::Failed),
managed_credential_active: Some(true),
injection_mode: Some(nono::undo::NetworkAuditInjectionMode::OAuth2),
denial_category: Some(nono::undo::NetworkAuditDenialCategory::AuthenticationFailed),
};
audit::log_denied(
ctx.audit_log,
audit::ProxyMode::Reverse,
&deny_ctx,
service,
0,
&e.to_string(),
);
send_error(stream, 401, "Unauthorized").await?;
return Ok(());
}
let upstream_url = format!(
"{}{}",
oauth2_route.upstream.trim_end_matches('/'),
upstream_path
);
debug!("OAuth2 forwarding to upstream: {} {}", method, upstream_url);
let (upstream_scheme, upstream_host, upstream_port, upstream_path_full) =
parse_upstream_url(&upstream_url)?;
let check = ctx.filter.check_host(&upstream_host, upstream_port).await?;
if !check.result.is_allowed() {
let reason = check.result.reason();
warn!("Upstream host denied by filter: {}", reason);
send_error(stream, 403, "Forbidden").await?;
let route_ctx = audit::EventContext {
route_id: Some(service),
managed_credential_active: Some(true),
injection_mode: Some(nono::undo::NetworkAuditInjectionMode::OAuth2),
denial_category: Some(nono::undo::NetworkAuditDenialCategory::HostDenied),
..audit::EventContext::default()
};
audit::log_denied(
ctx.audit_log,
audit::ProxyMode::Reverse,
&route_ctx,
service,
0,
&reason,
);
return Ok(());
}
if let Err(reason) =
validate_http_upstream_target(upstream_scheme, &upstream_host, &check.resolved_addrs)
{
warn!("{}", reason);
send_error(stream, 502, "Bad Gateway").await?;
let route_ctx = audit::EventContext {
route_id: Some(service),
managed_credential_active: Some(true),
injection_mode: Some(nono::undo::NetworkAuditInjectionMode::OAuth2),
denial_category: Some(nono::undo::NetworkAuditDenialCategory::UpstreamConnectFailed),
..audit::EventContext::default()
};
audit::log_denied(
ctx.audit_log,
audit::ProxyMode::Reverse,
&route_ctx,
service,
0,
&reason,
);
return Ok(());
}
let filtered_headers = filter_headers(remaining_header, "Authorization");
let content_length = extract_content_length(remaining_header);
let body = match read_request_body(stream, content_length, buffered_body).await? {
Some(body) => body,
None => return Ok(()),
};
let upstream_authority = format_host_header(upstream_scheme, &upstream_host, upstream_port);
let mut request = Zeroizing::new(format!(
"{} {} {}\r\nHost: {}\r\n",
method, upstream_path_full, version, upstream_authority
));
request.push_str(&format!(
"Authorization: Bearer {}\r\n",
access_token.as_str()
));
for (name, value) in &filtered_headers {
request.push_str(&format!("{}: {}\r\n", name, value));
}
if !body.is_empty() {
request.push_str(&format!("Content-Length: {}\r\n", body.len()));
}
request.push_str("\r\n");
let connector = route.tls_connector.as_ref().unwrap_or(ctx.tls_connector);
let upstream_spec = UpstreamSpec {
scheme: upstream_scheme,
host: &upstream_host,
port: upstream_port,
strategy: UpstreamStrategy::Direct {
resolved_addrs: &check.resolved_addrs,
},
tls_connector: connector,
};
let audit_ctx = AuditCtx {
log: ctx.audit_log,
mode: audit::ProxyMode::Reverse,
event_ctx: audit::EventContext {
route_id: Some(service),
auth_mechanism: Some(nono::undo::NetworkAuditAuthMechanism::PhantomHeader),
auth_outcome: Some(nono::undo::NetworkAuditAuthOutcome::Succeeded),
managed_credential_active: Some(true),
injection_mode: Some(nono::undo::NetworkAuditInjectionMode::OAuth2),
denial_category: None,
},
target: service,
method,
path: upstream_path,
};
if let Err(e) =
forward::forward_request(stream, request.as_bytes(), &body, upstream_spec, audit_ctx).await
{
warn!("Upstream connection failed: {}", e);
send_error(stream, 502, "Bad Gateway").await?;
audit::log_denied(
ctx.audit_log,
audit::ProxyMode::Reverse,
&audit::EventContext {
route_id: Some(service),
auth_mechanism: Some(nono::undo::NetworkAuditAuthMechanism::PhantomHeader),
auth_outcome: Some(nono::undo::NetworkAuditAuthOutcome::Succeeded),
managed_credential_active: Some(true),
injection_mode: Some(nono::undo::NetworkAuditInjectionMode::OAuth2),
denial_category: Some(
nono::undo::NetworkAuditDenialCategory::UpstreamConnectFailed,
),
},
service,
0,
&e.to_string(),
);
}
Ok(())
}
pub(crate) async fn read_request_body<S>(
stream: &mut S,
content_length: Option<usize>,
buffered_body: &[u8],
) -> Result<Option<Vec<u8>>>
where
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
{
if let Some(len) = content_length {
if len > MAX_REQUEST_BODY {
send_error_generic(stream, 413, "Payload Too Large").await?;
return Ok(None);
}
let mut buf = Vec::with_capacity(len);
let pre = buffered_body.len().min(len);
buf.extend_from_slice(&buffered_body[..pre]);
let remaining = len - pre;
if remaining > 0 {
let mut rest = vec![0u8; remaining];
stream.read_exact(&mut rest).await?;
buf.extend_from_slice(&rest);
}
Ok(Some(buf))
} else {
Ok(Some(Vec::new()))
}
}
pub(crate) async fn send_error_generic<S>(stream: &mut S, status: u16, reason: &str) -> Result<()>
where
S: tokio::io::AsyncWrite + Unpin,
{
let body = format!("{{\"error\":\"{}\"}}", reason);
let response = format!(
"HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
status,
reason,
body.len(),
body
);
stream.write_all(response.as_bytes()).await?;
stream.flush().await?;
Ok(())
}
fn parse_request_line(line: &str) -> Result<(String, String, String)> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 3 {
return Err(ProxyError::HttpParse(format!(
"malformed request line: {}",
line
)));
}
Ok((
parts[0].to_string(),
parts[1].to_string(),
parts[2].to_string(),
))
}
fn parse_service_prefix(path: &str) -> Result<(String, String)> {
let trimmed = path.strip_prefix('/').unwrap_or(path);
if let Some((prefix, rest)) = trimmed.split_once('/') {
Ok((prefix.to_string(), format!("/{}", rest)))
} else {
Ok((trimmed.to_string(), "/".to_string()))
}
}
fn validate_phantom_token(
header_bytes: &[u8],
header_name: &str,
session_token: &Zeroizing<String>,
) -> Result<()> {
let header_str = std::str::from_utf8(header_bytes).map_err(|_| ProxyError::InvalidToken)?;
let header_name_lower = header_name.to_lowercase();
for line in header_str.lines() {
let lower = line.to_lowercase();
if lower.starts_with(&format!("{}:", header_name_lower)) {
let value = line.split_once(':').map(|(_, v)| v.trim()).unwrap_or("");
let value_lower = value.to_lowercase();
let token_value = if value_lower.starts_with("bearer ") {
value[7..].trim()
} else {
value
};
if token::constant_time_eq(token_value.as_bytes(), session_token.as_bytes()) {
return Ok(());
}
warn!("Invalid phantom token in {} header", header_name);
return Err(ProxyError::InvalidToken);
}
}
warn!(
"Missing {} header for phantom token validation",
header_name
);
Err(ProxyError::InvalidToken)
}
pub(crate) fn filter_headers(header_bytes: &[u8], cred_header: &str) -> Vec<(String, String)> {
let header_str = std::str::from_utf8(header_bytes).unwrap_or("");
let cred_header_lower = if cred_header.is_empty() {
String::new()
} else {
format!("{}:", cred_header.to_lowercase())
};
let mut headers = Vec::new();
for line in header_str.lines() {
let lower = line.to_lowercase();
if lower.starts_with("host:")
|| lower.starts_with("content-length:")
|| lower.starts_with("connection:")
|| lower.starts_with("proxy-authorization:")
|| (!cred_header_lower.is_empty() && lower.starts_with(&cred_header_lower))
|| line.trim().is_empty()
{
continue;
}
if let Some((name, value)) = line.split_once(':') {
headers.push((name.trim().to_string(), value.trim().to_string()));
}
}
headers
}
pub(crate) fn extract_content_length(header_bytes: &[u8]) -> Option<usize> {
let header_str = std::str::from_utf8(header_bytes).ok()?;
for line in header_str.lines() {
if line.to_lowercase().starts_with("content-length:") {
let value = line.split_once(':')?.1.trim();
return value.parse().ok();
}
}
None
}
fn validate_http_upstream_target(
scheme: UpstreamScheme,
host: &str,
resolved_addrs: &[SocketAddr],
) -> std::result::Result<(), String> {
if matches!(scheme, UpstreamScheme::Https) {
return Ok(());
}
if is_local_only_target(host, resolved_addrs) {
Ok(())
} else {
Err(format!(
"refusing insecure http upstream for non-local host '{}'; http is only allowed for loopback addresses",
host
))
}
}
fn is_local_only_target(host: &str, resolved_addrs: &[SocketAddr]) -> bool {
if !resolved_addrs.is_empty() {
return resolved_addrs.iter().all(|addr| addr.ip().is_loopback());
}
match host.parse::<std::net::IpAddr>() {
Ok(std::net::IpAddr::V4(ip)) => ip.is_loopback(),
Ok(std::net::IpAddr::V6(ip)) => ip.is_loopback(),
Err(_) => false,
}
}
pub(crate) fn format_host_header(scheme: UpstreamScheme, host: &str, port: u16) -> String {
let default_port = match scheme {
UpstreamScheme::Http => 80,
UpstreamScheme::Https => 443,
};
let bracketed_host = if host.contains(':') && !host.starts_with('[') {
format!("[{}]", host)
} else {
host.to_string()
};
if port == default_port {
bracketed_host
} else {
format!("{}:{}", bracketed_host, port)
}
}
fn parse_upstream_url(url_str: &str) -> Result<(UpstreamScheme, String, u16, String)> {
let parsed = url::Url::parse(url_str)
.map_err(|e| ProxyError::HttpParse(format!("invalid upstream URL '{}': {}", url_str, e)))?;
let scheme = match parsed.scheme() {
"https" => UpstreamScheme::Https,
"http" => UpstreamScheme::Http,
_ => {
return Err(ProxyError::HttpParse(format!(
"unsupported URL scheme: {}",
url_str
)));
}
};
let host = parsed
.host_str()
.ok_or_else(|| ProxyError::HttpParse(format!("missing host in URL: {}", url_str)))?
.to_string();
let default_port = if matches!(scheme, UpstreamScheme::Https) {
443
} else {
80
};
let port = parsed.port().unwrap_or(default_port);
let path = parsed.path().to_string();
let path = if path.is_empty() {
"/".to_string()
} else {
path
};
let path_with_query = if let Some(query) = parsed.query() {
format!("{}?{}", path, query)
} else {
path
};
Ok((scheme, host, port, path_with_query))
}
async fn send_error(stream: &mut TcpStream, status: u16, reason: &str) -> Result<()> {
let body = format!("{{\"error\":\"{}\"}}", reason);
let response = format!(
"HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
status,
reason,
body.len(),
body
);
stream.write_all(response.as_bytes()).await?;
stream.flush().await?;
Ok(())
}
pub(crate) fn validate_phantom_token_for_mode(
mode: &InjectMode,
header_bytes: &[u8],
path: &str,
header_name: &str,
path_pattern: Option<&str>,
query_param_name: Option<&str>,
session_token: &Zeroizing<String>,
) -> Result<()> {
match mode {
InjectMode::Header | InjectMode::BasicAuth => {
validate_phantom_token(header_bytes, header_name, session_token)
}
InjectMode::UrlPath => {
let pattern = path_pattern.ok_or_else(|| {
ProxyError::HttpParse("url_path mode requires path_pattern".to_string())
})?;
validate_phantom_token_in_path(path, pattern, session_token)
}
InjectMode::QueryParam => {
let param_name = query_param_name.ok_or_else(|| {
ProxyError::HttpParse("query_param mode requires query_param_name".to_string())
})?;
validate_phantom_token_in_query(path, param_name, session_token)
}
}
}
fn validate_phantom_token_in_path(
path: &str,
pattern: &str,
session_token: &Zeroizing<String>,
) -> Result<()> {
let parts: Vec<&str> = pattern.split("{}").collect();
if parts.len() != 2 {
return Err(ProxyError::HttpParse(format!(
"invalid path_pattern '{}': must contain exactly one {{}}",
pattern
)));
}
let (prefix, suffix) = (parts[0], parts[1]);
if let Some(start) = path.find(prefix) {
let after_prefix = start + prefix.len();
let end_offset = if suffix.is_empty() {
path[after_prefix..]
.find(['/', '?'])
.unwrap_or(path[after_prefix..].len())
} else {
match path[after_prefix..].find(suffix) {
Some(offset) => offset,
None => {
warn!("Missing phantom token in URL path (pattern: {})", pattern);
return Err(ProxyError::InvalidToken);
}
}
};
let token = &path[after_prefix..after_prefix + end_offset];
if token::constant_time_eq(token.as_bytes(), session_token.as_bytes()) {
return Ok(());
}
warn!("Invalid phantom token in URL path");
return Err(ProxyError::InvalidToken);
}
warn!("Missing phantom token in URL path (pattern: {})", pattern);
Err(ProxyError::InvalidToken)
}
fn validate_phantom_token_in_query(
path: &str,
param_name: &str,
session_token: &Zeroizing<String>,
) -> Result<()> {
if let Some(query_start) = path.find('?') {
let query = &path[query_start + 1..];
for pair in query.split('&') {
if let Some((name, value)) = pair.split_once('=') {
if name == param_name {
let decoded = urlencoding::decode(value).unwrap_or_else(|_| value.into());
if token::constant_time_eq(decoded.as_bytes(), session_token.as_bytes()) {
return Ok(());
}
warn!("Invalid phantom token in query parameter '{}'", param_name);
return Err(ProxyError::InvalidToken);
}
}
}
}
warn!("Missing phantom token in query parameter '{}'", param_name);
Err(ProxyError::InvalidToken)
}
pub(crate) fn transform_path_for_mode(
mode: &InjectMode,
path: &str,
path_pattern: Option<&str>,
path_replacement: Option<&str>,
query_param_name: Option<&str>,
credential: &Zeroizing<String>,
) -> Result<String> {
match mode {
InjectMode::Header | InjectMode::BasicAuth => {
Ok(path.to_string())
}
InjectMode::UrlPath => {
let pattern = path_pattern.ok_or_else(|| {
ProxyError::HttpParse("url_path mode requires path_pattern".to_string())
})?;
let replacement = path_replacement.unwrap_or(pattern);
transform_url_path(path, pattern, replacement, credential)
}
InjectMode::QueryParam => {
let param_name = query_param_name.ok_or_else(|| {
ProxyError::HttpParse("query_param mode requires query_param_name".to_string())
})?;
transform_query_param(path, param_name, credential)
}
}
}
fn transform_url_path(
path: &str,
pattern: &str,
replacement: &str,
credential: &Zeroizing<String>,
) -> Result<String> {
let parts: Vec<&str> = pattern.split("{}").collect();
if parts.len() != 2 {
return Err(ProxyError::HttpParse(format!(
"invalid path_pattern '{}': must contain exactly one {{}}",
pattern
)));
}
let (pattern_prefix, pattern_suffix) = (parts[0], parts[1]);
let repl_parts: Vec<&str> = replacement.split("{}").collect();
if repl_parts.len() != 2 {
return Err(ProxyError::HttpParse(format!(
"invalid path_replacement '{}': must contain exactly one {{}}",
replacement
)));
}
let (repl_prefix, repl_suffix) = (repl_parts[0], repl_parts[1]);
if let Some(start) = path.find(pattern_prefix) {
let after_prefix = start + pattern_prefix.len();
let end_offset = if pattern_suffix.is_empty() {
path[after_prefix..]
.find(['/', '?'])
.unwrap_or(path[after_prefix..].len())
} else {
match path[after_prefix..].find(pattern_suffix) {
Some(offset) => offset,
None => {
return Err(ProxyError::HttpParse(format!(
"path '{}' does not match pattern '{}'",
path, pattern
)));
}
}
};
let before = &path[..start];
let after = &path[after_prefix + end_offset + pattern_suffix.len()..];
return Ok(format!(
"{}{}{}{}{}",
before,
repl_prefix,
credential.as_str(),
repl_suffix,
after
));
}
Err(ProxyError::HttpParse(format!(
"path '{}' does not match pattern '{}'",
path, pattern
)))
}
fn transform_query_param(
path: &str,
param_name: &str,
credential: &Zeroizing<String>,
) -> Result<String> {
let encoded_value = urlencoding::encode(credential.as_str());
if let Some(query_start) = path.find('?') {
let base_path = &path[..query_start];
let query = &path[query_start + 1..];
let mut found = false;
let new_query: Vec<String> = query
.split('&')
.map(|pair| {
if let Some((name, _)) = pair.split_once('=') {
if name == param_name {
found = true;
return format!("{}={}", param_name, encoded_value);
}
}
pair.to_string()
})
.collect();
if found {
Ok(format!("{}?{}", base_path, new_query.join("&")))
} else {
Ok(format!(
"{}?{}&{}={}",
base_path, query, param_name, encoded_value
))
}
} else {
Ok(format!("{}?{}={}", path, param_name, encoded_value))
}
}
pub(crate) fn strip_proxy_artifacts(
path: &str,
proxy_mode: &InjectMode,
upstream_mode: &InjectMode,
proxy_path_pattern: Option<&str>,
proxy_query_param_name: Option<&str>,
) -> String {
if proxy_mode == upstream_mode {
return path.to_string();
}
match proxy_mode {
InjectMode::UrlPath => {
if let Some(pattern) = proxy_path_pattern {
strip_proxy_path_token(path, pattern)
} else {
path.to_string()
}
}
InjectMode::QueryParam => {
if let Some(param_name) = proxy_query_param_name {
strip_proxy_query_param(path, param_name)
} else {
path.to_string()
}
}
InjectMode::Header | InjectMode::BasicAuth => path.to_string(),
}
}
fn strip_proxy_path_token(path: &str, pattern: &str) -> String {
let parts: Vec<&str> = pattern.split("{}").collect();
if parts.len() != 2 {
return path.to_string();
}
let (prefix, suffix) = (parts[0], parts[1]);
let start = if path.starts_with(prefix) {
Some(0)
} else {
path.find(prefix)
};
if let Some(start) = start {
let after_prefix = start + prefix.len();
let end_offset = if suffix.is_empty() {
path[after_prefix..]
.find(['/', '?'])
.unwrap_or(path[after_prefix..].len())
} else {
match path[after_prefix..].find(suffix) {
Some(offset) => offset,
None => return path.to_string(),
}
};
let before = &path[..start];
let after = &path[after_prefix + end_offset + suffix.len()..];
let joined = match (before.ends_with('/'), after.starts_with('/')) {
(true, true) => format!("{}{}", before, &after[1..]),
(false, false) if !before.is_empty() && !after.is_empty() => {
format!("{}/{}", before, after)
}
_ => format!("{}{}", before, after),
};
if joined.is_empty() || !joined.starts_with('/') {
format!("/{}", joined)
} else {
joined
}
} else {
path.to_string()
}
}
fn strip_proxy_query_param(path: &str, param_name: &str) -> String {
if let Some(query_start) = path.find('?') {
let base_path = &path[..query_start];
let query = &path[query_start + 1..];
let remaining: Vec<&str> = query
.split('&')
.filter(|pair| {
pair.split_once('=')
.map(|(name, _)| name != param_name)
.unwrap_or(true)
})
.collect();
if remaining.is_empty() {
base_path.to_string()
} else {
format!("{}?{}", base_path, remaining.join("&"))
}
} else {
path.to_string()
}
}
pub(crate) fn inject_credential_for_mode(cred: &LoadedCredential, request: &mut Zeroizing<String>) {
match cred.inject_mode {
InjectMode::Header | InjectMode::BasicAuth => {
request.push_str(&format!(
"{}: {}\r\n",
cred.header_name,
cred.header_value.as_str()
));
}
InjectMode::UrlPath | InjectMode::QueryParam => {
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_parse_request_line() {
let (method, path, version) = parse_request_line("POST /openai/v1/chat HTTP/1.1").unwrap();
assert_eq!(method, "POST");
assert_eq!(path, "/openai/v1/chat");
assert_eq!(version, "HTTP/1.1");
}
#[test]
fn test_parse_request_line_malformed() {
assert!(parse_request_line("GET").is_err());
}
#[test]
fn test_parse_service_prefix() {
let (service, path) = parse_service_prefix("/openai/v1/chat/completions").unwrap();
assert_eq!(service, "openai");
assert_eq!(path, "/v1/chat/completions");
}
#[test]
fn test_parse_service_prefix_no_subpath() {
let (service, path) = parse_service_prefix("/anthropic").unwrap();
assert_eq!(service, "anthropic");
assert_eq!(path, "/");
}
#[test]
fn test_validate_phantom_token_bearer_valid() {
let token = Zeroizing::new("secret123".to_string());
let header = b"Authorization: Bearer secret123\r\nContent-Type: application/json\r\n\r\n";
assert!(validate_phantom_token(header, "Authorization", &token).is_ok());
}
#[test]
fn test_validate_phantom_token_bearer_invalid() {
let token = Zeroizing::new("secret123".to_string());
let header = b"Authorization: Bearer wrong\r\n\r\n";
assert!(validate_phantom_token(header, "Authorization", &token).is_err());
}
#[test]
fn test_validate_phantom_token_x_api_key_valid() {
let token = Zeroizing::new("secret123".to_string());
let header = b"x-api-key: secret123\r\nContent-Type: application/json\r\n\r\n";
assert!(validate_phantom_token(header, "x-api-key", &token).is_ok());
}
#[test]
fn test_validate_phantom_token_x_goog_api_key_valid() {
let token = Zeroizing::new("secret123".to_string());
let header = b"x-goog-api-key: secret123\r\nContent-Type: application/json\r\n\r\n";
assert!(validate_phantom_token(header, "x-goog-api-key", &token).is_ok());
}
#[test]
fn test_validate_phantom_token_missing() {
let token = Zeroizing::new("secret123".to_string());
let header = b"Content-Type: application/json\r\n\r\n";
assert!(validate_phantom_token(header, "Authorization", &token).is_err());
}
#[test]
fn test_validate_phantom_token_case_insensitive_header() {
let token = Zeroizing::new("secret123".to_string());
let header = b"AUTHORIZATION: Bearer secret123\r\n\r\n";
assert!(validate_phantom_token(header, "Authorization", &token).is_ok());
}
#[test]
fn test_filter_headers_removes_host_auth() {
let header = b"Host: localhost:8080\r\nAuthorization: Bearer old\r\nContent-Type: application/json\r\nAccept: */*\r\n\r\n";
let filtered = filter_headers(header, "Authorization");
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0].0, "Content-Type");
assert_eq!(filtered[1].0, "Accept");
}
#[test]
fn test_filter_headers_removes_x_api_key() {
let header = b"x-api-key: sk-old\r\nContent-Type: application/json\r\n\r\n";
let filtered = filter_headers(header, "x-api-key");
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].0, "Content-Type");
}
#[test]
fn test_filter_headers_removes_custom_header() {
let header = b"PRIVATE-TOKEN: phantom123\r\nContent-Type: application/json\r\n\r\n";
let filtered = filter_headers(header, "PRIVATE-TOKEN");
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].0, "Content-Type");
}
#[test]
fn test_extract_content_length() {
let header = b"Content-Type: application/json\r\nContent-Length: 42\r\n\r\n";
assert_eq!(extract_content_length(header), Some(42));
}
#[test]
fn test_extract_content_length_missing() {
let header = b"Content-Type: application/json\r\n\r\n";
assert_eq!(extract_content_length(header), None);
}
#[test]
fn test_parse_upstream_url_https() {
let (scheme, host, port, path) =
parse_upstream_url("https://api.openai.com/v1/chat/completions").unwrap();
assert_eq!(scheme, UpstreamScheme::Https);
assert_eq!(host, "api.openai.com");
assert_eq!(port, 443);
assert_eq!(path, "/v1/chat/completions");
}
#[test]
fn test_parse_upstream_url_http_with_port() {
let (scheme, host, port, path) = parse_upstream_url("http://localhost:8080/api").unwrap();
assert_eq!(scheme, UpstreamScheme::Http);
assert_eq!(host, "localhost");
assert_eq!(port, 8080);
assert_eq!(path, "/api");
}
#[test]
fn test_parse_upstream_url_no_path() {
let (scheme, host, port, path) = parse_upstream_url("https://api.anthropic.com").unwrap();
assert_eq!(scheme, UpstreamScheme::Https);
assert_eq!(host, "api.anthropic.com");
assert_eq!(port, 443);
assert_eq!(path, "/");
}
#[test]
fn test_parse_upstream_url_invalid_scheme() {
assert!(parse_upstream_url("ftp://example.com").is_err());
}
#[test]
fn test_validate_http_upstream_target_rejects_non_local_host() {
let err = validate_http_upstream_target(UpstreamScheme::Http, "api.example.com", &[])
.expect_err("non-local http upstream should be rejected");
assert!(err.contains("refusing insecure http upstream"));
}
#[test]
fn test_validate_http_upstream_target_allows_loopback() {
let loopback = [SocketAddr::from(([127, 0, 0, 1], 8080))];
assert!(validate_http_upstream_target(UpstreamScheme::Http, "127.0.0.1", &[]).is_ok());
assert!(validate_http_upstream_target(UpstreamScheme::Http, "::1", &[]).is_ok());
assert!(
validate_http_upstream_target(UpstreamScheme::Http, "localhost", &loopback).is_ok()
);
}
#[test]
fn test_validate_http_upstream_target_rejects_unspecified_addresses() {
let unspecified = [SocketAddr::from(([0, 0, 0, 0], 8080))];
let err = validate_http_upstream_target(UpstreamScheme::Http, "0.0.0.0", &[])
.expect_err("unspecified http upstream should be rejected");
assert!(err.contains("loopback addresses"));
let err = validate_http_upstream_target(UpstreamScheme::Http, "localhost", &unspecified)
.expect_err("localhost resolving to unspecified should be rejected");
assert!(err.contains("loopback addresses"));
}
#[test]
fn test_validate_http_upstream_target_rejects_localhost_resolving_non_loopback() {
let poisoned = [SocketAddr::from(([203, 0, 113, 10], 8080))];
let err = validate_http_upstream_target(UpstreamScheme::Http, "localhost", &poisoned)
.expect_err("localhost resolving off-host should be rejected");
assert!(err.contains("refusing insecure http upstream"));
}
#[test]
fn test_format_host_header_uses_port_for_non_default_http() {
assert_eq!(
format_host_header(UpstreamScheme::Http, "localhost", 8080),
"localhost:8080"
);
}
#[test]
fn test_format_host_header_omits_default_https_port() {
assert_eq!(
format_host_header(UpstreamScheme::Https, "api.openai.com", 443),
"api.openai.com"
);
}
#[test]
fn test_format_host_header_brackets_ipv6() {
assert_eq!(
format_host_header(UpstreamScheme::Http, "::1", 8080),
"[::1]:8080"
);
}
#[test]
fn test_validate_phantom_token_in_path_valid() {
let token = Zeroizing::new("session123".to_string());
let path = "/bot/session123/getMe";
let pattern = "/bot/{}/";
assert!(validate_phantom_token_in_path(path, pattern, &token).is_ok());
}
#[test]
fn test_validate_phantom_token_in_path_invalid() {
let token = Zeroizing::new("session123".to_string());
let path = "/bot/wrong_token/getMe";
let pattern = "/bot/{}/";
assert!(validate_phantom_token_in_path(path, pattern, &token).is_err());
}
#[test]
fn test_validate_phantom_token_in_path_missing() {
let token = Zeroizing::new("session123".to_string());
let path = "/api/getMe";
let pattern = "/bot/{}/";
assert!(validate_phantom_token_in_path(path, pattern, &token).is_err());
}
#[test]
fn test_transform_url_path_basic() {
let credential = Zeroizing::new("real_token".to_string());
let path = "/bot/phantom_token/getMe";
let pattern = "/bot/{}/";
let replacement = "/bot/{}/";
let result = transform_url_path(path, pattern, replacement, &credential).unwrap();
assert_eq!(result, "/bot/real_token/getMe");
}
#[test]
fn test_transform_url_path_different_replacement() {
let credential = Zeroizing::new("real_token".to_string());
let path = "/api/v1/phantom_token/chat";
let pattern = "/api/v1/{}/";
let replacement = "/v2/bot/{}/";
let result = transform_url_path(path, pattern, replacement, &credential).unwrap();
assert_eq!(result, "/v2/bot/real_token/chat");
}
#[test]
fn test_transform_url_path_no_trailing_slash() {
let credential = Zeroizing::new("real_token".to_string());
let path = "/bot/phantom_token";
let pattern = "/bot/{}";
let replacement = "/bot/{}";
let result = transform_url_path(path, pattern, replacement, &credential).unwrap();
assert_eq!(result, "/bot/real_token");
}
#[test]
fn test_validate_phantom_token_in_query_valid() {
let token = Zeroizing::new("session123".to_string());
let path = "/api/data?api_key=session123&other=value";
assert!(validate_phantom_token_in_query(path, "api_key", &token).is_ok());
}
#[test]
fn test_validate_phantom_token_in_query_invalid() {
let token = Zeroizing::new("session123".to_string());
let path = "/api/data?api_key=wrong_token";
assert!(validate_phantom_token_in_query(path, "api_key", &token).is_err());
}
#[test]
fn test_validate_phantom_token_in_query_missing_param() {
let token = Zeroizing::new("session123".to_string());
let path = "/api/data?other=value";
assert!(validate_phantom_token_in_query(path, "api_key", &token).is_err());
}
#[test]
fn test_validate_phantom_token_in_query_no_query_string() {
let token = Zeroizing::new("session123".to_string());
let path = "/api/data";
assert!(validate_phantom_token_in_query(path, "api_key", &token).is_err());
}
#[test]
fn test_validate_phantom_token_in_query_url_encoded() {
let token = Zeroizing::new("token with spaces".to_string());
let path = "/api/data?api_key=token%20with%20spaces";
assert!(validate_phantom_token_in_query(path, "api_key", &token).is_ok());
}
#[test]
fn test_transform_query_param_add_to_no_query() {
let credential = Zeroizing::new("real_key".to_string());
let path = "/api/data";
let result = transform_query_param(path, "api_key", &credential).unwrap();
assert_eq!(result, "/api/data?api_key=real_key");
}
#[test]
fn test_transform_query_param_add_to_existing_query() {
let credential = Zeroizing::new("real_key".to_string());
let path = "/api/data?other=value";
let result = transform_query_param(path, "api_key", &credential).unwrap();
assert_eq!(result, "/api/data?other=value&api_key=real_key");
}
#[test]
fn test_transform_query_param_replace_existing() {
let credential = Zeroizing::new("real_key".to_string());
let path = "/api/data?api_key=phantom&other=value";
let result = transform_query_param(path, "api_key", &credential).unwrap();
assert_eq!(result, "/api/data?api_key=real_key&other=value");
}
#[test]
fn test_transform_query_param_url_encodes_special_chars() {
let credential = Zeroizing::new("key with spaces".to_string());
let path = "/api/data";
let result = transform_query_param(path, "api_key", &credential).unwrap();
assert_eq!(result, "/api/data?api_key=key%20with%20spaces");
}
#[test]
fn test_validate_phantom_token_uses_proxy_mode_over_upstream_mode() {
let token = Zeroizing::new("session123".to_string());
let header = b"Authorization: Bearer session123\r\n\r\n";
let path = "/api/data?api_key=wrong";
let result = validate_phantom_token_for_mode(
&InjectMode::Header,
header,
path,
"Authorization",
None,
Some("api_key"),
&token,
);
assert!(result.is_ok());
}
#[test]
fn test_transform_path_uses_upstream_mode_independently() {
let credential = Zeroizing::new("real_key".to_string());
let path = "/api/data?api_key=phantom";
let transformed = transform_path_for_mode(
&InjectMode::QueryParam,
path,
None,
None,
Some("api_key"),
&credential,
)
.expect("query-param transform should succeed");
assert_eq!(transformed, "/api/data?api_key=real_key");
}
#[test]
fn test_strip_proxy_path_token_basic() {
let result = strip_proxy_path_token("/PHANTOM123/api/v1/pods", "/{}/");
assert_eq!(result, "/api/v1/pods");
}
#[test]
fn test_strip_proxy_path_token_nested_pattern() {
let result = strip_proxy_path_token("/auth/PHANTOM123/api/v1/pods", "/auth/{}/");
assert_eq!(result, "/api/v1/pods");
}
#[test]
fn test_strip_proxy_path_token_no_trailing_slash() {
let result = strip_proxy_path_token("/PHANTOM123", "/{}");
assert_eq!(result, "/");
}
#[test]
fn test_strip_proxy_path_token_preserves_query() {
let result = strip_proxy_path_token("/PHANTOM123/api?limit=10", "/{}/");
assert_eq!(result, "/api?limit=10");
}
#[test]
fn test_strip_proxy_path_token_no_match() {
let result = strip_proxy_path_token("/api/v1/pods", "/auth/{}/");
assert_eq!(result, "/api/v1/pods");
}
#[test]
fn test_strip_proxy_path_token_mid_path_slash_join() {
let result = strip_proxy_path_token("/api/k8s/PHANTOM/data", "/k8s/{}/");
assert_eq!(result, "/api/data");
}
#[test]
fn test_strip_proxy_path_token_no_double_slash() {
let result = strip_proxy_path_token("/prefix/PHANTOM//suffix", "/prefix/{}/");
assert_eq!(result, "/suffix");
}
#[test]
fn test_strip_proxy_query_param_only_param() {
let result = strip_proxy_query_param("/api/v1/pods?token=PHANTOM123", "token");
assert_eq!(result, "/api/v1/pods");
}
#[test]
fn test_strip_proxy_query_param_with_other_params() {
let result = strip_proxy_query_param("/api/v1/pods?token=PHANTOM123&limit=10", "token");
assert_eq!(result, "/api/v1/pods?limit=10");
}
#[test]
fn test_strip_proxy_query_param_middle() {
let result =
strip_proxy_query_param("/api/v1/pods?limit=10&token=PHANTOM123&watch=true", "token");
assert_eq!(result, "/api/v1/pods?limit=10&watch=true");
}
#[test]
fn test_strip_proxy_query_param_no_match() {
let result = strip_proxy_query_param("/api/v1/pods?limit=10", "token");
assert_eq!(result, "/api/v1/pods?limit=10");
}
#[test]
fn test_strip_proxy_query_param_no_query_string() {
let result = strip_proxy_query_param("/api/v1/pods", "token");
assert_eq!(result, "/api/v1/pods");
}
#[test]
fn test_strip_proxy_artifacts_same_mode_noop() {
let path = "/PHANTOM123/api/v1/pods";
let result = strip_proxy_artifacts(
path,
&InjectMode::UrlPath,
&InjectMode::UrlPath,
Some("/{}/"),
None,
);
assert_eq!(result, path);
}
#[test]
fn test_strip_proxy_artifacts_url_path_to_header() {
let result = strip_proxy_artifacts(
"/PHANTOM123/api/v1/pods",
&InjectMode::UrlPath,
&InjectMode::Header,
Some("/{}/"),
None,
);
assert_eq!(result, "/api/v1/pods");
}
#[test]
fn test_strip_proxy_artifacts_query_param_to_header() {
let result = strip_proxy_artifacts(
"/api/v1/pods?token=PHANTOM123",
&InjectMode::QueryParam,
&InjectMode::Header,
None,
Some("token"),
);
assert_eq!(result, "/api/v1/pods");
}
#[test]
fn test_strip_proxy_artifacts_header_to_query_param() {
let path = "/api/v1/pods";
let result = strip_proxy_artifacts(
path,
&InjectMode::Header,
&InjectMode::QueryParam,
None,
None,
);
assert_eq!(result, path);
}
#[test]
fn test_end_to_end_url_path_proxy_header_upstream() {
let token = Zeroizing::new("session456".to_string());
let credential = Zeroizing::new("real_bearer_token".to_string());
let path = "/session456/api/v1/namespaces";
assert!(validate_phantom_token_for_mode(
&InjectMode::UrlPath,
b"\r\n\r\n", path,
"Authorization",
Some("/{}/"),
None,
&token,
)
.is_ok());
let cleaned = strip_proxy_artifacts(
path,
&InjectMode::UrlPath,
&InjectMode::Header,
Some("/{}/"),
None,
);
assert_eq!(cleaned, "/api/v1/namespaces");
let transformed =
transform_path_for_mode(&InjectMode::Header, &cleaned, None, None, None, &credential)
.unwrap();
assert_eq!(transformed, "/api/v1/namespaces");
}
#[test]
fn test_end_to_end_query_param_proxy_header_upstream() {
let token = Zeroizing::new("session789".to_string());
let credential = Zeroizing::new("real_bearer_token".to_string());
let path = "/api/v1/pods?token=session789&limit=100";
assert!(validate_phantom_token_for_mode(
&InjectMode::QueryParam,
b"\r\n\r\n",
path,
"Authorization",
None,
Some("token"),
&token,
)
.is_ok());
let cleaned = strip_proxy_artifacts(
path,
&InjectMode::QueryParam,
&InjectMode::Header,
None,
Some("token"),
);
assert_eq!(cleaned, "/api/v1/pods?limit=100");
let transformed =
transform_path_for_mode(&InjectMode::Header, &cleaned, None, None, None, &credential)
.unwrap();
assert_eq!(transformed, "/api/v1/pods?limit=100");
}
}