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 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 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}