diff --git a/Cargo.lock b/Cargo.lock index c9b869226..9b793c219 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -276,7 +276,7 @@ dependencies = [ "ethcore-util 1.3.0", "hyper 0.9.4 (git+https://github.com/ethcore/hyper)", "jsonrpc-core 2.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-http-server 5.1.0 (git+https://github.com/ethcore/jsonrpc-http-server.git)", + "jsonrpc-http-server 6.1.0 (git+https://github.com/ethcore/jsonrpc-http-server.git)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "parity-dapps 0.6.0 (git+https://github.com/ethcore/parity-ui.git)", @@ -357,7 +357,7 @@ dependencies = [ "ethsync 1.3.0", "json-ipc-server 0.2.4 (git+https://github.com/ethcore/json-ipc-server.git)", "jsonrpc-core 2.0.7 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-http-server 5.1.0 (git+https://github.com/ethcore/jsonrpc-http-server.git)", + "jsonrpc-http-server 6.1.0 (git+https://github.com/ethcore/jsonrpc-http-server.git)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)", @@ -629,8 +629,8 @@ dependencies = [ [[package]] name = "jsonrpc-http-server" -version = "5.1.0" -source = "git+https://github.com/ethcore/jsonrpc-http-server.git#f16b956c61e60b3a530ad4bac82112a8974cf505" +version = "6.1.0" +source = "git+https://github.com/ethcore/jsonrpc-http-server.git#517a0d7b8c7fd099995ce4cc93f52789e83f2cdc" dependencies = [ "hyper 0.9.4 (git+https://github.com/ethcore/hyper)", "jsonrpc-core 2.0.7 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/dapps/src/handlers/content.rs b/dapps/src/handlers/content.rs index a589e5492..b9d8d55d6 100644 --- a/dapps/src/handlers/content.rs +++ b/dapps/src/handlers/content.rs @@ -38,6 +38,15 @@ impl ContentHandler { } } + pub fn forbidden(content: String, mimetype: String) -> Self { + ContentHandler { + code: StatusCode::Forbidden, + content: content, + mimetype: mimetype, + write_pos: 0 + } + } + pub fn not_found(content: String, mimetype: String) -> Self { ContentHandler { code: StatusCode::NotFound, diff --git a/dapps/src/lib.rs b/dapps/src/lib.rs index 133b4ebaa..d9fa06591 100644 --- a/dapps/src/lib.rs +++ b/dapps/src/lib.rs @@ -129,6 +129,7 @@ impl Server { special.insert(router::SpecialEndpoint::Utils, apps::utils()); special }); + let bind_address = format!("{}", addr); try!(hyper::Server::http(addr)) .handle(move |_| router::Router::new( @@ -136,6 +137,7 @@ impl Server { endpoints.clone(), special.clone(), authorization.clone(), + bind_address.clone(), )) .map(|(l, srv)| { diff --git a/dapps/src/router/host_validation.rs b/dapps/src/router/host_validation.rs new file mode 100644 index 000000000..3b065cd0c --- /dev/null +++ b/dapps/src/router/host_validation.rs @@ -0,0 +1,45 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + + +use DAPPS_DOMAIN; +use hyper::server; +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() + .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()); + + is_host_header_valid(request, &endpoints) +} + +pub fn host_invalid_response() -> Box + Send> { + Box::new(ContentHandler::forbidden( + r#" +

Request with disallowed Host header has been blocked.

+

Check the URL in your browser address bar.

+ "#.into(), + "text/html".into() + )) +} diff --git a/dapps/src/router/mod.rs b/dapps/src/router/mod.rs index 8fbb37cf3..bdd5fd291 100644 --- a/dapps/src/router/mod.rs +++ b/dapps/src/router/mod.rs @@ -18,6 +18,7 @@ //! Processes request handling authorization and dispatching it to proper application. pub mod auth; +mod host_validation; use DAPPS_DOMAIN; use std::sync::Arc; @@ -44,40 +45,46 @@ pub struct Router { endpoints: Arc, special: Arc>>, authorization: Arc, + bind_address: String, handler: Box + Send>, } 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); + } + // Check authorization let auth = self.authorization.is_authorized(&req); + if let Authorized::No(handler) = auth { + self.handler = handler; + return self.handler.on_request(req); + } // Choose proper handler depending on path / domain - self.handler = match auth { - Authorized::No(handler) => handler, - Authorized::Yes => { - let url = extract_url(&req); - let endpoint = extract_endpoint(&url); + let url = extract_url(&req); + let endpoint = extract_endpoint(&url); - match endpoint { - // First check special endpoints - (ref path, ref endpoint) if self.special.contains_key(endpoint) => { - self.special.get(endpoint).unwrap().to_handler(path.clone().unwrap_or_default()) - }, - // Then delegate to dapp - (Some(ref path), _) if self.endpoints.contains_key(&path.app_id) => { - self.endpoints.get(&path.app_id).unwrap().to_handler(path.clone()) - }, - // Redirection to main page - _ if *req.method() == hyper::method::Method::Get => { - Redirection::new(self.main_page) - }, - // RPC by default - _ => { - self.special.get(&SpecialEndpoint::Rpc).unwrap().to_handler(EndpointPath::default()) - } - } + self.handler = match endpoint { + // First check special endpoints + (ref path, ref endpoint) if self.special.contains_key(endpoint) => { + self.special.get(endpoint).unwrap().to_handler(path.clone().unwrap_or_default()) + }, + // Then delegate to dapp + (Some(ref path), _) if self.endpoints.contains_key(&path.app_id) => { + self.endpoints.get(&path.app_id).unwrap().to_handler(path.clone()) + }, + // Redirection to main page + _ if *req.method() == hyper::method::Method::Get => { + Redirection::new(self.main_page) + }, + // RPC by default + _ => { + self.special.get(&SpecialEndpoint::Rpc).unwrap().to_handler(EndpointPath::default()) } }; @@ -106,7 +113,9 @@ impl Router { main_page: &'static str, endpoints: Arc, special: Arc>>, - authorization: Arc) -> Self { + authorization: Arc, + bind_address: String, + ) -> Self { let handler = special.get(&SpecialEndpoint::Rpc).unwrap().to_handler(EndpointPath::default()); Router { @@ -114,6 +123,7 @@ impl Router { endpoints: endpoints, special: special, authorization: authorization, + bind_address: bind_address, handler: handler, } } diff --git a/dapps/src/rpc.rs b/dapps/src/rpc.rs index e282c0440..04470bcc1 100644 --- a/dapps/src/rpc.rs +++ b/dapps/src/rpc.rs @@ -23,19 +23,22 @@ pub fn rpc(handler: Arc, panic_handler: Arc Box::new(RpcEndpoint { handler: handler, panic_handler: panic_handler, - cors_domain: vec![AccessControlAllowOrigin::Null], + cors_domain: Some(vec![AccessControlAllowOrigin::Null]), + // NOTE [ToDr] We don't need to do any hosts validation here. It's already done in router. + allowed_hosts: None, }) } struct RpcEndpoint { handler: Arc, panic_handler: Arc () + Send>>>>, - cors_domain: Vec, + cors_domain: Option>, + allowed_hosts: Option>, } impl Endpoint for RpcEndpoint { fn to_handler(&self, _path: EndpointPath) -> Box { let panic_handler = PanicHandler { handler: self.panic_handler.clone() }; - Box::new(ServerHandler::new(self.handler.clone(), self.cors_domain.clone(), panic_handler)) + Box::new(ServerHandler::new(self.handler.clone(), self.cors_domain.clone(), self.allowed_hosts.clone(), panic_handler)) } } diff --git a/parity/cli.rs b/parity/cli.rs index 928237d72..60aca8310 100644 --- a/parity/cli.rs +++ b/parity/cli.rs @@ -107,6 +107,11 @@ API and Console Options: name. Possible name are web3, eth, net, personal, ethcore, ethcore_set, traces. [default: web3,eth,net,ethcore,personal,traces]. + --jsonrpc-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]. --no-ipc Disable JSON-RPC over IPC service. --ipc-path PATH Specify custom path for JSON-RPC over IPC service @@ -118,8 +123,8 @@ API and Console Options: --dapps-port PORT Specify the port portion of the Dapps server [default: 8080]. --dapps-interface IP Specify the hostname portion of the Dapps - server, IP should be an interface's IP address, or - all (all interfaces) or local [default: local]. + server, IP should be an interface's IP address, + or local [default: local]. --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 @@ -311,6 +316,7 @@ pub struct Args { pub flag_jsonrpc_interface: String, pub flag_jsonrpc_port: u16, pub flag_jsonrpc_cors: Option, + pub flag_jsonrpc_hosts: String, pub flag_jsonrpc_apis: String, pub flag_no_ipc: bool, pub flag_ipc_path: String, diff --git a/parity/configuration.rs b/parity/configuration.rs index 266f400a7..6094ba01b 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -424,9 +424,19 @@ impl Configuration { self.args.flag_rpcapi.clone().unwrap_or(self.args.flag_jsonrpc_apis.clone()) } - pub fn rpc_cors(&self) -> Vec { + pub fn rpc_cors(&self) -> Option> { let cors = self.args.flag_jsonrpc_cors.clone().or(self.args.flag_rpccorsdomain.clone()); - cors.map_or_else(Vec::new, |c| c.split(',').map(|s| s.to_owned()).collect()) + cors.map(|c| c.split(',').map(|s| s.to_owned()).collect()) + } + + pub fn rpc_hosts(&self) -> Option> { + match self.args.flag_jsonrpc_hosts.as_ref() { + "none" => return Some(Vec::new()), + "all" => return None, + _ => {} + } + let hosts = self.args.flag_jsonrpc_hosts.split(',').map(|h| h.into()).collect(); + Some(hosts) } fn geth_ipc_path(&self) -> String { @@ -541,7 +551,6 @@ impl Configuration { pub fn dapps_interface(&self) -> String { match self.args.flag_dapps_interface.as_str() { - "all" => "0.0.0.0", "local" => "127.0.0.1", x => x, }.into() @@ -597,7 +606,7 @@ mod tests { assert_eq!(net.rpc_enabled, true); assert_eq!(net.rpc_interface, "all".to_owned()); assert_eq!(net.rpc_port, 8000); - assert_eq!(conf.rpc_cors(), vec!["*".to_owned()]); + assert_eq!(conf.rpc_cors(), Some(vec!["*".to_owned()])); assert_eq!(conf.rpc_apis(), "web3,eth".to_owned()); } @@ -619,5 +628,22 @@ mod tests { assert(conf1); assert(conf2); } + + #[test] + fn should_parse_rpc_hosts() { + // given + + // when + let conf0 = parse(&["parity"]); + let conf1 = parse(&["parity", "--jsonrpc-hosts", "none"]); + let conf2 = parse(&["parity", "--jsonrpc-hosts", "all"]); + let conf3 = parse(&["parity", "--jsonrpc-hosts", "ethcore.io,something.io"]); + + // then + assert_eq!(conf0.rpc_hosts(), Some(Vec::new())); + assert_eq!(conf1.rpc_hosts(), Some(Vec::new())); + assert_eq!(conf2.rpc_hosts(), None); + assert_eq!(conf3.rpc_hosts(), Some(vec!["ethcore.io".into(), "something.io".into()])); + } } diff --git a/parity/main.rs b/parity/main.rs index a8fd63d2a..fe5107d66 100644 --- a/parity/main.rs +++ b/parity/main.rs @@ -280,6 +280,7 @@ fn execute_client(conf: Configuration, spec: Spec, client_config: ClientConfig, port: network_settings.rpc_port, apis: conf.rpc_apis(), cors: conf.rpc_cors(), + hosts: conf.rpc_hosts(), }, &dependencies); // setup ipc rpc diff --git a/parity/rpc.rs b/parity/rpc.rs index 7317aa2e6..2b0599962 100644 --- a/parity/rpc.rs +++ b/parity/rpc.rs @@ -32,7 +32,8 @@ pub struct HttpConfiguration { pub interface: String, pub port: u16, pub apis: String, - pub cors: Vec, + pub cors: Option>, + pub hosts: Option>, } pub struct IpcConfiguration { @@ -66,7 +67,7 @@ pub fn new_http(conf: HttpConfiguration, deps: &Dependencies) -> Option, deps: &Dependencies) -> Server { @@ -78,21 +79,17 @@ fn setup_rpc_server(apis: Vec<&str>, deps: &Dependencies) -> Server { pub fn setup_http_rpc_server( dependencies: &Dependencies, url: &SocketAddr, - cors_domains: Vec, + cors_domains: Option>, + allowed_hosts: Option>, apis: Vec<&str>, ) -> RpcServer { let server = setup_rpc_server(apis, dependencies); - let start_result = server.start_http(url, cors_domains); let ph = dependencies.panic_handler.clone(); + let start_result = server.start_http(url, cors_domains, allowed_hosts, ph); match start_result { Err(RpcServerError::IoError(err)) => die_with_io_error("RPC", err), Err(e) => die!("RPC: {:?}", e), - Ok(server) => { - server.set_panic_handler(move || { - ph.notify_all("Panic in RPC thread.".to_owned()); - }); - server - }, + Ok(server) => server, } } diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index 73a769b13..5899d0027 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -41,9 +41,10 @@ extern crate ethcore_devtools as devtools; use std::sync::Arc; use std::net::SocketAddr; +use util::panics::PanicHandler; use self::jsonrpc_core::{IoHandler, IoDelegate}; -pub use jsonrpc_http_server::{Server, RpcServerError}; +pub use jsonrpc_http_server::{ServerBuilder, Server, RpcServerError}; pub mod v1; pub use v1::{SigningQueue, ConfirmationsQueue}; @@ -74,15 +75,31 @@ impl RpcServer { } /// Start http server asynchronously and returns result with `Server` handle on success or an error. - pub fn start_http(&self, addr: &SocketAddr, cors_domains: Vec) -> Result { - let cors_domains = cors_domains.into_iter() - .map(|v| match v.as_str() { - "*" => jsonrpc_http_server::AccessControlAllowOrigin::Any, - "null" => jsonrpc_http_server::AccessControlAllowOrigin::Null, - v => jsonrpc_http_server::AccessControlAllowOrigin::Value(v.into()), + pub fn start_http( + &self, + addr: &SocketAddr, + cors_domains: Option>, + allowed_hosts: Option>, + panic_handler: Arc, + ) -> Result { + + let cors_domains = cors_domains.map(|domains| { + domains.into_iter() + .map(|v| match v.as_str() { + "*" => jsonrpc_http_server::AccessControlAllowOrigin::Any, + "null" => jsonrpc_http_server::AccessControlAllowOrigin::Null, + v => jsonrpc_http_server::AccessControlAllowOrigin::Value(v.into()), + }) + .collect() + }); + + ServerBuilder::new(self.handler.clone()) + .cors(cors_domains.into()) + .allowed_hosts(allowed_hosts.into()) + .panic_handler(move || { + panic_handler.notify_all("Panic in RPC thread.".to_owned()); }) - .collect(); - Server::start(addr, self.handler.clone(), cors_domains) + .start_http(addr) } /// Start ipc server asynchronously and returns result with `Server` handle on success or an error.