diff --git a/relay/sources/relayd/Cargo.lock b/relay/sources/relayd/Cargo.lock index 67b36ac1454..73183d024de 100644 --- a/relay/sources/relayd/Cargo.lock +++ b/relay/sources/relayd/Cargo.lock @@ -261,9 +261,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" dependencies = [ "cfg-if", "crossbeam-epoch", @@ -788,9 +788,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "js-sys" -version = "0.3.51" +version = "0.3.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +checksum = "ce791b7ca6638aae45be056e068fc756d871eb3b3b10b8efa62d1c9cec616752" dependencies = [ "wasm-bindgen", ] @@ -1432,48 +1432,6 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" -[[package]] -name = "relayd" -version = "0.0.0-dev" -dependencies = [ - "anyhow", - "base64", - "bytes", - "chrono", - "criterion", - "diesel", - "filetime", - "flate2", - "futures", - "hex", - "humantime", - "hyper", - "inotify", - "lazy_static", - "log", - "md-5", - "nom", - "openssl", - "pretty_assertions", - "prometheus", - "proptest", - "regex", - "reqwest", - "serde", - "serde_json", - "sha2", - "structopt", - "tempfile", - "thiserror", - "tokio", - "tokio-stream", - "toml", - "tracing", - "tracing-subscriber", - "warp", - "zip", -] - [[package]] name = "remove_dir_all" version = "0.5.3" @@ -1517,6 +1475,48 @@ dependencies = [ "winreg", ] +[[package]] +name = "rudder-relayd" +version = "0.0.0-dev" +dependencies = [ + "anyhow", + "base64", + "bytes", + "chrono", + "criterion", + "diesel", + "filetime", + "flate2", + "futures", + "hex", + "humantime", + "hyper", + "inotify", + "lazy_static", + "log", + "md-5", + "nom", + "openssl", + "pretty_assertions", + "prometheus", + "proptest", + "regex", + "reqwest", + "serde", + "serde_json", + "sha2", + "structopt", + "tempfile", + "thiserror", + "tokio", + "tokio-stream", + "toml", + "tracing", + "tracing-subscriber", + "warp", + "zip", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -1609,15 +1609,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f3aac57ee7f3272d8395c6e4f502f434f0e289fcd62876f70daa008c20dcabe" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" [[package]] name = "serde" -version = "1.0.126" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" dependencies = [ "serde_derive", ] @@ -1634,9 +1634,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.126" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" dependencies = [ "proc-macro2", "quote", @@ -1645,9 +1645,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.65" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c5e91e4240b46c4c19219d6cc84784444326131a4210f496f948d5cc827a29" +checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" dependencies = [ "itoa", "ryu", @@ -1694,9 +1694,9 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c719719ee05df97490f80a45acfc99e5a30ce98a1e4fb67aee422745ae14e3" +checksum = "740223c51853f3145fe7c90360d2d4232f2b62e3449489c207eccde818979982" dependencies = [ "lazy_static", ] @@ -2153,9 +2153,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586" dependencies = [ "cfg-if", "serde", @@ -2165,9 +2165,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f" dependencies = [ "bumpalo", "lazy_static", @@ -2180,9 +2180,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.24" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" +checksum = "16646b21c3add8e13fdb8f20172f8a28c3dbf62f45406bcff0233188226cfe0c" dependencies = [ "cfg-if", "js-sys", @@ -2192,9 +2192,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2202,9 +2202,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f" dependencies = [ "proc-macro2", "quote", @@ -2215,15 +2215,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.74" +version = "0.2.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" +checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2" [[package]] name = "web-sys" -version = "0.3.51" +version = "0.3.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +checksum = "01c70a82d842c9979078c772d4a1344685045f1a5628f677c2b2eab4dd7d2696" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/relay/sources/relayd/Cargo.toml b/relay/sources/relayd/Cargo.toml index 688153a2543..c70aa18ee41 100644 --- a/relay/sources/relayd/Cargo.toml +++ b/relay/sources/relayd/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "relayd" +name = "rudder-relayd" version = "0.0.0-dev" authors = ["Rudder developers "] edition = "2018" @@ -9,10 +9,6 @@ homepage = "https://www.rudder.io" repository = "https://github.com/Normation/rudder" license = "GPL-3.0-or-later" -[[bin]] -name = "rudder-relayd" -path = "src/relayd.rs" - [[bench]] harness = false name = "benches" diff --git a/relay/sources/relayd/src/api/remote_run.rs b/relay/sources/relayd/src/api/remote_run.rs index 32d36a20f55..e55ffd5308a 100644 --- a/relay/sources/relayd/src/api/remote_run.rs +++ b/relay/sources/relayd/src/api/remote_run.rs @@ -278,7 +278,7 @@ impl RemoteRun { params.insert("nodes", nodes.join(",")); } - let client = match job_config.downstream_clients.get(&id) { + let client = match job_config.downstream_clients.read().await.get(&id) { Some(c) => c.clone(), None => { error!("unknown sub-relay '{}'", id); @@ -287,6 +287,7 @@ impl RemoteRun { }; let response = client + .client() .post(&format!( "https://{}:{}/rudder/relay-api/remote-run/{}", hostname, diff --git a/relay/sources/relayd/src/api/shared_files.rs b/relay/sources/relayd/src/api/shared_files.rs index 1e74b9187ac..66bee35ad6b 100644 --- a/relay/sources/relayd/src/api/shared_files.rs +++ b/relay/sources/relayd/src/api/shared_files.rs @@ -160,9 +160,8 @@ async fn put_forward( job_config: Arc, body: Bytes, ) -> Result { - job_config - .upstream_client - .clone() + let client = job_config.upstream_client.read().await.client().clone(); + client .put(&format!( "{}/{}/{}", job_config.cfg.upstream_url(), @@ -299,9 +298,9 @@ async fn head_forward( params: SharedFilesHeadParams, job_config: Arc, ) -> Result { - job_config - .upstream_client - .clone() + let client = job_config.upstream_client.read().await.client().clone(); + + client .head(&format!( "{}/{}/{}", job_config.cfg.upstream_url(), diff --git a/relay/sources/relayd/src/configuration/main.rs b/relay/sources/relayd/src/configuration/main.rs index 8cd139f15f2..8abab324d6f 100644 --- a/relay/sources/relayd/src/configuration/main.rs +++ b/relay/sources/relayd/src/configuration/main.rs @@ -121,6 +121,16 @@ impl Configuration { }) } + pub fn peer_authentication(&self) -> PeerAuthentication { + // compute actual model + if !self.output.upstream.verify_certificates { + warn!("output.upstream.verify_certificates parameter is deprecated, use general.peer_authentication instead"); + PeerAuthentication::DangerousNone + } else { + self.general.peer_authentication + } + } + /// Gives current url of the upstream relay API /// Can be removed once upstream.url is removed pub fn upstream_url(&self) -> String { @@ -175,7 +185,7 @@ pub struct GeneralConfig { pub https_port: u16, /// Which certificate validation model to use #[serde(default = "GeneralConfig::default_peer_authentication")] - pub peer_authentication: PeerAuthentication, + peer_authentication: PeerAuthentication, } impl GeneralConfig { diff --git a/relay/sources/relayd/src/http_client.rs b/relay/sources/relayd/src/http_client.rs new file mode 100644 index 00000000000..2dc05f2cc5c --- /dev/null +++ b/relay/sources/relayd/src/http_client.rs @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2019-2020 Normation SAS + +// WARNING we do not reload content of main.conf, only data files it points at +// FIXME add to doc +// AJOUTER DES LOGS POUR POUVOIR TESTER + +// ON POURRAIT JUSTE TOUT RECHARGER ??? + +use anyhow::Error; +use lazy_static::lazy_static; +use reqwest::{Certificate, Client}; +use structopt::clap::{crate_name, crate_version}; +use tracing::debug; + +lazy_static! { + /// User-Agent used in our HTTP requests + /// "rudder-relayd/7.0.0" + static ref USER_AGENT: String = format!("{}/{}", crate_name!(), crate_version!()); +} + +type PemCertificate = Vec; + +#[derive(Clone, Debug)] +pub enum HttpClient { + /// Keep the associated certificates to be able to compare afterwards + /// + /// We can't currently compare reqwest::Certificate or openssl::X509Ref, + /// so we'll compare pem exports. + Pinned(Client, Vec), + /// For compatibility for 6.X + System(Client), + NoVerify(Client), +} + +// With Rudder cert model we currently need one client for each host we talk to. +// Fortunately we only talk with other policy servers, which are only a few. +// +// Not efficient in "System" case, but it's deprecated anyway. +// +// A future improvement could be to implement public key pinning in the reqwest-hyper-tokio stack. +impl HttpClient { + /// Common parameters + fn new_client_builder() -> reqwest::ClientBuilder { + Client::builder() + // enforce HTTPS to prevent misconfigurations + .https_only(true) + .user_agent(USER_AGENT.clone()) + } + + // Not very efficient as we parse a cert just dumped by openssl + pub fn new_pinned(certs: Vec) -> Result { + debug!("Creating HTTP client with pinned certificates"); + let mut client = Self::new_client_builder() + .danger_accept_invalid_hostnames(true) + .tls_built_in_root_certs(false); + for cert in &certs { + client = client.add_root_certificate(Certificate::from_pem(cert)?); + } + Ok(Self::Pinned(client.build()?, certs)) + } + + pub fn new_system() -> Result { + debug!("Creating HTTP client with system root certificates"); + Ok(Self::System(Self::new_client_builder().build()?)) + } + + pub fn new_no_verify() -> Result { + debug!("Creating HTTP client with no certificate verification"); + Ok(Self::System( + Self::new_client_builder() + .danger_accept_invalid_certs(true) + .build()?, + )) + } + + // FIXME rename or remove + pub fn client(&self) -> &Client { + match *self { + Self::Pinned(ref c, _) => c, + Self::System(ref c) => c, + Self::NoVerify(ref c) => c, + } + } + + /// If order is not good, reload + /// We have only one certificate anyway + pub fn outdated(&self, certs: &[PemCertificate]) -> bool { + match *self { + Self::Pinned(_, ref current) => current.as_slice() != certs, + _ => unreachable!("Reload is only possible for cert-pinning based-clients"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn it_creates_pinned_cert_client() { + let cert = fs::read("tests/files/keys/37817c4d-fbf7-4850-a985-50021f4e8f41.cert").unwrap(); + let certs = vec![cert]; + + let client = HttpClient::new_pinned(certs.clone()).unwrap(); + + assert!(!client.outdated(&certs)); + + let new_cert = + fs::read("tests/files/keys/e745a140-40bc-4b86-b6dc-084488fc906b.cert").unwrap(); + let new_certs = vec![new_cert]; + + assert!(client.outdated(&new_certs)); + } +} diff --git a/relay/sources/relayd/src/lib.rs b/relay/sources/relayd/src/lib.rs index 0882e8ab1d5..743ff946b37 100644 --- a/relay/sources/relayd/src/lib.rs +++ b/relay/sources/relayd/src/lib.rs @@ -11,6 +11,7 @@ pub mod configuration; pub mod data; pub mod error; pub mod hashing; +pub mod http_client; pub mod input; pub mod metrics; pub mod output; @@ -23,14 +24,13 @@ use crate::{ main::{Configuration, InventoryOutputSelect, OutputSelect, ReportingOutputSelect}, }, data::node::{NodeId, NodesList}, + http_client::HttpClient, metrics::{MANAGED_NODES, SUB_NODES}, output::database::{pg_pool, PgPool}, processing::{inventory, reporting}, }; use anyhow::Error; use configuration::main::PeerAuthentication; -use lazy_static::lazy_static; -use reqwest::{Certificate, Client}; use std::{ collections::HashMap, fs, fs::create_dir_all, path::Path, process::exit, string::ToString, sync::Arc, @@ -50,10 +50,6 @@ use tracing_subscriber::{ reload::Handle, }; -lazy_static! { - static ref USER_AGENT: String = format!("rudder-relayd/{}", crate_version!()); -} - // There are two main phases in execution: // // * Startup, when all config files are loaded, various structures are initialized, @@ -210,39 +206,22 @@ fn signal_handlers(job_config: Arc) { }); } -// Graceful reload/restart -// -// If we reload parts of the config we need to reload everything. -// That means restarting all tokio tasks. -// -// We could use a channel to tell all tasks to finish what they are doing and stop. -// -// Potentially remote-run could take minutes to run, we need to decide what to do in this case. -// -// Tasks could start making a copy if the config they use to allow correct reload. -// - pub struct JobConfig { /// Does not reload, by definition pub cli_cfg: CliConfiguration, - pub cfg: Configuration, pub nodes: RwLock, pub pool: Option, /// Parent policy server - pub upstream_client: Client, + pub upstream_client: RwLock, /// Sub relays // TODO could be lazily created - pub downstream_clients: HashMap, + pub downstream_clients: RwLock>, handle: LogHandle, } impl JobConfig { - pub fn new( - cli_cfg: CliConfiguration, - cfg: Configuration, - handle: LogHandle, - ) -> Result, Error> { + fn create_dirs(cfg: &Configuration) -> Result<(), Error> { // Create needed directories if cfg.processing.inventory.output != InventoryOutputSelect::Disabled { create_dir_all(cfg.processing.inventory.directory.join("incoming"))?; @@ -259,81 +238,65 @@ impl JobConfig { create_dir_all(cfg.processing.reporting.directory.join("failed"))?; } + Ok(()) + } + + pub fn new( + cli_cfg: CliConfiguration, + cfg: Configuration, + handle: LogHandle, + ) -> Result, Error> { + Self::create_dirs(&cfg)?; + let pool = if cfg.processing.reporting.output == ReportingOutputSelect::Database { Some(pg_pool(&cfg.output.database)?) } else { None }; + let nodes = NodesList::new( + cfg.node_id()?, + &cfg.general.nodes_list_file, + Some(&cfg.general.nodes_certs_file), + )?; + // HTTP client // + let model = cfg.peer_authentication(); + if model == PeerAuthentication::DangerousNone { + warn!("Certificate verification is disabled, it should not be done in production"); + } - // compute actual model - let model = if !cfg.output.upstream.verify_certificates { - warn!("output.upstream.verify_certificates parameter is deprecated, use general.certificate_verification_model instead"); - PeerAuthentication::DangerousNone - } else { - cfg.general.peer_authentication - }; - + debug!("Creating HTTP client for upstream"); let upstream_client = match model { PeerAuthentication::CertPinning => { - let cert = Certificate::from_pem(&fs::read( - &cfg.output.upstream.server_certificate_file, - )?)?; - Self::new_http_client(vec![cert])? + let cert = fs::read(&cfg.output.upstream.server_certificate_file)?; + HttpClient::new_pinned(vec![cert]) } - PeerAuthentication::SystemRootCerts => Client::builder() - .user_agent(USER_AGENT.clone()) - .https_only(true) - .build()?, - PeerAuthentication::DangerousNone => { - warn!("Certificate verification is disabled, it should not be done in production"); - Client::builder() - .user_agent(USER_AGENT.clone()) - .danger_accept_invalid_certs(true) - .build()? - } - }; - - let nodes = NodesList::new( - cfg.node_id()?, - &cfg.general.nodes_list_file, - Some(&cfg.general.nodes_certs_file), - )?; + PeerAuthentication::SystemRootCerts => HttpClient::new_system(), + PeerAuthentication::DangerousNone => HttpClient::new_no_verify(), + }?; let mut downstream_clients = HashMap::new(); - // remote-run is the only use-case for downstream requests - if cfg.remote_run.enabled { - for (id, certs) in nodes.my_sub_relays_certs() { - let client = match model { - PeerAuthentication::CertPinning => { - let certs = match certs { - Some(stack) => stack - .into_iter() - // certificate has already be parsed by openssl, assume it's correct - .map(|c| Certificate::from_pem(&c.to_pem().unwrap()).unwrap()) - .collect(), - None => vec![], - }; - Self::new_http_client(certs)? - } - PeerAuthentication::SystemRootCerts => Client::builder() - .user_agent(USER_AGENT.clone()) - .https_only(true) - .build()?, - PeerAuthentication::DangerousNone => { - warn!( - "Certificate verification is disabled, it should not be done in production" - ); - Client::builder() - .user_agent(USER_AGENT.clone()) - .danger_accept_invalid_certs(true) - .build()? - } - }; - downstream_clients.insert(id, client); - } + + for (id, certs) in nodes.my_sub_relays_certs() { + debug!("Creating HTTP client for '{}'", id); + let client = match model { + PeerAuthentication::CertPinning => { + let certs = match certs { + Some(stack) => stack + .into_iter() + // certificate has already be parsed by openssl, assume it's correct + .map(|c| c.to_pem().unwrap()) + .collect(), + None => vec![], + }; + HttpClient::new_pinned(certs) + } + PeerAuthentication::SystemRootCerts => HttpClient::new_system(), + PeerAuthentication::DangerousNone => HttpClient::new_no_verify(), + }?; + downstream_clients.insert(id, client); } let nodes = RwLock::new(nodes); @@ -344,11 +307,19 @@ impl JobConfig { nodes, pool, handle, - upstream_client, - downstream_clients, + upstream_client: RwLock::new(upstream_client), + downstream_clients: RwLock::new(downstream_clients), })) } + fn reload_logging(&self) -> Result<(), Error> { + LogConfig::new(&self.cli_cfg.configuration_dir).and_then(|log_cfg| { + self.handle + .reload(EnvFilter::try_new(log_cfg.to_string())?) + .map_err(|e| e.into()) + }) + } + async fn reload_nodeslist(&self) -> Result<(), Error> { let mut nodes = self.nodes.write().await; *nodes = NodesList::new( @@ -360,12 +331,63 @@ impl JobConfig { Ok(()) } - fn reload_logging(&self) -> Result<(), Error> { - LogConfig::new(&self.cli_cfg.configuration_dir).and_then(|log_cfg| { - self.handle - .reload(EnvFilter::try_new(log_cfg.to_string())?) - .map_err(|e| e.into()) - }) + async fn reload_http_clients(&self) -> Result<(), Error> { + // only case we need specific reload + if self.cfg.peer_authentication() == PeerAuthentication::CertPinning { + // upstream client + let cert = fs::read(&self.cfg.output.upstream.server_certificate_file)?; + let certs = vec![cert]; + + let needs_reload = self.upstream_client.read().await.outdated(&certs); + if needs_reload { + debug!( + "Upstream HTTP client has outdated certificate, updating from {}", + &self.cfg.output.upstream.server_certificate_file.display() + ); + let mut upstream_client = self.upstream_client.write().await; + *upstream_client = HttpClient::new_pinned(certs)?; + } else { + debug!("Upstream HTTP client has up-to-date certificate"); + } + + // sub-relay clients + // recreate up to date map, keep existing clients if possible + // to preserve connections + let mut new_downstream_clients = HashMap::new(); + let mut downstream_clients = self.downstream_clients.write().await; + + for (id, certs) in self.nodes.read().await.my_sub_relays_certs() { + let certs = match certs { + Some(stack) => stack + .into_iter() + // certificate has already be parsed by openssl, assume it's correct + .map(|c| c.to_pem().unwrap()) + .collect(), + None => vec![], + }; + + match downstream_clients.get(&id) { + Some(c) => { + if c.outdated(&certs) { + debug!("HTTP client for '{}' has outdated certificate", id); + let client = HttpClient::new_pinned(certs)?; + new_downstream_clients.insert(id, client); + } else { + debug!("HTTP client for '{}' is up-to-date", id); + new_downstream_clients.insert(id, c.clone()); + } + } + None => { + debug!("Creating HTTP client for '{}'", id); + let client = HttpClient::new_pinned(certs)?; + new_downstream_clients.insert(id, client); + } + } + } + *downstream_clients = new_downstream_clients; + } + + Ok(()) } async fn reload_metrics(&self) { @@ -379,25 +401,11 @@ impl JobConfig { info!("Configuration reload requested"); self.reload_logging()?; self.reload_nodeslist().await?; + // We need up-to-date certs + // so run after nodes list refresh + self.reload_http_clients().await?; + // After reload for updated metrics self.reload_metrics().await; Ok(()) } - - // With Rudder cert model we currently need one client for each host we talk to - // - // Not efficient in "System" case, but it's deprecated anyway - fn new_http_client(certs: Vec) -> Result { - let mut client = Client::builder().user_agent(USER_AGENT.clone()); - - client = client - // Let's enforce https to prevent misconfigurations - .https_only(true) - .danger_accept_invalid_hostnames(true) - .tls_built_in_root_certs(false); - for cert in certs { - client = client.add_root_certificate(cert); - } - - Ok(client.build()?) - } } diff --git a/relay/sources/relayd/src/relayd.rs b/relay/sources/relayd/src/main.rs similarity index 98% rename from relay/sources/relayd/src/relayd.rs rename to relay/sources/relayd/src/main.rs index a019ae86f2a..c0544ff28da 100644 --- a/relay/sources/relayd/src/relayd.rs +++ b/relay/sources/relayd/src/main.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2019-2020 Normation SAS -use relayd::{ +use rudder_relayd::{ check_configuration, configuration::cli::CliConfiguration, init_logger, start, ExitStatus, }; use std::{env, process::exit}; diff --git a/relay/sources/relayd/src/output/upstream.rs b/relay/sources/relayd/src/output/upstream.rs index 5e1fcdc1ae0..fe2ca5109bd 100644 --- a/relay/sources/relayd/src/output/upstream.rs +++ b/relay/sources/relayd/src/output/upstream.rs @@ -48,9 +48,9 @@ async fn forward_file( ) -> Result<(), Error> { let content = tokio::fs::read(path.clone()).await?; - let result = job_config - .upstream_client - .clone() + let client = job_config.upstream_client.read().await.client().clone(); + + let result = client .put(&format!( "{}/{}/{}", job_config.cfg.upstream_url(), diff --git a/relay/sources/relayd/tests/api_system_info.rs b/relay/sources/relayd/tests/api_system_info.rs index 3f6d356a025..9c31e13896c 100644 --- a/relay/sources/relayd/tests/api_system_info.rs +++ b/relay/sources/relayd/tests/api_system_info.rs @@ -3,7 +3,7 @@ mod common; -use relayd::{configuration::cli::CliConfiguration, init_logger, start}; +use rudder_relayd::{configuration::cli::CliConfiguration, init_logger, start}; use std::thread; #[cfg(test)] diff --git a/relay/sources/relayd/tests/api_system_reload.rs b/relay/sources/relayd/tests/api_system_reload.rs index 86acaf7567d..e12fed01804 100644 --- a/relay/sources/relayd/tests/api_system_reload.rs +++ b/relay/sources/relayd/tests/api_system_reload.rs @@ -3,7 +3,7 @@ mod common; -use relayd::{configuration::cli::CliConfiguration, init_logger, start}; +use rudder_relayd::{configuration::cli::CliConfiguration, init_logger, start}; use std::thread; #[cfg(test)] diff --git a/relay/sources/relayd/tests/api_system_status.rs b/relay/sources/relayd/tests/api_system_status.rs index 94b5fae3b22..6575bf2ed32 100644 --- a/relay/sources/relayd/tests/api_system_status.rs +++ b/relay/sources/relayd/tests/api_system_status.rs @@ -3,7 +3,7 @@ mod common; -use relayd::{configuration::cli::CliConfiguration, init_logger, start}; +use rudder_relayd::{configuration::cli::CliConfiguration, init_logger, start}; use std::{fs::rename, thread}; #[cfg(test)] diff --git a/relay/sources/relayd/tests/common.rs b/relay/sources/relayd/tests/common.rs deleted file mode 100644 index f31b7579c72..00000000000 --- a/relay/sources/relayd/tests/common.rs +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// SPDX-FileCopyrightText: 2019-2020 Normation SAS - -use std::{thread, time}; - -#[allow(clippy::result_unit_err)] -pub fn start_api() -> Result<(), ()> { - let mut retry = 10; - while retry > 0 { - thread::sleep(time::Duration::from_millis(200)); - retry -= 1; - - let resp = reqwest::blocking::get("http://localhost:3030/rudder/relay-api/1/system/status"); - - if resp.is_ok() { - return Ok(()); - } - } - Err(()) -} diff --git a/relay/sources/relayd/tests/common/mod.rs b/relay/sources/relayd/tests/common/mod.rs new file mode 100644 index 00000000000..a804ed0ad80 --- /dev/null +++ b/relay/sources/relayd/tests/common/mod.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2019-2020 Normation SAS + +use std::{process::Command, thread, time}; + +#[allow(clippy::result_unit_err)] +pub fn start_api() -> Result<(), ()> { + let mut retry = 10; + while retry > 0 { + thread::sleep(time::Duration::from_millis(200)); + retry -= 1; + + let resp = reqwest::blocking::get("http://localhost:3030/rudder/relay-api/1/system/status"); + + if resp.is_ok() { + return Ok(()); + } + } + Err(()) +} + +#[allow(dead_code)] +pub fn fake_server_start(id: String) { + thread::spawn(|| { + Command::new("tests/server.py") + .arg(id) + .spawn() + .expect("failed to execute process") + }); + thread::sleep(time::Duration::from_millis(400)); + + let client = reqwest::blocking::Client::builder() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + let response = client.get("https://localhost:4443/uuid").send().unwrap(); + assert_eq!(response.status(), hyper::StatusCode::OK); +} + +#[allow(dead_code)] +pub fn fake_server_stop() { + let client = reqwest::blocking::Client::builder() + .danger_accept_invalid_certs(true) + .build() + .unwrap(); + let response = client.get("https://localhost:4443/stop").send().unwrap(); + assert_eq!(response.status(), hyper::StatusCode::OK); +} diff --git a/relay/sources/relayd/tests/http_client.rs b/relay/sources/relayd/tests/http_client.rs new file mode 100644 index 00000000000..f319c3fb70d --- /dev/null +++ b/relay/sources/relayd/tests/http_client.rs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2019-2020 Normation SAS + +mod common; + +use common::{fake_server_start, fake_server_stop}; +use rudder_relayd::{configuration::cli::CliConfiguration, init_logger, start}; +use std::{fs, thread}; + +fn upstream_call_ok(client: &reqwest::blocking::Client, should_be_ok: bool) { + let params_sync = [ + ("asynchronous", "false"), + ("keep_output", "true"), + ("classes", "class2,class5"), + ("nodes", "root,c745a140-40bc-4b86-b6dc-084488fc906b"), + ]; + + let response = client + .post("http://localhost:3030/rudder/relay-api/1/remote-run/nodes") + .form(¶ms_sync) + .send() + .unwrap(); + assert_eq!(response.status(), hyper::StatusCode::OK); + if should_be_ok { + // No remote answer = upstream request failed + assert_eq!(response.text().unwrap(), "OK\nEND\nREMOTE\n".to_string()); + } else { + assert_eq!(response.text().unwrap(), "OK\nEND\n".to_string()); + } +} + +fn reload_config(client: &reqwest::blocking::Client) { + let response: serde_json::Value = serde_json::from_str( + &client + .post("http://localhost:3030/rudder/relay-api/1/system/reload") + .send() + .unwrap() + .text() + .unwrap(), + ) + .unwrap(); + + let reference: serde_json::Value = + serde_json::from_str("{\"result\":\"success\",\"action\":\"reloadConfiguration\"}") + .unwrap(); + assert_eq!(reference, response); +} + +#[test] +fn it_reloads_http_clients() { + let cli_cfg = CliConfiguration::new("tests/files/config/", false); + + thread::spawn(move || { + start(cli_cfg, init_logger().unwrap()).unwrap(); + }); + + assert!(common::start_api().is_ok()); + let client = reqwest::blocking::Client::new(); + + fake_server_start("37817c4d-fbf7-4850-a985-50021f4e8f41".to_string()); + + // First successful request + upstream_call_ok(&client, true); + + fs::copy( + "tests/files/keys/nodescerts.pem", + "target/tmp/nodescerts.pem.back", + ) + .unwrap(); + // Replace by wrong certificate + fs::copy( + "tests/files/keys/e745a140-40bc-4b86-b6dc-084488fc906b.cert", + "tests/files/keys/nodescerts.pem", + ) + .unwrap(); + + // Reload configuration + reload_config(&client); + + // Fail as certificate is wrong + upstream_call_ok(&client, false); + + // Put correct cert back in place + fs::copy( + "target/tmp/nodescerts.pem.back", + "tests/files/keys/nodescerts.pem", + ) + .unwrap(); + + // Reload configuration + reload_config(&client); + + // Should be back + upstream_call_ok(&client, true); + + fake_server_stop(); +} diff --git a/relay/sources/relayd/tests/processing_reporting.rs b/relay/sources/relayd/tests/processing_reporting.rs index 27893118f30..5ea65b54b57 100644 --- a/relay/sources/relayd/tests/processing_reporting.rs +++ b/relay/sources/relayd/tests/processing_reporting.rs @@ -3,7 +3,7 @@ use diesel::{self, prelude::*, PgConnection}; use filetime::{set_file_times, FileTime}; -use relayd::{ +use rudder_relayd::{ configuration::cli::CliConfiguration, data::report::QueryableReport, init_logger, diff --git a/relay/sources/relayd/tests/processing_reporting_old.rs b/relay/sources/relayd/tests/processing_reporting_old.rs index 7f4ee44979d..1e9568897af 100644 --- a/relay/sources/relayd/tests/processing_reporting_old.rs +++ b/relay/sources/relayd/tests/processing_reporting_old.rs @@ -4,7 +4,7 @@ mod processing_reporting; use filetime::{set_file_times, FileTime}; -use relayd::{configuration::main::CleanupConfig, input::watch::cleanup}; +use rudder_relayd::{configuration::main::CleanupConfig, input::watch::cleanup}; use std::{ fs::{copy, create_dir_all, remove_dir_all}, path::{Path, PathBuf}, diff --git a/relay/sources/relayd/tests/remote_run.rs b/relay/sources/relayd/tests/remote_run.rs index 620e54e82eb..0201fcb86a9 100644 --- a/relay/sources/relayd/tests/remote_run.rs +++ b/relay/sources/relayd/tests/remote_run.rs @@ -3,40 +3,14 @@ mod common; -use relayd::{configuration::cli::CliConfiguration, init_logger, start}; +use common::{fake_server_start, fake_server_stop}; +use rudder_relayd::{configuration::cli::CliConfiguration, init_logger, start}; use std::{ fs::{read_to_string, remove_file}, path::Path, - process::Command, thread, time, }; -fn fake_server_start(id: String) { - thread::spawn(|| { - Command::new("tests/server.py") - .arg(id) - .spawn() - .expect("failed to execute process") - }); - thread::sleep(time::Duration::from_millis(400)); - - let client = reqwest::blocking::Client::builder() - .danger_accept_invalid_certs(true) - .build() - .unwrap(); - let response = client.get("https://localhost:4443/uuid").send().unwrap(); - assert_eq!(response.status(), hyper::StatusCode::OK); -} - -fn fake_server_stop() { - let client = reqwest::blocking::Client::builder() - .danger_accept_invalid_certs(true) - .build() - .unwrap(); - let response = client.get("https://localhost:4443/stop").send().unwrap(); - assert_eq!(response.status(), hyper::StatusCode::OK); -} - #[cfg(test)] mod tests { use super::*; diff --git a/relay/sources/relayd/tests/shared_files.rs b/relay/sources/relayd/tests/shared_files.rs index 6d890ea83a8..8e1a9dd2674 100644 --- a/relay/sources/relayd/tests/shared_files.rs +++ b/relay/sources/relayd/tests/shared_files.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2019-2020 Normation SAS -use relayd::{ +use rudder_relayd::{ configuration::cli::CliConfiguration, data::shared_file::Metadata, init_logger, start, }; use std::{ diff --git a/relay/sources/relayd/tests/shared_folder.rs b/relay/sources/relayd/tests/shared_folder.rs index 079ffcfd80d..85e42e67e89 100644 --- a/relay/sources/relayd/tests/shared_folder.rs +++ b/relay/sources/relayd/tests/shared_folder.rs @@ -3,7 +3,7 @@ mod common; -use relayd::{configuration::cli::CliConfiguration, init_logger, start}; +use rudder_relayd::{configuration::cli::CliConfiguration, init_logger, start}; use std::thread; #[cfg(test)]