Merge pull request #906 from ethcore/webapps-auth

WebApps HTTP Basic Auth Support
This commit is contained in:
Gav Wood 2016-04-09 10:19:59 -07:00
commit d823fd7685
6 changed files with 209 additions and 41 deletions

6
Cargo.lock generated
View File

@ -321,7 +321,7 @@ dependencies = [
"jsonrpc-core 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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-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)", "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]] [[package]]
name = "parity-status" name = "parity-status"
version = "0.1.4" version = "0.1.5"
source = "git+https://github.com/tomusdrw/parity-status.git#380d13c8aafc3847a731968a6532edb09c78f2cf" source = "git+https://github.com/tomusdrw/parity-status.git#6a075228e9248055a37c55dec41461856f5a9f19"
dependencies = [ dependencies = [
"parity-webapp 0.1.0 (git+https://github.com/tomusdrw/parity-webapp.git)", "parity-webapp 0.1.0 (git+https://github.com/tomusdrw/parity-webapp.git)",
] ]

View File

@ -136,13 +136,19 @@ API and Console Options:
interface. APIS is a comma-delimited list of API interface. APIS is a comma-delimited list of API
name. Possible name are web3, eth and net. name. Possible name are web3, eth and net.
[default: web3,eth,net,personal]. [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 --webapp-port PORT Specify the port portion of the WebApps server
[default: 8080]. [default: 8080].
--webapp-interface IP Specify the hostname portion of the WebApps --webapp-interface IP Specify the hostname portion of the WebApps
server, IP should be an interface's IP address, or server, IP should be an interface's IP address, or
all (all interfaces) or local [default: local]. 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: Sealing/Mining Options:
--usd-per-tx USD Amount of USD to be paid for a basic transaction --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: bool,
flag_webapp_port: u16, flag_webapp_port: u16,
flag_webapp_interface: String, flag_webapp_interface: String,
flag_webapp_user: Option<String>,
flag_webapp_pass: Option<String>,
flag_author: String, flag_author: String,
flag_usd_per_tx: String, flag_usd_per_tx: String,
flag_usd_per_eth: String, flag_usd_per_eth: String,
@ -288,7 +296,7 @@ fn setup_rpc_server(
miner: Arc<Miner>, miner: Arc<Miner>,
url: &SocketAddr, url: &SocketAddr,
cors_domain: &str, cors_domain: &str,
apis: Vec<&str> apis: Vec<&str>,
) -> RpcServer { ) -> RpcServer {
use rpc::v1::*; use rpc::v1::*;
@ -321,7 +329,8 @@ fn setup_webapp_server(
sync: Arc<EthSync>, sync: Arc<EthSync>,
secret_store: Arc<AccountService>, secret_store: Arc<AccountService>,
miner: Arc<Miner>, miner: Arc<Miner>,
url: &str url: &str,
auth: Option<(String, String)>,
) -> WebappServer { ) -> WebappServer {
use rpc::v1::*; 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(EthClient::new(&client, &sync, &secret_store, &miner).to_delegate());
server.add_delegate(EthFilterClient::new(&client, &miner).to_delegate()); server.add_delegate(EthFilterClient::new(&client, &miner).to_delegate());
server.add_delegate(PersonalClient::new(&secret_store).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 { match start_result {
Err(webapp::WebappServerError::IoError(err)) => die_with_io_error(err), Err(webapp::WebappServerError::IoError(err)) => die_with_io_error(err),
Err(e) => die!("{:?}", e), Err(e) => die!("{:?}", e),
@ -351,7 +367,7 @@ fn setup_rpc_server(
_miner: Arc<Miner>, _miner: Arc<Miner>,
_url: &str, _url: &str,
_cors_domain: &str, _cors_domain: &str,
_apis: Vec<&str> _apis: Vec<&str>,
) -> ! { ) -> ! {
die!("Your Parity version has been compiled without JSON-RPC support.") die!("Your Parity version has been compiled without JSON-RPC support.")
} }
@ -365,7 +381,8 @@ fn setup_webapp_server(
_sync: Arc<EthSync>, _sync: Arc<EthSync>,
_secret_store: Arc<AccountService>, _secret_store: Arc<AccountService>,
_miner: Arc<Miner>, _miner: Arc<Miner>,
_url: &str _url: &str,
_auth: Option<(String, String)>,
) -> ! { ) -> ! {
die!("Your Parity version has been compiled without WebApps support.") die!("Your Parity version has been compiled without WebApps support.")
} }
@ -683,12 +700,24 @@ impl Configuration {
}, },
self.args.flag_webapp_port 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( Some(setup_webapp_server(
service.client(), service.client(),
sync.clone(), sync.clone(),
account_service.clone(), account_service.clone(),
miner.clone(), miner.clone(),
&url, &url,
auth,
)) ))
} else { } else {
None None

View File

@ -17,7 +17,7 @@ ethcore-rpc = { path = "../rpc" }
ethcore-util = { path = "../util" } ethcore-util = { path = "../util" }
parity-webapp = { git = "https://github.com/tomusdrw/parity-webapp.git" } parity-webapp = { git = "https://github.com/tomusdrw/parity-webapp.git" }
# List of apps # 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 } parity-wallet = { git = "https://github.com/tomusdrw/parity-wallet.git", optional = true }
clippy = { version = "0.0.61", optional = true} clippy = { version = "0.0.61", optional = true}

View File

@ -35,6 +35,8 @@ mod apps;
mod page; mod page;
mod router; mod router;
use router::auth::{Authorization, NoAuth, HttpBasicAuth};
/// Http server. /// Http server.
pub struct WebappServer { pub struct WebappServer {
handler: Arc<IoHandler>, handler: Arc<IoHandler>,
@ -53,14 +55,25 @@ impl WebappServer {
self.handler.add_delegate(delegate); self.handler.add_delegate(delegate);
} }
/// Start server asynchronously and returns result with `Listening` handle on success or an error. /// Asynchronously start server with no authentication,
pub fn start_http(&self, addr: &str, threads: usize) -> Result<Listening, WebappServerError> { /// return result with `Listening` handle on success or an error.
pub fn start_unsecure_http(&self, addr: &str, threads: usize) -> Result<Listening, WebappServerError> {
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<Listening, WebappServerError> {
self.start_http(addr, threads, HttpBasicAuth::single_user(username, password))
}
fn start_http<A: Authorization + 'static>(&self, addr: &str, threads: usize, authorization: A) -> Result<Listening, WebappServerError> {
let addr = addr.to_owned(); let addr = addr.to_owned();
let handler = self.handler.clone(); let handler = self.handler.clone();
let cors_domain = jsonrpc_http_server::AccessControlAllowOrigin::Null; let cors_domain = jsonrpc_http_server::AccessControlAllowOrigin::Null;
let rpc = ServerHandler::new(handler, cors_domain); 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)) try!(hyper::Server::http(addr.as_ref() as &str))
.handle_threads(router, threads) .handle_threads(router, threads)

112
webapp/src/router/auth.rs Normal file
View File

@ -0,0 +1,112 @@
// 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://www.gnu.org/licenses/>.
//! 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 : 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>;
}
/// HTTP Basic Authorization handler
pub struct HttpBasicAuth {
users: HashMap<String, String>,
}
/// 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::<header::Authorization<header::Basic>>() {
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,
}
}
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()]);
}
}

