1use crate::prelude::*;
2use std::{path::PathBuf, rc::Rc};
3
4use orion_conf::YamlIO;
5
6use crate::{
7 addr::{
8 AddrError, AddrReason, GitRepository, HttpResource,
9 access_ctrl::{
10 auth::AuthConfig,
11 unit::{RedirectResult, Unit},
12 },
13 proxy::ProxyConfig,
14 },
15 timeout::TimeoutConfig,
16};
17
18use super::{UnitCtrl, rule::Rule};
19use serde_derive::{Deserialize, Serialize};
20
21#[derive(Clone, Debug, Serialize, Deserialize, Getters, PartialEq)]
22#[getset(get = "pub")]
23pub struct NetAccessCtrl {
24 units: Vec<Unit>,
25 enable: bool,
26}
27
28pub type ServHandle = Rc<NetAccessCtrl>;
29
30impl NetAccessCtrl {
31 pub fn new(units: Vec<Unit>, enable: bool) -> Self {
32 Self { units, enable }
33 }
34 pub fn redirect(&self, url: &str) -> RedirectResult {
35 let mut path = RedirectResult::Origin(url.to_string());
36 for unit in &self.units {
37 path = unit.redirect(path.path());
38 if path.is_proxy() {
39 break;
40 }
41 }
42 path
43 }
44 pub fn direct_http_addr(&self, origin: HttpResource) -> HttpResource {
45 for unit in &self.units {
46 if let Some(dirct) = unit.direct_http_addr(&origin) {
47 return dirct;
48 }
49 }
50 origin
51 }
52 pub fn direct_git_addr(&self, origin: GitRepository) -> GitRepository {
53 for unit in &self.units {
54 if let Some(dirct) = unit.direct_git_addr(&origin) {
55 return dirct;
56 }
57 }
58 origin
59 }
60 pub fn direct_git_ctrl(&self, origin: &GitRepository) -> Option<UnitCtrl> {
61 for unit in &self.units {
62 if unit.direct_git_addr(origin).is_some() {
63 return Some(UnitCtrl::new(
64 unit.auth().clone(),
65 unit.timeout().clone(),
66 unit.proxy().clone(),
67 ));
68 }
69 }
70 None
71 }
72 pub fn proxy_git(&self, origin: &GitRepository) -> Option<ProxyConfig> {
73 self.direct_git_ctrl(origin).and_then(|x| x.proxy().clone())
74 }
75 pub fn proxy_http(&self, origin: &HttpResource) -> Option<ProxyConfig> {
76 self.direct_http_ctrl(origin)
77 .and_then(|x| x.proxy().clone())
78 }
79
80 pub fn timeout_git(&self, origin: &GitRepository) -> Option<TimeoutConfig> {
81 self.direct_git_ctrl(origin)
82 .and_then(|x| x.timeout().clone())
83 }
84 pub fn timeout_http(&self, origin: &HttpResource) -> Option<TimeoutConfig> {
85 self.direct_http_ctrl(origin)
86 .and_then(|x| x.timeout().clone())
87 }
88
89 pub fn auth_git(&self, origin: &GitRepository) -> Option<AuthConfig> {
90 self.direct_git_ctrl(origin).and_then(|x| x.auth().clone())
91 }
92 pub fn auth_http(&self, origin: &HttpResource) -> Option<AuthConfig> {
93 self.direct_http_ctrl(origin).and_then(|x| x.auth().clone())
94 }
95
96 pub fn direct_http_ctrl(&self, origin: &HttpResource) -> Option<UnitCtrl> {
97 for unit in &self.units {
98 if unit.direct_http_addr(origin).is_some() {
99 return Some(UnitCtrl::new(
100 unit.auth().clone(),
101 unit.timeout().clone(),
102 unit.proxy().clone(),
103 ));
104 }
105 }
106 None
107 }
108
109 pub fn from_rule(rule: Rule, auth: Option<AuthConfig>, proxy: Option<ProxyConfig>) -> Self {
110 let unit = Unit::new(vec![rule], auth, proxy);
111 Self::new(vec![unit], true)
112 }
113}
114impl TryFrom<&PathBuf> for NetAccessCtrl {
115 type Error = AddrError;
116
117 fn try_from(value: &PathBuf) -> Result<Self, Self::Error> {
118 NetAccessCtrl::load_yaml(value)
119 .map_err(|e| {
120 AddrReason::resource_error()
121 .to_err()
122 .with_detail(e.to_string())
123 })
124 .with_context(value)
125 }
126}
127
128impl EnvEvalable<NetAccessCtrl> for NetAccessCtrl {
129 fn env_eval(self, dict: &EnvDict) -> NetAccessCtrl {
130 NetAccessCtrl {
131 units: self
132 .units
133 .into_iter()
134 .map(|unit| unit.env_eval(dict))
135 .collect(),
136 enable: self.enable,
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use crate::addr::proxy::ProxyConfig;
144
145 use super::*;
146 use tempfile::tempdir;
147
148 #[test]
149 fn test_serv_serialization_basic() {
150 let serv = NetAccessCtrl::new(vec![], false);
151 let serialized = serde_yaml::to_string(&serv).unwrap();
152 let deserialized: NetAccessCtrl = serde_yaml::from_str(&serialized).unwrap();
153
154 assert_eq!(deserialized.units().len(), 0);
155 assert!(!deserialized.enable());
156 }
157
158 #[test]
159 fn test_serv_serialization_with_units() {
160 let auth = Some(AuthConfig::new("test_user", "test_pass"));
161 let rules = vec![
162 Rule::new("https://github.com/*", "https://mirror.github.com/"),
163 Rule::new("https://gitlab.com/*", "https://mirror.gitlab.com/"),
164 ];
165 let proxy = ProxyConfig::new("https://proxy.example.com:8080");
166 let unit = Unit::new(rules, auth, Some(proxy));
167 let serv = NetAccessCtrl::new(vec![unit], true);
168
169 let serialized = serde_json::to_string(&serv).unwrap();
170 let deserialized: NetAccessCtrl = serde_json::from_str(&serialized).unwrap();
171
172 assert_eq!(deserialized.units().len(), 1);
173 assert!(deserialized.enable());
174 assert_eq!(deserialized.units()[0].rules().len(), 2);
175 assert!(deserialized.units()[0].auth().is_some());
176 }
177
178 #[test]
179 fn test_serv_serialization_yaml_format() {
180 let yaml_content = r#"
181units:
182 - rules:
183 - pattern: "https://example.com/*"
184 target: "https://proxy.com/"
185enable: true
186"#;
187
188 let deserialized: NetAccessCtrl = serde_yaml::from_str(yaml_content).unwrap();
189 assert!(deserialized.enable());
190 assert_eq!(deserialized.units().len(), 1);
191 assert_eq!(
192 deserialized.units()[0].rules()[0].pattern(),
193 "https://example.com/*"
194 );
195 }
196
197 #[test]
198 fn test_serv_from_rule_serialization() {
199 let rule = Rule::new("https://test.com/*", "https://redirect.com/");
200 let auth = Some(AuthConfig::new("admin", "secret"));
201 let serv = NetAccessCtrl::from_rule(rule, auth, None);
202
203 let serialized = serde_json::to_string_pretty(&serv).unwrap();
204 let deserialized: NetAccessCtrl = serde_json::from_str(&serialized).unwrap();
205
206 assert!(deserialized.enable());
207 assert_eq!(deserialized.units().len(), 1);
208 assert_eq!(deserialized.units()[0].rules().len(), 1);
209 assert!(deserialized.units()[0].auth().is_some());
210 }
211
212 #[test]
213 fn test_serv_multiple_units_serialization() {
214 let unit1 = Unit::new(
215 vec![Rule::new("https://api1.com/*", "https://proxy1.com/")],
216 Some(AuthConfig::new("user1", "pass1")),
217 None,
218 );
219
220 let unit2 = Unit::new(
221 vec![Rule::new("https://api2.com/*", "https://proxy2.com/")],
222 None,
223 None,
224 );
225
226 let unit3 = Unit::new(
227 vec![
228 Rule::new("https://api3.com/v1/*", "https://proxy3.com/v1/"),
229 Rule::new("https://api3.com/v2/*", "https://proxy3.com/v2/"),
230 ],
231 Some(AuthConfig::new("user3", "pass3")),
232 None,
233 );
234
235 let serv = NetAccessCtrl::new(vec![unit1, unit2, unit3], true);
236
237 let serialized = serde_yaml::to_string(&serv).unwrap();
238 let deserialized: NetAccessCtrl = serde_yaml::from_str(&serialized).unwrap();
239
240 assert_eq!(deserialized.units().len(), 3);
241 assert!(deserialized.enable());
242 }
243
244 #[test]
245 fn test_serv_complex_yaml_structure() {
246 let yaml_content = r#"
247units:
248 - rules:
249 - pattern: "https://github.com/*"
250 target: "https://ghproxy.com/"
251 - pattern: "https://raw.githubusercontent.com/*"
252 target: "https://raw.ghproxy.com/"
253 auth:
254 username: "proxy_user"
255 password: "proxy_pass"
256 - rules:
257 - pattern: "https://npmjs.com/*"
258 target: "https://npmmirror.com/"
259 - pattern: "https://registry.npmjs.org/*"
260 target: "https://registry.npmmirror.com/"
261enable: true
262"#;
263
264 let deserialized: NetAccessCtrl = serde_yaml::from_str(yaml_content).unwrap();
265
266 assert_eq!(deserialized.units().len(), 2);
267 assert!(deserialized.enable());
268
269 let first_unit = &deserialized.units()[0];
270 assert_eq!(first_unit.rules().len(), 2);
271 assert_eq!(first_unit.rules()[0].pattern(), "https://github.com/*");
272 assert_eq!(first_unit.rules()[0].target(), "https://ghproxy.com/");
273 assert!(first_unit.auth().is_some());
274 assert_eq!(first_unit.auth().as_ref().unwrap().username(), "proxy_user");
275
276 let second_unit = &deserialized.units()[1];
277 assert_eq!(second_unit.rules().len(), 2);
278 assert!(second_unit.auth().is_none());
279 }
280
281 #[test]
282 fn test_serv_empty_yaml() {
283 let yaml_content = r#"
284units: []
285enable: false
286"#;
287
288 let deserialized: NetAccessCtrl = serde_yaml::from_str(yaml_content).unwrap();
289
290 assert_eq!(deserialized.units().len(), 0);
291 assert!(!deserialized.enable());
292 }
293
294 #[test]
295 fn test_serv_json_format() {
296 let json_content = r#"
297{
298 "units": [
299 {
300 "rules": [
301 {
302 "pattern": "https://test.example.com/*",
303 "target": "https://proxy.example.com/"
304 }
305 ],
306 "auth": {
307 "username": "testuser",
308 "password": "testpass"
309 }
310 }
311 ],
312 "enable": true
313}
314"#;
315
316 let deserialized: NetAccessCtrl = serde_json::from_str(json_content).unwrap();
317
318 assert_eq!(deserialized.units().len(), 1);
319 assert!(deserialized.enable());
320 assert_eq!(
321 deserialized.units()[0].rules()[0].pattern(),
322 "https://test.example.com/*"
323 );
324 }
325
326 #[test]
327 fn test_serv_redirect_functionality() {
328 let rules = vec![Rule::new("https://github.com/*", "https://mirror.com/")];
329 let unit = Unit::new(rules, None, None);
330 let serv = NetAccessCtrl::new(vec![unit], true);
331
332 let result = serv.redirect("https://github.com/user/repo");
333 match result {
334 RedirectResult::Direct(path, _) => {
335 assert_eq!(path, "https://mirror.com/user/repo");
336 }
337 RedirectResult::Origin(_) => panic!("Expected proxy path"),
338 }
339 }
340
341 #[test]
342 fn test_serv_no_redirect_match() {
343 let rules = vec![Rule::new("https://github.com/*", "https://mirror.com/")];
344 let unit = Unit::new(rules, None, None);
345 let serv = NetAccessCtrl::new(vec![unit], true);
346
347 let result = serv.redirect("https://gitlab.com/user/repo");
348 match result {
349 RedirectResult::Origin(path) => {
350 assert_eq!(path, "https://gitlab.com/user/repo");
351 }
352 RedirectResult::Direct(_, _) => panic!("Expected origin path"),
353 }
354 }
355
356 #[test]
357 fn test_serv_file_roundtrip() {
358 let temp_dir = tempdir().unwrap();
359 let file_path = temp_dir.path().join("test_serv.yml");
360
361 let rules = vec![Rule::new(
362 "https://file-test.com/*",
363 "https://file-proxy.com/",
364 )];
365 let unit = Unit::new(rules, Some(AuthConfig::new("file_user", "file_pass")), None);
366 let original_serv = NetAccessCtrl::new(vec![unit], true);
367
368 original_serv.save_yaml(&file_path).unwrap();
370
371 let loaded_serv = NetAccessCtrl::try_from(&file_path).unwrap();
373
374 assert_eq!(loaded_serv.units().len(), original_serv.units().len());
375 assert_eq!(loaded_serv.enable(), original_serv.enable());
376 }
377
378 #[test]
379 fn test_redirect_service() {
380 let service = NetAccessCtrl::new(
381 vec![Unit::new(
382 vec![Rule::new(
383 "https://github.com/galaxy-sec/galaxy-flow*",
384 "https://gflow.com",
385 )],
386 None,
387 None,
388 )],
389 true,
390 );
391 let result = service.redirect("https://github.com/galaxy-sec/galaxy-flow");
392 match result {
393 RedirectResult::Direct(path, _) => {
394 assert_eq!(path, "https://gflow.com");
395 }
396 RedirectResult::Origin(_) => panic!("Expected proxy path"),
397 }
398 }
399
400 #[test]
401 fn test_redirect_service_env_eval() {
402 let mut env_dict = EnvDict::new();
403 env_dict.insert(
404 "DOMAIN".to_string(),
405 ValueType::String("example.com".to_string()),
406 );
407 env_dict.insert(
408 "TARGET".to_string(),
409 ValueType::String("redirect.com".to_string()),
410 );
411 env_dict.insert(
412 "USERNAME".to_string(),
413 ValueType::String("test_user".to_string()),
414 );
415
416 let service = NetAccessCtrl::new(
417 vec![
418 Unit::new(
419 vec![Rule::new("https://${DOMAIN}/*", "https://${TARGET}")],
420 None,
421 None,
422 ),
423 Unit::new(
424 vec![Rule::new(
425 "https://github.com/*",
426 "https://mirror.${DOMAIN}",
427 )],
428 Some(AuthConfig::new("${USERNAME}", "password")),
429 None,
430 ),
431 ],
432 true,
433 );
434
435 let evaluated = service.env_eval(&env_dict);
436
437 assert_eq!(evaluated.units().len(), 2);
438 assert_eq!(
439 evaluated.units()[0].rules()[0].pattern(),
440 "https://example.com/*"
441 );
442 assert_eq!(
443 evaluated.units()[0].rules()[0].target(),
444 "https://redirect.com"
445 );
446 assert!(evaluated.units()[0].auth().is_none());
447
448 assert_eq!(
449 evaluated.units()[1].rules()[0].pattern(),
450 "https://github.com/*"
451 );
452 assert_eq!(
453 evaluated.units()[1].rules()[0].target(),
454 "https://mirror.example.com"
455 );
456 assert!(evaluated.units()[1].auth().is_some());
457 assert_eq!(
458 evaluated.units()[1].auth().as_ref().unwrap().username(),
459 "test_user"
460 );
461 }
462
463 #[test]
464 fn test_redirect_service_env_eval_disabled() {
465 let env_dict = EnvDict::new();
466
467 let service = NetAccessCtrl::new(
468 vec![Unit::new(
469 vec![Rule::new("https://github.com/*", "https://mirror.com")],
470 None,
471 None,
472 )],
473 false,
474 );
475
476 let evaluated = service.env_eval(&env_dict);
477
478 assert_eq!(evaluated.units().len(), 1);
479 assert!(!evaluated.enable());
480 }
481}