Supporting /api/ping for dapps server (#1543)

* Refactoring dapps to support API endpoints.

* Using ContentHandler for unauthorized requests

* Extracting url stuff

* Adding ping endpoint

* CORS support for ping request

* Fixing url.is_none()

* minor formatting fix

[ci:skip]
This commit is contained in:
Tomasz Drwięga 2016-07-07 03:42:49 -04:00 committed by Gav Wood
parent 8282c7dd50
commit 7af366c5b1
11 changed files with 233 additions and 52 deletions

1
Cargo.lock generated
View File

@ -283,6 +283,7 @@ dependencies = [
"serde_codegen 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)", "serde_codegen 0.7.9 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"syntex 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "syntex 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
"unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
] ]

View File

@ -13,6 +13,7 @@ log = "0.3"
jsonrpc-core = "2.0" jsonrpc-core = "2.0"
jsonrpc-http-server = { git = "https://github.com/ethcore/jsonrpc-http-server.git" } jsonrpc-http-server = { git = "https://github.com/ethcore/jsonrpc-http-server.git" }
hyper = { default-features = false, git = "https://github.com/ethcore/hyper" } hyper = { default-features = false, git = "https://github.com/ethcore/hyper" }
unicase = "1.3"
url = "1.0" url = "1.0"
rustc-serialize = "0.3" rustc-serialize = "0.3"
serde = "0.7.0" serde = "0.7.0"

View File

