From f81914351d8ef743e38246cab0b83b54866db584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= Date: Thu, 14 Apr 2016 20:38:48 +0200 Subject: [PATCH] Rewriting webapps to use hyper-mio branch --- Cargo.lock | 14 +--- parity/main.rs | 19 +++--- webapp/Cargo.toml | 4 +- webapp/src/api.rs | 93 ++++++++++++++++++++++++++ webapp/src/apps.rs | 20 +++--- webapp/src/endpoint.rs | 27 ++++++++ webapp/src/lib.rs | 112 ++++++++++++++++++++------------ webapp/src/page/mod.rs | 111 +++++++++++++++++++++---------- webapp/src/router/api.rs | 53 --------------- webapp/src/router/auth.rs | 95 ++++++++++++++++++++------- webapp/src/router/mod.rs | 119 ++++++++++++++++++++-------------- webapp/src/router/redirect.rs | 56 ++++++++++++++++ webapp/src/rpc.rs | 41 ++++++++++++ 13 files changed, 532 insertions(+), 232 deletions(-) create mode 100644 webapp/src/api.rs create mode 100644 webapp/src/endpoint.rs delete mode 100644 webapp/src/router/api.rs create mode 100644 webapp/src/router/redirect.rs create mode 100644 webapp/src/rpc.rs diff --git a/Cargo.lock b/Cargo.lock index a1fd76643..1dc8f6750 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,9 +300,9 @@ dependencies = [ "clippy 0.0.63 (registry+https://github.com/rust-lang/crates.io-index)", "ethcore-rpc 1.1.0", "ethcore-util 1.1.0", - "hyper 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hyper 0.9.0-mio (git+https://github.com/hyperium/hyper?branch=mio)", "jsonrpc-core 2.0.2 (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 5.0.1 (git+https://github.com/debris/jsonrpc-http-server.git)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "parity-status 0.1.6 (git+https://github.com/tomusdrw/parity-status.git)", "parity-wallet 0.1.1 (git+https://github.com/tomusdrw/parity-wallet.git)", @@ -491,16 +491,6 @@ dependencies = [ "syntex 0.31.0 (registry+https://github.com/rust-lang/crates.io-index)", ] -[[package]] -name = "jsonrpc-http-server" -version = "4.0.0" -source = "git+https://github.com/tomusdrw/jsonrpc-http-server.git?branch=old-hyper#46bd4e7cf8352e0efc940cf76d3dff99f1a3da15" -dependencies = [ - "hyper 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "jsonrpc-http-server" version = "5.0.1" diff --git a/parity/main.rs b/parity/main.rs index c2673cc8a..c676d514c 100644 --- a/parity/main.rs +++ b/parity/main.rs @@ -69,7 +69,7 @@ use number_prefix::{binary_prefix, Standalone, Prefixed}; #[cfg(feature = "rpc")] use rpc::Server as RpcServer; #[cfg(feature = "webapp")] -use webapp::Listening as WebappServer; +use webapp::Server as WebappServer; mod price_info; mod upgrade; @@ -342,12 +342,12 @@ fn setup_webapp_server( sync: Arc, secret_store: Arc, miner: Arc, - url: &str, + url: &SocketAddr, auth: Option<(String, String)>, ) -> WebappServer { use rpc::v1::*; - let server = webapp::WebappServer::new(); + let server = webapp::ServerBuilder::new(); server.add_delegate(Web3Client::new().to_delegate()); server.add_delegate(NetClient::new(&sync).to_delegate()); server.add_delegate(EthClient::new(&client, &sync, &secret_store, &miner).to_delegate()); @@ -356,14 +356,14 @@ fn setup_webapp_server( server.add_delegate(EthcoreClient::new(&miner).to_delegate()); let start_result = match auth { None => { - server.start_unsecure_http(url, ::num_cpus::get()) + server.start_unsecure_http(url) }, Some((username, password)) => { - server.start_basic_auth_http(url, ::num_cpus::get(), &username, &password) + server.start_basic_auth_http(url, &username, &password) }, }; match start_result { - Err(webapp::WebappServerError::IoError(err)) => die_with_io_error(err), + Err(webapp::ServerError::IoError(err)) => die_with_io_error(err), Err(e) => die!("{:?}", e), Ok(handle) => handle, } @@ -379,7 +379,7 @@ fn setup_rpc_server( _sync: Arc, _secret_store: Arc, _miner: Arc, - _url: &str, + _url: &SocketAddr, _cors_domain: &str, _apis: Vec<&str>, ) -> ! { @@ -395,7 +395,7 @@ fn setup_webapp_server( _sync: Arc, _secret_store: Arc, _miner: Arc, - _url: &str, + _url: &SocketAddr, _auth: Option<(String, String)>, ) -> ! { die!("Your Parity version has been compiled without WebApps support.") @@ -736,6 +736,7 @@ impl Configuration { }, self.args.flag_webapp_port ); + let addr = SocketAddr::from_str(&url).unwrap_or_else(|_| die!("{}: Invalid Webapps listen host/port given.", url)); 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; @@ -752,7 +753,7 @@ impl Configuration { sync.clone(), account_service.clone(), miner.clone(), - &url, + &addr, auth, )) } else { diff --git a/webapp/Cargo.toml b/webapp/Cargo.toml index 157d9e506..54aef60a9 100644 --- a/webapp/Cargo.toml +++ b/webapp/Cargo.toml @@ -10,8 +10,8 @@ authors = ["Ethcore . + +//! Simple REST API + +use std::io::Write; +use std::sync::Arc; +use hyper::status::StatusCode; +use hyper::{header, server, Decoder, Encoder, Next}; +use hyper::net::HttpStream; +use endpoint::{Endpoint, Endpoints}; + +pub struct RestApi { + endpoints: Arc, +} + +impl RestApi { + pub fn new(endpoints: Arc) -> Box { + Box::new(RestApi { + endpoints: endpoints + }) + } + + fn list_pages(&self) -> String { + let mut s = "[".to_owned(); + for name in self.endpoints.keys() { + s.push_str(&format!("\"{}\",", name)); + } + s.push_str("\"rpc\""); + s.push_str("]"); + s + } +} + +impl Endpoint for RestApi { + fn to_handler(&self, _prefix: &str) -> Box> { + Box::new(RestApiHandler { + pages: self.list_pages(), + write_pos: 0, + }) + } +} + +struct RestApiHandler { + pages: String, + write_pos: usize, +} + +impl server::Handler for RestApiHandler { + fn on_request(&mut self, _request: server::Request) -> Next { + Next::write() + } + + fn on_request_readable(&mut self, _decoder: &mut Decoder) -> Next { + Next::write() + } + + fn on_response(&mut self, res: &mut server::Response) -> Next { + res.set_status(StatusCode::Ok); + res.headers_mut().set(header::ContentType("application/json".parse().unwrap())); + Next::write() + } + fn on_response_writable(&mut self, encoder: &mut Encoder) -> Next { + let bytes = self.pages.as_bytes(); + if self.write_pos == bytes.len() { + return Next::end(); + } + + match encoder.write(&bytes[self.write_pos..]) { + Ok(bytes) => { + self.write_pos += bytes; + Next::write() + }, + Err(e) => match e.kind() { + ::std::io::ErrorKind::WouldBlock => Next::write(), + _ => Next::end() + }, + } + } +} diff --git a/webapp/src/apps.rs b/webapp/src/apps.rs index 364186d04..2f9d40e45 100644 --- a/webapp/src/apps.rs +++ b/webapp/src/apps.rs @@ -14,28 +14,28 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -use std::collections::HashMap; -use page::{Page, PageHandler}; +use endpoint::Endpoints; +use page::PageEndpoint; extern crate parity_status; extern crate parity_wallet; -pub type Pages = HashMap>; -pub fn main_page() -> Box { - Box::new(PageHandler { app: parity_status::App::default() }) +pub fn main_page() -> &'static str { + "/status/" } -pub fn all_pages() -> Pages { - let mut pages = Pages::new(); +pub fn all_endpoints() -> Endpoints { + let mut pages = Endpoints::new(); + pages.insert("status".to_owned(), Box::new(PageEndpoint::new(parity_status::App::default()))); wallet_page(&mut pages); pages } #[cfg(feature = "parity-wallet")] -fn wallet_page(pages: &mut Pages) { - pages.insert("wallet".to_owned(), Box::new(PageHandler { app: parity_wallet::App::default() })); +fn wallet_page(pages: &mut Endpoints) { + pages.insert("wallet".to_owned(), Box::new(PageEndpoint::new(parity_wallet::App::default()))); } #[cfg(not(feature = "parity-wallet"))] -fn wallet_page(_pages: &mut Pages) {} +fn wallet_page(_pages: &mut Endpoints) {} diff --git a/webapp/src/endpoint.rs b/webapp/src/endpoint.rs new file mode 100644 index 000000000..dee2cadd8 --- /dev/null +++ b/webapp/src/endpoint.rs @@ -0,0 +1,27 @@ +// 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 . + +//! URL Endpoint traits + +use hyper::server; +use hyper::net::HttpStream; +use std::collections::HashMap; + +pub trait Endpoint : Send + Sync { + fn to_handler(&self, prefix: &str) -> Box>; +} + +pub type Endpoints = HashMap>; diff --git a/webapp/src/lib.rs b/webapp/src/lib.rs index 8452f6453..412c15245 100644 --- a/webapp/src/lib.rs +++ b/webapp/src/lib.rs @@ -15,6 +15,31 @@ // along with Parity. If not, see . //! Ethcore Webapplications for Parity +//! ``` +//! extern crate jsonrpc_core; +//! extern crate ethcore_webapp; +//! +//! use std::sync::Arc; +//! use jsonrpc_core::IoHandler; +//! use ethcore_webapp::*; +//! +//! struct SayHello; +//! impl MethodCommand for SayHello { +//! fn execute(&self, _params: Params) -> Result { +//! Ok(Value::String("hello".to_string())) +//! } +//! } +//! +//! fn main() { +//! let io = IoHandler::new(); +//! io.add_method("say_hello", SayHello); +//! let _server = Server::start_unsecure_http( +//! &"127.0.0.1:3030".parse().unwrap(), +//! Arc::new(io) +//! ); +//! } +//! ``` +//! #![warn(missing_docs)] #![cfg_attr(feature="nightly", plugin(clippy))] @@ -24,29 +49,30 @@ extern crate url; extern crate hyper; extern crate jsonrpc_core; extern crate jsonrpc_http_server; -extern crate ethcore_rpc as rpc; extern crate parity_webapp; -use std::sync::Arc; -use self::jsonrpc_core::{IoHandler, IoDelegate}; -use jsonrpc_http_server::ServerHandler; - +mod endpoint; mod apps; mod page; mod router; +mod rpc; +mod api; +use std::sync::Arc; +use std::net::SocketAddr; +use jsonrpc_core::{IoHandler, IoDelegate}; use router::auth::{Authorization, NoAuth, HttpBasicAuth}; -/// Http server. -pub struct WebappServer { +/// Webapps HTTP+RPC server build. +pub struct ServerBuilder { handler: Arc, } -impl WebappServer { - /// Construct new http server object +impl ServerBuilder { + /// Construct new webapps pub fn new() -> Self { - WebappServer { - handler: Arc::new(IoHandler::new()), + ServerBuilder { + handler: Arc::new(IoHandler::new()) } } @@ -56,57 +82,63 @@ impl WebappServer { } /// 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) + /// 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()) } /// 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(), authorization); - - try!(hyper::Server::http(addr.as_ref() as &str)) - .handle_threads(router, threads) - .map(|l| Listening { listening: l }) - .map_err(WebappServerError::from) + /// 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()) } } -/// Listening handle -pub struct Listening { - listening: hyper::server::Listening +/// Webapps HTTP server. +pub struct Server { + server: Option, } -impl Drop for Listening { +impl Server { + fn start_http(addr: &SocketAddr, authorization: A, handler: Arc) -> Result { + let endpoints = Arc::new(apps::all_endpoints()); + let authorization = Arc::new(authorization); + let rpc_endpoint = Arc::new(rpc::rpc(handler)); + let api = Arc::new(api::RestApi::new(endpoints.clone())); + + try!(hyper::Server::http(addr)) + .handle(move |_| router::Router::new( + apps::main_page(), + endpoints.clone(), + rpc_endpoint.clone(), + api.clone(), + authorization.clone(), + )) + .map(|l| Server { server: Some(l) }) + .map_err(ServerError::from) + } +} + +impl Drop for Server { fn drop(&mut self) { - self.listening.close().unwrap(); + self.server.take().unwrap().close() } } /// Webapp Server startup error #[derive(Debug)] -pub enum WebappServerError { +pub enum ServerError { /// Wrapped `std::io::Error` IoError(std::io::Error), /// Other `hyper` error Other(hyper::error::Error), } -impl From for WebappServerError { +impl From for ServerError { fn from(err: hyper::error::Error) -> Self { match err { - hyper::error::Error::Io(e) => WebappServerError::IoError(e), - e => WebappServerError::Other(e), + hyper::error::Error::Io(e) => ServerError::IoError(e), + e => ServerError::Other(e), } } } diff --git a/webapp/src/page/mod.rs b/webapp/src/page/mod.rs index ce7b32947..8e097b3a4 100644 --- a/webapp/src/page/mod.rs +++ b/webapp/src/page/mod.rs @@ -14,54 +14,97 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +use std::sync::Arc; use std::io::Write; use hyper::uri::RequestUri; use hyper::server; use hyper::header; use hyper::status::StatusCode; +use hyper::net::HttpStream; +use hyper::{Decoder, Encoder, Next}; +use endpoint::Endpoint; use parity_webapp::WebApp; -pub trait Page : Send + Sync { - fn serve_file(&self, mut path: &str, mut res: server::Response); +pub struct PageEndpoint { + pub app: Arc, } -pub struct PageHandler { - pub app: T, -} - -impl Page for PageHandler { - fn serve_file(&self, mut path: &str, mut res: server::Response) { - // Support index file - if path == "" { - path = "index.html" +impl PageEndpoint { + pub fn new(app: T) -> Self { + PageEndpoint { + app: Arc::new(app) } - let file = self.app.file(path); - if let Some(f) = file { - *res.status_mut() = StatusCode::Ok; + } +} + +impl Endpoint for PageEndpoint { + fn to_handler(&self, prefix: &str) -> Box> { + Box::new(PageHandler { + app: self.app.clone(), + prefix: prefix.to_owned(), + prefix2: prefix.to_owned() + "/", + path: None, + write_pos: 0, + }) + } +} + +struct PageHandler { + app: Arc, + prefix: String, + prefix2: String, + path: Option, + write_pos: usize, +} + +impl server::Handler for PageHandler { + fn on_request(&mut self, req: server::Request) -> Next { + if let RequestUri::AbsolutePath(ref path) = *req.uri() { + // Support index file + if path == &self.prefix || path == &self.prefix2 { + self.path = Some("index.html".to_owned()); + } else { + self.path = Some(path[self.prefix2.len()..].to_owned()); + } + } + Next::write() + } + + fn on_request_readable(&mut self, _decoder: &mut Decoder) -> Next { + Next::write() + } + + fn on_response(&mut self, res: &mut server::Response) -> Next { + if let Some(f) = self.path.as_ref().and_then(|f| self.app.file(f)) { + res.set_status(StatusCode::Ok); res.headers_mut().set(header::ContentType(f.content_type.parse().unwrap())); - - let _ = match res.start() { - Ok(mut raw_res) => { - for chunk in f.content.chunks(1024 * 20) { - let _ = raw_res.write(chunk); - } - raw_res.end() - }, - Err(_) => { - println!("Error while writing response."); - Ok(()) - }, - }; + Next::write() + } else { + res.set_status(StatusCode::NotFound); + Next::write() } } -} -impl server::Handler for Page { - fn handle(&self, req: server::Request, mut res: server::Response) { - *res.status_mut() = StatusCode::NotFound; - - if let RequestUri::AbsolutePath(ref path) = req.uri { - self.serve_file(path, res); + fn on_response_writable(&mut self, encoder: &mut Encoder) -> Next { + let (wrote, res) = { + let file = self.path.as_ref().and_then(|f| self.app.file(f)); + match file { + None => (None, Next::end()), + Some(f) if self.write_pos == f.content.len() => (None, Next::end()), + Some(f) => match encoder.write(&f.content[self.write_pos..]) { + Ok(bytes) => { + (Some(bytes), Next::write()) + }, + Err(e) => match e.kind() { + ::std::io::ErrorKind::WouldBlock => (None, Next::write()), + _ => (None, Next::end()) + }, + } + } + }; + if let Some(bytes) = wrote { + self.write_pos += bytes; } + res } } diff --git a/webapp/src/router/api.rs b/webapp/src/router/api.rs deleted file mode 100644 index 046b75957..000000000 --- a/webapp/src/router/api.rs +++ /dev/null @@ -1,53 +0,0 @@ -// 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 . - -//! Simple REST API - -use std::sync::Arc; -use hyper; -use hyper::status::StatusCode; -use hyper::header; -use hyper::uri::RequestUri::AbsolutePath as Path; -use apps::Pages; - -pub struct RestApi { - pub pages: Arc, -} - -impl RestApi { - fn list_pages(&self) -> String { - let mut s = "[".to_owned(); - for name in self.pages.keys() { - s.push_str(&format!("\"{}\",", name)); - } - s.push_str("\"rpc\""); - s.push_str("]"); - s - } -} - -impl hyper::server::Handler for RestApi { - fn handle<'b, 'a>(&'a self, req: hyper::server::Request<'a, 'b>, mut res: hyper::server::Response<'a>) { - match req.uri { - Path(ref path) if path == "apps" => { - *res.status_mut() = StatusCode::Ok; - res.headers_mut().set(header::ContentType("application/json".parse().unwrap())); - let _ = res.send(self.list_pages().as_bytes()); - }, - _ => (), - } - } -} diff --git a/webapp/src/router/auth.rs b/webapp/src/router/auth.rs index 95c558bef..b05ffc3da 100644 --- a/webapp/src/router/auth.rs +++ b/webapp/src/router/auth.rs @@ -16,22 +16,24 @@ //! HTTP Authorization implementations +use std::io::Write; use std::collections::HashMap; -use hyper::{header, server}; +use hyper::{header, server, Decoder, Encoder, Next}; +use hyper::net::HttpStream; 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, +pub enum Authorized { + /// Authorization was successful. + Yes, + /// Unsuccessful authorization. Handler for further work is returned. + No(Box>), } /// 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>; + /// Checks if authorization is valid. + fn is_authorized(&self, req: &server::Request)-> Authorized; } /// HTTP Basic Authorization handler @@ -43,27 +45,24 @@ pub struct HttpBasicAuth { 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) + fn is_authorized(&self, _req: &server::Request)-> Authorized { + Authorized::Yes } } impl Authorization for HttpBasicAuth { - - fn handle<'b, 'a>(&'a self, req: server::Request<'a, 'b>, res: server::Response<'a>)-> Authorized<'a, 'b> { + fn is_authorized(&self, req: &server::Request) -> Authorized { let auth = self.check_auth(&req); match auth { Access::Denied => { - self.respond_with_unauthorized(res); - Authorized::No + Authorized::No(Box::new(UnauthorizedHandler { write_pos: 0 })) }, Access::AuthRequired => { - self.respond_with_auth_required(res); - Authorized::No + Authorized::No(Box::new(AuthRequiredHandler)) }, Access::Granted => { - Authorized::Yes(req, res) + Authorized::Yes }, } } @@ -90,7 +89,7 @@ impl HttpBasicAuth { } fn check_auth(&self, req: &server::Request) -> Access { - match req.headers.get::>() { + match req.headers().get::>() { Some(&header::Authorization( header::Basic { ref username, password: Some(ref password) } )) if self.is_authorized(username, password) => Access::Granted, @@ -98,15 +97,63 @@ impl HttpBasicAuth { None => Access::AuthRequired, } } +} - fn respond_with_unauthorized(&self, mut res: server::Response) { - *res.status_mut() = StatusCode::Unauthorized; - let _ = res.send(b"Unauthorized"); +pub struct UnauthorizedHandler { + write_pos: usize, +} + +impl server::Handler for UnauthorizedHandler { + fn on_request(&mut self, _request: server::Request) -> Next { + Next::write() } - 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()]); + fn on_request_readable(&mut self, _decoder: &mut Decoder) -> Next { + Next::write() + } + + fn on_response(&mut self, res: &mut server::Response) -> Next { + res.set_status(StatusCode::Unauthorized); + Next::write() + } + fn on_response_writable(&mut self, encoder: &mut Encoder) -> Next { + let response = "Unauthorized".as_bytes(); + + if self.write_pos == response.len() { + return Next::end(); + } + + match encoder.write(&response[self.write_pos..]) { + Ok(bytes) => { + self.write_pos += bytes; + Next::write() + }, + Err(e) => match e.kind() { + ::std::io::ErrorKind::WouldBlock => Next::write(), + _ => Next::end() + }, + } } } +pub struct AuthRequiredHandler; + +impl server::Handler for AuthRequiredHandler { + fn on_request(&mut self, _request: server::Request) -> Next { + Next::write() + } + + fn on_request_readable(&mut self, _decoder: &mut Decoder) -> Next { + Next::write() + } + + fn on_response(&mut self, res: &mut server::Response) -> Next { + res.set_status(StatusCode::Unauthorized); + res.headers_mut().set_raw("WWW-Authenticate", vec![b"Basic realm=\"Parity\"".to_vec()]); + Next::end() + } + + fn on_response_writable(&mut self, _encoder: &mut Encoder) -> Next { + Next::end() + } +} diff --git a/webapp/src/router/mod.rs b/webapp/src/router/mod.rs index f91e837a3..15fc496f9 100644 --- a/webapp/src/router/mod.rs +++ b/webapp/src/router/mod.rs @@ -17,67 +17,97 @@ //! Router implementation //! Processes request handling authorization and dispatching it to proper application. -mod api; mod url; +mod redirect; pub mod auth; use std::sync::Arc; use hyper; use hyper::{server, uri, header}; -use page::Page; -use apps::Pages; +use hyper::{Next, Encoder, Decoder}; +use hyper::net::HttpStream; +use endpoint::{Endpoint, Endpoints}; use self::url::Url; -use jsonrpc_http_server::ServerHandler; use self::auth::{Authorization, Authorized}; +use self::redirect::Redirection; -pub struct Router { - authorization: A, - rpc: ServerHandler, - api: api::RestApi, - main_page: Box, - pages: Arc, +pub struct Router { + main_page: &'static str, + endpoints: Arc, + rpc: Arc>, + api: Arc>, + authorization: Arc, + handler: Box>, } -impl server::Handler for Router { - fn handle<'b, 'a>(&'a self, req: server::Request<'a, 'b>, res: server::Response<'a>) { - let auth = self.authorization.handle(req, res); +impl server::Handler for Router { - if let Authorized::Yes(req, res) = auth { - 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); - }, - 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), + fn on_request(&mut self, req: server::Request) -> Next { + let auth = self.authorization.is_authorized(&req); + self.handler = match auth { + Authorized::No(handler) => handler, + Authorized::Yes => { + let path = self.extract_request_path(&req); + match path { + Some(ref url) if self.endpoints.contains_key(url) => { + let prefix = "/".to_owned() + url; + self.endpoints.get(url).unwrap().to_handler(&prefix) + }, + Some(ref url) if url == "api" => { + self.api.to_handler("/api") + }, + _ if *req.method() == hyper::method::Method::Get => { + Redirection::new(self.main_page) + }, + _ => { + self.rpc.to_handler(&"/") + } + } } - } + }; + self.handler.on_request(req) + // Check authorization + // Choose proper handler depending on path + // Delegate on_request to proper handler + } + + /// This event occurs each time the `Request` is ready to be read from. + fn on_request_readable(&mut self, decoder: &mut Decoder) -> Next { + self.handler.on_request_readable(decoder) + } + + /// This event occurs after the first time this handled signals `Next::write()`. + fn on_response(&mut self, response: &mut server::Response) -> Next { + self.handler.on_response(response) + } + + /// This event occurs each time the `Response` is ready to be written to. + fn on_response_writable(&mut self, encoder: &mut Encoder) -> Next { + self.handler.on_response_writable(encoder) } } impl Router { pub fn new( - rpc: ServerHandler, - main_page: Box, - pages: Pages, - authorization: A) -> Self { - let pages = Arc::new(pages); + main_page: &'static str, + endpoints: Arc, + rpc: Arc>, + api: Arc>, + authorization: Arc) -> Self { + + let handler = rpc.to_handler(&"/"); Router { - authorization: authorization, - rpc: rpc, - api: api::RestApi { pages: pages.clone() }, main_page: main_page, - pages: pages, + endpoints: endpoints, + rpc: rpc, + api: api, + authorization: authorization, + handler: handler, } } fn extract_url(&self, req: &server::Request) -> Option { - match req.uri { + match *req.uri() { uri::RequestUri::AbsoluteUri(ref url) => { match Url::from_generic_url(url.clone()) { Ok(url) => Some(url), @@ -86,7 +116,7 @@ impl Router { }, 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) }, @@ -102,22 +132,15 @@ impl Router { } } - fn extract_request_path<'a, 'b>(&self, mut req: server::Request<'a, 'b>) -> (Option, server::Request<'a, 'b>) { + fn extract_request_path(&self, req: &server::Request) -> Option { let url = self.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 = uri::RequestUri::AbsolutePath(url); - (Some(part), req) - }, - Some(url) => { - let url = url.path.join("/"); - req.uri = uri::RequestUri::AbsolutePath(url); - (None, req) + Some(part) }, _ => { - (None, req) + None }, } } diff --git a/webapp/src/router/redirect.rs b/webapp/src/router/redirect.rs new file mode 100644 index 000000000..a78df4046 --- /dev/null +++ b/webapp/src/router/redirect.rs @@ -0,0 +1,56 @@ +// 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 Redirection hyper handler + +use std::io::Write; +use hyper::{header, server, Decoder, Encoder, Next}; +use hyper::net::HttpStream; +use hyper::status::StatusCode; + +pub struct Redirection { + to_url: &'static str +} + +impl Redirection { + pub fn new(url: &'static str) -> Box { + Box::new(Redirection { + to_url: url + }) + } +} + +impl server::Handler for Redirection { + fn on_request(&mut self, _request: server::Request) -> Next { + Next::write() + } + + fn on_request_readable(&mut self, _decoder: &mut Decoder) -> Next { + Next::write() + } + + fn on_response(&mut self, res: &mut server::Response) -> Next { + res.set_status(StatusCode::MovedPermanently); + res.headers_mut().set(header::Location(self.to_url.to_owned())); + Next::end() + } + fn on_response_writable(&mut self, _encoder: &mut Encoder) -> Next { + Next::end() + } +} + + + diff --git a/webapp/src/rpc.rs b/webapp/src/rpc.rs new file mode 100644 index 000000000..cc2a64376 --- /dev/null +++ b/webapp/src/rpc.rs @@ -0,0 +1,41 @@ +// 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 std::sync::Arc; +use hyper::server; +use hyper::net::HttpStream; +use jsonrpc_core::IoHandler; +use jsonrpc_http_server::ServerHandler; +use jsonrpc_http_server::AccessControlAllowOrigin; +use endpoint::Endpoint; + +pub fn rpc(handler: Arc) -> Box { + Box::new(RpcEndpoint { + handler: handler, + cors_domain: AccessControlAllowOrigin::Null + }) +} + +struct RpcEndpoint { + handler: Arc, + cors_domain: AccessControlAllowOrigin, +} + +impl Endpoint for RpcEndpoint { + fn to_handler(&self, _prefix: &str) -> Box> { + Box::new(ServerHandler::new(self.handler.clone(), self.cors_domain.clone())) + } +}