Skip to content

Commit 1008eee

Browse files
committed
feat(local-http): support HTTP Basic auth (#1994)
1 parent d5b08c0 commit 1008eee

File tree

31 files changed

+541
-204
lines changed

31 files changed

+541
-204
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,10 @@ Example configuration:
614614
"local_address": "127.0.0.1",
615615
"local_port": 3128,
616616
// OPTIONAL. macOS launchd activate socket
617-
"launchd_tcp_socket_name": "TCPListener"
617+
"launchd_tcp_socket_name": "TCPListener",
618+
// OPTIONAL. Authentication configuration file
619+
// Configuration file document could be found in the next section.
620+
"http_auth_config_path": "/path/to/auth.json",
618621
},
619622
{
620623
// DNS local server (feature = "local-dns")
@@ -955,6 +958,24 @@ The configuration file is set by `socks5_auth_config_path` in `locals`.
955958
}
956959
```
957960

961+
### HTTP Authentication Configuration
962+
963+
The configuration file is set by `http_auth_config_path` in `locals`.
964+
965+
```jsonc
966+
{
967+
// Basic Authentication (RFC9110)
968+
"basic": {
969+
"users": [
970+
{
971+
"user_name": "USERNAME in UTF-8",
972+
"password": "PASSWORD in UTF-8"
973+
}
974+
]
975+
}
976+
}
977+
```
978+
958979
### Environment Variables
959980

960981
- `SS_SERVER_PASSWORD`: A default password for servers that created from command line argument (`--server-addr`)

bin/ssservice.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@ fn main() -> ExitCode {
1919

2020
// Allow running `ssservice` as symlink of `sslocal`, `ssserver` and `ssmanager`
2121
if let Some(program_path) = env::args().next()
22-
&& let Some(program_name) = Path::new(&program_path).file_name() {
23-
match program_name.to_str() {
24-
Some("sslocal") => return local::main(&local::define_command_line_options(app).get_matches()),
25-
Some("ssserver") => return server::main(&server::define_command_line_options(app).get_matches()),
26-
Some("ssmanager") => return manager::main(&manager::define_command_line_options(app).get_matches()),
27-
_ => {}
28-
}
22+
&& let Some(program_name) = Path::new(&program_path).file_name()
23+
{
24+
match program_name.to_str() {
25+
Some("sslocal") => return local::main(&local::define_command_line_options(app).get_matches()),
26+
Some("ssserver") => return server::main(&server::define_command_line_options(app).get_matches()),
27+
Some("ssmanager") => return manager::main(&manager::define_command_line_options(app).get_matches()),
28+
_ => {}
2929
}
30+
}
3031

3132
let matches = app
3233
.subcommand_required(true)

crates/shadowsocks-service/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ local-dns-relay = ["local-dns"]
6060
# Currently is only used in Android
6161
local-flow-stat = ["local"]
6262
# Enable HTTP protocol for sslocal
63-
local-http = ["local", "hyper", "http", "http-body-util"]
63+
local-http = ["local", "hyper", "http", "http-body-util", "base64"]
6464
local-http-native-tls = ["local-http", "tokio-native-tls", "native-tls"]
6565
local-http-native-tls-vendored = [
6666
"local-http-native-tls",
@@ -179,6 +179,7 @@ mime = { version = "0.3", optional = true }
179179
flate2 = { version = "1.0", optional = true }
180180
brotli = { version = "8.0", optional = true }
181181
zstd = { version = "0.13", optional = true }
182+
base64 = { version = "0.22", optional = true }
182183

183184
etherparse = { version = "0.19", optional = true }
184185
smoltcp = { version = "0.12", optional = true, default-features = false, features = [

crates/shadowsocks-service/src/acl/mod.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,14 @@ impl ParsingRules {
209209
}
210210
}
211211
} else if let Some(set_rule) = caps.get(2)
212-
&& let Ok(set_rule) = str::from_utf8(set_rule.as_bytes()) {
213-
let set_rule = set_rule.replace("\\.", ".");
214-
if self.add_set_rule_inner(&set_rule).is_ok() {
215-
trace!("REGEX-RULE {} => SET-RULE {}", rule, set_rule);
216-
return;
217-
}
212+
&& let Ok(set_rule) = str::from_utf8(set_rule.as_bytes())
213+
{
214+
let set_rule = set_rule.replace("\\.", ".");
215+
if self.add_set_rule_inner(&set_rule).is_ok() {
216+
trace!("REGEX-RULE {} => SET-RULE {}", rule, set_rule);
217+
return;
218218
}
219+
}
219220
}
220221

221222
trace!("REGEX-RULE {}", rule);

crates/shadowsocks-service/src/config.rs

Lines changed: 69 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ use shadowsocks::{
8080
use crate::acl::AccessControl;
8181
#[cfg(feature = "local-dns")]
8282
use crate::local::dns::NameServerAddr;
83+
#[cfg(feature = "local-http")]
84+
use crate::local::http::config::HttpAuthConfig;
8385
#[cfg(feature = "local")]
8486
use crate::local::socks::config::Socks5AuthConfig;
8587

@@ -323,6 +325,11 @@ struct SSLocalExtConfig {
323325
#[serde(skip_serializing_if = "Option::is_none")]
324326
socks5_auth_config_path: Option<String>,
325327

328+
/// HTTP
329+
#[cfg(feature = "local")]
330+
#[serde(skip_serializing_if = "Option::is_none")]
331+
http_auth_config_path: Option<String>,
332+
326333
/// Fake DNS
327334
#[cfg(feature = "local-fake-dns")]
328335
#[serde(skip_serializing_if = "Option::is_none")]
@@ -1038,6 +1045,10 @@ pub struct LocalConfig {
10381045
#[cfg(feature = "local")]
10391046
pub socks5_auth: Socks5AuthConfig,
10401047

1048+
/// HTTP Authentication configuration
1049+
#[cfg(feature = "local")]
1050+
pub http_auth: HttpAuthConfig,
1051+
10411052
/// Fake DNS record expire seconds
10421053
#[cfg(feature = "local-fake-dns")]
10431054
pub fake_dns_record_expire_duration: Option<Duration>,
@@ -1108,6 +1119,9 @@ impl LocalConfig {
11081119
#[cfg(feature = "local")]
11091120
socks5_auth: Socks5AuthConfig::default(),
11101121

1122+
#[cfg(feature = "local-http")]
1123+
http_auth: HttpAuthConfig::default(),
1124+
11111125
#[cfg(feature = "local-fake-dns")]
11121126
fake_dns_record_expire_duration: None,
11131127
#[cfg(feature = "local-fake-dns")]
@@ -1832,6 +1846,11 @@ impl Config {
18321846
local_config.socks5_auth = Socks5AuthConfig::load_from_file(&socks5_auth_config_path)?;
18331847
}
18341848

1849+
#[cfg(feature = "local-http")]
1850+
if let Some(http_auth_config_path) = local.http_auth_config_path {
1851+
local_config.http_auth = HttpAuthConfig::load_from_file(&http_auth_config_path)?;
1852+
}
1853+
18351854
#[cfg(feature = "local-fake-dns")]
18361855
{
18371856
if let Some(d) = local.fake_dns_record_expire_duration {
@@ -2390,15 +2409,16 @@ impl Config {
23902409
// Security
23912410
if let Some(sec) = config.security
23922411
&& let Some(replay_attack) = sec.replay_attack
2393-
&& let Some(policy) = replay_attack.policy {
2394-
match policy.parse::<ReplayAttackPolicy>() {
2395-
Ok(p) => nconfig.security.replay_attack.policy = p,
2396-
Err(..) => {
2397-
let err = Error::new(ErrorKind::Invalid, "invalid replay attack policy", None);
2398-
return Err(err);
2399-
}
2400-
}
2412+
&& let Some(policy) = replay_attack.policy
2413+
{
2414+
match policy.parse::<ReplayAttackPolicy>() {
2415+
Ok(p) => nconfig.security.replay_attack.policy = p,
2416+
Err(..) => {
2417+
let err = Error::new(ErrorKind::Invalid, "invalid replay attack policy", None);
2418+
return Err(err);
24012419
}
2420+
}
2421+
}
24022422

24032423
if let Some(balancer) = config.balancer {
24042424
nconfig.balancer = BalancerConfig {
@@ -2619,16 +2639,18 @@ impl Config {
26192639

26202640
// Balancer related checks
26212641
if let Some(rtt) = self.balancer.max_server_rtt
2622-
&& rtt.as_secs() == 0 {
2623-
let err = Error::new(ErrorKind::Invalid, "balancer.max_server_rtt must be > 0", None);
2624-
return Err(err);
2625-
}
2642+
&& rtt.as_secs() == 0
2643+
{
2644+
let err = Error::new(ErrorKind::Invalid, "balancer.max_server_rtt must be > 0", None);
2645+
return Err(err);
2646+
}
26262647

26272648
if let Some(intv) = self.balancer.check_interval
2628-
&& intv.as_secs() == 0 {
2629-
let err = Error::new(ErrorKind::Invalid, "balancer.check_interval must be > 0", None);
2630-
return Err(err);
2631-
}
2649+
&& intv.as_secs() == 0
2650+
{
2651+
let err = Error::new(ErrorKind::Invalid, "balancer.check_interval must be > 0", None);
2652+
return Err(err);
2653+
}
26322654
}
26332655

26342656
if self.config_type.is_server() && self.server.is_empty() {
@@ -2664,10 +2686,11 @@ impl Config {
26642686

26652687
// Plugin shouldn't be an empty string
26662688
if let Some(plugin) = server.plugin()
2667-
&& plugin.plugin.trim().is_empty() {
2668-
let err = Error::new(ErrorKind::Malformed, "`plugin` shouldn't be an empty string", None);
2669-
return Err(err);
2670-
}
2689+
&& plugin.plugin.trim().is_empty()
2690+
{
2691+
let err = Error::new(ErrorKind::Malformed, "`plugin` shouldn't be an empty string", None);
2692+
return Err(err);
2693+
}
26712694

26722695
// Server's domain name shouldn't be an empty string
26732696
match server.addr() {
@@ -2899,6 +2922,9 @@ impl fmt::Display for Config {
28992922
#[cfg(feature = "local")]
29002923
socks5_auth_config_path: None,
29012924

2925+
#[cfg(feature = "local-http")]
2926+
http_auth_config_path: None,
2927+
29022928
#[cfg(feature = "local-fake-dns")]
29032929
fake_dns_record_expire_duration: local.fake_dns_record_expire_duration.map(|d| d.as_secs()),
29042930
#[cfg(feature = "local-fake-dns")]
@@ -3068,20 +3094,22 @@ impl fmt::Display for Config {
30683094
}
30693095

30703096
if jconf.method.is_none()
3071-
&& let Some(ref m) = m.method {
3072-
jconf.method = Some(m.to_string());
3073-
}
3097+
&& let Some(ref m) = m.method
3098+
{
3099+
jconf.method = Some(m.to_string());
3100+
}
30743101

30753102
if jconf.plugin.is_none()
3076-
&& let Some(ref p) = m.plugin {
3077-
jconf.plugin = Some(p.plugin.clone());
3078-
if let Some(ref o) = p.plugin_opts {
3079-
jconf.plugin_opts = Some(o.clone());
3080-
}
3081-
if !p.plugin_args.is_empty() {
3082-
jconf.plugin_args = Some(p.plugin_args.clone());
3083-
}
3103+
&& let Some(ref p) = m.plugin
3104+
{
3105+
jconf.plugin = Some(p.plugin.clone());
3106+
if let Some(ref o) = p.plugin_opts {
3107+
jconf.plugin_opts = Some(o.clone());
30843108
}
3109+
if !p.plugin_args.is_empty() {
3110+
jconf.plugin_args = Some(p.plugin_args.clone());
3111+
}
3112+
}
30853113
}
30863114

30873115
if self.no_delay {
@@ -3188,17 +3216,18 @@ impl fmt::Display for Config {
31883216
/// It will return the original value if fails to read `${VAR_NAME}`.
31893217
pub fn read_variable_field_value(value: &str) -> Cow<'_, str> {
31903218
if let Some(left_over) = value.strip_prefix("${")
3191-
&& let Some(var_name) = left_over.strip_suffix('}') {
3192-
match env::var(var_name) {
3193-
Ok(value) => return value.into(),
3194-
Err(err) => {
3195-
warn!(
3196-
"couldn't read password from environment variable {}, error: {}",
3197-
var_name, err
3198-
);
3199-
}
3219+
&& let Some(var_name) = left_over.strip_suffix('}')
3220+
{
3221+
match env::var(var_name) {
3222+
Ok(value) => return value.into(),
3223+
Err(err) => {
3224+
warn!(
3225+
"couldn't read password from environment variable {}, error: {}",
3226+
var_name, err
3227+
);
32003228
}
32013229
}
3230+
}
32023231

32033232
value.into()
32043233
}

0 commit comments

Comments
 (0)