From 8f16515d824c039889a25be64d032e27e76c2669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Fri, 8 Apr 2016 15:25:20 +0200 Subject: [PATCH 1/3] HTTP Authorization support in router --- Cargo.lock | 6 +- webapp/Cargo.toml | 2 +- webapp/src/router/auth.rs | 120 ++++++++++++++++++++++++++++++++++++++ webapp/src/router/mod.rs | 56 ++++++++++-------- 4 files changed, 157 insertions(+), 27 deletions(-) create mode 100644 webapp/src/router/auth.rs diff --git a/Cargo.lock b/Cargo.lock index 210b23666..f0f64af94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,7 +321,7 @@ dependencies = [ "jsonrpc-core 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-http-server 4.0.0 (git+https://github.com/tomusdrw/jsonrpc-http-server.git?branch=old-hyper)", "log 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-status 0.1.4 (git+https://github.com/tomusdrw/parity-status.git)", + "parity-status 0.1.5 (git+https://github.com/tomusdrw/parity-status.git)", "parity-wallet 0.1.0 (git+https://github.com/tomusdrw/parity-wallet.git)", "parity-webapp 0.1.0 (git+https://github.com/tomusdrw/parity-webapp.git)", ] @@ -727,8 +727,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "parity-status" -version = "0.1.4" -source = "git+https://github.com/tomusdrw/parity-status.git#380d13c8aafc3847a731968a6532edb09c78f2cf" +version = "0.1.5" +source = "git+https://github.com/tomusdrw/parity-status.git#6a075228e9248055a37c55dec41461856f5a9f19" dependencies = [ "parity-webapp 0.1.0 (git+https://github.com/tomusdrw/parity-webapp.git)", ] diff --git a/webapp/Cargo.toml b/webapp/Cargo.toml index 59452f32d..126c5fbfb 100644 --- a/webapp/Cargo.toml +++ b/webapp/Cargo.toml @@ -17,7 +17,7 @@ ethcore-rpc = { path = "../rpc" } ethcore-util = { path = "../util" } parity-webapp = { git = "https://github.com/tomusdrw/parity-webapp.git" } # List of apps -parity-status = { git = "https://github.com/tomusdrw/parity-status.git", version = "0.1.4" } +parity-status = { git = "https://github.com/tomusdrw/parity-status.git", version = "0.1.5" } parity-wallet = { git = "https://github.com/tomusdrw/parity-wallet.git", optional = true } clippy = { version = "0.0.61", optional = true} diff --git a/webapp/src/router/auth.rs b/webapp/src/router/auth.rs new file mode 100644 index 000000000..96a27f189 --- /dev/null +++ b/webapp/src/router/auth.rs @@ -0,0 +1,120 @@ +// 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 . + +//! HTTP Authorization implementations + +use std::collections::HashMap; +use hyper::{header, server}; +use hyper::status::StatusCode; + +/// Authorization result +pub enum Authorized<'a, 'b> where 'b : 'a { + /// Authorization was successful. Request and Response are returned for further processing. + Yes(server::Request<'a, 'b>, server::Response<'a>), + /// Unsuccessful authorization. Request and Response has been consumed. + No, +} + +/// Authorization interface +pub trait Authorization { + /// Handle authorization process and return `Request` and `Response` when authorization is successful. + fn handle<'b, 'a>(&'a self, req: server::Request<'a, 'b>, res: server::Response<'a>)-> Authorized<'a, 'b>; +} + +/// HTTP Basic Authorization handler +pub struct HttpBasicAuth { + users: HashMap, +} + +/// No-authorization implementation (authorization disabled) +pub struct NoAuth; + +impl Authorization for NoAuth { + fn handle<'b, 'a>(&'a self, req: server::Request<'a, 'b>, res: server::Response<'a>)-> Authorized<'a, 'b> { + Authorized::Yes(req, res) + } +} + +impl Authorization for HttpBasicAuth { + + fn handle<'b, 'a>(&'a self, req: server::Request<'a, 'b>, res: server::Response<'a>)-> Authorized<'a, 'b> { + let auth = self.check_auth(&req); + + match auth { + Access::Denied => { + self.respond_with_unauthorized(res); + Authorized::No + }, + Access::AuthRequired => { + self.respond_with_auth_required(res); + Authorized::No + }, + Access::Granted => { + Authorized::Yes(req, res) + }, + } + } +} + +enum Access { + Granted, + Denied, + AuthRequired, +} + +impl HttpBasicAuth { + /// Creates `HttpBasicAuth` instance with only one user. + pub fn single_user(username: &str, password: &str) -> Self { + let mut users = HashMap::new(); + users.insert(username.to_owned(), password.to_owned()); + HttpBasicAuth { + users: users + } + } + + fn is_authorized(&self, username: &str, password: &str) -> bool { + self.users.get(&username.to_owned()).map_or(false, |pass| pass == password) + } + + fn check_auth(&self, req: &server::Request) -> Access { + match req.headers.get::>() { + Some(&header::Authorization(header::Basic { ref username, password: Some(ref password) })) => { + if self.is_authorized(username, password) { + Access::Granted + } else { + Access::Denied + } + }, + Some(&header::Authorization(header::Basic { username: _, password: None })) => { + Access::Denied + }, + None => { + Access::AuthRequired + }, + } + } + + fn respond_with_unauthorized(&self, mut res: server::Response) { + *res.status_mut() = StatusCode::Unauthorized; + let _ = res.send(b"Unauthorized"); + } + + fn respond_with_auth_required(&self, mut res: server::Response) { + *res.status_mut() = StatusCode::Unauthorized; + res.headers_mut().set_raw("WWW-Authenticate", vec![b"Basic realm=\"Parity\"".to_vec()]); + } +} + diff --git a/webapp/src/router/mod.rs b/webapp/src/router/mod.rs index bd0d2ff18..a545d81c0 100644 --- a/webapp/src/router/mod.rs +++ b/webapp/src/router/mod.rs @@ -15,37 +15,46 @@ // along with Parity. If not, see . //! Router implementation +//! Processes request handling authorization and dispatching it to proper application. + +mod api; +mod auth; use std::sync::Arc; use hyper; +use hyper::{server, uri, header}; use page::Page; use apps::Pages; use iron::request::Url; use jsonrpc_http_server::ServerHandler; - -mod api; +use self::auth::{Authorization, NoAuth, Authorized}; pub struct Router { + auth: NoAuth, rpc: ServerHandler, api: api::RestApi, main_page: Box, pages: Arc, } -impl hyper::server::Handler for Router { - fn handle<'b, 'a>(&'a self, req: hyper::server::Request<'a, 'b>, res: hyper::server::Response<'a>) { - let (path, req) = Router::extract_request_path(req); - match path { - Some(ref url) if self.pages.contains_key(url) => { - self.pages.get(url).unwrap().handle(req, res); - }, - Some(ref url) if url == "api" => { - self.api.handle(req, res); - }, - _ if req.method == hyper::method::Method::Post => { - self.rpc.handle(req, res) - }, - _ => self.main_page.handle(req, res), +impl server::Handler for Router { + fn handle<'b, 'a>(&'a self, req: server::Request<'a, 'b>, res: server::Response<'a>) { + let auth = self.auth.handle(req, res); + + if let Authorized::Yes(req, res) = auth { + let (path, req) = Router::extract_request_path(req); + match path { + Some(ref url) if self.pages.contains_key(url) => { + self.pages.get(url).unwrap().handle(req, res); + }, + Some(ref url) if url == "api" => { + self.api.handle(req, res); + }, + _ if req.method == hyper::method::Method::Post => { + self.rpc.handle(req, res) + }, + _ => self.main_page.handle(req, res), + } } } } @@ -54,6 +63,7 @@ impl Router { pub fn new(rpc: ServerHandler, main_page: Box, pages: Pages) -> Self { let pages = Arc::new(pages); Router { + auth: NoAuth, rpc: rpc, api: api::RestApi { pages: pages.clone() }, main_page: main_page, @@ -61,17 +71,17 @@ impl Router { } } - fn extract_url(req: &hyper::server::Request) -> Option { + fn extract_url(req: &server::Request) -> Option { match req.uri { - hyper::uri::RequestUri::AbsoluteUri(ref url) => { + uri::RequestUri::AbsoluteUri(ref url) => { match Url::from_generic_url(url.clone()) { Ok(url) => Some(url), _ => None, } }, - hyper::uri::RequestUri::AbsolutePath(ref path) => { + uri::RequestUri::AbsolutePath(ref path) => { // Attempt to prepend the Host header (mandatory in HTTP/1.1) - let url_string = match req.headers.get::() { + let url_string = match req.headers.get::() { Some(ref host) => { format!("http://{}:{}{}", host.hostname, host.port.unwrap_or(80), path) }, @@ -87,18 +97,18 @@ impl Router { } } - fn extract_request_path<'a, 'b>(mut req: hyper::server::Request<'a, 'b>) -> (Option, hyper::server::Request<'a, 'b>) { + fn extract_request_path<'a, 'b>(mut req: server::Request<'a, 'b>) -> (Option, server::Request<'a, 'b>) { let url = Router::extract_url(&req); match url { Some(ref url) if url.path.len() > 1 => { let part = url.path[0].clone(); let url = url.path[1..].join("/"); - req.uri = hyper::uri::RequestUri::AbsolutePath(url); + req.uri = uri::RequestUri::AbsolutePath(url); (Some(part), req) }, Some(url) => { let url = url.path.join("/"); - req.uri = hyper::uri::RequestUri::AbsolutePath(url); + req.uri = uri::RequestUri::AbsolutePath(url); (None, req) }, _ => { From dab54cf2a7c65eab060f529207d59b63bc4dfc45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Fri, 8 Apr 2016 16:11:58 +0200 Subject: [PATCH 2/3] HTTP Basic Authorization for WebApps server. --- parity/main.rs | 43 ++++++++++++++++++++++++++++++++------- webapp/src/lib.rs | 19 ++++++++++++++--- webapp/src/router/auth.rs | 2 +- webapp/src/router/mod.rs | 30 +++++++++++++++------------ 4 files changed, 70 insertions(+), 24 deletions(-) diff --git a/parity/main.rs b/parity/main.rs index b9f95eee3..d9382e645 100644 --- a/parity/main.rs +++ b/parity/main.rs @@ -136,13 +136,19 @@ API and Console Options: interface. APIS is a comma-delimited list of API name. Possible name are web3, eth and net. [default: web3,eth,net,personal]. - -w --webapp Enable the web applications server (e.g. status page). + -w --webapp Enable the web applications server (e.g. + status page). --webapp-port PORT Specify the port portion of the WebApps server [default: 8080]. --webapp-interface IP Specify the hostname portion of the WebApps server, IP should be an interface's IP address, or all (all interfaces) or local [default: local]. - + --webapp-user USERNAME Specify username for WebApps server. It will be + used in HTTP Basic Authentication Scheme. + If --webapp-pass is not specified you will be + asked for password on startup. + --webapp-pass PASSWORD Specify password for WebApps server. Use only in + conjunction with --webapp-user. Sealing/Mining Options: --usd-per-tx USD Amount of USD to be paid for a basic transaction @@ -230,6 +236,8 @@ struct Args { flag_webapp: bool, flag_webapp_port: u16, flag_webapp_interface: String, + flag_webapp_user: Option, + flag_webapp_pass: Option, flag_author: String, flag_usd_per_tx: String, flag_usd_per_eth: String, @@ -288,7 +296,7 @@ fn setup_rpc_server( miner: Arc, url: &SocketAddr, cors_domain: &str, - apis: Vec<&str> + apis: Vec<&str>, ) -> RpcServer { use rpc::v1::*; @@ -321,7 +329,8 @@ fn setup_webapp_server( sync: Arc, secret_store: Arc, miner: Arc, - url: &str + url: &str, + auth: Option<(String, String)>, ) -> WebappServer { use rpc::v1::*; @@ -331,7 +340,14 @@ fn setup_webapp_server( server.add_delegate(EthClient::new(&client, &sync, &secret_store, &miner).to_delegate()); server.add_delegate(EthFilterClient::new(&client, &miner).to_delegate()); server.add_delegate(PersonalClient::new(&secret_store).to_delegate()); - let start_result = server.start_http(url, ::num_cpus::get()); + let start_result = match auth { + None => { + server.start_unsecure_http(url, ::num_cpus::get()) + }, + Some((username, password)) => { + server.start_basic_auth_http(url, ::num_cpus::get(), &username, &password) + }, + }; match start_result { Err(webapp::WebappServerError::IoError(err)) => die_with_io_error(err), Err(e) => die!("{:?}", e), @@ -351,7 +367,7 @@ fn setup_rpc_server( _miner: Arc, _url: &str, _cors_domain: &str, - _apis: Vec<&str> + _apis: Vec<&str>, ) -> ! { die!("Your Parity version has been compiled without JSON-RPC support.") } @@ -365,7 +381,8 @@ fn setup_webapp_server( _sync: Arc, _secret_store: Arc, _miner: Arc, - _url: &str + _url: &str, + _auth: Option<(String, String)>, ) -> ! { die!("Your Parity version has been compiled without WebApps support.") } @@ -683,12 +700,24 @@ impl Configuration { }, self.args.flag_webapp_port ); + let auth = self.args.flag_webapp_user.as_ref().map(|username| { + let password = self.args.flag_webapp_pass.as_ref().map_or_else(|| { + use rpassword::read_password; + println!("Type password for WebApps server (user: {}): ", username); + let pass = read_password().unwrap(); + println!("OK, got it. Starting server..."); + pass + }, |pass| pass.to_owned()); + (username.to_owned(), password) + }); + Some(setup_webapp_server( service.client(), sync.clone(), account_service.clone(), miner.clone(), &url, + auth, )) } else { None diff --git a/webapp/src/lib.rs b/webapp/src/lib.rs index 35ebb4a44..ed9a13967 100644 --- a/webapp/src/lib.rs +++ b/webapp/src/lib.rs @@ -35,6 +35,8 @@ mod apps; mod page; mod router; +use router::auth::{Authorization, NoAuth, HttpBasicAuth}; + /// Http server. pub struct WebappServer { handler: Arc, @@ -53,14 +55,25 @@ impl WebappServer { self.handler.add_delegate(delegate); } - /// Start server asynchronously and returns result with `Listening` handle on success or an error. - pub fn start_http(&self, addr: &str, threads: usize) -> Result { + /// Asynchronously start server with no authentication, + /// return result with `Listening` handle on success or an error. + pub fn start_unsecure_http(&self, addr: &str, threads: usize) -> Result { + self.start_http(addr, threads, NoAuth) + } + + /// Asynchronously start server with `HTTP Basic Authentication`, + /// return result with `Listening` handle on success or an error. + pub fn start_basic_auth_http(&self, addr: &str, threads: usize, username: &str, password: &str) -> Result { + self.start_http(addr, threads, HttpBasicAuth::single_user(username, password)) + } + + fn start_http(&self, addr: &str, threads: usize, authorization: A) -> Result { let addr = addr.to_owned(); let handler = self.handler.clone(); let cors_domain = jsonrpc_http_server::AccessControlAllowOrigin::Null; let rpc = ServerHandler::new(handler, cors_domain); - let router = router::Router::new(rpc, apps::main_page(), apps::all_pages()); + let router = router::Router::new(rpc, apps::main_page(), apps::all_pages(), authorization); try!(hyper::Server::http(addr.as_ref() as &str)) .handle_threads(router, threads) diff --git a/webapp/src/router/auth.rs b/webapp/src/router/auth.rs index 96a27f189..6122b9309 100644 --- a/webapp/src/router/auth.rs +++ b/webapp/src/router/auth.rs @@ -29,7 +29,7 @@ pub enum Authorized<'a, 'b> where 'b : 'a { } /// Authorization interface -pub trait Authorization { +pub trait Authorization : Send + Sync { /// Handle authorization process and return `Request` and `Response` when authorization is successful. fn handle<'b, 'a>(&'a self, req: server::Request<'a, 'b>, res: server::Response<'a>)-> Authorized<'a, 'b>; } diff --git a/webapp/src/router/mod.rs b/webapp/src/router/mod.rs index a545d81c0..070f94a34 100644 --- a/webapp/src/router/mod.rs +++ b/webapp/src/router/mod.rs @@ -18,7 +18,7 @@ //! Processes request handling authorization and dispatching it to proper application. mod api; -mod auth; +pub mod auth; use std::sync::Arc; use hyper; @@ -27,22 +27,22 @@ use page::Page; use apps::Pages; use iron::request::Url; use jsonrpc_http_server::ServerHandler; -use self::auth::{Authorization, NoAuth, Authorized}; +use self::auth::{Authorization, Authorized}; -pub struct Router { - auth: NoAuth, +pub struct Router { + authorization: A, rpc: ServerHandler, api: api::RestApi, main_page: Box, pages: Arc, } -impl server::Handler for Router { +impl server::Handler for Router { fn handle<'b, 'a>(&'a self, req: server::Request<'a, 'b>, res: server::Response<'a>) { - let auth = self.auth.handle(req, res); + let auth = self.authorization.handle(req, res); if let Authorized::Yes(req, res) = auth { - let (path, req) = Router::extract_request_path(req); + let (path, req) = self.extract_request_path(req); match path { Some(ref url) if self.pages.contains_key(url) => { self.pages.get(url).unwrap().handle(req, res); @@ -59,11 +59,15 @@ impl server::Handler for Router { } } -impl Router { - pub fn new(rpc: ServerHandler, main_page: Box, pages: Pages) -> Self { +impl Router { + pub fn new( + rpc: ServerHandler, + main_page: Box, + pages: Pages, + authorization: A) -> Self { let pages = Arc::new(pages); Router { - auth: NoAuth, + authorization: authorization, rpc: rpc, api: api::RestApi { pages: pages.clone() }, main_page: main_page, @@ -71,7 +75,7 @@ impl Router { } } - fn extract_url(req: &server::Request) -> Option { + fn extract_url(&self, req: &server::Request) -> Option { match req.uri { uri::RequestUri::AbsoluteUri(ref url) => { match Url::from_generic_url(url.clone()) { @@ -97,8 +101,8 @@ impl Router { } } - fn extract_request_path<'a, 'b>(mut req: server::Request<'a, 'b>) -> (Option, server::Request<'a, 'b>) { - let url = Router::extract_url(&req); + fn extract_request_path<'a, 'b>(&self, mut req: server::Request<'a, 'b>) -> (Option, server::Request<'a, 'b>) { + let url = self.extract_url(&req); match url { Some(ref url) if url.path.len() > 1 => { let part = url.path[0].clone(); From 1d5b29fb48aa6e42d6fe82ae74ff65955bcc2a76 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Fri, 8 Apr 2016 17:51:20 -0700 Subject: [PATCH 3/3] Update auth.rs --- webapp/src/router/auth.rs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/webapp/src/router/auth.rs b/webapp/src/router/auth.rs index 6122b9309..95c558bef 100644 --- a/webapp/src/router/auth.rs +++ b/webapp/src/router/auth.rs @@ -91,19 +91,11 @@ impl HttpBasicAuth { fn check_auth(&self, req: &server::Request) -> Access { match req.headers.get::>() { - Some(&header::Authorization(header::Basic { ref username, password: Some(ref password) })) => { - if self.is_authorized(username, password) { - Access::Granted - } else { - Access::Denied - } - }, - Some(&header::Authorization(header::Basic { username: _, password: None })) => { - Access::Denied - }, - None => { - Access::AuthRequired - }, + Some(&header::Authorization( + header::Basic { ref username, password: Some(ref password) } + )) if self.is_authorized(username, password) => Access::Granted, + Some(_) => Access::Denied, + None => Access::AuthRequired, } }