1use std::collections::HashMap;
2use std::net::Ipv4Addr;
3
4#[derive(Debug, Clone)]
6pub struct ExposeHostMapping {
7 pub host_port: u16,
9 pub guest_port: u16,
11}
12
13#[derive(Debug, Clone, Default)]
15pub struct ProxyConfig {
16 pub secrets: HashMap<String, SecretConfig>,
20 pub network: NetworkConfig,
22 pub expose_host: Vec<ExposeHostMapping>,
24}
25
26#[derive(Debug, Clone)]
28pub struct SecretConfig {
29 pub from: String,
31 pub hosts: Vec<String>,
34 pub value: Option<String>,
36}
37
38#[derive(Debug, Clone, Default)]
40pub struct NetworkConfig {
41 pub allow: Vec<String>,
44}
45
46impl NetworkConfig {
47 pub fn has_allowlist(&self) -> bool {
49 !self.allow.is_empty()
50 }
51}
52
53impl ProxyConfig {
54 pub fn is_domain_allowed(&self, domain: &str) -> bool {
57 if self.network.allow.is_empty() {
58 return true;
59 }
60 self.network
61 .allow
62 .iter()
63 .any(|pattern| domain_matches(pattern, domain))
64 }
65
66 pub fn exposed_host_port(&self, dst_ip: Ipv4Addr, guest_port: u16) -> Option<u16> {
69 const GATEWAY: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
70 if dst_ip != GATEWAY {
71 return None;
72 }
73 self.expose_host
74 .iter()
75 .find(|m| m.guest_port == guest_port)
76 .map(|m| m.host_port)
77 }
78
79 pub fn secrets_for_domain(
81 &self,
82 domain: &str,
83 placeholders: &HashMap<String, String>,
84 ) -> Vec<(String, String)> {
85 let mut substitutions = Vec::new();
86 for (name, secret) in &self.secrets {
87 if secret
88 .hosts
89 .iter()
90 .any(|pattern| domain_matches(pattern, domain))
91 {
92 if let Some(placeholder) = placeholders.get(name) {
93 let real_value = secret
94 .value
95 .clone()
96 .or_else(|| std::env::var(&secret.from).ok());
97 if let Some(real_value) = real_value {
98 substitutions.push((placeholder.clone(), real_value));
99 }
100 }
101 }
102 }
103 substitutions
104 }
105}
106
107fn domain_matches(pattern: &str, domain: &str) -> bool {
112 if pattern == "*" {
113 true
114 } else if let Some(suffix) = pattern.strip_prefix("*.") {
115 domain.ends_with(suffix) && domain.len() > suffix.len() && domain.as_bytes()[domain.len() - suffix.len() - 1] == b'.'
116 } else {
117 pattern == domain
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_exposed_host_port() {
127 use std::net::Ipv4Addr;
128 let config = ProxyConfig {
129 expose_host: vec![
130 ExposeHostMapping { host_port: 3000, guest_port: 8080 },
131 ExposeHostMapping { host_port: 5432, guest_port: 5432 },
132 ],
133 ..Default::default()
134 };
135 assert_eq!(config.exposed_host_port(Ipv4Addr::new(10, 0, 0, 1), 8080), Some(3000));
137 assert_eq!(config.exposed_host_port(Ipv4Addr::new(10, 0, 0, 1), 5432), Some(5432));
138 assert_eq!(config.exposed_host_port(Ipv4Addr::new(10, 0, 0, 1), 9999), None);
140 assert_eq!(config.exposed_host_port(Ipv4Addr::new(1, 2, 3, 4), 8080), None);
142 }
143
144 #[test]
145 fn test_domain_matching() {
146 assert!(domain_matches("*", "anything.com"));
147 assert!(domain_matches("*", "api.example.com"));
148 assert!(domain_matches("example.com", "example.com"));
149 assert!(!domain_matches("example.com", "api.example.com"));
150 assert!(domain_matches("*.example.com", "api.example.com"));
151 assert!(domain_matches("*.example.com", "deep.api.example.com"));
152 assert!(!domain_matches("*.example.com", "example.com"));
153 assert!(!domain_matches("*.example.com", "notexample.com"));
154 }
155
156 #[test]
157 fn test_has_allowlist() {
158 let empty = NetworkConfig::default();
159 assert!(!empty.has_allowlist());
160
161 let with_entries = NetworkConfig {
162 allow: vec!["api.example.com".into()],
163 };
164 assert!(with_entries.has_allowlist());
165 }
166}