kz_proxy/types.rs
1//! Public API types: secret/string mappings, host patterns, connection policies, sandbox config.
2
3use regex::Regex;
4
5/// Format a struct with a masked `value` field for Debug output (security: redact secrets).
6fn debug_masked(f: &mut std::fmt::Formatter<'_>, name: &str, key_field: &str, key_val: &str) -> std::fmt::Result {
7 f.debug_struct(name).field(key_field, &key_val).field("value", &"[REDACTED]").finish()
8}
9
10/// Mapping from environment variable name to the real secret value.
11/// The sandbox will inject a masked token into the subprocess env and
12/// the proxy will replace that token with this value in outgoing HTTP requests.
13/// Real values must not contain CR, LF, or NUL (validated at run time).
14#[derive(Clone, serde::Serialize, serde::Deserialize)]
15pub struct SecretMapping {
16 pub var: String,
17 pub value: String,
18}
19
20impl std::fmt::Debug for SecretMapping {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 debug_masked(f, "SecretMapping", "var", &self.var)
23 }
24}
25
26impl SecretMapping {
27 /// Create a secret mapping from env var name to real value.
28 pub fn new(var: impl Into<String>, value: impl Into<String>) -> Self {
29 Self {
30 var: var.into(),
31 value: value.into(),
32 }
33 }
34}
35
36/// Mapping from a unique string identifier (token) to the actual value.
37/// The proxy will replace occurrences of the token with the value in URIs, headers, and body.
38/// Used when the process already uses placeholders; no env injection. Values must not contain CR, LF, or NUL.
39#[derive(Clone, serde::Serialize, serde::Deserialize)]
40pub struct StringMapping {
41 pub token: String,
42 pub value: String,
43}
44
45impl std::fmt::Debug for StringMapping {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 debug_masked(f, "StringMapping", "token", &self.token)
48 }
49}
50
51impl StringMapping {
52 /// Create a string mapping from token to value.
53 pub fn new(token: impl Into<String>, value: impl Into<String>) -> Self {
54 Self {
55 token: token.into(),
56 value: value.into(),
57 }
58 }
59}
60
61/// Pattern for matching a host in connection allow/deny rules.
62#[derive(Clone, Debug)]
63pub enum HostPattern {
64 /// Exact host string match (e.g. `example.com`).
65 Exact(String),
66 /// Regex match over the host (e.g. `^api\.example\.com$`). Pattern string is kept for serialization.
67 Regex { pattern: String, re: Regex },
68}
69
70impl HostPattern {
71 /// Create an exact host pattern.
72 pub fn exact(host: impl Into<String>) -> Self {
73 Self::Exact(host.into())
74 }
75
76 /// Create a regex host pattern. Returns an error if the pattern is invalid.
77 pub fn regex(pattern: &str) -> Result<Self, regex::Error> {
78 let re = Regex::new(pattern)?;
79 Ok(Self::Regex {
80 pattern: pattern.to_string(),
81 re,
82 })
83 }
84
85 /// Returns true if the given host matches this pattern.
86 pub fn matches(&self, host: &str) -> bool {
87 match self {
88 HostPattern::Exact(s) => s == host,
89 HostPattern::Regex { re, .. } => re.is_match(host),
90 }
91 }
92}
93
94/// Allow or deny outbound connections to a host (or hosts matching a regex).
95/// Policies are evaluated in order; the first matching policy wins. If no policy matches and rules exist, the connection is denied (allowlist behavior).
96#[derive(Clone, Debug)]
97pub struct ConnectionPolicy {
98 pub pattern: HostPattern,
99 /// If true, allow the connection; if false, deny (proxy returns an error to the client).
100 pub allow: bool,
101}
102
103impl ConnectionPolicy {
104 /// Create an allow rule for the given host pattern.
105 pub fn allow(pattern: HostPattern) -> Self {
106 Self {
107 pattern,
108 allow: true,
109 }
110 }
111
112 /// Create a deny rule for the given host pattern.
113 pub fn deny(pattern: HostPattern) -> Self {
114 Self {
115 pattern,
116 allow: false,
117 }
118 }
119}
120
121/// Configuration for the sandbox: secrets, string mappings, connection allow/deny, and proxy options.
122#[derive(Clone, Debug)]
123pub struct SandboxConfig {
124 /// Env-based secret mappings (env var name → value). Default empty.
125 pub secrets: Vec<SecretMapping>,
126 /// String token → value mappings. Default empty.
127 pub strings: Vec<StringMapping>,
128 /// Allow/deny rules for outbound connections (host or host regex). Evaluated in order; first match wins; no match = allow. Default empty.
129 pub connections: Vec<ConnectionPolicy>,
130 /// If true, CONNECT to private/local addresses (e.g. 127.0.0.1) is allowed. For testing only; default false.
131 pub allow_private_connect: bool,
132 /// Optional path to PEM file with extra CA cert(s) to trust for upstream (e.g. self-signed server).
133 pub upstream_ca: Option<std::path::PathBuf>,
134}
135
136impl Default for SandboxConfig {
137 fn default() -> Self {
138 Self {
139 secrets: Vec::new(),
140 strings: Vec::new(),
141 connections: Vec::new(),
142 allow_private_connect: false,
143 upstream_ca: None,
144 }
145 }
146}
147
148/// Sandbox for running a subprocess with masked secrets and an HTTP proxy that rewrites tokens.
149///
150/// HTTP requests are forwarded with token replacement. HTTPS (CONNECT) is always handled by MITM:
151/// the proxy decrypts, rewrites tokens, and re-encrypts to upstream. The subprocess must trust our CA
152/// (we set `SSL_CERT_FILE`). For self-signed or custom upstream servers, set `upstream_ca` on [`SandboxConfig`].
153#[derive(Clone, Debug)]
154pub struct Sandbox {
155 pub(crate) config: SandboxConfig,
156}
157
158impl Sandbox {
159 /// Create a sandbox from the given config.
160 pub fn new(config: SandboxConfig) -> Self {
161 Self { config }
162 }
163
164 /// Run a command in the sandbox: start an HTTP proxy that rewrites masked tokens
165 /// and string tokens to real values, set subprocess env with masked tokens (if any) and
166 /// HTTP_PROXY/HTTPS_PROXY, then wait for the process to exit.
167 pub async fn run(
168 &self,
169 program: &str,
170 args: &[String],
171 ) -> Result<std::process::ExitStatus, Box<dyn std::error::Error + Send + Sync>> {
172 crate::proxy::run_impl(
173 program,
174 args,
175 self.config.secrets.clone(),
176 self.config.strings.clone(),
177 self.config.allow_private_connect,
178 self.config.upstream_ca.clone(),
179 self.config.connections.clone(),
180 )
181 .await
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn secret_mapping_creation() {
191 let m = SecretMapping {
192 var: "API_KEY".to_string(),
193 value: "secret123".to_string(),
194 };
195 assert_eq!(m.var, "API_KEY");
196 assert_eq!(m.value, "secret123");
197 }
198
199 #[test]
200 fn string_mapping_creation() {
201 let m = StringMapping {
202 token: "__API_KEY__".to_string(),
203 value: "secret123".to_string(),
204 };
205 assert_eq!(m.token, "__API_KEY__");
206 assert_eq!(m.value, "secret123");
207 }
208}