diff --git a/Cargo.lock b/Cargo.lock index 681e04a..c29959f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,7 @@ dependencies = [ "serde", "simplelog", "toml", + "toml_config", ] [[package]] @@ -80,6 +81,7 @@ dependencies = [ "privdrop", "rand", "serde", + "toml_config", ] [[package]] @@ -109,6 +111,7 @@ dependencies = [ "signal-hook", "slab", "smartstring", + "toml_config", ] [[package]] @@ -130,6 +133,7 @@ dependencies = [ "rand_distr", "rustls", "serde", + "toml_config", ] [[package]] @@ -179,6 +183,7 @@ dependencies = [ "slab", "socket2 0.4.2", "tinytemplate", + "toml_config", ] [[package]] @@ -196,6 +201,7 @@ dependencies = [ "rand", "rand_distr", "serde", + "toml_config", ] [[package]] @@ -215,6 +221,7 @@ dependencies = [ "rand_distr", "serde", "socket2 0.4.2", + "toml_config", ] [[package]] @@ -259,6 +266,7 @@ dependencies = [ "signal-hook", "slab", "socket2 0.4.2", + "toml_config", "tungstenite", ] @@ -284,6 +292,7 @@ dependencies = [ "rustls", "serde", "serde_json", + "toml_config", "tungstenite", ] @@ -2066,6 +2075,26 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_config" +version = "0.1.0" +dependencies = [ + "quickcheck", + "quickcheck_macros", + "serde", + "toml", + "toml_config_derive", +] + +[[package]] +name = "toml_config_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing" version = "0.1.29" diff --git a/Cargo.toml b/Cargo.toml index 2d2faf1..2305376 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ members = [ "aquatic_ws", "aquatic_ws_load_test", "aquatic_ws_protocol", + "toml_config", + "toml_config_derive", ] [patch.crates-io] @@ -30,6 +32,8 @@ aquatic_udp_protocol = { path = "aquatic_udp_protocol" } aquatic_ws = { path = "aquatic_ws" } aquatic_ws_load_test = { path = "aquatic_ws_load_test" } aquatic_ws_protocol = { path = "aquatic_ws_protocol" } +toml_config = { path = "toml_config" } +toml_config_derive = { path = "toml_config_derive" } [profile.release] debug = true diff --git a/README.md b/README.md index 1724998..c16974f 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,9 @@ Begin by generating configuration files. They differ between protocols. Make adjustments to the files. You will likely want to adjust `address` (listening address) under the `network` section. -`aquatic_http` and `aquatic_ws` both require configuring a TLS certificate file as well as a -private key file to run. More information is available below. +Note that both `aquatic_http` and `aquatic_ws` require configuring TLS +certificate and private key files. More details are available in the +respective configuration files. Once done, run the tracker: @@ -79,25 +80,12 @@ Once done, run the tracker: ### Configuration values -Starting more socket workers than request workers is recommended. All +Starting more `socket_workers` than `request_workers` is recommended. All implementations are quite IO-bound and spend a lot of their time reading from -and writing to sockets. This is handled by the `socket_workers`, which +and writing to sockets. This is handled by the socket workers, which also do parsing, serialisation and access control. They pass announce and -scrape requests to the `request_workers`, which update internal tracker state -and pass back responses. - -#### TLS - -`aquatic_ws` and `aquatic_http` both require access to a TLS certificate file -(DER-encoded X.509) and a corresponding private key file (DER-encoded ASN.1 in -either PKCS#8 or PKCS#1 format) to run. Set their paths in the configuration file, e.g.: - -```toml -[network] -address = '0.0.0.0:3000' -tls_certificate_path = './cert.pem' -tls_private_key_path = './key.pem' -``` +scrape requests to the request workers, which update internal tracker state +and pass back responses for sending. #### Access control @@ -106,8 +94,10 @@ of configuration is: ```toml [access_list] -mode = 'off' # Change to 'black' (blacklist) or 'white' (whitelist) -path = '' # Path to text file with newline-delimited hex-encoded info hashes +# Access list mode. Available modes are white, black and off. +mode = "off" +# Path to access list file consisting of newline-separated hex-encoded info hashes. +path = "" ``` The file is read on start and when the program receives `SIGUSR1`. If initial @@ -115,12 +105,6 @@ parsing fails, the program exits. Later failures result in in emitting of an error-level log message, while successful updates of the access list result in emitting of an info-level log message. -#### More information - -More documentation of the various configuration options might be available -under `src/config.rs` in directories `aquatic_udp`, `aquatic_http` and -`aquatic_ws`. - ## Details on implementations ### aquatic_udp: UDP BitTorrent tracker @@ -146,6 +130,7 @@ More details are available [here](./documents/aquatic-udp-load-test-2021-11-28.p * Using glommio * Using io-uring * Using zerocopy + vectored sends for responses +* Using sendmmsg ### aquatic_http: HTTP BitTorrent tracker diff --git a/aquatic_cli_helpers/Cargo.toml b/aquatic_cli_helpers/Cargo.toml index 60ee058..9c21e77 100644 --- a/aquatic_cli_helpers/Cargo.toml +++ b/aquatic_cli_helpers/Cargo.toml @@ -12,3 +12,4 @@ anyhow = "1" serde = { version = "1", features = ["derive"] } simplelog = "0.11" toml = "0.5" +toml_config = "0.1.0" diff --git a/aquatic_cli_helpers/src/lib.rs b/aquatic_cli_helpers/src/lib.rs index db8818e..6c7d6f5 100644 --- a/aquatic_cli_helpers/src/lib.rs +++ b/aquatic_cli_helpers/src/lib.rs @@ -4,8 +4,10 @@ use std::io::Read; use anyhow::Context; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use simplelog::{ColorChoice, ConfigBuilder, LevelFilter, TermLogger, TerminalMode}; +use toml_config::TomlConfig; -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +/// Log level. Available values are off, error, warn, info, debug and trace. +#[derive(Debug, Clone, Copy, PartialEq, TomlConfig, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum LogLevel { Off, @@ -22,7 +24,7 @@ impl Default for LogLevel { } } -pub trait Config: Default + Serialize + DeserializeOwned { +pub trait Config: Default + TomlConfig + DeserializeOwned { fn get_log_level(&self) -> Option { None } @@ -169,9 +171,9 @@ where fn default_config_as_toml() -> String where - T: Default + Serialize, + T: Default + TomlConfig, { - toml::to_string_pretty(&T::default()).expect("Could not serialize default config to toml") + ::default_to_string() } fn start_logger(log_level: LogLevel) -> ::anyhow::Result<()> { diff --git a/aquatic_common/Cargo.toml b/aquatic_common/Cargo.toml index 7a07209..a6137aa 100644 --- a/aquatic_common/Cargo.toml +++ b/aquatic_common/Cargo.toml @@ -24,6 +24,7 @@ log = "0.4" privdrop = "0.5" rand = { version = "0.8", features = ["small_rng"] } serde = { version = "1", features = ["derive"] } +toml_config = "0.1.0" # cpu-pinning hwloc = { version = "0.5", optional = true } diff --git a/aquatic_common/src/access_list.rs b/aquatic_common/src/access_list.rs index f5a1076..eaf49e6 100644 --- a/aquatic_common/src/access_list.rs +++ b/aquatic_common/src/access_list.rs @@ -7,8 +7,10 @@ use anyhow::Context; use arc_swap::{ArcSwap, Cache}; use hashbrown::HashSet; use serde::{Deserialize, Serialize}; +use toml_config::TomlConfig; -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +/// Access list mode. Available modes are white, black and off. +#[derive(Clone, Copy, Debug, PartialEq, TomlConfig, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum AccessListMode { /// Only serve torrents with info hash present in file @@ -25,7 +27,7 @@ impl AccessListMode { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] pub struct AccessListConfig { pub mode: AccessListMode, /// Path to access list file consisting of newline-separated hex-encoded info hashes. diff --git a/aquatic_common/src/cpu_pinning.rs b/aquatic_common/src/cpu_pinning.rs index a4065df..4fd826a 100644 --- a/aquatic_common/src/cpu_pinning.rs +++ b/aquatic_common/src/cpu_pinning.rs @@ -1,7 +1,8 @@ use hwloc::{CpuSet, ObjectType, Topology, CPUBIND_THREAD}; use serde::{Deserialize, Serialize}; +use toml_config::TomlConfig; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum CpuPinningMode { Ascending, @@ -14,7 +15,7 @@ impl Default for CpuPinningMode { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] pub struct CpuPinningConfig { pub active: bool, pub mode: CpuPinningMode, diff --git a/aquatic_common/src/privileges.rs b/aquatic_common/src/privileges.rs index a898969..058e7cf 100644 --- a/aquatic_common/src/privileges.rs +++ b/aquatic_common/src/privileges.rs @@ -7,9 +7,10 @@ use std::{ }; use privdrop::PrivDrop; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; +use toml_config::TomlConfig; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct PrivilegeConfig { /// Chroot and switch user after binding to sockets diff --git a/aquatic_http/Cargo.toml b/aquatic_http/Cargo.toml index fd6a1a5..5d1df0b 100644 --- a/aquatic_http/Cargo.toml +++ b/aquatic_http/Cargo.toml @@ -38,6 +38,7 @@ serde = { version = "1", features = ["derive"] } signal-hook = { version = "0.3" } slab = "0.4" smartstring = "0.2" +toml_config = "0.1.0" [dev-dependencies] quickcheck = "1" diff --git a/aquatic_http/src/config.rs b/aquatic_http/src/config.rs index b7c1d31..789f1d4 100644 --- a/aquatic_http/src/config.rs +++ b/aquatic_http/src/config.rs @@ -1,16 +1,18 @@ use std::{net::SocketAddr, path::PathBuf}; use aquatic_common::{access_list::AccessListConfig, privileges::PrivilegeConfig}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; +use toml_config::TomlConfig; use aquatic_cli_helpers::LogLevel; -#[derive(Clone, Debug, Serialize, Deserialize)] +/// aquatic_http configuration +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct Config { /// Socket workers receive requests from the socket, parse them and send - /// them on to the request handler. They then recieve responses from the - /// request handler, encode them and send them back over the socket. + /// them on to the request workers. They then receive responses from the + /// request workers, encode them and send them back over the socket. pub socket_workers: usize, /// Request workers receive a number of requests from socket workers, /// generate responses and send them back to the socket workers. @@ -31,18 +33,22 @@ impl aquatic_cli_helpers::Config for Config { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct NetworkConfig { /// Bind to this address pub address: SocketAddr, - pub tls_certificate_path: PathBuf, - pub tls_private_key_path: PathBuf, + /// Only allow access over IPv6 pub ipv6_only: bool, + /// Path to TLS certificate (DER-encoded X.509) + pub tls_certificate_path: PathBuf, + /// Path to TLS private key (DER-encoded ASN.1 in PKCS#8 or PKCS#1 format) + pub tls_private_key_path: PathBuf, + /// Keep connections alive pub keep_alive: bool, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct ProtocolConfig { /// Maximum number of torrents to accept in scrape request @@ -53,12 +59,12 @@ pub struct ProtocolConfig { pub peer_announce_interval: usize, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct CleaningConfig { /// Clean peers this often (seconds) pub torrent_cleaning_interval: u64, - /// Remove peers that haven't announced for this long (seconds) + /// Remove peers that have not announced for this long (seconds) pub max_peer_age: u64, } @@ -109,3 +115,10 @@ impl Default for CleaningConfig { } } } + +#[cfg(test)] +mod tests { + use super::Config; + + ::toml_config::gen_serialize_deserialize_test!(Config); +} diff --git a/aquatic_http_load_test/Cargo.toml b/aquatic_http_load_test/Cargo.toml index eb36e87..78991d1 100644 --- a/aquatic_http_load_test/Cargo.toml +++ b/aquatic_http_load_test/Cargo.toml @@ -26,6 +26,7 @@ rand = { version = "0.8", features = ["small_rng"] } rand_distr = "0.4" rustls = { version = "0.20", features = ["dangerous_configuration"] } serde = { version = "1", features = ["derive"] } +toml_config = "0.1.0" [dev-dependencies] quickcheck = "1" diff --git a/aquatic_http_load_test/src/config.rs b/aquatic_http_load_test/src/config.rs index 3352957..392007b 100644 --- a/aquatic_http_load_test/src/config.rs +++ b/aquatic_http_load_test/src/config.rs @@ -1,9 +1,11 @@ use std::net::SocketAddr; use aquatic_cli_helpers::LogLevel; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; +use toml_config::TomlConfig; -#[derive(Clone, Debug, Serialize, Deserialize)] +/// aquatic_http_load_test configuration +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct Config { pub server_address: SocketAddr, @@ -14,7 +16,7 @@ pub struct Config { /// How often to check if num_connections connections are open, and /// open a new one otherwise. A value of 0 means that connections are /// opened as quickly as possible, which is useful when the tracker - /// doesn't keep connections alive. + /// does not keep connections alive. pub connection_creation_interval_ms: u64, pub duration: usize, pub torrents: TorrentConfig, @@ -28,7 +30,7 @@ impl aquatic_cli_helpers::Config for Config { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct TorrentConfig { pub number_of_torrents: usize, @@ -73,3 +75,10 @@ impl Default for TorrentConfig { } } } + +#[cfg(test)] +mod tests { + use super::Config; + + ::toml_config::gen_serialize_deserialize_test!(Config); +} diff --git a/aquatic_udp/Cargo.toml b/aquatic_udp/Cargo.toml index 84a65ad..1059b41 100644 --- a/aquatic_udp/Cargo.toml +++ b/aquatic_udp/Cargo.toml @@ -36,6 +36,7 @@ slab = "0.4" signal-hook = { version = "0.3" } socket2 = { version = "0.4", features = ["all"] } tinytemplate = "1" +toml_config = "0.1.0" [dev-dependencies] quickcheck = "1" diff --git a/aquatic_udp/src/config.rs b/aquatic_udp/src/config.rs index c3c5f52..5e3a10b 100644 --- a/aquatic_udp/src/config.rs +++ b/aquatic_udp/src/config.rs @@ -1,15 +1,17 @@ use std::{net::SocketAddr, path::PathBuf}; use aquatic_common::{access_list::AccessListConfig, privileges::PrivilegeConfig}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use aquatic_cli_helpers::LogLevel; +use toml_config::TomlConfig; -#[derive(Clone, Debug, Serialize, Deserialize)] +/// aquatic_udp configuration +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct Config { /// Socket workers receive requests from the socket, parse them and send - /// them on to the request workers. They then recieve responses from the + /// them on to the request workers. They then receive responses from the /// request workers, encode them and send them back over the socket. pub socket_workers: usize, /// Request workers receive a number of requests from socket workers, @@ -62,22 +64,21 @@ impl aquatic_cli_helpers::Config for Config { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct NetworkConfig { /// Bind to this address pub address: SocketAddr, + /// Only allow access over IPv6 pub only_ipv6: bool, /// Size of socket recv buffer. Use 0 for OS default. /// /// This setting can have a big impact on dropped packages. It might /// require changing system defaults. Some examples of commands to set - /// recommended values for different operating systems: + /// values for different operating systems: /// /// macOS: /// $ sudo sysctl net.inet.udp.recvspace=6000000 - /// $ sudo sysctl net.inet.udp.maxdgram=500000 # Not necessary, but recommended - /// $ sudo sysctl kern.ipc.maxsockbuf=8388608 # Not necessary, but recommended /// /// Linux: /// $ sudo sysctl -w net.core.rmem_max=104857600 @@ -108,7 +109,7 @@ impl Default for NetworkConfig { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct ProtocolConfig { /// Maximum number of torrents to accept in scrape request @@ -129,7 +130,7 @@ impl Default for ProtocolConfig { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct StatisticsConfig { /// Collect and print/write statistics this often (seconds) @@ -138,7 +139,7 @@ pub struct StatisticsConfig { pub print_to_stdout: bool, /// Save statistics as HTML to a file pub write_html_to_file: bool, - /// Path to save HTML file + /// Path to save HTML file to pub html_file_path: PathBuf, } @@ -159,7 +160,7 @@ impl Default for StatisticsConfig { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct CleaningConfig { /// Clean connections this often (seconds) @@ -174,9 +175,9 @@ pub struct CleaningConfig { pub pending_scrape_cleaning_interval: u64, /// Remove connections that are older than this (seconds) pub max_connection_age: u64, - /// Remove peers that haven't announced for this long (seconds) + /// Remove peers who have not announced for this long (seconds) pub max_peer_age: u64, - /// Remove pending scrape responses that haven't been returned from request + /// Remove pending scrape responses that have not been returned from request /// workers for this long (seconds) pub max_pending_scrape_age: u64, } @@ -193,3 +194,10 @@ impl Default for CleaningConfig { } } } + +#[cfg(test)] +mod tests { + use super::Config; + + ::toml_config::gen_serialize_deserialize_test!(Config); +} diff --git a/aquatic_udp_bench/Cargo.toml b/aquatic_udp_bench/Cargo.toml index f46b5c3..8b6d831 100644 --- a/aquatic_udp_bench/Cargo.toml +++ b/aquatic_udp_bench/Cargo.toml @@ -21,3 +21,4 @@ num-format = "0.4" rand = { version = "0.8", features = ["small_rng"] } rand_distr = "0.4" serde = { version = "1", features = ["derive"] } +toml_config = "0.1.0" diff --git a/aquatic_udp_bench/src/config.rs b/aquatic_udp_bench/src/config.rs index 242b1ea..d3f20f7 100644 --- a/aquatic_udp_bench/src/config.rs +++ b/aquatic_udp_bench/src/config.rs @@ -1,6 +1,7 @@ -use serde::{Deserialize, Serialize}; +use serde::Deserialize; +use toml_config::TomlConfig; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] pub struct BenchConfig { pub num_rounds: usize, pub num_threads: usize, @@ -24,3 +25,10 @@ impl Default for BenchConfig { } impl aquatic_cli_helpers::Config for BenchConfig {} + +#[cfg(test)] +mod tests { + use super::BenchConfig; + + ::toml_config::gen_serialize_deserialize_test!(BenchConfig); +} diff --git a/aquatic_udp_load_test/Cargo.toml b/aquatic_udp_load_test/Cargo.toml index 2764137..17eb888 100644 --- a/aquatic_udp_load_test/Cargo.toml +++ b/aquatic_udp_load_test/Cargo.toml @@ -24,6 +24,7 @@ rand = { version = "0.8", features = ["small_rng"] } rand_distr = "0.4" serde = { version = "1", features = ["derive"] } socket2 = { version = "0.4", features = ["all"] } +toml_config = "0.1.0" [dev-dependencies] quickcheck = "1" diff --git a/aquatic_udp_load_test/src/config.rs b/aquatic_udp_load_test/src/config.rs index 55ee0e2..79900db 100644 --- a/aquatic_udp_load_test/src/config.rs +++ b/aquatic_udp_load_test/src/config.rs @@ -1,12 +1,14 @@ use std::net::SocketAddr; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use aquatic_cli_helpers::LogLevel; #[cfg(feature = "cpu-pinning")] use aquatic_common::cpu_pinning::CpuPinningConfig; +use toml_config::TomlConfig; -#[derive(Clone, Debug, Serialize, Deserialize)] +/// aquatic_udp_load_test configuration +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct Config { /// Server address @@ -15,6 +17,7 @@ pub struct Config { /// address here. pub server_address: SocketAddr, pub log_level: LogLevel, + /// Number of workers sending requests pub workers: u8, /// Run duration (quit and generate report after this many seconds) pub duration: usize, @@ -39,7 +42,7 @@ impl Default for Config { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct NetworkConfig { /// True means bind to one localhost IP per socket. @@ -59,12 +62,10 @@ pub struct NetworkConfig { /// /// This setting can have a big impact on dropped packages. It might /// require changing system defaults. Some examples of commands to set - /// recommended values for different operating systems: + /// values for different operating systems: /// /// macOS: /// $ sudo sysctl net.inet.udp.recvspace=6000000 - /// $ sudo sysctl net.inet.udp.maxdgram=500000 # Not necessary, but recommended - /// $ sudo sysctl kern.ipc.maxsockbuf=8388608 # Not necessary, but recommended /// /// Linux: /// $ sudo sysctl -w net.core.rmem_max=104857600 @@ -84,7 +85,7 @@ impl Default for NetworkConfig { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct RequestConfig { /// Number of torrents to simulate @@ -125,3 +126,10 @@ impl Default for RequestConfig { } } } + +#[cfg(test)] +mod tests { + use super::Config; + + ::toml_config::gen_serialize_deserialize_test!(Config); +} diff --git a/aquatic_ws/Cargo.toml b/aquatic_ws/Cargo.toml index d159bb6..885e841 100644 --- a/aquatic_ws/Cargo.toml +++ b/aquatic_ws/Cargo.toml @@ -36,6 +36,7 @@ rustls-pemfile = "0.2" serde = { version = "1", features = ["derive"] } signal-hook = { version = "0.3" } slab = "0.4" +toml_config = "0.1.0" tungstenite = "0.16" # mio diff --git a/aquatic_ws/src/config.rs b/aquatic_ws/src/config.rs index 8b4839e..87726ae 100644 --- a/aquatic_ws/src/config.rs +++ b/aquatic_ws/src/config.rs @@ -4,16 +4,18 @@ use std::path::PathBuf; #[cfg(feature = "cpu-pinning")] use aquatic_common::cpu_pinning::CpuPinningConfig; use aquatic_common::{access_list::AccessListConfig, privileges::PrivilegeConfig}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use aquatic_cli_helpers::LogLevel; +use toml_config::TomlConfig; -#[derive(Clone, Debug, Serialize, Deserialize)] +/// aquatic_ws configuration +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct Config { /// Socket workers receive requests from the socket, parse them and send - /// them on to the request handler. They then recieve responses from the - /// request handler, encode them and send them back over the socket. + /// them on to the request workers. They then receive responses from the + /// request workers, encode them and send them back over the socket. pub socket_workers: usize, /// Request workers receive a number of requests from socket workers, /// generate responses and send them back to the socket workers. @@ -38,25 +40,29 @@ impl aquatic_cli_helpers::Config for Config { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct NetworkConfig { /// Bind to this address pub address: SocketAddr, + /// Only allow access over IPv6 pub ipv6_only: bool, + + /// Path to TLS certificate (DER-encoded X.509) + pub tls_certificate_path: PathBuf, + /// Path to TLS private key (DER-encoded ASN.1 in PKCS#8 or PKCS#1 format) + pub tls_private_key_path: PathBuf, + pub websocket_max_message_size: usize, pub websocket_max_frame_size: usize, - pub tls_certificate_path: PathBuf, - pub tls_private_key_path: PathBuf, - #[cfg(feature = "with-mio")] pub poll_event_capacity: usize, #[cfg(feature = "with-mio")] pub poll_timeout_microseconds: u64, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct ProtocolConfig { /// Maximum number of torrents to accept in scrape request @@ -68,7 +74,7 @@ pub struct ProtocolConfig { } #[cfg(feature = "with-mio")] -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct HandlerConfig { /// Maximum number of requests to receive from channel before locking @@ -77,12 +83,12 @@ pub struct HandlerConfig { pub channel_recv_timeout_microseconds: u64, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct CleaningConfig { /// Clean peers this often (seconds) pub torrent_cleaning_interval: u64, - /// Remove peers that haven't announced for this long (seconds) + /// Remove peers that have not announced for this long (seconds) pub max_peer_age: u64, // Clean connections this often (seconds) @@ -98,10 +104,10 @@ pub struct CleaningConfig { } #[cfg(feature = "with-mio")] -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct StatisticsConfig { - /// Print statistics this often (seconds). Don't print when set to zero. + /// Print statistics this often (seconds). Do not print when set to zero. pub interval: u64, } @@ -131,12 +137,13 @@ impl Default for NetworkConfig { Self { address: SocketAddr::from(([0, 0, 0, 0], 3000)), ipv6_only: false, - websocket_max_message_size: 64 * 1024, - websocket_max_frame_size: 16 * 1024, tls_certificate_path: "".into(), tls_private_key_path: "".into(), + websocket_max_message_size: 64 * 1024, + websocket_max_frame_size: 16 * 1024, + #[cfg(feature = "with-mio")] poll_event_capacity: 4096, #[cfg(feature = "with-mio")] @@ -187,3 +194,10 @@ impl Default for StatisticsConfig { Self { interval: 0 } } } + +#[cfg(test)] +mod tests { + use super::Config; + + ::toml_config::gen_serialize_deserialize_test!(Config); +} diff --git a/aquatic_ws_load_test/Cargo.toml b/aquatic_ws_load_test/Cargo.toml index e8ba249..4d7e059 100644 --- a/aquatic_ws_load_test/Cargo.toml +++ b/aquatic_ws_load_test/Cargo.toml @@ -29,6 +29,7 @@ rand_distr = "0.4" rustls = { version = "0.20", features = ["dangerous_configuration"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +toml_config = "0.1.0" tungstenite = "0.16" [dev-dependencies] diff --git a/aquatic_ws_load_test/src/config.rs b/aquatic_ws_load_test/src/config.rs index 8812562..09974e6 100644 --- a/aquatic_ws_load_test/src/config.rs +++ b/aquatic_ws_load_test/src/config.rs @@ -3,9 +3,11 @@ use std::net::SocketAddr; use aquatic_cli_helpers::LogLevel; #[cfg(feature = "cpu-pinning")] use aquatic_common::cpu_pinning::CpuPinningConfig; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; +use toml_config::TomlConfig; -#[derive(Clone, Debug, Serialize, Deserialize)] +/// aquatic_ws_load_test configuration +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct Config { pub server_address: SocketAddr, @@ -41,7 +43,7 @@ impl Default for Config { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, TomlConfig, Deserialize)] #[serde(default)] pub struct TorrentConfig { pub offers_per_request: usize, @@ -72,3 +74,10 @@ impl Default for TorrentConfig { } } } + +#[cfg(test)] +mod tests { + use super::Config; + + ::toml_config::gen_serialize_deserialize_test!(Config); +} diff --git a/toml_config/Cargo.toml b/toml_config/Cargo.toml new file mode 100644 index 0000000..75b1a3e --- /dev/null +++ b/toml_config/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "toml_config" +version = "0.1.0" +authors = ["Joakim FrostegÄrd "] +edition = "2021" +license = "Apache-2.0" +description = "WebTorrent tracker protocol" +repository = "https://github.com/greatest-ape/aquatic" +exclude = ["target"] + +[lib] +name = "toml_config" + +[dependencies] +toml = "0.5" +toml_config_derive = { path = "../toml_config_derive" } + +[dev-dependencies] +serde = { version = "1.0", features = ["derive"] } +quickcheck = "1" +quickcheck_macros = "1" diff --git a/toml_config/src/lib.rs b/toml_config/src/lib.rs new file mode 100644 index 0000000..45db513 --- /dev/null +++ b/toml_config/src/lib.rs @@ -0,0 +1,126 @@ +pub use toml; +pub use toml_config_derive::TomlConfig; + +/// Run this on your struct implementing TomlConfig to generate a +/// serialization/deserialization test for it. +#[macro_export] +macro_rules! gen_serialize_deserialize_test { + ($ident:ident) => { + #[test] + fn test_cargo_toml_serialize_deserialize() { + use ::toml_config::TomlConfig; + let serialized = $ident::default_to_string(); + let deserialized = ::toml_config::toml::de::from_str(&serialized).unwrap(); + + assert_eq!($ident::default(), deserialized); + } + }; +} + +/// Export structs to toml, converting Rust doc strings to comments. +/// +/// Supports one level of nesting. Fields containing structs must come +/// after regular fields. +/// +/// Usage: +/// ``` +/// use toml_config::TomlConfig; +/// +/// #[derive(TomlConfig)] +/// struct SubConfig { +/// /// A +/// a: usize, +/// /// B +/// b: String, +/// } +/// +/// impl Default for SubConfig { +/// fn default() -> Self { +/// Self { +/// a: 200, +/// b: "subconfig hello".into(), +/// } +/// } +/// } +/// +/// #[derive(TomlConfig)] +/// struct Config { +/// /// A +/// a: usize, +/// /// B +/// b: String, +/// /// C +/// c: SubConfig, +/// } +/// +/// impl Default for Config { +/// fn default() -> Self { +/// Self { +/// a: 100, +/// b: "hello".into(), +/// c: Default::default(), +/// } +/// } +/// } +/// +/// let expected = "# A\na = 100\n# B\nb = \"hello\"\n\n# C\n[c]\n# A\na = 200\n# B\nb = \"subconfig hello\"\n"; +/// +/// assert_eq!( +/// Config::default_to_string(), +/// expected, +/// ); +/// ``` +pub trait TomlConfig: Default { + fn default_to_string() -> String; +} + +pub mod __private { + use std::net::SocketAddr; + use std::path::PathBuf; + + pub trait Private { + fn __to_string(&self, comment: Option, field_name: String) -> String; + } + + macro_rules! impl_trait { + ($ident:ident) => { + impl Private for $ident { + fn __to_string(&self, comment: Option, field_name: String) -> String { + let mut output = String::new(); + + if let Some(comment) = comment { + output.push_str(&comment); + } + + let value = crate::toml::ser::to_string(self).unwrap(); + + output.push_str(&format!("{} = {}\n", field_name, value)); + + output + } + } + }; + } + + impl_trait!(isize); + impl_trait!(i8); + impl_trait!(i16); + impl_trait!(i32); + impl_trait!(i64); + + impl_trait!(usize); + impl_trait!(u8); + impl_trait!(u16); + impl_trait!(u32); + impl_trait!(u64); + + impl_trait!(f32); + impl_trait!(f64); + + impl_trait!(bool); + + impl_trait!(String); + + impl_trait!(PathBuf); + impl_trait!(SocketAddr); +} diff --git a/toml_config/tests/test.rs b/toml_config/tests/test.rs new file mode 100644 index 0000000..0c4bb31 --- /dev/null +++ b/toml_config/tests/test.rs @@ -0,0 +1,46 @@ +use serde::Deserialize; + +use toml_config::{gen_serialize_deserialize_test, TomlConfig}; + +#[derive(Clone, Debug, PartialEq, Eq, TomlConfig, Deserialize)] +struct TestConfigInnerA { + /// Comment for a + a: String, + /// Comment for b + b: usize, +} + +impl Default for TestConfigInnerA { + fn default() -> Self { + Self { + a: "Inner hello world".into(), + b: 100, + } + } +} + +/// Comment for TestConfig +#[derive(Clone, Debug, PartialEq, Eq, TomlConfig, Deserialize)] +struct TestConfig { + /// Comment for a that stretches over + /// multiple lines + a: String, + /// Comment for b + b: usize, + c: bool, + /// Comment for TestConfigInnerA + inner_a: TestConfigInnerA, +} + +impl Default for TestConfig { + fn default() -> Self { + Self { + a: "Hello, world!".into(), + b: 100, + c: true, + inner_a: Default::default(), + } + } +} + +gen_serialize_deserialize_test!(TestConfig); diff --git a/toml_config_derive/Cargo.toml b/toml_config_derive/Cargo.toml new file mode 100644 index 0000000..b8fc1a1 --- /dev/null +++ b/toml_config_derive/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "toml_config_derive" +version = "0.1.0" +authors = ["Joakim FrostegÄrd "] +edition = "2021" +license = "Apache-2.0" +description = "WebTorrent tracker protocol" +repository = "https://github.com/greatest-ape/aquatic" +exclude = ["target"] + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = "1" diff --git a/toml_config_derive/src/lib.rs b/toml_config_derive/src/lib.rs new file mode 100644 index 0000000..42d62d2 --- /dev/null +++ b/toml_config_derive/src/lib.rs @@ -0,0 +1,177 @@ +use proc_macro2::{TokenStream, TokenTree}; +use quote::quote; +use syn::{parse_macro_input, Attribute, Data, DataStruct, DeriveInput, Fields, Ident, Type}; + +#[proc_macro_derive(TomlConfig)] +pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let comment = extract_comment_string(input.attrs); + let ident = input.ident; + + match input.data { + Data::Struct(struct_data) => { + let mut output_stream = quote! { + let mut output = String::new(); + }; + + extract_from_struct(ident.clone(), struct_data, &mut output_stream); + + proc_macro::TokenStream::from(quote! { + impl ::toml_config::TomlConfig for #ident { + fn default_to_string() -> String { + let mut output = String::new(); + + let comment: Option = #comment; + + if let Some(comment) = comment { + output.push_str(&comment); + output.push('\n'); + } + + let body = { + #output_stream + + output + }; + + output.push_str(&body); + + output + } + } + impl ::toml_config::__private::Private for #ident { + fn __to_string(&self, comment: Option, field_name: String) -> String { + let mut output = String::new(); + + output.push('\n'); + + if let Some(comment) = comment { + output.push_str(&comment); + } + output.push_str(&format!("[{}]\n", field_name)); + + let body = { + #output_stream + + output + }; + + output.push_str(&body); + + output + } + } + }) + } + Data::Enum(_) => proc_macro::TokenStream::from(quote! { + impl ::toml_config::__private::Private for #ident { + fn __to_string(&self, comment: Option, field_name: String) -> String { + let mut output = String::new(); + let wrapping_comment: Option = #comment; + + if let Some(comment) = wrapping_comment { + output.push_str(&comment); + } + + if let Some(comment) = comment { + output.push_str(&comment); + } + + let value = match ::toml_config::toml::ser::to_string(self) { + Ok(value) => value, + Err(err) => panic!("Couldn't serialize enum to toml: {:#}", err), + }; + + output.push_str(&format!("{} = {}\n", field_name, value)); + + output + } + } + }), + Data::Union(_) => panic!("Unions are not supported"), + } +} + +fn extract_from_struct( + struct_ty_ident: Ident, + struct_data: DataStruct, + output_stream: &mut TokenStream, +) { + let fields = if let Fields::Named(fields) = struct_data.fields { + fields + } else { + panic!("Fields are not named"); + }; + + output_stream.extend(::std::iter::once(quote! { + let struct_default = #struct_ty_ident::default(); + })); + + for field in fields.named.into_iter() { + let ident = field.ident.expect("Encountered unnamed field"); + let ident_string = format!("{}", ident); + let comment = extract_comment_string(field.attrs); + + if let Type::Path(path) = field.ty { + output_stream.extend(::std::iter::once(quote! { + { + let comment: Option = #comment; + let field_default: #path = struct_default.#ident; + + let s: String = ::toml_config::__private::Private::__to_string( + &field_default, + comment, + #ident_string.to_string() + ); + output.push_str(&s); + } + })); + } + } +} + +fn extract_comment_string(attrs: Vec) -> TokenStream { + let mut output = String::new(); + + for attr in attrs.into_iter() { + let path_ident = if let Some(path_ident) = attr.path.get_ident() { + path_ident + } else { + continue; + }; + + if format!("{}", path_ident) != "doc" { + continue; + } + + for token_tree in attr.tokens { + match token_tree { + TokenTree::Literal(literal) => { + let mut comment = format!("{}", literal); + + // Strip leading and trailing quotation marks + comment.remove(comment.len() - 1); + comment.remove(0); + + // Add toml comment indicator + comment.insert(0, '#'); + + output.push_str(&comment); + output.push('\n'); + } + _ => {} + } + } + } + + if output.is_empty() { + quote! { + None + } + } else { + quote! { + Some(#output.to_string()) + } + } +}