ss_uri/
sip008.rs

1use std::collections::HashMap;
2
3use url::Url;
4
5pub struct SIP008Config {
6    pub location: String,
7    pub cert_finger_print: Option<String>,
8    pub http_method: Option<String>,
9}
10
11#[derive(Debug, Clone, Copy)]
12pub enum SIP008ParseError {
13    InvalidUrl,
14    InvalidProtocol,
15    InvalidPort,
16    InvalidHost,
17}
18
19impl SIP008Config {
20    // parses shadowsocks SIP008 https://shadowsocks.org/en/wiki/SIP008-Online-Configuration-Delivery.html
21    pub fn parse(input: &str) -> Result<Self, SIP008ParseError> {
22        let url = Url::parse(input).map_err(|_| SIP008ParseError::InvalidUrl)?;
23        Self::validate_protocol(&url)?;
24        let params = url::form_urlencoded::parse(url.fragment().unwrap_or("").as_ref())
25            .map(|(a, b)| (a.to_string(), b.to_string()))
26            .collect::<HashMap<String, String>>();
27        Ok(Self {
28            location: format!(
29                "https://{}:{}{}",
30                url.host_str().ok_or(SIP008ParseError::InvalidUrl)?,
31                url.port_or_known_default().unwrap_or(443),
32                url.path()
33            ),
34            cert_finger_print: params.get("certFp").cloned(),
35            http_method: params.get("httpMethod").cloned(),
36        })
37    }
38    pub(crate) fn validate_protocol(url: &Url) -> Result<(), SIP008ParseError> {
39        if !url.scheme().starts_with("ssconf") {
40            return Err(SIP008ParseError::InvalidProtocol);
41        }
42        Ok(())
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
50    use url::Url;
51
52    #[test]
53    fn can_parse_a_valid_ssconf_uri_with_domain_name_and_extras() {
54        let input =
55            "ssconf://my.domain.com/secret/long/path#certFp=AA:BB:CC:DD:EE:FF&httpMethod=POST";
56        let online_config = SIP008Config::parse(input).unwrap();
57        let url = Url::parse(&online_config.location).unwrap();
58        assert_eq!(
59            url,
60            Url::parse("https://my.domain.com/secret/long/path").unwrap()
61        );
62        assert_eq!(
63            online_config.cert_finger_print,
64            Some("AA:BB:CC:DD:EE:FF".to_string())
65        );
66        assert_eq!(online_config.http_method, Some("POST".to_string()));
67    }
68    #[test]
69    fn can_parse_a_valid_ssconf_uri_with_domain_name_and_custom_port() {
70        let input = "ssconf://my.domain.com:9090/secret/long/path#certFp=AA:BB:CC:DD:EE:FF";
71        let online_config = SIP008Config::parse(input).unwrap();
72        let url = Url::parse(&online_config.location).unwrap();
73        assert_eq!(
74            url,
75            Url::parse("https://my.domain.com:9090/secret/long/path").unwrap()
76        );
77        assert_eq!(
78            online_config.cert_finger_print,
79            Some("AA:BB:CC:DD:EE:FF".to_string())
80        );
81    }
82    #[test]
83    fn can_parse_a_valid_ssconf_uri_with_hostname_and_no_path() {
84        let input = "ssconf://my.domain.com";
85        let online_config = SIP008Config::parse(input).unwrap();
86        let url = Url::parse(&online_config.location).unwrap();
87        assert_eq!(url, Url::parse("https://my.domain.com").unwrap());
88        assert_eq!(online_config.cert_finger_print, None);
89    }
90
91    #[test]
92    fn can_parse_a_valid_ssconf_uri_with_ipv4_address() {
93        let input = "ssconf://1.2.3.4/secret/long/path#certFp=AA:BB:CC:DD:EE:FF&other=param";
94        let online_config = SIP008Config::parse(input).unwrap();
95        let url = Url::parse(&online_config.location).unwrap();
96        assert_eq!(url, Url::parse("https://1.2.3.4/secret/long/path").unwrap());
97        assert_eq!(
98            online_config.cert_finger_print,
99            Some("AA:BB:CC:DD:EE:FF".to_string())
100        );
101    }
102
103    #[test]
104    fn can_parse_a_valid_ssconf_uri_with_ipv6_address_and_custom_port() {
105        // encodeURI encodes the IPv6 address brackets.
106        let input = "ssconf://[2001:0:ce49:7601:e866:efff:62c3:fffe]:8081/secret/long/path#certFp=AA:BB:CC:DD:EE:FF";
107        let online_config = SIP008Config::parse(input).unwrap();
108        let url = Url::parse(&online_config.location).unwrap();
109        assert_eq!(
110            url,
111            Url::parse("https://[2001:0:ce49:7601:e866:efff:62c3:fffe]:8081/secret/long/path")
112                .unwrap()
113        );
114        assert_eq!(
115            online_config.cert_finger_print,
116            Some("AA:BB:CC:DD:EE:FF".to_string())
117        );
118    }
119
120    #[test]
121    fn can_parse_a_valid_ssconf_uri_with_uri_encoded_tag() {
122        let cert_fp = percent_encode("&=?:%".as_ref(), NON_ALPHANUMERIC).to_string();
123        let input = format!("ssconf://1.2.3.4/secret#certFp={cert_fp}&httpMethod=GET");
124        let online_config = SIP008Config::parse(&input).unwrap();
125        let url = Url::parse(&online_config.location).unwrap();
126        assert_eq!(url, Url::parse("https://1.2.3.4/secret").unwrap());
127        assert_eq!(online_config.cert_finger_print, Some("&=?:%".to_string()));
128        assert_eq!(online_config.http_method, Some("GET".to_string()));
129    }
130}