Skip to main content

orion_accessor/addr/access_ctrl/
serv.rs

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        // 写入文件
369        original_serv.save_yaml(&file_path).unwrap();
370
371        // 从文件读取
372        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}