diff --git a/Cargo.lock b/Cargo.lock index 64506eb..76a9e36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,11 @@ name = "autocfg" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "base64" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bitflags" version = "1.2.1" @@ -148,6 +153,16 @@ dependencies = [ "wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "iovec" version = "0.1.4" @@ -225,6 +240,11 @@ dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "memchr" version = "2.3.3" @@ -321,6 +341,11 @@ dependencies = [ "vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "pin-project-lite" version = "0.1.5" @@ -459,9 +484,11 @@ name = "shadowrocks" version = "0.1.0" dependencies = [ "async-trait 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)", + "base64 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "openssl 0.10.29 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "ring 0.16.13 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)", @@ -469,6 +496,7 @@ dependencies = [ "sodiumoxide 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "stderrlog 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -476,6 +504,11 @@ name = "slab" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "smallvec" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "sodiumoxide" version = "0.2.5" @@ -604,6 +637,22 @@ dependencies = [ "syn 1.0.22 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "smallvec 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "unicode-width" version = "0.1.7" @@ -627,6 +676,16 @@ name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "url" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "vcpkg" version = "0.2.8" @@ -765,6 +824,7 @@ dependencies = [ "checksum async-trait 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "26c4f3195085c36ea8d24d32b2f828d23296a9370a28aa39d111f6f16bef9f3b" "checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" "checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" +"checksum base64 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "53d1ccbaf7d9ec9537465a97bf19edc1a4e158ecb49fc16178202238c569cc42" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum bumpalo 3.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5356f1d23ee24a1f785a56d1d1a5f0fd5b0f6a0c0fb2412ce11da71649ab78f6" "checksum bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" @@ -780,6 +840,7 @@ dependencies = [ "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum futures-core 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" "checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" "checksum iovec 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" "checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" "checksum js-sys 0.3.39 (registry+https://github.com/rust-lang/crates.io-index)" = "fa5a448de267e7358beaf4a5d849518fe9a0c13fce7afd44b06e68550e5562a7" @@ -790,6 +851,7 @@ dependencies = [ "checksum libflate 0.1.27 (registry+https://github.com/rust-lang/crates.io-index)" = "d9135df43b1f5d0e333385cb6e7897ecd1a43d7d11b91ac003f4d2c2d2401fdd" "checksum libsodium-sys 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1c344ff12b90ef8fa1f0fffacd348c1fd041db331841fec9eab23fdb991f5e73" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" "checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" "checksum mio 0.6.22 (registry+https://github.com/rust-lang/crates.io-index)" = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" "checksum miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" @@ -800,6 +862,7 @@ dependencies = [ "checksum once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" "checksum openssl 0.10.29 (registry+https://github.com/rust-lang/crates.io-index)" = "cee6d85f4cb4c4f59a6a85d5b68a233d280c82e29e822913b9c8b129fbf20bdd" "checksum openssl-sys 0.9.56 (registry+https://github.com/rust-lang/crates.io-index)" = "f02309a7f127000ed50594f0b50ecc69e7c654e16d41b4e8156d1b3df8e0b52e" +"checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" "checksum pin-project-lite 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f7505eeebd78492e0f6108f7171c4948dbb120ee8119d9d77d0afa5469bef67f" "checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" "checksum ppv-lite86 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" @@ -818,6 +881,7 @@ dependencies = [ "checksum serde_derive 1.0.110 (registry+https://github.com/rust-lang/crates.io-index)" = "818fbf6bfa9a42d3bfcaca148547aa00c7b915bec71d1757aa2d44ca68771984" "checksum serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)" = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +"checksum smallvec 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" "checksum sodiumoxide 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585232e78a4fc18133eef9946d3080befdf68b906c51b621531c37e91787fa2b" "checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" "checksum stderrlog 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "32e5ee9b90a5452c570a0b0ac1c99ae9498db7e56e33d74366de7f2a7add7f25" @@ -832,10 +896,13 @@ dependencies = [ "checksum time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" "checksum tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d099fa27b9702bed751524694adbe393e18b36b204da91eb1cbbbbb4a5ee2d58" "checksum tokio-macros 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" +"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +"checksum unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" "checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" "checksum untrusted 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +"checksum url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" "checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" "checksum vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" diff --git a/Cargo.toml b/Cargo.toml index 85f9380..633647d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,9 @@ sodiumoxide = "0.2" tokio = { version = "0.2", features = ["dns", "io-util", "macros", "rt-core", "stream", "tcp"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +url = "2.1" +base64 = "0.12" +percent-encoding = "2.1" [dev-dependencies] ring = "~0.16.7" diff --git a/README.md b/README.md index a9ba1fc..7c1f62c 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Features - [x] Replay attack mitigation in non-compatible mode - [ ] Native obfuscation - [ ] [Manager][10] API to create servers on the fly -- [ ] `ss://` URL and JSON config file +- [x] `ss://` URL and JSON config file Crypto dependencies ------------------- @@ -136,7 +136,7 @@ still terrible. [1]: https://github.com/shadowsocks/shadowsocks "shadowsocks" [2]: https://github.com/shadowsocks/shadowsocks-rust "shadowsocks-rust" -[3]: https://github.com/shadowsocks/shadowsocks-org/issues/27 "SIP002" +[3]: https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html "SIP002" [4]: https://tools.ietf.org/html/rfc2898#section-5.2 "RFC 2898" [5]: https://tools.ietf.org/html/rfc2898#section-5.1 "RFC 2898" [6]: https://tools.ietf.org/html/rfc5869 "RFC 5869" diff --git a/src/bin/local.rs b/src/bin/local.rs index 0234d27..e5fafbd 100644 --- a/src/bin/local.rs +++ b/src/bin/local.rs @@ -23,10 +23,13 @@ async fn main() -> Result<()> { (@arg password: -k +takes_value display_order(6) "password") (@arg method: -m +takes_value display_order(7) default_value("aes-256-gcm") possible_values(CipherType::possible_ciphers()) "encryption method to use") - (@arg timeout: -t +takes_value display_order(8) default_value("300") "timeout in seconds") + (@arg server_url: --("server-url") +takes_value display_order(8) + "A url that identifies a server. See https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html") + + (@arg timeout: -t +takes_value display_order(9) default_value("300") "timeout in seconds") // Quote to escape hyphen "-" - (@arg fast_open: --("fast-open") display_order(9) "use TCP_FASTOPEN, requires Linux 3.7+") - (@arg compatible_mode: --("compatible-mode") display_order(10) "keep compatible with Shadowsocks") + (@arg fast_open: --("fast-open") display_order(10) "use TCP_FASTOPEN, requires Linux 3.7+") + (@arg compatible_mode: --("compatible-mode") display_order(11) "keep compatible with Shadowsocks") ); let matches = app.get_matches(); diff --git a/src/bin/utils/mod.rs b/src/bin/utils/mod.rs index aefaf54..abc7946 100644 --- a/src/bin/utils/mod.rs +++ b/src/bin/utils/mod.rs @@ -4,7 +4,7 @@ extern crate stderrlog; use std::net::{SocketAddr, ToSocketAddrs}; use std::time::Duration; -use shadowrocks::{GlobalConfig, ParsedFlags, Result}; +use shadowrocks::{GlobalConfig, ParsedFlags, ParsedServerUrl, Result}; fn choose_log_level() -> log::LevelFilter { if cfg!(debug_assertions) { @@ -33,6 +33,12 @@ pub fn parse_commandline_args( .transpose()?; let parsed_flags = parsed_flags.as_ref(); + let server_url = matches + .value_of("server-url") + .map(ParsedServerUrl::from_url_string) + .transpose()?; + let server_url = server_url.as_ref(); + let server_addr = matches.value_of("server_addr").unwrap_or("0.0.0.0"); let server_port: u16 = matches .value_of("server_port") @@ -41,6 +47,7 @@ pub fn parse_commandline_args( .expect("Server port must be a valid number."); let server_socket_addr = parsed_flags .and_then(ParsedFlags::server_addr) + .or_else(|| server_url.map(|s| s.server_addr())) .unwrap_or((server_addr, server_port)) .to_socket_addrs()? .next() @@ -59,16 +66,20 @@ pub fn parse_commandline_args( .next() .expect("Expecting a valid server address and port."); - let password = parsed_flags.map(|c| c.password()).unwrap_or_else(|| { - matches - .value_of("password") - .expect("Password is required.") - .as_bytes() - }); + let password = parsed_flags + .map(|c| c.password()) + .or_else(|| server_url.map(|s| s.password())) + .unwrap_or_else(|| { + matches + .value_of("password") + .expect("Password is required.") + .as_bytes() + }); let cipher_name = parsed_flags .and_then(|c| c.encryption_method()) .or_else(|| matches.value_of("method")) + .or_else(|| server_url.map(|s| s.encryption_method())) .unwrap_or("aes-256-gcm"); let timeout = matches diff --git a/src/error.rs b/src/error.rs index 80081e1..e810b50 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,6 +12,7 @@ pub enum Error { DecryptionError, UnknownCipher(String), InvalidConfigFile(String), + InvalidServerUrl(String), } impl std::fmt::Display for Error { @@ -44,6 +45,7 @@ impl std::fmt::Display for Error { Error::InvalidConfigFile(s) => { write!(f, "Invalid config file {}", s) } + Error::InvalidServerUrl(s) => write!(f, "Invalid server URL {}", s), } } } diff --git a/src/lib.rs b/src/lib.rs index e56b303..5a51a20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,9 @@ extern crate async_trait; +extern crate base64; +extern crate clap; extern crate log; extern crate openssl; +extern crate percent_encoding; extern crate rand; #[cfg(feature = "ring")] extern crate ring; @@ -8,6 +11,7 @@ extern crate serde; extern crate serde_json; extern crate sodiumoxide; extern crate tokio; +extern crate url; // Don't move! macros defined in test_utils must be included first. #[cfg(test)] @@ -20,6 +24,7 @@ mod encrypted_stream; mod error; mod global_config; mod parsed_flags; +mod parsed_server_url; pub mod shadow_server; mod socks5_addr; pub mod socks_server; @@ -30,5 +35,6 @@ pub type Result = std::result::Result; pub use crypto::CipherType; pub use global_config::GlobalConfig; pub use parsed_flags::ParsedFlags; +pub use parsed_server_url::ParsedServerUrl; pub use shadow_server::ShadowServer; pub use socks_server::SocksServer; diff --git a/src/main.rs b/src/main.rs index 8115ef5..4ffe6e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,8 @@ use std::net::ToSocketAddrs; use std::time::Duration; use shadowrocks::{ - shadow_server, socks_server, GlobalConfig, ParsedFlags, Result, + shadow_server, socks_server, GlobalConfig, ParsedFlags, ParsedServerUrl, + Result, }; #[path = "bin/utils/mod.rs"] @@ -30,6 +31,7 @@ async fn main() -> Result<()> { -t [TIMEOUT] 'timeout in seconds, default: 300' -c [CONFIG_FILE] 'path to config file. See https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File' + --server-url [SS_URL] 'A url that identifies a server. See https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html.' --fast-open 'use TCP_FASTOPEN, requires Linux 3.7+, default: false' --compatible-mode 'keep compatible with Shadowsocks in encryption-related areas, default: false' @@ -43,12 +45,22 @@ async fn main() -> Result<()> { .map(ParsedFlags::from_config_file) .transpose()?; let parsed_flags = parsed_flags.as_ref(); - let password = parsed_flags.map(|c| c.password()).unwrap_or_else(|| { - matches - .value_of("k") - .expect("Password is required.") - .as_bytes() - }); + + let server_url = matches + .value_of("server-url") + .map(ParsedServerUrl::from_url_string) + .transpose()?; + let server_url = server_url.as_ref(); + + let password = parsed_flags + .map(|c| c.password()) + .or_else(|| server_url.map(|s| s.password())) + .unwrap_or_else(|| { + matches + .value_of("k") + .expect("Password is required.") + .as_bytes() + }); let server_addr = matches.value_of("s").unwrap_or("0.0.0.0"); let server_port: u16 = matches @@ -58,6 +70,7 @@ async fn main() -> Result<()> { .expect("Server port must be a valid number."); let server_socket_addr = parsed_flags .and_then(ParsedFlags::server_addr) + .or_else(|| server_url.map(|s| s.server_addr())) .unwrap_or((server_addr, server_port)) .to_socket_addrs()? .next() @@ -81,6 +94,7 @@ async fn main() -> Result<()> { let cipher_name = matches.value_of("m").unwrap_or("aes-256-gcm"); let cipher_name = parsed_flags .and_then(|c| c.encryption_method()) + .or_else(|| server_url.map(|s| s.encryption_method())) .unwrap_or(cipher_name); let timeout = matches .value_of("t") diff --git a/src/parsed_server_url.rs b/src/parsed_server_url.rs new file mode 100644 index 0000000..3636475 --- /dev/null +++ b/src/parsed_server_url.rs @@ -0,0 +1,255 @@ +use std::str::FromStr; + +use percent_encoding::percent_decode_str; +use url::Url; + +use crate::{Error, Result}; + +pub struct ParsedServerUrl { + server_addr: (String, u16), + encryption_method: String, + password: Vec, +} + +impl FromStr for ParsedServerUrl { + type Err = Error; + + fn from_str(url_str: &str) -> Result { + Self::from_url_string(url_str) + } +} + +impl ParsedServerUrl { + pub fn server_addr(&self) -> (&str, u16) { + (self.server_addr.0.as_str(), self.server_addr.1) + } + + pub fn encryption_method(&self) -> &str { + self.encryption_method.as_str() + } + + pub fn password(&self) -> &[u8] { + self.password.as_slice() + } +} + +impl ParsedServerUrl { + /// Parse SS URL, the format of which is specified in + /// https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html + pub fn from_url_string(url_str: &str) -> Result { + let parsed_url = match Url::parse(url_str) { + Ok(parsed_url) => parsed_url, + Err(e) => { + return Err(Error::InvalidServerUrl(format!( + "Not a valid URL: {}", + e + ))) + } + }; + + if parsed_url.scheme() != "ss" { + return Err(Error::InvalidServerUrl("Scheme is not 'ss'".into())); + } + + // userinfo = web-safe-base64.encode("{auth-method}:{password}") + let userinfo = + match percent_decode_str(parsed_url.username()).decode_utf8() { + Ok(userinfo) => userinfo.to_string(), + Err(e) => { + return Err(Error::InvalidServerUrl(format!( + "Cannot parse userinfo into UTF8: {}", + e + ))) + } + }; + let auth_and_password = + match base64::decode_config(userinfo, base64::URL_SAFE) { + Ok(auth_and_password) => auth_and_password, + Err(e) => { + return Err(Error::InvalidServerUrl(format!( + "Failed to decode base64 userinfo: {}", + e + ))) + } + }; + + // Split into (auth method, password) pair. + let mut iter = auth_and_password.split(|c| *c == b':'); + let auth = iter.next().expect("Split returns at least one element"); + + let auth_str = std::str::from_utf8(auth).map_err(|e| { + Error::InvalidServerUrl(format!( + "Cannot parse auth method into UTF8 string: {}", + e + )) + })?; + + let password = iter.next().ok_or_else(|| { + Error::InvalidServerUrl("Cannot find password in userinfo".into()) + })?; + if iter.next().is_some() { + return Err(Error::InvalidServerUrl( + "There are more than two components in userinfo".into(), + )); + } + + let host = parsed_url.host_str().ok_or_else(|| { + // This actually will never happen. An "empty host" error will be + // thrown at the beginning if there is no host. + Error::InvalidServerUrl("Cannot find server address".into()) + })?; + let port = parsed_url.port().ok_or_else(|| { + Error::InvalidServerUrl("Cannot find server port".into()) + })?; + + Ok(ParsedServerUrl { + server_addr: (host.to_owned(), port), + encryption_method: auth_str.into(), + password: password.to_vec(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_invalid_server_url(result: Result, msg: &str) { + if let Err(Error::InvalidServerUrl(s)) = result { + assert_eq!(s, msg); + } else { + panic!("Expecting invalid server URL error."); + } + } + + #[test] + fn test_server_url_parse() -> Result<()> { + let server_url = ParsedServerUrl::from_url_string( + "ss://cmM0LW1kNTpwYXNzd2Q=@192.168.100.1:8888/\ + ?plugin=obfs-local%3Bobfs%3Dhttp#Example2", + )?; + assert_eq!(server_url.server_addr, ("192.168.100.1".to_owned(), 8888)); + assert_eq!(server_url.encryption_method, "rc4-md5"); + assert_eq!(server_url.password, b"passwd"); + + Ok(()) + } + + #[test] + fn test_server_url_parse_two() -> Result<()> { + let server_url = ParsedServerUrl::from_url_string( + "ss://eGNoYWNoYTIwLWlldGYtcG9seTEzMDU6dGVzdC10ZXN0@127.0.0.1:51986", + )?; + assert_eq!(server_url.server_addr, ("127.0.0.1".to_owned(), 51986)); + assert_eq!(server_url.encryption_method, "xchacha20-ietf-poly1305"); + assert_eq!(server_url.password, b"test-test"); + + Ok(()) + } + + #[test] + fn test_server_url_not_url() -> Result<()> { + let result = ParsedServerUrl::from_url_string("/"); + assert_invalid_server_url( + result, + "Not a valid URL: relative URL without a base", + ); + + Ok(()) + } + + #[test] + fn test_server_url_scheme_mismatch() -> Result<()> { + let result = ParsedServerUrl::from_url_string( + "sss://eGNoYWNoYTIwLWlldGYtcG9seTEzMDU6dGVzdC10ZXN0@127.0.0.1:51986", + ); + assert_invalid_server_url(result, "Scheme is not \'ss\'"); + + Ok(()) + } + + #[test] + fn test_server_url_not_utf8() -> Result<()> { + let result = + ParsedServerUrl::from_url_string("ss://%a0%a1@127.0.0.1:51986"); + assert_invalid_server_url( + result, + "Cannot parse userinfo into UTF8: \ + invalid utf-8 sequence of 1 bytes from index 0", + ); + + Ok(()) + } + + #[test] + fn test_server_url_not_base64() -> Result<()> { + let result = ParsedServerUrl::from_url_string( + "ss://eGNoYWNoYTIwLWlldGYtcG9seTEzMDU6dGVzdC10ZXN0=@127.0.0.1:51986", + ); + assert_invalid_server_url( + result, + "Failed to decode base64 userinfo: \ + Encoded text cannot have a 6-bit remainder.", + ); + + Ok(()) + } + + #[test] + fn test_server_url_auth_not_utf8() -> Result<()> { + // echo -n "\xa0\xa1xchacha20-ietf-poly1305:test-test" | base64 + let result = ParsedServerUrl::from_url_string( + "ss://oKF4Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTp0ZXN0LXRlc3Q=@127.0.0.1:51986" + ); + assert_invalid_server_url( + result, + "Cannot parse auth method into UTF8 string: \ + invalid utf-8 sequence of 1 bytes from index 0", + ); + + Ok(()) + } + + #[test] + fn test_server_url_no_password() -> Result<()> { + let result = ParsedServerUrl::from_url_string( + "ss://eGNoYWNoYTIwLWlldGYtcG9seTEzMDU=@127.0.0.1:51986", + ); + assert_invalid_server_url(result, "Cannot find password in userinfo"); + + Ok(()) + } + + #[test] + fn test_server_url_too_many_passwords() -> Result<()> { + let result = ParsedServerUrl::from_url_string( + "ss://eGNoYWNoYTIwLWlldGYtcG9seTEzMDU6dGVzdC10ZXN0Og==@127.0.0.1:51986", + ); + assert_invalid_server_url( + result, + "There are more than two components in userinfo", + ); + + Ok(()) + } + + #[test] + fn test_server_url_no_host() -> Result<()> { + let result = ParsedServerUrl::from_url_string( + "ss://eGNoYWNoYTIwLWlldGYtcG9seTEzMDU6dGVzdC10ZXN0@", + ); + assert_invalid_server_url(result, "Not a valid URL: empty host"); + + Ok(()) + } + + #[test] + fn test_server_url_no_port() -> Result<()> { + let result = ParsedServerUrl::from_url_string( + "ss://eGNoYWNoYTIwLWlldGYtcG9seTEzMDU6dGVzdC10ZXN0@127.0.0.1", + ); + assert_invalid_server_url(result, "Cannot find server port"); + + Ok(()) + } +}