aboutsummaryrefslogtreecommitdiffstats
path: root/src/config
diff options
context:
space:
mode:
Diffstat (limited to 'src/config')
-rw-r--r--src/config/mod.rs97
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