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] 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) }, _ => {