@ -15,20 +15,23 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::sync::Arc; use std::sync::Arc;
use endpoint::{Endpoint, Endpoints, Handler, EndpointPath};
use api::types::{App, ApiError};
use api::response::{as_json, as_json_error};
use hyper::{server, net, Decoder, Encoder, Next}; use hyper::{server, net, Decoder, Encoder, Next};
use api::types::{App, ApiError};
use api::response::{as_json, as_json_error, ping_response};
use handlers::extract_url;
use endpoint::{Endpoint, Endpoints, Handler, EndpointPath};
#[derive(Clone)] #[derive(Clone)]
pub struct RestApi { pub struct RestApi {
local_domain: String,
endpoints: Arc<Endpoints>, endpoints: Arc<Endpoints>,
} }
impl RestApi { impl RestApi {
pub fn new(endpoints: Arc<Endpoints>) -> Box<Endpoint> { pub fn new(local_domain: String, endpoints: Arc<Endpoints>) -> Box<Endpoint> {
Box::new(RestApi { Box::new(RestApi {
endpoints: endpoints local_domain: local_domain,
endpoints: endpoints,
}) })
} }
@ -59,9 +62,28 @@ struct RestApiRouter {
impl server::Handler<net::HttpStream> for RestApiRouter { impl server::Handler<net::HttpStream> for RestApiRouter {
fn on_request(&mut self, _request: server::Request<net::HttpStream>) -> Next { fn on_request(&mut self, request: server::Request<net::HttpStream>) -> Next {
self.handler = as_json(&self.api.list_apps()); let url = extract_url(&request);
Next::write() if url.is_none() {
// Just return 404 if we can't parse URL
return Next::write();
}
let url = url.expect("Check for None is above; qed");
let endpoint = url.path.get(1).map(|v| v.as_str());
let handler = endpoint.and_then(|v| match v {
"apps" => Some(as_json(&self.api.list_apps())),
"ping" => Some(ping_response(&self.api.local_domain)),
_ => None,
});
// Overwrite default
if let Some(h) = handler {
self.handler = h;
}
self.handler.on_request(request)
} }
fn on_request_readable(&mut self, decoder: &mut Decoder<net::HttpStream>) -> Next { fn on_request_readable(&mut self, decoder: &mut Decoder<net::HttpStream>) -> Next {

View File

@ -17,7 +17,7 @@
use serde::Serialize; use serde::Serialize;
use serde_json; use serde_json;
use endpoint::Handler; use endpoint::Handler;
use handlers::ContentHandler; use handlers::{ContentHandler, EchoHandler};
pub fn as_json<T : Serialize>(val: &T) -> Box<Handler> { pub fn as_json<T : Serialize>(val: &T) -> Box<Handler> {
Box::new(ContentHandler::ok(serde_json::to_string(val).unwrap(), "application/json".to_owned())) Box::new(ContentHandler::ok(serde_json::to_string(val).unwrap(), "application/json".to_owned()))
@ -26,3 +26,11 @@ pub fn as_json<T : Serialize>(val: &T) -> Box<Handler> {
pub fn as_json_error<T : Serialize>(val: &T) -> Box<Handler> { pub fn as_json_error<T : Serialize>(val: &T) -> Box<Handler> {
Box::new(ContentHandler::not_found(serde_json::to_string(val).unwrap(), "application/json".to_owned())) Box::new(ContentHandler::not_found(serde_json::to_string(val).unwrap(), "application/json".to_owned()))
} }
pub fn ping_response(local_domain: &str) -> Box<Handler> {
Box::new(EchoHandler::cors(vec![
format!("http://{}", local_domain),
// Allow CORS calls also for localhost
format!("http://{}", local_domain.replace("127.0.0.1", "localhost")),
]))
}

View File

@ -19,5 +19,3 @@ include!("types.rs.in");
#[cfg(not(feature = "serde_macros"))] #[cfg(not(feature = "serde_macros"))]
include!(concat!(env!("OUT_DIR"), "/types.rs")); include!(concat!(env!("OUT_DIR"), "/types.rs"));

View File

@ -48,4 +48,3 @@ pub struct ApiError {
pub detail: String, pub detail: String,
} }

148
dapps/src/handlers/echo.rs Normal file
View File

@ -0,0 +1,148 @@
// 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/>.
//! Echo Handler
use std::io::Read;
use hyper::{header, server, Decoder, Encoder, Next};
use hyper::method::Method;
use hyper::net::HttpStream;
use unicase::UniCase;
use super::ContentHandler;
#[derive(Debug, PartialEq)]
/// Type of Cross-Origin request
enum Cors {
/// Not a Cross-Origin request - no headers needed
No,
/// Cross-Origin request with valid Origin
Allowed(String),
/// Cross-Origin request with invalid Origin
Forbidden,
}
pub struct EchoHandler {
safe_origins: Vec<String>,
content: String,
cors: Cors,
handler: Option<ContentHandler>,
}
impl EchoHandler {
pub fn cors(safe_origins: Vec<String>) -> Self {
EchoHandler {
safe_origins: safe_origins,
content: String::new(),
cors: Cors::Forbidden,
handler: None,
}
}
fn cors_header(&self, origin: Option<String>) -> Cors {
fn origin_is_allowed(origin: &str, safe_origins: &[String]) -> bool {
for safe in safe_origins {
if origin.starts_with(safe) {
return true;
}
}
false
}
match origin {
Some(ref origin) if origin_is_allowed(origin, &self.safe_origins) => {
Cors::Allowed(origin.clone())
},
None => Cors::No,
_ => Cors::Forbidden,
}
}
}
impl server::Handler<HttpStream> for EchoHandler {
fn on_request(&mut self, request: server::Request<HttpStream>) -> Next {
let origin = request.headers().get_raw("origin")
.and_then(|list| list.get(0))
.and_then(|origin| String::from_utf8(origin.clone()).ok());
self.cors = self.cors_header(origin);
// Don't even read the payload if origin is forbidden!
if let Cors::Forbidden = self.cors {
self.handler = Some(ContentHandler::ok(String::new(), "text/plain".into()));
Next::write()
} else {
Next::read()
}
}
fn on_request_readable(&mut self, decoder: &mut Decoder<HttpStream>) -> Next {
match decoder.read_to_string(&mut self.content) {
Ok(0) => {
self.handler = Some(ContentHandler::ok(self.content.clone(), "application/json".into()));
Next::write()
},
Ok(_) => Next::read(),
Err(e) => match e.kind() {
::std::io::ErrorKind::WouldBlock => Next::read(),
_ => Next::end(),
}
}
}
fn on_response(&mut self, res: &mut server::Response) -> Next {
if let Cors::Allowed(ref domain) = self.cors {
let mut headers = res.headers_mut();
headers.set(header::Allow(vec![Method::Options, Method::Post, Method::Get]));
headers.set(header::AccessControlAllowHeaders(vec![
UniCase("origin".to_owned()),
UniCase("content-type".to_owned()),
UniCase("accept".to_owned()),
]));
headers.set(header::AccessControlAllowOrigin::Value(domain.clone()));
}
self.handler.as_mut().unwrap().on_response(res)
}
fn on_response_writable(&mut self, encoder: &mut Encoder<HttpStream>) -> Next {
self.handler.as_mut().unwrap().on_response_writable(encoder)
}
}
#[test]
fn should_return_correct_cors_value() {
// given
let safe_origins = vec!["chrome-extension://".to_owned(), "http://localhost:8080".to_owned()];
let cut = EchoHandler {
safe_origins: safe_origins,
content: String::new(),
cors: Cors::No,
handler: None,
};
// when
let res1 = cut.cors_header(Some("http://ethcore.io".into()));
let res2 = cut.cors_header(Some("http://localhost:8080".into()));
let res3 = cut.cors_header(Some("chrome-extension://deadbeefcafe".into()));
let res4 = cut.cors_header(None);
// then
assert_eq!(res1, Cors::Forbidden);
assert_eq!(res2, Cors::Allowed("http://localhost:8080".into()));
assert_eq!(res3, Cors::Allowed("chrome-extension://deadbeefcafe".into()));
assert_eq!(res4, Cors::No);
}

View File

@ -17,9 +17,41 @@
//! Hyper handlers implementations. //! Hyper handlers implementations.
mod auth; mod auth;
mod echo;
mod content; mod content;
mod redirect; mod redirect;
pub use self::auth::AuthRequiredHandler; pub use self::auth::AuthRequiredHandler;
pub use self::echo::EchoHandler;
pub use self::content::ContentHandler; pub use self::content::ContentHandler;
pub use self::redirect::Redirection; pub use self::redirect::Redirection;
use url::Url;
use hyper::{server, header, net, uri};
pub fn extract_url(req: &server::Request<net::HttpStream>) -> Option<Url> {
match *req.uri() {
uri::RequestUri::AbsoluteUri(ref url) => {
match Url::from_generic_url(url.clone()) {
Ok(url) => Some(url),
_ => None,
}
},
uri::RequestUri::AbsolutePath(ref path) => {
// Attempt to prepend the Host header (mandatory in HTTP/1.1)
let url_string = match req.headers().get::<header::Host>() {
Some(ref host) => {
format!("http://{}:{}{}", host.hostname, host.port.unwrap_or(80), path)
},
None => return None,
};
match Url::parse(&url_string) {
Ok(url) => Some(url),
_ => None,
}
},
_ => None,
}
}

View File

@ -45,8 +45,9 @@
#[macro_use] #[macro_use]
extern crate log; extern crate log;
extern crate url; extern crate url as url_lib;
extern crate hyper; extern crate hyper;
extern crate unicase;
extern crate serde; extern crate serde;
extern crate serde_json; extern crate serde_json;
extern crate jsonrpc_core; extern crate jsonrpc_core;
@ -63,6 +64,7 @@ mod handlers;
mod rpc; mod rpc;
mod api; mod api;
mod proxypac; mod proxypac;
mod url;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::net::SocketAddr; use std::net::SocketAddr;
@ -121,7 +123,7 @@ impl Server {
let special = Arc::new({ let special = Arc::new({
let mut special = HashMap::new(); let mut special = HashMap::new();
special.insert(router::SpecialEndpoint::Rpc, rpc::rpc(handler, panic_handler.clone())); special.insert(router::SpecialEndpoint::Rpc, rpc::rpc(handler, panic_handler.clone()));
special.insert(router::SpecialEndpoint::Api, api::RestApi::new(endpoints.clone())); special.insert(router::SpecialEndpoint::Api, api::RestApi::new(format!("{}", addr), endpoints.clone()));
special.insert(router::SpecialEndpoint::Utils, apps::utils()); special.insert(router::SpecialEndpoint::Utils, apps::utils());
special special
}); });

View File

@ -17,22 +17,18 @@
//! Router implementation //! Router implementation
//! Processes request handling authorization and dispatching it to proper application. //! Processes request handling authorization and dispatching it to proper application.
mod url;
pub mod auth; pub mod auth;
use DAPPS_DOMAIN; use DAPPS_DOMAIN;
use std::sync::Arc; use std::sync::Arc;
use std::collections::HashMap; use std::collections::HashMap;
use url::Host; use url::{Url, Host};
use hyper; use hyper::{self, server, Next, Encoder, Decoder};
use hyper::{server, uri, header};
use hyper::{Next, Encoder, Decoder};
use hyper::net::HttpStream; use hyper::net::HttpStream;
use apps; use apps;
use endpoint::{Endpoint, Endpoints, EndpointPath}; use endpoint::{Endpoint, Endpoints, EndpointPath};
use self::url::Url; use handlers::{Redirection, extract_url};
use self::auth::{Authorization, Authorized}; use self::auth::{Authorization, Authorized};
use handlers::Redirection;
/// Special endpoints are accessible on every domain (every dapp) /// Special endpoints are accessible on every domain (every dapp)
#[derive(Debug, PartialEq, Hash, Eq)] #[derive(Debug, PartialEq, Hash, Eq)]
@ -123,32 +119,6 @@ impl<A: Authorization> Router<A> {
} }
} }
fn extract_url(req: &server::Request<HttpStream>) -> Option<Url> {
match *req.uri() {
uri::RequestUri::AbsoluteUri(ref url) => {
match Url::from_generic_url(url.clone()) {
Ok(url) => Some(url),
_ => None,
}
},
uri::RequestUri::AbsolutePath(ref path) => {
// Attempt to prepend the Host header (mandatory in HTTP/1.1)
let url_string = match req.headers().get::<header::Host>() {
Some(ref host) => {
format!("http://{}:{}{}", host.hostname, host.port.unwrap_or(80), path)
},
None => return None,
};
match Url::parse(&url_string) {
Ok(url) => Some(url),
_ => None,
}
},
_ => None,
}
}
fn extract_endpoint(url: &Option<Url>) -> (Option<EndpointPath>, SpecialEndpoint) { fn extract_endpoint(url: &Option<Url>) -> (Option<EndpointPath>, SpecialEndpoint) {
fn special_endpoint(url: &Url) -> SpecialEndpoint { fn special_endpoint(url: &Url) -> SpecialEndpoint {
if url.path.len() <= 1 { if url.path.len() <= 1 {

View File

@ -16,14 +16,14 @@
//! HTTP/HTTPS URL type. Based on URL type from Iron library. //! HTTP/HTTPS URL type. Based on URL type from Iron library.
use url::Host; use url_lib::{self};
use url::{self}; pub use url_lib::Host;
/// HTTP/HTTPS URL type for Iron. /// HTTP/HTTPS URL type for Iron.
#[derive(PartialEq, Eq, Clone, Debug)] #[derive(PartialEq, Eq, Clone, Debug)]
pub struct Url { pub struct Url {
/// Raw url of url /// Raw url of url
pub raw: url::Url, pub raw: url_lib::Url,
/// The host field of the URL, probably a domain. /// The host field of the URL, probably a domain.
pub host: Host, pub host: Host,
@ -62,14 +62,14 @@ impl Url {
/// See: http://url.spec.whatwg.org/#special-scheme /// See: http://url.spec.whatwg.org/#special-scheme
pub fn parse(input: &str) -> Result<Url, String> { pub fn parse(input: &str) -> Result<Url, String> {
// Parse the string using rust-url, then convert. // Parse the string using rust-url, then convert.
match url::Url::parse(input) { match url_lib::Url::parse(input) {
Ok(raw_url) => Url::from_generic_url(raw_url), Ok(raw_url) => Url::from_generic_url(raw_url),
Err(e) => Err(format!("{}", e)) Err(e) => Err(format!("{}", e))
} }
} }
/// Create a `Url` from a `rust-url` `Url`. /// Create a `Url` from a `rust-url` `Url`.
pub fn from_generic_url(raw_url: url::Url) -> Result<Url, String> { pub fn from_generic_url(raw_url: url_lib::Url) -> Result<Url, String> {
// Map empty usernames to None. // Map empty usernames to None.
let username = match raw_url.username() { let username = match raw_url.username() {
"" => None, "" => None,