diff --git a/Cargo.lock b/Cargo.lock index 29c1843..62c423f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,6 +22,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "atty" version = "0.2.14" @@ -57,7 +63,7 @@ version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ce18265ec2324ad075345d5814fbeed4f41f0a660055dc78840b74d19b874b1" dependencies = [ - "serde", + "serde 1.0.131", ] [[package]] @@ -108,6 +114,22 @@ dependencies = [ "bitflags", ] +[[package]] +name = "config" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1b9d958c2b1368a663f05538fc1b5975adce1e19f435acceae987aceeeb369" +dependencies = [ + "lazy_static", + "nom", + "rust-ini", + "serde 1.0.131", + "serde-hjson", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "cpufeatures" version = "0.2.1" @@ -365,6 +387,25 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.111" @@ -381,6 +422,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "lock_api" version = "0.4.5" @@ -433,19 +480,32 @@ dependencies = [ "winapi", ] +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "nostr-rs-relay" version = "0.1.6" dependencies = [ "bitcoin_hashes", + "config", "env_logger", "futures", "futures-util", "hex", + "lazy_static", "log", "rusqlite", "secp256k1", - "serde", + "serde 1.0.131", "serde_json", "thiserror", "tokio", @@ -463,6 +523,24 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" +dependencies = [ + "num-traits 0.2.14", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg 1.0.1", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -754,6 +832,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-ini" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2" + [[package]] name = "ryu" version = "1.0.7" @@ -775,7 +859,7 @@ dependencies = [ "bitcoin_hashes", "rand 0.6.5", "secp256k1-sys", - "serde", + "serde 1.0.131", ] [[package]] @@ -787,6 +871,12 @@ dependencies = [ "cc", ] +[[package]] +name = "serde" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" + [[package]] name = "serde" version = "1.0.131" @@ -796,6 +886,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-hjson" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8" +dependencies = [ + "lazy_static", + "num-traits 0.1.43", + "regex", + "serde 0.8.23", +] + [[package]] name = "serde_derive" version = "1.0.131" @@ -815,7 +917,7 @@ checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" dependencies = [ "itoa", "ryu", - "serde", + "serde 1.0.131", ] [[package]] @@ -852,6 +954,12 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" version = "1.0.82" @@ -950,6 +1058,15 @@ dependencies = [ "tungstenite", ] +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde 1.0.131", +] + [[package]] name = "tungstenite" version = "0.16.0" @@ -1071,3 +1188,12 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index 2cdf1d6..fc6b4ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,11 @@ tokio-tungstenite = "^0.16" tungstenite = "^0.16" thiserror = "^1" uuid = { version = "^0.8", features = ["v4"] } - +config = { version = "0.11", features = ["toml"] } bitcoin_hashes = { version = "^0.9", features = ["serde"] } secp256k1 = { version = "^0.20", features = ["rand", "rand-std", "serde", "bitcoin_hashes"] } serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0" hex = "^0.4" rusqlite = "^0.26" +lazy_static = "^1.4" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..936d225 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,81 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +#[allow(unused)] +pub struct Network { + pub port: u16, + pub address: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[allow(unused)] +pub struct Retention { + // TODO: implement + pub max_events: Option, // max events + pub max_bytes: Option, // max size + pub persist_days: Option, // oldest message + pub whitelist_addresses: Vec, // whitelisted addresses (never delete) +} + +#[derive(Debug, Serialize, Deserialize)] +#[allow(unused)] +pub struct Limits { + pub messages_per_sec: Option, // Artificially slow down event writing to limit disk consumption + pub max_event_bytes: Option, + pub max_ws_message_bytes: Option, + pub max_ws_frame_bytes: Option, + pub broadcast_buffer: usize, // events to buffer for subscribers (prevents slow readers from consuming memory) + pub event_persist_buffer: usize, // events to buffer for database commits (block senders if database writes are too slow) +} + +#[derive(Debug, Serialize, Deserialize)] +#[allow(unused)] +pub struct Settings { + pub network: Network, + pub limits: Limits, + pub retention: Retention, +} + +impl Settings { + pub fn new() -> Self { + let d = Self::default(); + // attempt to construct settings with file + Self::new_from_default(&d).unwrap_or(d) + } + + fn new_from_default(default: &Settings) -> Result { + let config: config::Config = config::Config::new(); + let settings: Settings = config + // use defaults + .with_merged(config::Config::try_from(default).unwrap())? + // override with file contents + .with_merged(config::File::with_name("config"))? + .try_into()?; + Ok(settings) + } +} + +impl Default for Settings { + fn default() -> Self { + Settings { + network: Network { + port: 8080, + address: "0.0.0.0".to_owned(), + }, + limits: Limits { + messages_per_sec: None, + max_event_bytes: Some(2 << 17), // 128K + max_ws_message_bytes: Some(2 << 17), // 128K + max_ws_frame_bytes: Some(2 << 17), // 128K + broadcast_buffer: 4096, + event_persist_buffer: 16, + }, + retention: Retention { + max_events: None, // max events + max_bytes: None, // max size + persist_days: None, // oldest message + whitelist_addresses: vec![], // whitelisted addresses (never delete) + }, + } + } +} diff --git a/src/error.rs b/src/error.rs index dac58ac..a20119a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -34,6 +34,8 @@ pub enum Error { CommandUnknownError, #[error("SQL error")] SqlError(rusqlite::Error), + #[error("Config error")] + ConfigError(config::ConfigError), } impl From for Error { @@ -56,3 +58,10 @@ impl From for Error { Error::WebsocketError(r) } } + +impl From for Error { + /// Wrap Config error + fn from(r: config::ConfigError) -> Self { + Error::ConfigError(r) + } +} diff --git a/src/lib.rs b/src/lib.rs index e464664..1be173c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod close; +pub mod config; pub mod conn; pub mod db; pub mod error; diff --git a/src/main.rs b/src/main.rs index d094fae..6553ecb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,10 @@ //! Server process use futures::SinkExt; use futures::StreamExt; +use lazy_static::lazy_static; use log::*; use nostr_rs_relay::close::Close; +use nostr_rs_relay::config; use nostr_rs_relay::conn; use nostr_rs_relay::db; use nostr_rs_relay::error::{Error, Result}; @@ -11,7 +13,7 @@ use nostr_rs_relay::protostream; use nostr_rs_relay::protostream::NostrMessage::*; use nostr_rs_relay::protostream::NostrResponse::*; use std::collections::HashMap; -use std::env; +use std::sync::RwLock; use tokio::net::{TcpListener, TcpStream}; use tokio::runtime::Builder; use tokio::sync::broadcast; @@ -19,14 +21,24 @@ use tokio::sync::broadcast::{Receiver, Sender}; use tokio::sync::mpsc; use tokio::sync::oneshot; use tungstenite::protocol::WebSocketConfig; +// initialize a singleton default configuration +lazy_static! { + static ref SETTINGS: RwLock = RwLock::new(config::Settings::default()); +} /// Start running a Nostr relay server. fn main() -> Result<(), Error> { - // setup logger and environment + // setup logger let _ = env_logger::try_init(); - let addr = env::args() - .nth(1) - .unwrap_or_else(|| "0.0.0.0:8080".to_string()); + { + let mut settings = SETTINGS.write().unwrap(); + // replace default settings with those read from config.toml + let c = config::Settings::new(); + debug!("using settings: {:?}", c); + *settings = c; + } + let config = SETTINGS.read().unwrap(); + let addr = format!("{}:{}", config.network.address.trim(), config.network.port); // configure tokio runtime let rt = Builder::new_multi_thread() .enable_all() @@ -35,16 +47,17 @@ fn main() -> Result<(), Error> { .unwrap(); // start tokio rt.block_on(async { + let settings = SETTINGS.read().unwrap(); let listener = TcpListener::bind(&addr).await.expect("Failed to bind"); info!("listening on: {}", addr); // all client-submitted valid events are broadcast to every // other client on this channel. This should be large enough // to accomodate slower readers (messages are dropped if // clients can not keep up). - let (bcast_tx, _) = broadcast::channel::(4096); + let (bcast_tx, _) = broadcast::channel::(settings.limits.broadcast_buffer); // validated events that need to be persisted are sent to the // database on via this channel. - let (event_tx, event_rx) = mpsc::channel::(16); + let (event_tx, event_rx) = mpsc::channel::(settings.limits.event_persist_buffer); // start the database writer thread. Give it a channel for // writing events, and for publishing events that have been // written (to all connected clients). @@ -94,13 +107,12 @@ async fn nostr_server( ) { // get a broadcast channel for clients to communicate on let mut bcast_rx = broadcast.subscribe(); - // websocket configuration / limits - let config = WebSocketConfig { - max_send_queue: None, - max_message_size: Some(2 << 19), // 512K - max_frame_size: Some(2 << 19), // 512k - accept_unmasked_frames: false, // follow the spec - }; + let mut config = WebSocketConfig::default(); + { + let settings = SETTINGS.read().unwrap(); + config.max_message_size = settings.limits.max_ws_message_bytes; + config.max_frame_size = settings.limits.max_ws_frame_bytes; + } // upgrade the TCP connection to WebSocket let conn = tokio_tungstenite::accept_async_with_config(stream, Some(config)).await; let ws_stream = conn.expect("websocket handshake error");