use crate::network_policy::NetworkDecisionSource;use crate::network_policy::NetworkPolicyDecision;use crate::network_policy::NetworkProtocol;use crate::reasons::REASON_DENIED;use crate::reasons::REASON_METHOD_NOT_ALLOWED;use crate::reasons::REASON_MITM_HOOK_DENIED;use crate::reasons::REASON_MITM_REQUIRED;use crate::reasons::REASON_NOT_ALLOWED;use crate::reasons::REASON_NOT_ALLOWED_LOCAL;use crate::reasons::REASON_PROXY_DISABLED;use rama_http::Body;use rama_http::Response;use rama_http::StatusCode;use serde::Serialize;use tracing::error;pub struct PolicyDecisionDetails<'a> { pub decision: NetworkPolicyDecision, pub reason: &'a str, pub source: NetworkDecisionSource, pub protocol: NetworkProtocol, pub host: &'a str, pub port: u16,}pub fn text_response(status: StatusCode, body: &str) -> Response { Response::builder() .status(status) .header("content-type", "text/plain") .body(Body::from(body.to_string())) .unwrap_or_else(|_| Response::new(Body::from(body.to_string())))}pub fn json_response<T: Serialize>(value: &T) -> Response { let body = match serde_json::to_string(value) { Ok(body) => body, Err(err) => { error!("failed to serialize JSON response: {err}"); "{}".to_string() } }; Response::builder() .status(StatusCode::OK) .header("content-type", "application/json") .body(Body::from(body)) .unwrap_or_else(|err| { error!("failed to build JSON response: {err}"); Response::new(Body::from("{}")) })}pub fn blocked_header_value(reason: &str) -> &'static str { match reason { REASON_NOT_ALLOWED | REASON_NOT_ALLOWED_LOCAL => "blocked-by-allowlist", REASON_DENIED => "blocked-by-denylist", REASON_METHOD_NOT_ALLOWED => "blocked-by-method-policy", REASON_MITM_HOOK_DENIED => "blocked-by-mitm-hook", REASON_MITM_REQUIRED => "blocked-by-mitm-required", _ => "blocked-by-policy", }}pub fn blocked_message(reason: &str) -> &'static str { match reason { REASON_NOT_ALLOWED => "Domain not in allowlist.", REASON_NOT_ALLOWED_LOCAL => "Sandbox policy blocks local/private network addresses.", REASON_DENIED => "Domain denied by the sandbox policy.", REASON_METHOD_NOT_ALLOWED => "Method not allowed in limited mode.", REASON_MITM_HOOK_DENIED => "HTTPS request denied by MITM hook policy.", REASON_MITM_REQUIRED => "MITM required for limited HTTPS.", REASON_PROXY_DISABLED => "network proxy is disabled", _ => "Request blocked by network policy.", }}pub fn blocked_text_response(reason: &str) -> Response { Response::builder() .status(StatusCode::FORBIDDEN) .header("content-type", "text/plain") .header("x-proxy-error", blocked_header_value(reason)) .body(Body::from(blocked_message(reason))) .unwrap_or_else(|_| Response::new(Body::from("blocked")))}pub fn blocked_message_with_policy(reason: &str, details: &PolicyDecisionDetails<'_>) -> String { let _ = (details.reason, details.host); blocked_message(reason).to_string()}pub fn blocked_text_response_with_policy( reason: &str, details: &PolicyDecisionDetails<'_>,) -> Response { Response::builder() .status(StatusCode::FORBIDDEN) .header("content-type", "text/plain") .header("x-proxy-error", blocked_header_value(reason)) .body(Body::from(blocked_message_with_policy(reason, details))) .unwrap_or_else(|_| Response::new(Body::from("blocked")))}#[cfg(test)]mod tests { use super::*; use crate::reasons::REASON_NOT_ALLOWED; use pretty_assertions::assert_eq; #[test] fn blocked_message_with_policy_returns_human_message() { let details = PolicyDecisionDetails { decision: NetworkPolicyDecision::Ask, reason: REASON_NOT_ALLOWED, source: NetworkDecisionSource::Decider, protocol: NetworkProtocol::HttpsConnect, host: "api.example.com", port: 443, }; let message = blocked_message_with_policy(REASON_NOT_ALLOWED, &details); assert_eq!(message, "Domain not in allowlist."); }}