vpn_link_serde/
shadowsocks.rs1use crate::ProtocolParser;
18use crate::constants::{error_msg, scheme};
19use crate::error::{ProtocolError, Result};
20use base64::Engine;
21use serde::{Deserialize, Serialize};
22
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25pub struct ShadowsocksConfig {
26 pub method: String,
28 pub password: String,
30 pub address: String,
32 pub port: u16,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub tag: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub plugin: Option<String>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Shadowsocks {
45 pub config: ShadowsocksConfig,
47}
48
49impl ProtocolParser for Shadowsocks {
50 fn parse(link: &str) -> Result<Self> {
51 if !link.to_lowercase().starts_with(scheme::SHADOWSOCKS) {
52 return Err(ProtocolError::InvalidFormat(format!(
53 "{} {}",
54 error_msg::MUST_START_WITH,
55 scheme::SHADOWSOCKS
56 )));
57 }
58
59 let link_body = &link[scheme::SHADOWSOCKS.len()..];
60
61 let (main_part, tag) = {
63 if let Some(hash_pos) = link_body.find('#') {
64 let tag_str = &link_body[hash_pos + 1..];
65 let decoded_tag = urlencoding::decode(tag_str).map_err(|e| {
66 ProtocolError::UrlParseError(format!("Failed to decode tag: {}", e))
67 })?;
68 (&link_body[..hash_pos], Some(decoded_tag.to_string()))
69 } else {
70 (link_body, None)
71 }
72 };
73
74 let (address_part, plugin) = {
76 if let Some(query_pos) = main_part.find('?') {
77 let query_str = &main_part[query_pos + 1..];
78 let params: std::collections::HashMap<String, String> =
79 url::form_urlencoded::parse(query_str.as_bytes())
80 .into_owned()
81 .collect();
82 let plugin = params.get("plugin").cloned();
83 (&main_part[..query_pos], plugin)
84 } else {
85 (main_part, None)
86 }
87 };
88
89 let (method, password, address, port) = if address_part
91 .chars()
92 .all(|c| c.is_alphanumeric() || c == '+' || c == '/' || c == '=')
93 {
94 let decoded = base64::engine::general_purpose::STANDARD.decode(address_part)?;
96 let decoded_str = String::from_utf8(decoded)
97 .map_err(|e| ProtocolError::InvalidFormat(format!("Invalid UTF-8: {}", e)))?;
98
99 let at_pos = decoded_str
100 .rfind('@')
101 .ok_or_else(|| ProtocolError::InvalidFormat(error_msg::MISSING_AT.to_string()))?;
102
103 let method_password = &decoded_str[..at_pos];
104 let host_port = &decoded_str[at_pos + 1..];
105
106 let colon_pos = method_password.find(':').ok_or_else(|| {
107 ProtocolError::InvalidFormat("Missing ':' in method:password".to_string())
108 })?;
109
110 let method = &method_password[..colon_pos];
111 let password = &method_password[colon_pos + 1..];
112
113 let hp_colon = host_port.find(':').ok_or_else(|| {
114 ProtocolError::InvalidFormat(error_msg::MISSING_COLON_HOST_PORT.to_string())
115 })?;
116
117 let address = &host_port[..hp_colon];
118 let port_str = host_port[hp_colon + 1..].trim_end_matches('/');
119 let port: u16 = port_str.parse().map_err(|e| {
120 ProtocolError::InvalidField(format!("{}: {}", error_msg::INVALID_PORT, e))
121 })?;
122
123 (
124 method.to_string(),
125 password.to_string(),
126 address.to_string(),
127 port,
128 )
129 } else {
130 let at_pos = address_part
132 .rfind('@')
133 .ok_or_else(|| ProtocolError::InvalidFormat(error_msg::MISSING_AT.to_string()))?;
134
135 let user_info = &address_part[..at_pos];
136 let host_port = address_part[at_pos + 1..].trim_end_matches('/');
137
138 let decoded_user = base64::engine::general_purpose::STANDARD.decode(user_info)?;
139 let user_str = String::from_utf8(decoded_user)
140 .map_err(|e| ProtocolError::InvalidFormat(format!("Invalid UTF-8: {}", e)))?;
141
142 let colon_pos = user_str.find(':').ok_or_else(|| {
143 ProtocolError::InvalidFormat("Missing ':' in method:password".to_string())
144 })?;
145
146 let method = &user_str[..colon_pos];
147 let password = &user_str[colon_pos + 1..];
148
149 let hp_colon = host_port.find(':').ok_or_else(|| {
150 ProtocolError::InvalidFormat(error_msg::MISSING_COLON_HOST_PORT.to_string())
151 })?;
152
153 let address = &host_port[..hp_colon];
154 let port_str = &host_port[hp_colon + 1..];
155 let port: u16 = port_str.parse().map_err(|e| {
156 ProtocolError::InvalidField(format!("{}: {}", error_msg::INVALID_PORT, e))
157 })?;
158
159 (
160 method.to_string(),
161 password.to_string(),
162 address.to_string(),
163 port,
164 )
165 };
166
167 Ok(Shadowsocks {
168 config: ShadowsocksConfig {
169 method,
170 password,
171 address,
172 port,
173 tag,
174 plugin,
175 },
176 })
177 }
178
179 fn to_link(&self) -> Result<String> {
180 let user_info = format!("{}:{}", self.config.method, self.config.password);
182 let encoded_user = base64::engine::general_purpose::STANDARD.encode(user_info.as_bytes());
183
184 let mut link = format!(
186 "ss://{}@{}:{}",
187 encoded_user, self.config.address, self.config.port
188 );
189 if self.config.plugin.is_some() {
190 link.push('/');
191 }
192
193 if let Some(ref plugin) = self.config.plugin {
195 link.push_str(&format!("?plugin={}", urlencoding::encode(plugin)));
196 }
197
198 if let Some(ref tag) = self.config.tag {
200 link.push_str(&format!("#{}", urlencoding::encode(tag)));
201 }
202
203 Ok(link)
204 }
205}