1use serde::{Deserialize, Serialize};
7use std::net::IpAddr;
8
9#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum InjectMode {
13 #[default]
15 Header,
16 UrlPath,
18 QueryParam,
20 BasicAuth,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct ProxyConfig {
27 #[serde(default = "default_bind_addr")]
29 pub bind_addr: IpAddr,
30
31 #[serde(default)]
33 pub bind_port: u16,
34
35 #[serde(default)]
38 pub allowed_hosts: Vec<String>,
39
40 #[serde(default)]
42 pub routes: Vec<RouteConfig>,
43
44 #[serde(default)]
47 pub external_proxy: Option<ExternalProxyConfig>,
48
49 #[serde(default)]
51 pub max_connections: usize,
52}
53
54impl Default for ProxyConfig {
55 fn default() -> Self {
56 Self {
57 bind_addr: default_bind_addr(),
58 bind_port: 0,
59 allowed_hosts: Vec::new(),
60 routes: Vec::new(),
61 external_proxy: None,
62 max_connections: 256,
63 }
64 }
65}
66
67fn default_bind_addr() -> IpAddr {
68 IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct RouteConfig {
74 pub prefix: String,
76
77 pub upstream: String,
79
80 pub credential_key: Option<String>,
83
84 #[serde(default)]
86 pub inject_mode: InjectMode,
87
88 #[serde(default = "default_inject_header")]
92 pub inject_header: String,
93
94 #[serde(default = "default_credential_format")]
98 pub credential_format: String,
99
100 #[serde(default)]
105 pub path_pattern: Option<String>,
106
107 #[serde(default)]
111 pub path_replacement: Option<String>,
112
113 #[serde(default)]
117 pub query_param_name: Option<String>,
118
119 #[serde(default)]
126 pub env_var: Option<String>,
127}
128
129fn default_inject_header() -> String {
130 "Authorization".to_string()
131}
132
133fn default_credential_format() -> String {
134 "Bearer {}".to_string()
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct ExternalProxyConfig {
140 pub address: String,
142
143 pub auth: Option<ExternalProxyAuth>,
145
146 #[serde(default)]
150 pub bypass_hosts: Vec<String>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct ExternalProxyAuth {
156 pub keyring_account: String,
158
159 #[serde(default = "default_auth_scheme")]
161 pub scheme: String,
162}
163
164fn default_auth_scheme() -> String {
165 "basic".to_string()
166}
167
168#[cfg(test)]
169#[allow(clippy::unwrap_used)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_default_config() {
175 let config = ProxyConfig::default();
176 assert_eq!(config.bind_addr, IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
177 assert_eq!(config.bind_port, 0);
178 assert!(config.allowed_hosts.is_empty());
179 assert!(config.routes.is_empty());
180 assert!(config.external_proxy.is_none());
181 }
182
183 #[test]
184 fn test_config_serialization() {
185 let config = ProxyConfig {
186 allowed_hosts: vec!["api.openai.com".to_string()],
187 ..Default::default()
188 };
189 let json = serde_json::to_string(&config).unwrap();
190 let deserialized: ProxyConfig = serde_json::from_str(&json).unwrap();
191 assert_eq!(deserialized.allowed_hosts, vec!["api.openai.com"]);
192 }
193
194 #[test]
195 fn test_external_proxy_config_with_bypass_hosts() {
196 let config = ProxyConfig {
197 external_proxy: Some(ExternalProxyConfig {
198 address: "squid.corp:3128".to_string(),
199 auth: None,
200 bypass_hosts: vec!["internal.corp".to_string(), "*.private.net".to_string()],
201 }),
202 ..Default::default()
203 };
204 let json = serde_json::to_string(&config).unwrap();
205 let deserialized: ProxyConfig = serde_json::from_str(&json).unwrap();
206 let ext = deserialized.external_proxy.unwrap();
207 assert_eq!(ext.address, "squid.corp:3128");
208 assert_eq!(ext.bypass_hosts.len(), 2);
209 assert_eq!(ext.bypass_hosts[0], "internal.corp");
210 assert_eq!(ext.bypass_hosts[1], "*.private.net");
211 }
212
213 #[test]
214 fn test_external_proxy_config_bypass_hosts_default_empty() {
215 let json = r#"{"address": "proxy:3128", "auth": null}"#;
216 let ext: ExternalProxyConfig = serde_json::from_str(json).unwrap();
217 assert!(ext.bypass_hosts.is_empty());
218 }
219}