ss_rs/
url.rs

1//! SS-URL parser
2
3use std::{
4    fmt::{self, Display, Formatter},
5    str::FromStr,
6};
7
8use base64::{Engine as _, engine::general_purpose};
9
10use crate::crypto::cipher::Method;
11
12/// Represents a SS-URL.
13#[derive(Debug)]
14pub struct SsUrl {
15    pub method: Method,
16    pub password: String,
17    pub hostname: String,
18    pub port: u16,
19    pub plugin: Option<String>,
20    pub plugin_opts: Option<String>,
21    pub tag: Option<String>,
22}
23
24impl FromStr for SsUrl {
25    type Err = ErrorKind;
26
27    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
28        if !s.starts_with("ss://") {
29            return Err(ErrorKind::NotSsUrl);
30        }
31        s = &s[5..];
32
33        let (method, password) = parse_userinfo(&mut s)?;
34        let hostname = parse_hostname(&mut s)?;
35
36        let port: u16;
37        let mut plugin = None;
38        let mut plugin_opts = None;
39        let mut tag = None;
40
41        if let Some(pos) = s.find(['/', '?', '#']) {
42            port = parse_port(&s[..pos])?;
43
44            let mut pos = pos;
45            let mut has_plugin = false;
46
47            loop {
48                match s.as_bytes()[pos] {
49                    b'/' => {
50                        s = &s[pos + 1..];
51                        match s.find(['?', '#']) {
52                            Some(x) => pos = x,
53                            None => break,
54                        }
55                    }
56                    b'?' => {
57                        has_plugin = true;
58
59                        s = &s[pos + 1..];
60                        match s.find(['#']) {
61                            Some(x) => pos = x,
62                            None => {
63                                let (a, b) = parse_plugin(&s)?;
64                                plugin = a;
65                                plugin_opts = b;
66                                break;
67                            }
68                        }
69                    }
70                    b'#' => {
71                        if has_plugin {
72                            let (a, b) = parse_plugin(&s[..pos])?;
73                            plugin = a;
74                            plugin_opts = b;
75                        }
76
77                        tag = Some(s[pos + 1..].to_owned());
78                        break;
79                    }
80                    _ => {}
81                }
82            }
83        } else {
84            port = parse_port(&s)?;
85        }
86
87        Ok(SsUrl {
88            method,
89            password,
90            hostname,
91            port,
92            plugin,
93            plugin_opts,
94            tag,
95        })
96    }
97}
98
99impl Display for SsUrl {
100    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
101        let s = format!("{}:{}", self.method.to_string(), self.password);
102        let s = general_purpose::URL_SAFE.encode(&s);
103        let mut s = format!("ss://{}@{}:{}", s, self.hostname, self.port);
104
105        if let Some(ref plugin) = self.plugin {
106            s = format!("{}/?{}", s, plugin);
107        }
108
109        if let Some(ref plugin_opts) = self.plugin_opts {
110            s = format!("{}={}", s, urlencoding::encode(plugin_opts));
111        }
112
113        if let Some(ref tag) = self.tag {
114            s = format!("{}#{}", s, tag);
115        }
116
117        write!(f, "{}", s)
118    }
119}
120
121fn parse_userinfo(s: &mut &str) -> Result<(Method, String), ErrorKind> {
122    let pos = match s.find('@') {
123        Some(x) => x,
124        None => return Err(ErrorKind::Invalid),
125    };
126
127    let userinfo = &s[..pos];
128    let userinfo = match general_purpose::URL_SAFE.decode(userinfo) {
129        Ok(x) => String::from_utf8(x).unwrap(),
130        Err(_) => return Err(ErrorKind::Decode),
131    };
132
133    let (method, password) = match userinfo.split_once(':') {
134        Some(x) => x,
135        None => return Err(ErrorKind::UserInfo),
136    };
137
138    let method: Method = match method.parse() {
139        Ok(x) => x,
140        Err(_) => return Err(ErrorKind::Method),
141    };
142    let password = password.to_owned();
143
144    *s = &s[pos + 1..];
145    Ok((method, password))
146}
147
148fn parse_hostname(s: &mut &str) -> Result<String, ErrorKind> {
149    let pos = match s.find(':') {
150        Some(x) => x,
151        None => return Err(ErrorKind::Invalid),
152    };
153
154    let hostname = s[..pos].to_owned();
155
156    *s = &s[pos + 1..];
157    Ok(hostname)
158}
159
160fn parse_port(s: &str) -> Result<u16, ErrorKind> {
161    match s.parse() {
162        Ok(x) => Ok(x),
163        Err(_) => Err(ErrorKind::Port),
164    }
165}
166
167fn parse_plugin(s: &str) -> Result<(Option<String>, Option<String>), ErrorKind> {
168    match s.split_once('=') {
169        Some((a, b)) => Ok((Some(a.to_owned()), Some(match urlencoding::decode(b) {
170            Ok(x) => x.into_owned(),
171            Err(_) => return Err(ErrorKind::Plugin),
172        }))),
173        None => Ok((Some(s.to_owned()), None)),
174    }
175}
176
177/// Errors when parsing a SS-URL.
178#[derive(Debug)]
179pub enum ErrorKind {
180    /// Not a SS-URL.
181    NotSsUrl,
182
183    /// Base64 decode error.
184    Decode,
185
186    /// Invalid userinfo.
187    UserInfo,
188
189    /// Invalid method.
190    Method,
191
192    /// Invalid port number.
193    Port,
194
195    /// Invalid plugin.
196    Plugin,
197
198    /// Invalid url.
199    Invalid,
200}
201
202impl Display for ErrorKind {
203    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
204        match self {
205            ErrorKind::NotSsUrl => write!(f, "not a ss url"),
206            ErrorKind::Decode => write!(f, "base64 decode error"),
207            ErrorKind::UserInfo => write!(f, "invalid userinfo"),
208            ErrorKind::Method => write!(f, "invalid method"),
209            ErrorKind::Port => write!(f, "invalid port number"),
210            ErrorKind::Plugin => write!(f, "invalid plugin"),
211            ErrorKind::Invalid => write!(f, "invalid url"),
212        }
213    }
214}
215
216impl std::error::Error for ErrorKind {}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn it_works() {
224        let urllist = [
225            "ss://YWVzLTI1Ni1nY206dGVzdA==@192.168.100.1:8888",
226            "ss://YWVzLTI1Ni1nY206dGVzdA==@192.168.100.1:8888/?",
227            "ss://YWVzLTI1Ni1nY206dGVzdA==@192.168.100.1:8888/?#",
228            
229            "ss://YWVzLTI1Ni1nY206dGVzdA==@192.168.100.1:8888/?plugin",
230            "ss://YWVzLTI1Ni1nY206dGVzdA==@192.168.100.1:8888/?plugin=name%3Bplugin_opts",
231
232            "ss://YWVzLTI1Ni1nY206dGVzdA==@192.168.100.1:8888/?plugin=url-encoded-plugin-argument-value%26unsupported-arguments%3Dshould-be-ignored#Dummy+profile+name",
233            "ss://YWVzLTEyOC1nY206dGVzdA==@192.168.100.1:8888#Example1",
234            "ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpwYXNzd2Q=@192.168.100.1:8888/?plugin=obfs-local%3Bobfs%3Dhttp#Example2",
235        ];
236
237        for url in urllist.iter() {
238            let ss_url = url.parse::<SsUrl>().unwrap();
239            println!("{:#?}", ss_url);
240
241            assert_eq!(&ss_url.to_string(), url);
242        }
243
244        let urllist = [
245            "ss://YWVzLTI1Ni1nY206dGVzdA==@192.168.100.1:8888/",
246            "ss://YWVzLTI1Ni1nY206dGVzdA==@192.168.100.1:8888/#",
247            "ss://YWVzLTI1Ni1nY206dGVzdA==@192.168.100.1:8888?#",
248        ];
249
250        for url in urllist.iter() {
251            let ss_url = url.parse::<SsUrl>().unwrap();
252            println!("{:#?}", ss_url);
253        }
254    }
255}