diff options
Diffstat (limited to 'src/config/mod.rs')
| -rw-r--r-- | src/config/mod.rs | 97 |
1 files changed, 85 insertions, 12 deletions
diff --git a/src/config/mod.rs b/src/config/mod.rs index c7f96a3..a6f70aa 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -90,24 +90,38 @@ pub(crate) const FORCE: &[&str] = &["swarmId", "badDocumentFile", "replaceDocume pub(crate) const OPTIONS: &[OptionSpec] = &[ option( "bind-addr", - Kind::Text, - "HOSTNAME:PORT", + Kind::List, + "ADDRESS:PORT|unix:PATH,[...]", Some("127.0.0.1:3000"), - "Address of the HTTP API.", + "Where the HTTP API listens: TCP addresses, and Unix sockets (unix:PATH, made readable and writable by the node's user and group only), as many as given.", ), option( "htpasswd", Kind::Text, "PATH", None, - "Users of the HTTP API (htpasswd file with Argon2 hashes). Required, but with relay-only (then without it, no HTTP API).", + "Users of the HTTP API (htpasswd file with Argon2 hashes). Required, but with user-header or relay-only (then without either, no HTTP API).", + ), + option( + "user-header", + Kind::Text, + "NAME", + None, + "A request header naming the user, set by a reverse proxy in front of the HTTP API that authenticated the request itself (e.g. X-Remote-User, from nginx's $remote_user). Believed only from trusted-proxies; other requests need htpasswd's basic authentication. The proxy must set or clear it on every request.", + ), + option( + "trusted-proxies", + Kind::List, + "ADDRESS,[ADDRESS...]", + Some("127.0.0.1,::1,unix"), + "Where the reverse proxies whose user-header is believed connect from: IP addresses (or ranges, as ADDRESS/BITS), and unix for the API's Unix sockets (bind-addr). On a host with other users, leave out the addresses and have the proxy use a Unix socket: others can connect over loopback too.", ), option( "protocol-writers", Kind::List, "USER,[USER...]", None, - "HTTP API users (from htpasswd) who may write protocol data under /store/_continuum (remake records, members, forget records), e.g. to repair a stuck remake. Such writes act on the whole swarm and are logged. Epoch metadata and the swarm ID are never writable.", + "HTTP API users (from htpasswd or user-header) who may write protocol data under /store/_continuum (remake records, members, forget records), e.g. to repair a stuck remake. Such writes act on the whole swarm and are logged. Epoch metadata and the swarm ID are never writable.", ), option( "log-format", @@ -229,6 +243,20 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ "A name for this node, shown next to its node ID (/peers, metrics). Published in the swarm's member list; not unique, never an identity.", ), option( + "dashboard-url", + Kind::Text, + "URL", + None, + "Where people reach this node's dashboard (http:// or https://, e.g. behind a reverse proxy). Published in the swarm's member list: the other nodes' dashboards open it on a double-click on this node. A build without the dashboard feature warns and publishes none (removing one published before).", + ), + option( + "stream-heartbeat", + Kind::Duration, + "DURATION", + Some("30s"), + "How often GET /stream sends a blank line while nothing changes, so proxies in front keep it open: below their read timeout (nginx's proxy_read_timeout, 60 s by default). 0: never.", + ), + option( "keepalive", Kind::Duration, "DURATION", @@ -480,10 +508,16 @@ pub(crate) fn spec(name: &str) -> Option<&'static OptionSpec> { #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub(crate) struct Settings { - #[serde(default = "default_bind_addr")] - pub(crate) bind_addr: SocketAddr, - /// Required (`check`), but with `relay_only`. + #[serde( + default = "default_bind_addr", + deserialize_with = "crate::api::listen::one_or_more" + )] + pub(crate) bind_addr: Vec<crate::api::listen::Bind>, + /// Required (`check`), but with `user_header` or `relay_only`. pub(crate) htpasswd: Option<String>, + pub(crate) user_header: Option<String>, + #[serde(default = "default_trusted_proxies")] + pub(crate) trusted_proxies: Vec<String>, #[serde(default)] pub(crate) protocol_writers: Vec<String>, #[serde(default)] @@ -520,9 +554,15 @@ pub(crate) struct Settings { #[serde(default)] pub(crate) swarm_key_create: bool, pub(crate) alias: Option<String>, + pub(crate) dashboard_url: Option<String>, #[serde(default = "default_keepalive", with = "crate::units::duration_option")] pub(crate) keepalive: Duration, #[serde( + default = "default_stream_heartbeat", + with = "crate::units::duration_option" + )] + pub(crate) stream_heartbeat: Duration, + #[serde( default, deserialize_with = "crate::units::optional_duration", serialize_with = "crate::units::serialize_optional_duration" @@ -618,8 +658,14 @@ pub(crate) struct Settings { pub(crate) debug_epoch: Option<u64>, } -fn default_bind_addr() -> SocketAddr { - SocketAddr::from(([127, 0, 0, 1], 3000)) +fn default_bind_addr() -> Vec<crate::api::listen::Bind> { + vec![crate::api::listen::Bind::Tcp(SocketAddr::from(( + [127, 0, 0, 1], + 3000, + )))] +} +fn default_trusted_proxies() -> Vec<String> { + vec!["127.0.0.1".to_owned(), "::1".to_owned(), "unix".to_owned()] } fn default_document_append() -> Duration { Duration::from_secs(5) @@ -630,6 +676,9 @@ fn default_document_rewrite() -> Duration { fn default_keepalive() -> Duration { Duration::from_secs(60) } +fn default_stream_heartbeat() -> Duration { + Duration::from_secs(30) +} fn default_neighbours() -> usize { 12 } @@ -696,8 +745,10 @@ impl Settings { if self.relay_only { self.check_relay_only()?; } else { - if self.htpasswd.is_none() { - bail!("htpasswd is required: the users of the HTTP API"); + if self.htpasswd.is_none() && self.user_header.is_none() { + bail!( + "htpasswd is required: the users of the HTTP API (or user-header, with a reverse proxy that authenticates them)" + ); } if self.node_key_file.is_none() { bail!( @@ -766,6 +817,17 @@ impl Settings { if let Some(alias) = &self.alias { check_alias(alias)?; } + if let Some(url) = &self.dashboard_url { + let scheme = url.starts_with("http://") || url.starts_with("https://"); + if !scheme + || url.len() > 2048 + || url.chars().any(|c| c.is_whitespace() || c.is_control()) + { + bail!( + "dashboard-url: {url:?} is no http:// or https:// URL (of at most 2048 characters)" + ); + } + } Ok(()) } } @@ -820,6 +882,7 @@ impl Settings { ("swarm-key-create", self.swarm_key_create), ("protocol-writers", !self.protocol_writers.is_empty()), ("alias", self.alias.is_some()), + ("dashboard-url", self.dashboard_url.is_some()), ("debug-api", self.debug_api), ]; let set: Vec<&str> = swarm_options @@ -1496,6 +1559,16 @@ fn resolve(merged: Layer, files: Vec<PathBuf>) -> Result<Loaded> { settings.alias = settings.alias.map(|alias| alias.trim().to_owned()); settings.transports = settings.ordered_transports(); settings.check().context("invalid configuration")?; + // No dashboard to be reached there: none is published (and one published before is + // removed, `swarm::members`). + if !cfg!(feature = "dashboard") + && let Some(url) = settings.dashboard_url.take() + { + warn!( + url = %url, + "dashboard-url is set, but this build has no dashboard (the dashboard feature); not publishing it" + ); + } Ok(Loaded { settings, origins: merged |
