1use 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#[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#[derive(Debug)]
179pub enum ErrorKind {
180 NotSsUrl,
182
183 Decode,
185
186 UserInfo,
188
189 Method,
191
192 Port,
194
195 Plugin,
197
198 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}