diff --git a/Cargo.lock b/Cargo.lock index 967e7637..efcb53e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1679,15 +1679,18 @@ dependencies = [ "futures", "ipnet", "jemallocator", + "json5", "log", "log4rs", "mimalloc", "qrcode", "rpassword", "rpmalloc", + "serde", "shadowsocks-service", "snmalloc-rs", "tcmalloc", + "thiserror", "tokio", "xdg", ] diff --git a/Cargo.toml b/Cargo.toml index d5c87c65..4eb70778 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,6 +130,9 @@ replay-attack-detect = ["shadowsocks-service/replay-attack-detect"] [dependencies] log = "0.4" log4rs = { version = "1.0", optional = true } +serde = { version = "1.0", features = ["derive"] } +json5 = "0.4" +thiserror = "1.0" clap = { version = "2", features = ["wrap_help", "suggestions"] } cfg-if = "1" diff --git a/README.md b/README.md index 095bfe3d..ba650057 100644 --- a/README.md +++ b/README.md @@ -585,16 +585,34 @@ Example configuration: "max_server_rtt": 5, // Interval seconds between each check "check_interval": 10, + }, + + // Service configurations + // Logger configuration + "log": { + // Equivalent to `-v` command line option + "level": 1, + "format": { + // Euiqvalent to `--log-without-time` + "without_time": false, + }, + // Equivalent to `--log-config` + // More detail could be found in https://crates.io/crates/log4rs + "config_path": "/path/to/log4rs/config.yaml" + }, + // Runtime configuration + "runtime": { + // single_thread or multi_thread + "mode": "multi_thread", + // Worker threads that are used in multi-thread runtime + "worker_count": 10 } } ``` ### Environment Variables -- `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/src/config.rs b/src/config.rs index 413418df..df357e83 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,17 @@ //! Common configuration utilities +use std::{ + env, + fs::OpenOptions, + io::{self, Read}, + path::{Path, PathBuf}, + str::FromStr, +}; + +use cfg_if::cfg_if; +use clap::{ArgMatches, ErrorKind as ClapErrorKind}; use directories::ProjectDirs; -#[cfg(unix)] -use std::path::Path; -use std::{env, path::PathBuf}; +use serde::Deserialize; /// Default configuration file path pub fn get_default_config_path() -> Option { @@ -59,3 +67,240 @@ pub fn get_default_config_path() -> Option { None } + +/// Error while reading `Config` +#[derive(thiserror::Error, Debug)] +pub enum ConfigError { + /// Input/Output error + #[error("{0}")] + IoError(#[from] io::Error), + /// JSON parsing error + #[error("{0}")] + JsonError(#[from] json5::Error), + /// Invalid value + #[error("Invalid value: {0}")] + InvalidValue(String), +} + +/// Configuration Options for shadowsocks service runnables +#[derive(Debug, Clone, Default)] +pub struct Config { + /// Logger configuration + #[cfg(feature = "logging")] + pub log: LogConfig, + + /// Runtime configuration + pub runtime: RuntimeConfig, +} + +impl Config { + /// Load `Config` from file + pub fn load_from_file>(filename: &P) -> Result { + let filename = filename.as_ref(); + + let mut reader = OpenOptions::new().read(true).open(filename)?; + let mut content = String::new(); + reader.read_to_string(&mut content)?; + + Config::load_from_str(&content) + } + + /// Load `Config` from string + pub fn load_from_str(s: &str) -> Result { + let ssconfig = json5::from_str(s)?; + Config::load_from_ssconfig(ssconfig) + } + + fn load_from_ssconfig(ssconfig: SSConfig) -> Result { + let mut config = Config::default(); + + #[cfg(feature = "logging")] + if let Some(log) = ssconfig.log { + let mut nlog = LogConfig::default(); + if let Some(level) = log.level { + nlog.level = level; + } + + if let Some(format) = log.format { + let mut nformat = LogFormatConfig::default(); + if let Some(without_time) = format.without_time { + nformat.without_time = without_time; + } + nlog.format = nformat; + } + + if let Some(config_path) = log.config_path { + nlog.config_path = Some(PathBuf::from(config_path)); + } + + config.log = nlog; + } + + if let Some(runtime) = ssconfig.runtime { + let mut nruntime = RuntimeConfig::default(); + + #[cfg(feature = "multi-threaded")] + if let Some(worker_count) = runtime.worker_count { + nruntime.worker_count = Some(worker_count); + } + + if let Some(mode) = runtime.mode { + match mode.parse::() { + Ok(m) => nruntime.mode = m, + Err(..) => return Err(ConfigError::InvalidValue(mode)), + } + } + + config.runtime = nruntime; + } + + Ok(config) + } + + /// Set by command line options + pub fn set_options(&mut self, matches: &ArgMatches<'_>) { + #[cfg(feature = "logging")] + { + let debug_level = matches.occurrences_of("VERBOSE"); + if debug_level > 0 { + self.log.level = debug_level as u32; + } + + if matches.is_present("LOG_WITHOUT_TIME") { + self.log.format.without_time = true; + } + + if let Some(log_config) = matches.value_of("LOG_CONFIG") { + self.log.config_path = Some(log_config.into()); + } + } + + #[cfg(feature = "multi-threaded")] + if matches.is_present("SINGLE_THREADED") { + self.runtime.mode = RuntimeMode::SingleThread; + } + + #[cfg(feature = "multi-threaded")] + match clap::value_t!(matches.value_of("WORKER_THREADS"), usize) { + Ok(worker_count) => self.runtime.worker_count = Some(worker_count), + Err(ref err) if err.kind == ClapErrorKind::ArgumentNotFound => {} + Err(err) => err.exit(), + } + + let _ = matches; + } +} + +/// Logger configuration +#[cfg(feature = "logging")] +#[derive(Debug, Clone)] +pub struct LogConfig { + /// Default logger log level, [0, 3] + pub level: u32, + /// Default logger format configuration + pub format: LogFormatConfig, + /// Logging configuration file path + pub config_path: Option, +} + +#[cfg(feature = "logging")] +impl Default for LogConfig { + fn default() -> LogConfig { + LogConfig { + level: 0, + format: LogFormatConfig::default(), + config_path: None, + } + } +} + +/// Logger format configuration +#[cfg(feature = "logging")] +#[derive(Debug, Clone)] +pub struct LogFormatConfig { + pub without_time: bool, +} + +#[cfg(feature = "logging")] +impl Default for LogFormatConfig { + fn default() -> LogFormatConfig { + LogFormatConfig { without_time: false } + } +} + +/// Runtime mode (Tokio) +#[derive(Debug, Clone, Copy)] +pub enum RuntimeMode { + /// Single-Thread Runtime + SingleThread, + /// Multi-Thread Runtime + #[cfg(feature = "multi-threaded")] + MultiThread, +} + +impl Default for RuntimeMode { + fn default() -> RuntimeMode { + cfg_if! { + if #[cfg(feature = "multi-threaded")] { + RuntimeMode::MultiThread + } else { + RuntimeMode::SingleThread + } + } + } +} + +/// Parse `RuntimeMode` from string error +#[derive(Debug)] +pub struct RuntimeModeError; + +impl FromStr for RuntimeMode { + type Err = RuntimeModeError; + + fn from_str(s: &str) -> Result { + match s { + "single_thread" => Ok(RuntimeMode::SingleThread), + #[cfg(feature = "multi-threaded")] + "multi_thread" => Ok(RuntimeMode::MultiThread), + _ => Err(RuntimeModeError), + } + } +} + +/// Runtime configuration +#[derive(Debug, Clone, Default)] +pub struct RuntimeConfig { + /// Multithread runtime worker count, CPU count if not configured + #[cfg(feature = "multi-threaded")] + pub worker_count: Option, + /// Runtime Mode, single-thread, multi-thread + pub mode: RuntimeMode, +} + +#[derive(Deserialize)] +struct SSConfig { + #[cfg(feature = "logging")] + log: Option, + runtime: Option, +} + +#[cfg(feature = "logging")] +#[derive(Deserialize)] +struct SSLogConfig { + level: Option, + format: Option, + config_path: Option, +} + +#[cfg(feature = "logging")] +#[derive(Deserialize)] +struct SSLogFormat { + without_time: Option, +} + +#[derive(Deserialize)] +struct SSRuntimeConfig { + #[cfg(feature = "multi-threaded")] + worker_count: Option, + mode: Option, +} diff --git a/src/logging/mod.rs b/src/logging/mod.rs index 9da70e9c..e5ad78db 100644 --- a/src/logging/mod.rs +++ b/src/logging/mod.rs @@ -1,8 +1,7 @@ //! Logging facilities -use std::{env, path::Path}; +use std::path::Path; -use clap::ArgMatches; use log::LevelFilter; use log4rs::{ append::console::{ConsoleAppender, Target}, @@ -10,6 +9,8 @@ use log4rs::{ encode::pattern::PatternEncoder, }; +use crate::config::LogConfig; + /// Initialize logger ([log4rs](https://crates.io/crates/log4rs)) from yaml configuration file pub fn init_with_file

