Skip to main content

nono_proxy/
config.rs

1//! Proxy configuration types.
2//!
3//! Defines the configuration for the proxy server, including allowed hosts,
4//! credential routes, and external proxy settings.
5
6use ipnet::IpNet;
7use serde::{Deserialize, Serialize};
8use std::net::IpAddr;
9
10/// Credential injection mode determining how credentials are inserted into requests.
11#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum InjectMode {
14    /// Inject credential into an HTTP header (default)
15    #[default]
16    Header,
17    /// Replace a pattern in the URL path with the credential
18    UrlPath,
19    /// Add or replace a query parameter with the credential
20    QueryParam,
21    /// Use HTTP Basic Authentication (credential format: "username:password")
22    BasicAuth,
23}
24
25/// Configuration for the proxy server.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ProxyConfig {
28    /// Bind address (default: 127.0.0.1)
29    #[serde(default = "default_bind_addr")]
30    pub bind_addr: IpAddr,
31
32    /// Bind port (0 = OS-assigned ephemeral port)
33    #[serde(default)]
34    pub bind_port: u16,
35
36    /// Allowed hosts for CONNECT mode (exact match + wildcards).
37    /// Empty = allow all hosts (except deny list).
38    #[serde(default)]
39    pub allowed_hosts: Vec<String>,
40
41    /// Additional CIDR ranges to deny (on top of built-in defaults).
42    #[serde(default)]
43    pub deny_cidrs: Vec<IpNet>,
44
45    /// Reverse proxy credential routes.
46    #[serde(default)]
47    pub routes: Vec<RouteConfig>,
48
49    /// External (enterprise) proxy URL for passthrough mode.
50    /// When set, CONNECT requests are chained to this proxy.
51    #[serde(default)]
52    pub external_proxy: Option<ExternalProxyConfig>,
53
54    /// Maximum concurrent connections (0 = unlimited).
55    #[serde(default)]
56    pub max_connections: usize,
57}
58
59impl Default for ProxyConfig {
60    fn default() -> Self {
61        Self {
62            bind_addr: default_bind_addr(),
63            bind_port: 0,
64            allowed_hosts: Vec::new(),
65            deny_cidrs: Vec::new(),
66            routes: Vec::new(),
67            external_proxy: None,
68            max_connections: 256,
69        }
70    }
71}
72
73fn default_bind_addr() -> IpAddr {
74    IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
75}
76
77/// Configuration for a reverse proxy credential route.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct RouteConfig {
80    /// Path prefix for routing (e.g., "/openai")
81    pub prefix: String,
82
83    /// Upstream URL to forward to (e.g., "https://api.openai.com")
84    pub upstream: String,
85
86    /// Keystore account name to load the credential from.
87    /// If `None`, no credential is injected.
88    pub credential_key: Option<String>,
89
90    /// Injection mode (default: "header")
91    #[serde(default)]
92    pub inject_mode: InjectMode,
93
94    // --- Header mode fields ---
95    /// HTTP header name for the credential (default: "Authorization")
96    /// Only used when inject_mode is "header".
97    #[serde(default = "default_inject_header")]
98    pub inject_header: String,
99
100    /// Format string for the credential value. `{}` is replaced with the secret.
101    /// Default: "Bearer {}"
102    /// Only used when inject_mode is "header".
103    #[serde(default = "default_credential_format")]
104    pub credential_format: String,
105
106    // --- URL path mode fields ---
107    /// Pattern to match in incoming URL path. Use {} as placeholder for phantom token.
108    /// Example: "/bot{}/" matches "/bot<token>/getMe"
109    /// Only used when inject_mode is "url_path".
110    #[serde(default)]
111    pub path_pattern: Option<String>,
112
113    /// Pattern for outgoing URL path. Use {} as placeholder for real credential.
114    /// Defaults to same as path_pattern if not specified.
115    /// Only used when inject_mode is "url_path".
116    #[serde(default)]
117    pub path_replacement: Option<String>,
118
119    // --- Query param mode fields ---
120    /// Name of the query parameter to add/replace with the credential.
121    /// Only used when inject_mode is "query_param".
122    #[serde(default)]
123    pub query_param_name: Option<String>,
124
125    /// Explicit environment variable name for the phantom token (e.g., "OPENAI_API_KEY").
126    ///
127    /// When set, this is used as the SDK API key env var name instead of deriving
128    /// it from `credential_key.to_uppercase()`. Required when `credential_key` is
129    /// an `op://` URI (which would produce a nonsensical env var name).
130    #[serde(default)]
131    pub env_var: Option<String>,
132}
133
134fn default_inject_header() -> String {
135    "Authorization".to_string()
136}
137
138fn default_credential_format() -> String {
139    "Bearer {}".to_string()
140}
141
142/// Configuration for an external (enterprise) proxy.
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct ExternalProxyConfig {
145    /// Proxy address (e.g., "squid.corp.internal:3128")
146    pub address: String,
147
148    /// Optional authentication for the external proxy.
149    pub auth: Option<ExternalProxyAuth>,
150}
151
152/// Authentication for an external proxy.
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct ExternalProxyAuth {
155    /// Keystore account name for proxy credentials.
156    pub keyring_account: String,
157
158    /// Authentication scheme (only "basic" supported).
159    #[serde(default = "default_auth_scheme")]
160    pub scheme: String,
161}
162
163fn default_auth_scheme() -> String {
164    "basic".to_string()
165}
166
167#[cfg(test)]
168#[allow(clippy::unwrap_used)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_default_config() {
174        let config = ProxyConfig::default();
175        assert_eq!(config.bind_addr, IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
176        assert_eq!(config.bind_port, 0);
177        assert!(config.allowed_hosts.is_empty());
178        assert!(config.routes.is_empty());
179        assert!(config.external_proxy.is_none());
180    }
181
182    #[test]
183    fn test_config_serialization() {
184        let config = ProxyConfig {
185            allowed_hosts: vec!["api.openai.com".to_string()],
186            ..Default::default()
187        };
188        let json = serde_json::to_string(&config).unwrap();
189        let deserialized: ProxyConfig = serde_json::from_str(&json).unwrap();
190        assert_eq!(deserialized.allowed_hosts, vec!["api.openai.com"]);
191    }
192}