View File

@ -15,45 +15,59 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Router implementation //! Router implementation
//! Processes request handling authorization and dispatching it to proper application.
mod api;
pub mod auth;
use std::sync::Arc; use std::sync::Arc;
use hyper; use hyper;
use hyper::{server, uri, header};
use page::Page; use page::Page;
use apps::Pages; use apps::Pages;
use iron::request::Url; use iron::request::Url;
use jsonrpc_http_server::ServerHandler; use jsonrpc_http_server::ServerHandler;
use self::auth::{Authorization, Authorized};
mod api; pub struct Router<A: Authorization> {
authorization: A,
pub struct Router {
rpc: ServerHandler, rpc: ServerHandler,
api: api::RestApi, api: api::RestApi,
main_page: Box<Page>, main_page: Box<Page>,
pages: Arc<Pages>, pages: Arc<Pages>,
} }
impl hyper::server::Handler for Router { impl<A: Authorization> server::Handler for Router<A> {
fn handle<'b, 'a>(&'a self, req: hyper::server::Request<'a, 'b>, res: hyper::server::Response<'a>) { fn handle<'b, 'a>(&'a self, req: server::Request<'a, 'b>, res: server::Response<'a>) {
let (path, req) = Router::extract_request_path(req); let auth = self.authorization.handle(req, res);
match path {
Some(ref url) if self.pages.contains_key(url) => { if let Authorized::Yes(req, res) = auth {
self.pages.get(url).unwrap().handle(req, res); let (path, req) = self.extract_request_path(req);
}, match path {
Some(ref url) if url == "api" => { Some(ref url) if self.pages.contains_key(url) => {
self.api.handle(req, res); self.pages.get(url).unwrap().handle(req, res);
}, },
_ if req.method == hyper::method::Method::Post => { Some(ref url) if url == "api" => {
self.rpc.handle(req, res) self.api.handle(req, res);
}, },
_ => self.main_page.handle(req, res), _ if req.method == hyper::method::Method::Post => {
self.rpc.handle(req, res)
},
_ => self.main_page.handle(req, res),
}
} }
} }
} }
impl Router { impl<A: Authorization> Router<A> {
pub fn new(rpc: ServerHandler, main_page: Box<Page>, pages: Pages) -> Self { pub fn new(
rpc: ServerHandler,
main_page: Box<Page>,
pages: Pages,
authorization: A) -> Self {
let pages = Arc::new(pages); let pages = Arc::new(pages);
Router { Router {
authorization: authorization,
rpc: rpc, rpc: rpc,
api: api::RestApi { pages: pages.clone() }, api: api::RestApi { pages: pages.clone() },
main_page: main_page, main_page: main_page,
@ -61,17 +75,17 @@ impl Router {
} }
} }
fn extract_url(req: &hyper::server::Request) -> Option<Url> { fn extract_url(&self, req: &server::Request) -> Option<Url> {
match req.uri { match req.uri {
hyper::uri::RequestUri::AbsoluteUri(ref url) => { uri::RequestUri::AbsoluteUri(ref url) => {
match Url::from_generic_url(url.clone()) { match Url::from_generic_url(url.clone()) {
Ok(url) => Some(url), Ok(url) => Some(url),
_ => None, _ => None,
} }
}, },
hyper::uri::RequestUri::AbsolutePath(ref path) => { uri::RequestUri::AbsolutePath(ref path) => {
// Attempt to prepend the Host header (mandatory in HTTP/1.1) // Attempt to prepend the Host header (mandatory in HTTP/1.1)
let url_string = match req.headers.get::<hyper::header::Host>() { let url_string = match req.headers.get::<header::Host>() {
Some(ref host) => { Some(ref host) => {
format!("http://{}:{}{}", host.hostname, host.port.unwrap_or(80), path) format!("http://{}:{}{}", host.hostname, host.port.unwrap_or(80), path)
}, },
@ -87,18 +101,18 @@ impl Router {
} }
} }
fn extract_request_path<'a, 'b>(mut req: hyper::server::Request<'a, 'b>) -> (Option<String>, hyper::server::Request<'a, 'b>) { fn extract_request_path<'a, 'b>(&self, mut req: server::Request<'a, 'b>) -> (Option<String>, server::Request<'a, 'b>) {
let url = Router::extract_url(&req); let url = self.extract_url(&req);
match url { match url {
Some(ref url) if url.path.len() > 1 => { Some(ref url) if url.path.len() > 1 => {
let part = url.path[0].clone(); let part = url.path[0].clone();
let url = url.path[1..].join("/"); let url = url.path[1..].join("/");
req.uri = hyper::uri::RequestUri::AbsolutePath(url); req.uri = uri::RequestUri::AbsolutePath(url);
(Some(part), req) (Some(part), req)
}, },
Some(url) => { Some(url) => {
let url = url.path.join("/"); let url = url.path.join("/");
req.uri = hyper::uri::RequestUri::AbsolutePath(url); req.uri = uri::RequestUri::AbsolutePath(url);
(None, req) (None, req)
}, },
_ => { _ => {