diff --git a/Cargo.lock b/Cargo.lock
index e3dc752f2..eae4823f6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -283,6 +283,7 @@ dependencies = [
"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)",
"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)",
]
diff --git a/dapps/Cargo.toml b/dapps/Cargo.toml
index 94af56ca3..c42ed8eff 100644
--- a/dapps/Cargo.toml
+++ b/dapps/Cargo.toml
@@ -13,6 +13,7 @@ log = "0.3"
jsonrpc-core = "2.0"
jsonrpc-http-server = { git = "https://github.com/ethcore/jsonrpc-http-server.git" }
hyper = { default-features = false, git = "https://github.com/ethcore/hyper" }
+unicase = "1.3"
url = "1.0"
rustc-serialize = "0.3"
serde = "0.7.0"
diff --git a/dapps/src/api/api.rs b/dapps/src/api/api.rs
index ab3632074..84db93f63 100644
--- a/dapps/src/api/api.rs
+++ b/dapps/src/api/api.rs
@@ -15,20 +15,23 @@
// along with Parity. If not, see .
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 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)]
pub struct RestApi {
+ local_domain: String,
endpoints: Arc,
}
impl RestApi {
- pub fn new(endpoints: Arc) -> Box {
+ pub fn new(local_domain: String, endpoints: Arc) -> Box {
Box::new(RestApi {
- endpoints: endpoints
+ local_domain: local_domain,
+ endpoints: endpoints,
})
}
@@ -59,9 +62,28 @@ struct RestApiRouter {
impl server::Handler for RestApiRouter {
- fn on_request(&mut self, _request: server::Request) -> Next {
- self.handler = as_json(&self.api.list_apps());
- Next::write()
+ fn on_request(&mut self, request: server::Request) -> Next {
+ let url = extract_url(&request);
+ 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) -> Next {
diff --git a/dapps/src/api/response.rs b/dapps/src/api/response.rs
index ba0922e7a..ce6eb2921 100644
--- a/dapps/src/api/response.rs
+++ b/dapps/src/api/response.rs
@@ -17,7 +17,7 @@
use serde::Serialize;
use serde_json;
use endpoint::Handler;
-use handlers::ContentHandler;
+use handlers::{ContentHandler, EchoHandler};
pub fn as_json(val: &T) -> Box {
Box::new(ContentHandler::ok(serde_json::to_string(val).unwrap(), "application/json".to_owned()))
@@ -26,3 +26,11 @@ pub fn as_json(val: &T) -> Box {
pub fn as_json_error(val: &T) -> Box {
Box::new(ContentHandler::not_found(serde_json::to_string(val).unwrap(), "application/json".to_owned()))
}
+
+pub fn ping_response(local_domain: &str) -> Box {
+ 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")),
+ ]))
+}
diff --git a/dapps/src/api/types.rs b/dapps/src/api/types.rs
index d99d767eb..64697123b 100644
--- a/dapps/src/api/types.rs
+++ b/dapps/src/api/types.rs
@@ -19,5 +19,3 @@ include!("types.rs.in");
#[cfg(not(feature = "serde_macros"))]
include!(concat!(env!("OUT_DIR"), "/types.rs"));
-
-
diff --git a/dapps/src/api/types.rs.in b/dapps/src/api/types.rs.in
index 72a8cd221..a7961a144 100644
--- a/dapps/src/api/types.rs.in
+++ b/dapps/src/api/types.rs.in
@@ -48,4 +48,3 @@ pub struct ApiError {
pub detail: String,
}
-
diff --git a/dapps/src/handlers/echo.rs b/dapps/src/handlers/echo.rs
new file mode 100644
index 000000000..b2ea8f580
--- /dev/null
+++ b/dapps/src/handlers/echo.rs
@@ -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 .
+
+//! 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,
+ content: String,
+ cors: Cors,
+ handler: Option,
+}
+
+impl EchoHandler {
+
+ pub fn cors(safe_origins: Vec) -> Self {
+ EchoHandler {
+ safe_origins: safe_origins,
+ content: String::new(),
+ cors: Cors::Forbidden,
+ handler: None,
+ }
+ }
+
+ fn cors_header(&self, origin: Option) -> 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 for EchoHandler {
+ fn on_request(&mut self, request: server::Request) -> 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) -> 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) -> 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);
+}
diff --git a/dapps/src/handlers/mod.rs b/dapps/src/handlers/mod.rs
index 933538655..5fa9fda95 100644
--- a/dapps/src/handlers/mod.rs
+++ b/dapps/src/handlers/mod.rs
@@ -17,9 +17,41 @@
//! Hyper handlers implementations.
mod auth;
+mod echo;
mod content;
mod redirect;
pub use self::auth::AuthRequiredHandler;
+pub use self::echo::EchoHandler;
pub use self::content::ContentHandler;
pub use self::redirect::Redirection;
+
+use url::Url;
+use hyper::{server, header, net, uri};
+
+pub fn extract_url(req: &server::Request) -> Option {
+ 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::() {
+ 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,
+ }
+}
+
diff --git a/dapps/src/lib.rs b/dapps/src/lib.rs
index 1c550fb07..ccdf172ce 100644
--- a/dapps/src/lib.rs
+++ b/dapps/src/lib.rs
@@ -45,8 +45,9 @@
#[macro_use]
extern crate log;
-extern crate url;
+extern crate url as url_lib;
extern crate hyper;
+extern crate unicase;
extern crate serde;
extern crate serde_json;
extern crate jsonrpc_core;
@@ -63,6 +64,7 @@ mod handlers;
mod rpc;
mod api;
mod proxypac;
+mod url;
use std::sync::{Arc, Mutex};
use std::net::SocketAddr;
@@ -121,7 +123,7 @@ impl Server {
let special = Arc::new({
let mut special = HashMap::new();
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
});
diff --git a/dapps/src/router/mod.rs b/dapps/src/router/mod.rs
index f04c8b614..8fbb37cf3 100644
--- a/dapps/src/router/mod.rs
+++ b/dapps/src/router/mod.rs
@@ -17,22 +17,18 @@
//! Router implementation
//! Processes request handling authorization and dispatching it to proper application.
-mod url;
pub mod auth;
use DAPPS_DOMAIN;
use std::sync::Arc;
use std::collections::HashMap;
-use url::Host;
-use hyper;
-use hyper::{server, uri, header};
-use hyper::{Next, Encoder, Decoder};
+use url::{Url, Host};
+use hyper::{self, server, Next, Encoder, Decoder};
use hyper::net::HttpStream;
use apps;
use endpoint::{Endpoint, Endpoints, EndpointPath};
-use self::url::Url;
+use handlers::{Redirection, extract_url};
use self::auth::{Authorization, Authorized};
-use handlers::Redirection;
/// Special endpoints are accessible on every domain (every dapp)
#[derive(Debug, PartialEq, Hash, Eq)]
@@ -123,32 +119,6 @@ impl Router {
}
}
-fn extract_url(req: &server::Request) -> Option {
- 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::() {
- 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) -> (Option, SpecialEndpoint) {
fn special_endpoint(url: &Url) -> SpecialEndpoint {
if url.path.len() <= 1 {
diff --git a/dapps/src/router/url.rs b/dapps/src/url.rs
similarity index 96%
rename from dapps/src/router/url.rs
rename to dapps/src/url.rs
index b96168239..c9eb2f78f 100644
--- a/dapps/src/router/url.rs
+++ b/dapps/src/url.rs
@@ -16,14 +16,14 @@
//! HTTP/HTTPS URL type. Based on URL type from Iron library.
-use url::Host;
-use url::{self};
+use url_lib::{self};
+pub use url_lib::Host;
/// HTTP/HTTPS URL type for Iron.
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Url {
/// Raw url of url
- pub raw: url::Url,
+ pub raw: url_lib::Url,
/// The host field of the URL, probably a domain.
pub host: Host,
@@ -62,14 +62,14 @@ impl Url {
/// See: http://url.spec.whatwg.org/#special-scheme
pub fn parse(input: &str) -> Result {
// 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),
Err(e) => Err(format!("{}", e))
}
}
/// Create a `Url` from a `rust-url` `Url`.
- pub fn from_generic_url(raw_url: url::Url) -> Result {
+ pub fn from_generic_url(raw_url: url_lib::Url) -> Result {
// Map empty usernames to None.
let username = match raw_url.username() {
"" => None,
diff --git a/util/src/io/service.rs b/util/src/io/service.rs
index 07e60f766..1e2169808 100644
--- a/util/src/io/service.rs
+++ b/util/src/io/service.rs
@@ -248,6 +248,13 @@ impl Handler for IoManager where Message: Send + Clone + Sync
IoMessage::RemoveHandler { handler_id } => {
// TODO: flush event loop
self.handlers.remove(handler_id);
+ // unregister timers
+ let mut timers = self.timers.write().unwrap();
+ let to_remove: Vec<_> = timers.keys().cloned().filter(|timer_id| timer_id / TOKENS_PER_HANDLER == handler_id).collect();
+ for timer_id in to_remove {
+ let timer = timers.remove(&timer_id).expect("to_remove only contains keys from timers; qed");
+ event_loop.clear_timeout(timer.timeout);
+ }
},
IoMessage::AddTimer { handler_id, token, delay } => {
let timer_id = token + handler_id * TOKENS_PER_HANDLER;