From 0baa8a53a50dfeb5a3a3a95666ea84a422fa6f2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 25 Aug 2016 08:57:13 +0200 Subject: [PATCH] dapps-hosts configuration --- Cargo.lock | 1 - dapps/src/lib.rs | 62 ++++++++++++++++++++++++++--- dapps/src/router/host_validation.rs | 8 ++-- dapps/src/router/mod.rs | 14 ++++--- parity/cli.rs | 6 +++ parity/configuration.rs | 28 +++++++++++++ parity/dapps.rs | 10 +++-- 7 files changed, 108 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c06c5a62..119e87fdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -290,7 +290,6 @@ version = "1.4.0" dependencies = [ "clippy 0.0.85 (registry+https://github.com/rust-lang/crates.io-index)", "ethabi 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "ethcore-devtools 1.4.0", "ethcore-rpc 1.4.0", "ethcore-util 1.4.0", "hyper 0.9.4 (git+https://github.com/ethcore/hyper)", diff --git a/dapps/src/lib.rs b/dapps/src/lib.rs index 574c38acf..e50bc2006 100644 --- a/dapps/src/lib.rs +++ b/dapps/src/lib.rs @@ -108,14 +108,28 @@ impl ServerBuilder { /// Asynchronously start server with no authentication, /// returns result with `Server` handle on success or an error. - pub fn start_unsecure_http(&self, addr: &SocketAddr) -> Result { - Server::start_http(addr, NoAuth, self.handler.clone(), self.dapps_path.clone(), self.registrar.clone()) + pub fn start_unsecured_http(&self, addr: &SocketAddr, hosts: Option>) -> Result { + Server::start_http( + addr, + hosts, + NoAuth, + self.handler.clone(), + self.dapps_path.clone(), + self.registrar.clone() + ) } /// Asynchronously start server with `HTTP Basic Authentication`, /// return result with `Server` handle on success or an error. - pub fn start_basic_auth_http(&self, addr: &SocketAddr, username: &str, password: &str) -> Result { - Server::start_http(addr, HttpBasicAuth::single_user(username, password), self.handler.clone(), self.dapps_path.clone(), self.registrar.clone()) + pub fn start_basic_auth_http(&self, addr: &SocketAddr, hosts: Option>, username: &str, password: &str) -> Result { + Server::start_http( + addr, + hosts, + HttpBasicAuth::single_user(username, password), + self.handler.clone(), + self.dapps_path.clone(), + self.registrar.clone() + ) } } @@ -126,8 +140,24 @@ pub struct Server { } impl Server { + /// Returns a list of allowed hosts or `None` if all hosts are allowed. + fn allowed_hosts(hosts: Option>, bind_address: String) -> Option> { + let mut allowed = Vec::new(); + + match hosts { + Some(hosts) => allowed.extend_from_slice(&hosts), + None => return None, + } + + // Add localhost domain as valid too if listening on loopback interface. + allowed.push(bind_address.replace("127.0.0.1", "localhost").into()); + allowed.push(bind_address.into()); + Some(allowed) + } + fn start_http( addr: &SocketAddr, + hosts: Option>, authorization: A, handler: Arc, dapps_path: String, @@ -144,7 +174,7 @@ impl Server { special.insert(router::SpecialEndpoint::Utils, apps::utils()); special }); - let bind_address = format!("{}", addr); + let hosts = Self::allowed_hosts(hosts, format!("{}", addr)); try!(hyper::Server::http(addr)) .handle(move |ctrl| router::Router::new( @@ -154,7 +184,7 @@ impl Server { endpoints.clone(), special.clone(), authorization.clone(), - bind_address.clone(), + hosts.clone(), )) .map(|(l, srv)| { @@ -207,3 +237,23 @@ pub fn random_filename() -> String { rng.gen_ascii_chars().take(12).collect() } +#[cfg(test)] +mod tests { + use super::Server; + + #[test] + fn should_return_allowed_hosts() { + // given + let bind_address = "127.0.0.1".to_owned(); + + // when + let all = Server::allowed_hosts(None, bind_address.clone()); + let address = Server::allowed_hosts(Some(Vec::new()), bind_address.clone()); + let some = Server::allowed_hosts(Some(vec!["ethcore.io".into()]), bind_address.clone()); + + // then + assert_eq!(all, None); + assert_eq!(address, Some(vec!["localhost".into(), "127.0.0.1".into()])); + assert_eq!(some, Some(vec!["ethcore.io".into(), "localhost".into(), "127.0.0.1".into()])); + } +} diff --git a/dapps/src/router/host_validation.rs b/dapps/src/router/host_validation.rs index 62813500f..e0f974482 100644 --- a/dapps/src/router/host_validation.rs +++ b/dapps/src/router/host_validation.rs @@ -22,13 +22,11 @@ use hyper::net::HttpStream; use jsonrpc_http_server::{is_host_header_valid}; use handlers::ContentHandler; -pub fn is_valid(request: &server::Request, bind_address: &str, endpoints: Vec) -> bool { - let mut endpoints = endpoints.into_iter() +pub fn is_valid(request: &server::Request, allowed_hosts: &[String], endpoints: Vec) -> bool { + let mut endpoints = endpoints.iter() .map(|endpoint| format!("{}{}", endpoint, DAPPS_DOMAIN)) .collect::>(); - // Add localhost domain as valid too if listening on loopback interface. - endpoints.push(bind_address.replace("127.0.0.1", "localhost").into()); - endpoints.push(bind_address.into()); + endpoints.extend_from_slice(allowed_hosts); let header_valid = is_host_header_valid(request, &endpoints); diff --git a/dapps/src/router/mod.rs b/dapps/src/router/mod.rs index 568dc00da..359337047 100644 --- a/dapps/src/router/mod.rs +++ b/dapps/src/router/mod.rs @@ -48,7 +48,7 @@ pub struct Router { fetch: Arc, special: Arc>>, authorization: Arc, - bind_address: String, + allowed_hosts: Option>, handler: Box + Send>, } @@ -56,9 +56,11 @@ impl server::Handler for Router { fn on_request(&mut self, req: server::Request) -> Next { // Validate Host header - if !host_validation::is_valid(&req, &self.bind_address, self.endpoints.keys().cloned().collect()) { - self.handler = host_validation::host_invalid_response(); - return self.handler.on_request(req); + if let Some(ref hosts) = self.allowed_hosts { + if !host_validation::is_valid(&req, hosts, self.endpoints.keys().cloned().collect()) { + self.handler = host_validation::host_invalid_response(); + return self.handler.on_request(req); + } } // Check authorization @@ -125,7 +127,7 @@ impl Router { endpoints: Arc, special: Arc>>, authorization: Arc, - bind_address: String, + allowed_hosts: Option>, ) -> Self { let handler = special.get(&SpecialEndpoint::Rpc).unwrap().to_handler(EndpointPath::default()); @@ -136,7 +138,7 @@ impl Router { fetch: app_fetcher, special: special, authorization: authorization, - bind_address: bind_address, + allowed_hosts: allowed_hosts, handler: handler, } } diff --git a/parity/cli.rs b/parity/cli.rs index 366c73a5b..8f33489dc 100644 --- a/parity/cli.rs +++ b/parity/cli.rs @@ -134,6 +134,11 @@ API and Console Options: --dapps-interface IP Specify the hostname portion of the Dapps server, IP should be an interface's IP address, or local [default: local]. + --dapps-hosts HOSTS List of allowed Host header values. This option will + validate the Host header sent by the browser, it + is additional security against some attack + vectors. Special options: "all", "none", + [default: none]. --dapps-user USERNAME Specify username for Dapps server. It will be used in HTTP Basic Authentication Scheme. If --dapps-pass is not specified you will be @@ -346,6 +351,7 @@ pub struct Args { pub flag_no_dapps: bool, pub flag_dapps_port: u16, pub flag_dapps_interface: String, + pub flag_dapps_hosts: String, pub flag_dapps_user: Option, pub flag_dapps_pass: Option, pub flag_dapps_path: String, diff --git a/parity/configuration.rs b/parity/configuration.rs index b1dbaa3fe..f2fd34853 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -356,6 +356,7 @@ impl Configuration { enabled: self.dapps_enabled(), interface: self.dapps_interface(), port: self.args.flag_dapps_port, + hosts: self.dapps_hosts(), user: self.args.flag_dapps_user.clone(), pass: self.args.flag_dapps_pass.clone(), dapps_path: self.directories().dapps, @@ -485,6 +486,16 @@ impl Configuration { Some(hosts) } + fn dapps_hosts(&self) -> Option> { + match self.args.flag_dapps_hosts.as_ref() { + "none" => return Some(Vec::new()), + "all" => return None, + _ => {} + } + let hosts = self.args.flag_dapps_hosts.split(',').map(|h| h.into()).collect(); + Some(hosts) + } + fn ipc_config(&self) -> Result { let conf = IpcConfiguration { enabled: !(self.args.flag_ipcdisable || self.args.flag_ipc_off || self.args.flag_no_ipc), @@ -860,6 +871,23 @@ mod tests { assert_eq!(conf3.rpc_hosts(), Some(vec!["ethcore.io".into(), "something.io".into()])); } + #[test] + fn should_parse_dapps_hosts() { + // given + + // when + let conf0 = parse(&["parity"]); + let conf1 = parse(&["parity", "--dapps-hosts", "none"]); + let conf2 = parse(&["parity", "--dapps-hosts", "all"]); + let conf3 = parse(&["parity", "--dapps-hosts", "ethcore.io,something.io"]); + + // then + assert_eq!(conf0.dapps_hosts(), Some(Vec::new())); + assert_eq!(conf1.dapps_hosts(), Some(Vec::new())); + assert_eq!(conf2.dapps_hosts(), None); + assert_eq!(conf3.dapps_hosts(), Some(vec!["ethcore.io".into(), "something.io".into()])); + } + #[test] fn should_disable_signer_in_geth_compat() { // given diff --git a/parity/dapps.rs b/parity/dapps.rs index 71c6d7f9d..7f759ed3c 100644 --- a/parity/dapps.rs +++ b/parity/dapps.rs @@ -25,6 +25,7 @@ pub struct Configuration { pub enabled: bool, pub interface: String, pub port: u16, + pub hosts: Option>, pub user: Option, pub pass: Option, pub dapps_path: String, @@ -36,6 +37,7 @@ impl Default for Configuration { enabled: true, interface: "127.0.0.1".into(), port: 8080, + hosts: Some(Vec::new()), user: None, pass: None, dapps_path: replace_home("$HOME/.parity/dapps"), @@ -68,7 +70,7 @@ pub fn new(configuration: Configuration, deps: Dependencies) -> Result>, _auth: Option<(String, String)>, ) -> Result { Err("Your Parity version has been compiled without WebApps support.".into()) @@ -109,6 +112,7 @@ mod server { deps: Dependencies, dapps_path: String, url: &SocketAddr, + allowed_hosts: Option>, auth: Option<(String, String)> ) -> Result { use ethcore_dapps as dapps; @@ -119,10 +123,10 @@ mod server { let server = rpc_apis::setup_rpc(server, deps.apis.clone(), rpc_apis::ApiSet::UnsafeContext); let start_result = match auth { None => { - server.start_unsecure_http(url) + server.start_unsecured_http(url, allowed_hosts) }, Some((username, password)) => { - server.start_basic_auth_http(url, &username, &password) + server.start_basic_auth_http(url, allowed_hosts, &username, &password) }, };