(path: P) where @@ -19,25 +20,9 @@ where } /// Initialize logger with default configuration -pub fn init_with_config(bin_name: &str, matches: &ArgMatches) { - let mut debug_level = matches.occurrences_of("VERBOSE"); - if debug_level == 0 { - // Override by SS_LOG_VERBOSE_LEVEL - if let Ok(verbose_level) = env::var("SS_LOG_VERBOSE_LEVEL") { - if let Ok(verbose_level) = verbose_level.parse::() { - debug_level = verbose_level; - } - } - } - - let mut without_time = matches.is_present("LOG_WITHOUT_TIME"); - if !without_time { - if let Ok(log_without_time) = env::var("SS_LOG_WITHOUT_TIME") { - if let Ok(log_without_time) = log_without_time.parse::() { - without_time = log_without_time != 0; - } - } - } +pub fn init_with_config(bin_name: &str, config: &LogConfig) { + let debug_level = config.level; + let without_time = config.format.without_time; let mut pattern = String::new(); if !without_time { @@ -92,3 +77,8 @@ pub fn init_with_config(bin_name: &str, matches: &ArgMatches) { log4rs::init_config(config).expect("logging"); } + +/// Init a default logger +pub fn init_with_default(bin_name: &str) { + init_with_config(bin_name, &LogConfig::default()); +} diff --git a/src/service/local.rs b/src/service/local.rs index 7e57b522..016c1891 100644 --- a/src/service/local.rs +++ b/src/service/local.rs @@ -4,7 +4,7 @@ use std::{net::IpAddr, path::PathBuf, process, time::Duration}; use clap::{clap_app, App, Arg, ArgMatches, ErrorKind as ClapErrorKind}; use futures::future::{self, Either}; -use log::info; +use log::{info, trace}; use tokio::{self, runtime::Builder}; #[cfg(feature = "local-redir")] @@ -25,7 +25,11 @@ use shadowsocks_service::{ #[cfg(feature = "logging")] use crate::logging; -use crate::{monitor, validator}; +use crate::{ + config::{Config as ServiceConfig, RuntimeMode}, + monitor, + validator, +}; /// Defines command line options pub fn define_command_line_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { @@ -189,22 +193,12 @@ pub fn define_command_line_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { /// Program entrance `main` pub fn main(matches: &ArgMatches<'_>) { let (config, runtime) = { - #[cfg(feature = "logging")] - match matches.value_of("LOG_CONFIG") { - Some(path) => { - logging::init_with_file(path); - } - None => { - logging::init_with_config("sslocal", matches); - } - } - let config_path_opt = matches.value_of("CONFIG").map(PathBuf::from).or_else(|| { if !matches.is_present("SERVER_CONFIG") { match crate::config::get_default_config_path() { None => None, Some(p) => { - info!("loading default config from {:?}", p); + println!("loading default config {:?}", p); Some(p) } } @@ -213,6 +207,30 @@ pub fn main(matches: &ArgMatches<'_>) { } }); + let mut service_config = match config_path_opt { + Some(ref config_path) => match ServiceConfig::load_from_file(config_path) { + Ok(c) => c, + Err(err) => { + eprintln!("loading config {:?}, {}", config_path, err); + process::exit(crate::EXIT_CODE_LOAD_CONFIG_FAILURE); + } + }, + None => ServiceConfig::default(), + }; + service_config.set_options(&matches); + + #[cfg(feature = "logging")] + match service_config.log.config_path { + Some(ref path) => { + logging::init_with_file(path); + } + None => { + logging::init_with_config("sslocal", &service_config.log); + } + } + + trace!("{:?}", service_config); + let mut config = match config_path_opt { Some(cpath) => match Config::load_from_file(&cpath, ConfigType::Local) { Ok(cfg) => cfg, @@ -563,22 +581,18 @@ pub fn main(matches: &ArgMatches<'_>) { info!("shadowsocks local {} build {}", crate::VERSION, crate::BUILD_TIME); - #[cfg(feature = "multi-threaded")] - let mut builder = if matches.is_present("SINGLE_THREADED") { - Builder::new_current_thread() - } else { - let mut builder = Builder::new_multi_thread(); - match clap::value_t!(matches.value_of("WORKER_THREADS"), usize) { - Ok(worker_threads) => { + let mut builder = match service_config.runtime.mode { + RuntimeMode::SingleThread => Builder::new_current_thread(), + #[cfg(feature = "multi-threaded")] + RuntimeMode::MultiThread => { + let mut builder = Builder::new_multi_thread(); + if let Some(worker_threads) = service_config.runtime.worker_count { builder.worker_threads(worker_threads); } - Err(ref err) if err.kind == ClapErrorKind::ArgumentNotFound => {} - Err(err) => err.exit(), + + builder } - builder }; - #[cfg(not(feature = "multi-threaded"))] - let mut builder = Builder::new_current_thread(); let runtime = builder.enable_all().build().expect("create tokio Runtime"); diff --git a/src/service/manager.rs b/src/service/manager.rs index 5e024cdf..683e5f79 100644 --- a/src/service/manager.rs +++ b/src/service/manager.rs @@ -4,7 +4,7 @@ use std::{net::IpAddr, path::PathBuf, process, time::Duration}; use clap::{clap_app, App, Arg, ArgMatches, ErrorKind as ClapErrorKind}; use futures::future::{self, Either}; -use log::info; +use log::{info, trace}; use tokio::{self, runtime::Builder}; #[cfg(unix)] @@ -22,7 +22,11 @@ use shadowsocks_service::{ #[cfg(feature = "logging")] use crate::logging; -use crate::{monitor, validator}; +use crate::{ + config::{Config as ServiceConfig, RuntimeMode}, + monitor, + validator, +}; /// Defines command line options pub fn define_command_line_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { @@ -117,22 +121,12 @@ pub fn define_command_line_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { /// Program entrance `main` pub fn main(matches: &ArgMatches<'_>) { let (config, runtime) = { - #[cfg(feature = "logging")] - match matches.value_of("LOG_CONFIG") { - Some(path) => { - logging::init_with_file(path); - } - None => { - logging::init_with_config("ssmanager", matches); - } - } - let config_path_opt = matches.value_of("CONFIG").map(PathBuf::from).or_else(|| { if !matches.is_present("SERVER_CONFIG") { match crate::config::get_default_config_path() { None => None, Some(p) => { - info!("loading default config from {:?}", p); + println!("loading default config {:?}", p); Some(p) } } @@ -141,6 +135,30 @@ pub fn main(matches: &ArgMatches<'_>) { } }); + let mut service_config = match config_path_opt { + Some(ref config_path) => match ServiceConfig::load_from_file(config_path) { + Ok(c) => c, + Err(err) => { + eprintln!("loading config {:?}, {}", config_path, err); + process::exit(crate::EXIT_CODE_LOAD_CONFIG_FAILURE); + } + }, + None => ServiceConfig::default(), + }; + service_config.set_options(&matches); + + #[cfg(feature = "logging")] + match service_config.log.config_path { + Some(ref path) => { + logging::init_with_file(path); + } + None => { + logging::init_with_config("sslocal", &service_config.log); + } + } + + trace!("{:?}", service_config); + let mut config = match config_path_opt { Some(cpath) => match Config::load_from_file(&cpath, ConfigType::Manager) { Ok(cfg) => cfg, @@ -336,18 +354,18 @@ pub fn main(matches: &ArgMatches<'_>) { info!("shadowsocks manager {} build {}", crate::VERSION, crate::BUILD_TIME); - #[cfg(feature = "multi-threaded")] - let mut builder = if matches.is_present("SINGLE_THREADED") { - Builder::new_current_thread() - } else { - let mut builder = Builder::new_multi_thread(); - if let Some(worker_threads) = matches.value_of("WORKER_THREADS") { - builder.worker_threads(worker_threads.parse::().expect("worker-threads")); + let mut builder = match service_config.runtime.mode { + RuntimeMode::SingleThread => Builder::new_current_thread(), + #[cfg(feature = "multi-threaded")] + RuntimeMode::MultiThread => { + let mut builder = Builder::new_multi_thread(); + if let Some(worker_threads) = service_config.runtime.worker_count { + builder.worker_threads(worker_threads); + } + + builder } - builder }; - #[cfg(not(feature = "multi-threaded"))] - let mut builder = Builder::new_current_thread(); let runtime = builder.enable_all().build().expect("create tokio Runtime"); diff --git a/src/service/server.rs b/src/service/server.rs index 16b3f91e..f61be577 100644 --- a/src/service/server.rs +++ b/src/service/server.rs @@ -4,7 +4,7 @@ use std::{net::IpAddr, path::PathBuf, process, time::Duration}; use clap::{clap_app, App, Arg, ArgMatches, ErrorKind as ClapErrorKind}; use futures::future::{self, Either}; -use log::info; +use log::{info, trace}; use tokio::{self, runtime::Builder}; use shadowsocks_service::{ @@ -20,7 +20,11 @@ use shadowsocks_service::{ #[cfg(feature = "logging")] use crate::logging; -use crate::{monitor, validator}; +use crate::{ + config::{Config as ServiceConfig, RuntimeMode}, + monitor, + validator, +}; /// Defines command line options pub fn define_command_line_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { @@ -110,22 +114,12 @@ pub fn define_command_line_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { /// Program entrance `main` pub fn main(matches: &ArgMatches<'_>) { let (config, runtime) = { - #[cfg(feature = "logging")] - match matches.value_of("LOG_CONFIG") { - Some(path) => { - logging::init_with_file(path); - } - None => { - logging::init_with_config("ssserver", matches); - } - } - let config_path_opt = matches.value_of("CONFIG").map(PathBuf::from).or_else(|| { if !matches.is_present("SERVER_CONFIG") { match crate::config::get_default_config_path() { None => None, Some(p) => { - info!("loading default config from {:?}", p); + println!("loading default config {:?}", p); Some(p) } } @@ -134,6 +128,30 @@ pub fn main(matches: &ArgMatches<'_>) { } }); + let mut service_config = match config_path_opt { + Some(ref config_path) => match ServiceConfig::load_from_file(config_path) { + Ok(c) => c, + Err(err) => { + eprintln!("loading config {:?}, {}", config_path, err); + process::exit(crate::EXIT_CODE_LOAD_CONFIG_FAILURE); + } + }, + None => ServiceConfig::default(), + }; + service_config.set_options(&matches); + + #[cfg(feature = "logging")] + match service_config.log.config_path { + Some(ref path) => { + logging::init_with_file(path); + } + None => { + logging::init_with_config("sslocal", &service_config.log); + } + } + + trace!("{:?}", service_config); + let mut config = match config_path_opt { Some(cpath) => match Config::load_from_file(&cpath, ConfigType::Server) { Ok(cfg) => cfg, @@ -324,24 +342,18 @@ pub fn main(matches: &ArgMatches<'_>) { info!("shadowsocks server {} build {}", crate::VERSION, crate::BUILD_TIME); - #[cfg(feature = "multi-threaded")] - let mut builder = if matches.is_present("SINGLE_THREADED") { - Builder::new_current_thread() - } else { - let mut builder = Builder::new_multi_thread(); - - match clap::value_t!(matches.value_of("WORKER_THREADS"), usize) { - Ok(worker_threads) => { + let mut builder = match service_config.runtime.mode { + RuntimeMode::SingleThread => Builder::new_current_thread(), + #[cfg(feature = "multi-threaded")] + RuntimeMode::MultiThread => { + let mut builder = Builder::new_multi_thread(); + if let Some(worker_threads) = service_config.runtime.worker_count { builder.worker_threads(worker_threads); } - Err(ref err) if err.kind == ClapErrorKind::ArgumentNotFound => {} - Err(err) => err.exit(), - } - builder + builder + } }; - #[cfg(not(feature = "multi-threaded"))] - let mut builder = Builder::new_current_thread(); let runtime = builder.enable_all().build().expect("create tokio Runtime");