From 74e8a4f8f58ce60545d778f2f083caab72478eea Mon Sep 17 00:00:00 2001 From: zonyitoo Date: Thu, 25 Nov 2021 17:19:16 +0800 Subject: [PATCH] Reading password from environment variable or TTY - servers creating from command line options would use TTY, SS_SERVER_PASSWORD and SS_SERVER_${SERVER_ADDR}_PASSWORD environment variables - servers in configuration file "password" would allow ${VAR_NAME} to read from environment variable VAR_NAME fixes #419 --- Cargo.lock | 11 ++++++++ Cargo.toml | 1 + README.md | 10 +++++++ bin/common/config.rs | 2 ++ bin/common/mod.rs | 3 ++- bin/common/password.rs | 33 +++++++++++++++++++++++ bin/sslocal.rs | 16 +++++++++-- bin/ssserver.rs | 14 +++++++++- crates/shadowsocks-service/src/config.rs | 34 ++++++++++++++++++++++-- 9 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 bin/common/password.rs diff --git a/Cargo.lock b/Cargo.lock index 1a2e5a20..6d1ccfc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1381,6 +1381,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "rpassword" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "rpmalloc" version = "0.2.2" @@ -1673,6 +1683,7 @@ dependencies = [ "log4rs", "mimalloc", "qrcode", + "rpassword", "rpmalloc", "shadowsocks-service", "snmalloc-rs", diff --git a/Cargo.toml b/Cargo.toml index 34907126..46552ce8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,6 +130,7 @@ exitcode = "1" build-time = "0.1" directories = "4.0" xdg = "2.4" +rpassword = "5.0.1" futures = "0.3" tokio = { version = "1", features = ["rt", "signal"] } diff --git a/README.md b/README.md index dd353946..84889cf7 100644 --- a/README.md +++ b/README.md @@ -522,6 +522,14 @@ Example configuration: // The higher weight, the server may rank higher. "tcp_weight": 1.0, "udp_weight": 1.0, + }, + { + // Same key as basic format "server" and "server_port" + "server": "0.0.0.0", + "server_port": 8388, + "method": "chacha20-ietf-poly1305", + // Read the actual password from environment variable PASSWORD_FROM_ENV + "password": "${PASSWORD_FROM_ENV}" } ], @@ -583,6 +591,8 @@ Example configuration: - `SS_LOG_VERBOSE_LEVEL`: Logging level for binaries (`sslocal`, `ssserver` and `ssmanager`). It is valid only when command line argument `-v` is not applied. Example: `SS_LOG_VERBOSE_LEVEL=1` - `SS_LOG_WITHOUT_TIME`: Logging format for binaries (`sslocal`, `ssserver` and `ssmanager`). It is valid only when command line argument `--log-without-time` is not applied. Example `SS_LOG_WITHOUT_TIME=1` +- `SS_SERVER_PASSWORD`: A default password for servers that created from command line argument (`--server-addr`) +- `SS_SERVER_${SERVER_ADDR}_PASSWORD`: A default password for server with address `$SERVER_ADDR` that created from command line argument (`--server-addr`) ## Supported Ciphers diff --git a/bin/common/config.rs b/bin/common/config.rs index 1bfa2a11..54a9ccb5 100644 --- a/bin/common/config.rs +++ b/bin/common/config.rs @@ -1,3 +1,5 @@ +//! Common configuration utilities + use directories::ProjectDirs; use std::path::{Path, PathBuf}; diff --git a/bin/common/mod.rs b/bin/common/mod.rs index e833c833..901669a1 100644 --- a/bin/common/mod.rs +++ b/bin/common/mod.rs @@ -1,13 +1,14 @@ //! Shadowsocks service command line utilities pub mod allocator; +pub mod config; #[cfg(unix)] pub mod daemonize; #[cfg(feature = "logging")] pub mod logging; pub mod monitor; +pub mod password; pub mod validator; -pub mod config; pub const EXIT_CODE_SERVER_EXIT_UNEXPECTEDLY: i32 = exitcode::SOFTWARE; pub const EXIT_CODE_SERVER_ABORTED: i32 = exitcode::SOFTWARE; diff --git a/bin/common/password.rs b/bin/common/password.rs new file mode 100644 index 00000000..e2d9bfe7 --- /dev/null +++ b/bin/common/password.rs @@ -0,0 +1,33 @@ +//! Common password utilities + +use std::{env, io}; + +use log::debug; + +/// Read server's password from environment variable or TTY +pub fn read_server_password(server_name: &str) -> io::Result { + // specific SS_SERVER_${server_name}_PASSWORD + let key = format!("SS_SERVER_{}_PASSWORD", server_name); + if let Ok(pwd) = env::var(&key) { + debug!("got server {} password from environment variable {}", server_name, key); + return Ok(pwd); + } + + // common SS_SERVER_PASSWORD + if let Ok(pwd) = env::var("SS_SERVER_PASSWORD") { + debug!( + "got server {} password from environment variable SS_SERVER_PASSWORD", + server_name + ); + return Ok(pwd); + } + + // read from TTY + let tty_prompt = format!("({}) Password: ", server_name); + if let Ok(pwd) = rpassword::read_password_from_tty(Some(&tty_prompt)) { + debug!("got server {} password from tty prompt", server_name); + return Ok(pwd); + } + + Err(io::Error::new(io::ErrorKind::Other, "no server password found")) +} diff --git a/bin/sslocal.rs b/bin/sslocal.rs index 986ac6d3..05a424da 100644 --- a/bin/sslocal.rs +++ b/bin/sslocal.rs @@ -227,7 +227,19 @@ fn main() { }; if let Some(svr_addr) = matches.value_of("SERVER_ADDR") { - let password = matches.value_of("PASSWORD").expect("password"); + let password = match matches.value_of("PASSWORD") { + Some(pwd) => pwd.to_owned(), + None => { + // NOTE: svr_addr should have been checked by common::validator + match common::password::read_server_password(svr_addr) { + Ok(pwd) => pwd, + Err(..) => { + panic!("missing server's password"); + } + } + } + }; + let method = matches .value_of("ENCRYPT_METHOD") .expect("encrypt-method") @@ -240,7 +252,7 @@ fn main() { .map(|t| t.parse::().expect("timeout")) .map(Duration::from_secs); - let mut sc = ServerConfig::new(svr_addr, password.to_owned(), method); + let mut sc = ServerConfig::new(svr_addr, password, method); if let Some(timeout) = timeout { sc.set_timeout(timeout); } diff --git a/bin/ssserver.rs b/bin/ssserver.rs index 83e459fa..9c68dc87 100644 --- a/bin/ssserver.rs +++ b/bin/ssserver.rs @@ -151,7 +151,19 @@ fn main() { }; if let Some(svr_addr) = matches.value_of("SERVER_ADDR") { - let password = matches.value_of("PASSWORD").expect("password"); + let password = match matches.value_of("PASSWORD") { + Some(pwd) => pwd.to_owned(), + None => { + // NOTE: svr_addr should have been checked by common::validator + match common::password::read_server_password(svr_addr) { + Ok(pwd) => pwd, + Err(..) => { + panic!("missing server's password"); + } + } + } + }; + let method = matches .value_of("ENCRYPT_METHOD") .expect("encrypt-method") diff --git a/crates/shadowsocks-service/src/config.rs b/crates/shadowsocks-service/src/config.rs index f1fc5a7f..ddc3029a 100644 --- a/crates/shadowsocks-service/src/config.rs +++ b/crates/shadowsocks-service/src/config.rs @@ -42,8 +42,10 @@ //! These defined server will be used with a load balancing algorithm. use std::{ + borrow::Cow, convert::{From, Infallible}, default::Default, + env, fmt::{self, Debug, Display, Formatter}, fs::OpenOptions, io::Read, @@ -58,6 +60,7 @@ use std::{ use cfg_if::cfg_if; #[cfg(feature = "local-tun")] use ipnet::IpNet; +use log::warn; use serde::{Deserialize, Serialize}; #[cfg(any(feature = "local-tunnel", feature = "local-dns"))] use shadowsocks::relay::socks5::Address; @@ -1392,7 +1395,10 @@ impl Config { } }; - let mut nsvr = ServerConfig::new(addr, pwd, method); + // Only "password" support getting from environment variable. + let password = get_variable_field_value(&pwd); + + let mut nsvr = ServerConfig::new(addr, password, method); nsvr.set_mode(global_mode); if let Some(ref p) = config.plugin { @@ -1459,7 +1465,10 @@ impl Config { } }; - let mut nsvr = ServerConfig::new(addr, svr.password, method); + // Only "password" support getting from environment variable. + let password = get_variable_field_value(&svr.password); + + let mut nsvr = ServerConfig::new(addr, password, method); match svr.mode { Some(mode) => match mode.parse::() { @@ -2216,3 +2225,24 @@ impl fmt::Display for Config { write!(f, "{}", json5::to_string(&jconf).unwrap()) } } + +fn get_variable_field_value(value: &str) -> Cow<'_, str> { + if let Some(left_over) = value.strip_prefix("${") { + if let Some(var_name) = left_over.strip_suffix("}") { + return match env::var(var_name) { + Ok(value) => value.into(), + Err(err) => { + warn!( + "couldn't read password from environemnt variable {}, error: {}", + var_name, err + ); + + // NOTE: Just like shell, if environment variable not found, then the value of it is an empty string. + "".into() + } + }; + } + } + + value.into() +}