Missing changes required to make new UI work (#2793)

* Getting rid of old dapps

* Updating proxypac and allowing home.parity on signer

* CORS support for API

* Fixing CORS - origin is sent with protocol

* Fixing signer with proxy

* Fixing grumbles

* Fix expect msg [ci:skip]
This commit is contained in:
Tomasz Drwięga 2016-10-22 15:21:41 +02:00 committed by Gav Wood
parent 9150fce2f1
commit 3ff1ca81f4
22 changed files with 370 additions and 274 deletions

31
Cargo.lock generated
View File

@ -328,6 +328,7 @@ name = "ethcore-dapps"
version = "1.4.0" version = "1.4.0"
dependencies = [ dependencies = [
"clippy 0.0.90 (registry+https://github.com/rust-lang/crates.io-index)", "clippy 0.0.90 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"ethabi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "ethabi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"ethcore-devtools 1.4.0", "ethcore-devtools 1.4.0",
"ethcore-rpc 1.4.0", "ethcore-rpc 1.4.0",
@ -340,9 +341,7 @@ dependencies = [
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "mime 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
"parity-dapps-glue 1.4.0", "parity-dapps-glue 1.4.0",
"parity-dapps-home 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
"parity-ui 1.4.0", "parity-ui 1.4.0",
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
@ -854,7 +853,7 @@ dependencies = [
[[package]] [[package]]
name = "jsonrpc-http-server" name = "jsonrpc-http-server"
version = "6.1.1" version = "6.1.1"
source = "git+https://github.com/ethcore/jsonrpc-http-server.git#ee72e4778583daf901b5692468fc622f46abecb6" source = "git+https://github.com/ethcore/jsonrpc-http-server.git#cd6d4cb37d672cc3057aecd0692876f9e85f3ba5"
dependencies = [ dependencies = [
"hyper 0.9.4 (git+https://github.com/ethcore/hyper)", "hyper 0.9.4 (git+https://github.com/ethcore/hyper)",
"jsonrpc-core 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1175,20 +1174,6 @@ name = "owning_ref"
version = "0.2.2" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "parity-dapps"
version = "1.4.0"
source = "git+https://github.com/ethcore/parity-ui.git#8b1c31319228ad4cf9bd4ae740a0b933aa9e19c7"
dependencies = [
"aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
"quasi 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"quasi_codegen 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"syntex 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
"syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "parity-dapps-glue" name = "parity-dapps-glue"
version = "1.4.0" version = "1.4.0"
@ -1202,14 +1187,6 @@ dependencies = [
"syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "parity-dapps-home"
version = "1.4.0"
source = "git+https://github.com/ethcore/parity-ui.git#8b1c31319228ad4cf9bd4ae740a0b933aa9e19c7"
dependencies = [
"parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)",
]
[[package]] [[package]]
name = "parity-ui" name = "parity-ui"
version = "1.4.0" version = "1.4.0"
@ -1884,7 +1861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "ws" name = "ws"
version = "0.5.2" version = "0.5.2"
source = "git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable#609b21fdab96c8fffedec8699755ce3bea9454cb" source = "git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable#e3d21c119350e753fdf4475b8cd88103b2280540"
dependencies = [ dependencies = [
"bytes 0.4.0-dev (git+https://github.com/carllerche/bytes)", "bytes 0.4.0-dev (git+https://github.com/carllerche/bytes)",
"httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
@ -2016,8 +1993,6 @@ dependencies = [
"checksum number_prefix 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "084d05f4bf60621a9ac9bde941a410df548f4de9545f06e5ee9d3aef4b97cd77" "checksum number_prefix 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "084d05f4bf60621a9ac9bde941a410df548f4de9545f06e5ee9d3aef4b97cd77"
"checksum odds 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "b28c06e81b0f789122d415d6394b5fe849bde8067469f4c2980d3cdc10c78ec1" "checksum odds 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "b28c06e81b0f789122d415d6394b5fe849bde8067469f4c2980d3cdc10c78ec1"
"checksum owning_ref 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8d91377085359426407a287ab16884a0111ba473aa6844ff01d4ec20ce3d75e7" "checksum owning_ref 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8d91377085359426407a287ab16884a0111ba473aa6844ff01d4ec20ce3d75e7"
"checksum parity-dapps 1.4.0 (git+https://github.com/ethcore/parity-ui.git)" = "<none>"
"checksum parity-dapps-home 1.4.0 (git+https://github.com/ethcore/parity-ui.git)" = "<none>"
"checksum parking_lot 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "968f685642555d2f7e202c48b8b11de80569e9bfea817f7f12d7c61aac62d4e6" "checksum parking_lot 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "968f685642555d2f7e202c48b8b11de80569e9bfea817f7f12d7c61aac62d4e6"
"checksum parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "dbc5847584161f273e69edc63c1a86254a22f570a0b5dd87aa6f9773f6f7d125" "checksum parking_lot 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "dbc5847584161f273e69edc63c1a86254a22f570a0b5dd87aa6f9773f6f7d125"
"checksum parking_lot_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fb1b97670a2ffadce7c397fb80a3d687c4f3060140b885621ef1653d0e5d5068" "checksum parking_lot_core 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fb1b97670a2ffadce7c397fb80a3d687c4f3060140b885621ef1653d0e5d5068"

View File

@ -11,6 +11,7 @@ build = "build.rs"
[dependencies] [dependencies]
rand = "0.3.14" rand = "0.3.14"
log = "0.3" log = "0.3"
env_logger = "0.3"
jsonrpc-core = "3.0" jsonrpc-core = "3.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" }
@ -23,17 +24,13 @@ serde_macros = { version = "0.8", optional = true }
zip = { version = "0.1", default-features = false } zip = { version = "0.1", default-features = false }
ethabi = "0.2.2" ethabi = "0.2.2"
linked-hash-map = "0.3" linked-hash-map = "0.3"
mime = "0.2"
ethcore-devtools = { path = "../devtools" } ethcore-devtools = { path = "../devtools" }
ethcore-rpc = { path = "../rpc" } ethcore-rpc = { path = "../rpc" }
ethcore-util = { path = "../util" } ethcore-util = { path = "../util" }
fetch = { path = "../util/fetch" } fetch = { path = "../util/fetch" }
parity-ui = { path = "./ui" } parity-ui = { path = "./ui" }
parity-dapps-glue = { path = "./js-glue" } parity-dapps-glue = { path = "./js-glue" }
mime = "0.2"
### DEPRECATED
parity-dapps = { git = "https://github.com/ethcore/parity-ui.git", version = "1.4" }
parity-dapps-home = { git = "https://github.com/ethcore/parity-ui.git", version = "1.4" }
### /DEPRECATED
mime_guess = { version = "1.6.1" } mime_guess = { version = "1.6.1" }
clippy = { version = "0.0.90", optional = true} clippy = { version = "0.0.90", optional = true}
@ -46,7 +43,4 @@ default = ["serde_codegen"]
nightly = ["serde_macros"] nightly = ["serde_macros"]
dev = ["clippy", "ethcore-rpc/dev", "ethcore-util/dev"] dev = ["clippy", "ethcore-rpc/dev", "ethcore-util/dev"]
use-precompiled-js = [ use-precompiled-js = ["parity-ui/use-precompiled-js"]
"parity-ui/use-precompiled-js",
"parity-dapps-home/use-precompiled-js",
]

View File

@ -15,24 +15,31 @@
// 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 unicase::UniCase;
use hyper::{server, net, Decoder, Encoder, Next, Control}; use hyper::{server, net, Decoder, Encoder, Next, Control};
use hyper::header;
use hyper::method::Method;
use hyper::header::AccessControlAllowOrigin;
use api::types::{App, ApiError}; use api::types::{App, ApiError};
use api::response::{as_json, as_json_error, ping_response}; use api::response;
use apps::fetcher::ContentFetcher;
use handlers::extract_url; use handlers::extract_url;
use endpoint::{Endpoint, Endpoints, Handler, EndpointPath}; use endpoint::{Endpoint, Endpoints, Handler, EndpointPath};
use apps::fetcher::ContentFetcher; use jsonrpc_http_server::cors;
#[derive(Clone)] #[derive(Clone)]
pub struct RestApi { pub struct RestApi {
local_domain: String, cors_domains: Option<Vec<AccessControlAllowOrigin>>,
endpoints: Arc<Endpoints>, endpoints: Arc<Endpoints>,
fetcher: Arc<ContentFetcher>, fetcher: Arc<ContentFetcher>,
} }
impl RestApi { impl RestApi {
pub fn new(local_domain: String, endpoints: Arc<Endpoints>, fetcher: Arc<ContentFetcher>) -> Box<Endpoint> { pub fn new(cors_domains: Vec<String>, endpoints: Arc<Endpoints>, fetcher: Arc<ContentFetcher>) -> Box<Endpoint> {
Box::new(RestApi { Box::new(RestApi {
local_domain: local_domain, cors_domains: Some(cors_domains.into_iter().map(AccessControlAllowOrigin::Value).collect()),
endpoints: endpoints, endpoints: endpoints,
fetcher: fetcher, fetcher: fetcher,
}) })
@ -53,6 +60,7 @@ impl Endpoint for RestApi {
struct RestApiRouter { struct RestApiRouter {
api: RestApi, api: RestApi,
origin: Option<String>,
path: Option<EndpointPath>, path: Option<EndpointPath>,
control: Option<Control>, control: Option<Control>,
handler: Box<Handler>, handler: Box<Handler>,
@ -62,9 +70,10 @@ impl RestApiRouter {
fn new(api: RestApi, path: EndpointPath, control: Control) -> Self { fn new(api: RestApi, path: EndpointPath, control: Control) -> Self {
RestApiRouter { RestApiRouter {
path: Some(path), path: Some(path),
origin: None,
control: Some(control), control: Some(control),
api: api, api: api,
handler: as_json_error(&ApiError { handler: response::as_json_error(&ApiError {
code: "404".into(), code: "404".into(),
title: "Not Found".into(), title: "Not Found".into(),
detail: "Resource you requested has not been found.".into(), detail: "Resource you requested has not been found.".into(),
@ -80,11 +89,40 @@ impl RestApiRouter {
_ => None _ => None
} }
} }
/// Returns basic headers for a response (it may be overwritten by the handler)
fn response_headers(&self) -> header::Headers {
let mut headers = header::Headers::new();
headers.set(header::AccessControlAllowCredentials);
headers.set(header::AccessControlAllowMethods(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()),
]));
if let Some(cors_header) = cors::get_cors_header(&self.api.cors_domains, &self.origin) {
headers.set(cors_header);
}
headers
}
} }
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.origin = cors::read_origin(&request);
if let Method::Options = *request.method() {
self.handler = response::empty();
return Next::write();
}
let url = extract_url(&request); let url = extract_url(&request);
if url.is_none() { if url.is_none() {
// Just return 404 if we can't parse URL // Just return 404 if we can't parse URL
@ -99,11 +137,11 @@ impl server::Handler<net::HttpStream> for RestApiRouter {
let hash = url.path.get(2).map(|v| v.as_str()); let hash = url.path.get(2).map(|v| v.as_str());
// at this point path.app_id contains 'api', adjust it to the hash properly, otherwise // at this point path.app_id contains 'api', adjust it to the hash properly, otherwise
// we will try and retrieve 'api' as the hash when doing the /api/content route // we will try and retrieve 'api' as the hash when doing the /api/content route
if let Some(hash) = hash.clone() { path.app_id = hash.to_owned() } if let Some(ref hash) = hash { path.app_id = hash.clone().to_owned() }
let handler = endpoint.and_then(|v| match v { let handler = endpoint.and_then(|v| match v {
"apps" => Some(as_json(&self.api.list_apps())), "apps" => Some(response::as_json(&self.api.list_apps())),
"ping" => Some(ping_response(&self.api.local_domain)), "ping" => Some(response::ping()),
"content" => self.resolve_content(hash, path, control), "content" => self.resolve_content(hash, path, control),
_ => None _ => None
}); });
@ -121,6 +159,7 @@ impl server::Handler<net::HttpStream> for RestApiRouter {
} }
fn on_response(&mut self, res: &mut server::Response) -> Next { fn on_response(&mut self, res: &mut server::Response) -> Next {
*res.headers_mut() = self.response_headers();
self.handler.on_response(res) self.handler.on_response(res)
} }

0
dapps/src/api/cors.rs Normal file
View File

View File

@ -19,6 +19,10 @@ use serde_json;
use endpoint::Handler; use endpoint::Handler;
use handlers::{ContentHandler, EchoHandler}; use handlers::{ContentHandler, EchoHandler};
pub fn empty() -> Box<Handler> {
Box::new(ContentHandler::ok("".into(), mime!(Text/Plain)))
}
pub fn as_json<T: Serialize>(val: &T) -> Box<Handler> { pub fn as_json<T: Serialize>(val: &T) -> Box<Handler> {
let json = serde_json::to_string(val) let json = serde_json::to_string(val)
.expect("serialization to string is infallible; qed"); .expect("serialization to string is infallible; qed");
@ -31,10 +35,6 @@ pub fn as_json_error<T: Serialize>(val: &T) -> Box<Handler> {
Box::new(ContentHandler::not_found(json, mime!(Application/Json))) Box::new(ContentHandler::not_found(json, mime!(Application/Json)))
} }
pub fn ping_response(local_domain: &str) -> Box<Handler> { pub fn ping() -> Box<Handler> {
Box::new(EchoHandler::cors(vec![ Box::new(EchoHandler::default())
format!("http://{}", local_domain),
// Allow CORS calls also for localhost
format!("http://{}", local_domain.replace("127.0.0.1", "localhost")),
]))
} }

View File

@ -17,8 +17,7 @@
use endpoint::{Endpoints, Endpoint}; use endpoint::{Endpoints, Endpoint};
use page::PageEndpoint; use page::PageEndpoint;
use proxypac::ProxyPac; use proxypac::ProxyPac;
use parity_dapps::{self, WebApp}; use parity_dapps::WebApp;
use parity_dapps_glue::WebApp as NewWebApp;
mod cache; mod cache;
mod fs; mod fs;
@ -26,17 +25,14 @@ pub mod urlhint;
pub mod fetcher; pub mod fetcher;
pub mod manifest; pub mod manifest;
extern crate parity_dapps_home;
extern crate parity_ui; extern crate parity_ui;
pub const HOME_PAGE: &'static str = "home";
pub const DAPPS_DOMAIN : &'static str = ".parity"; pub const DAPPS_DOMAIN : &'static str = ".parity";
pub const RPC_PATH : &'static str = "rpc"; pub const RPC_PATH : &'static str = "rpc";
pub const API_PATH : &'static str = "api"; pub const API_PATH : &'static str = "api";
pub const UTILS_PATH : &'static str = "parity-utils"; pub const UTILS_PATH : &'static str = "parity-utils";
pub fn main_page() -> &'static str {
"home"
}
pub fn redirection_address(using_dapps_domains: bool, app_id: &str) -> String { pub fn redirection_address(using_dapps_domains: bool, app_id: &str) -> String {
if using_dapps_domains { if using_dapps_domains {
format!("http://{}{}/", app_id, DAPPS_DOMAIN) format!("http://{}{}/", app_id, DAPPS_DOMAIN)
@ -46,7 +42,7 @@ pub fn redirection_address(using_dapps_domains: bool, app_id: &str) -> String {
} }
pub fn utils() -> Box<Endpoint> { pub fn utils() -> Box<Endpoint> {
Box::new(PageEndpoint::with_prefix(parity_dapps_home::App::default(), UTILS_PATH.to_owned())) Box::new(PageEndpoint::with_prefix(parity_ui::App::default(), UTILS_PATH.to_owned()))
} }
pub fn all_endpoints(dapps_path: String, signer_port: Option<u16>) -> Endpoints { pub fn all_endpoints(dapps_path: String, signer_port: Option<u16>) -> Endpoints {
@ -54,64 +50,21 @@ pub fn all_endpoints(dapps_path: String, signer_port: Option<u16>) -> Endpoints
let mut pages = fs::local_endpoints(dapps_path); let mut pages = fs::local_endpoints(dapps_path);
// NOTE [ToDr] Dapps will be currently embeded on 8180 // NOTE [ToDr] Dapps will be currently embeded on 8180
pages.insert("ui".into(), Box::new( insert::<parity_ui::App>(&mut pages, "ui", Embeddable::Yes(signer_port));
PageEndpoint::new_safe_to_embed(NewUi::default(), signer_port) pages.insert("proxy".into(), ProxyPac::boxed(signer_port));
));
pages.insert("proxy".into(), ProxyPac::boxed());
insert::<parity_dapps_home::App>(&mut pages, "home");
pages pages
} }
fn insert<T : WebApp + Default + 'static>(pages: &mut Endpoints, id: &str) { fn insert<T : WebApp + Default + 'static>(pages: &mut Endpoints, id: &str, embed_at: Embeddable) {
pages.insert(id.to_owned(), Box::new(PageEndpoint::new(T::default()))); pages.insert(id.to_owned(), Box::new(match embed_at {
Embeddable::Yes(port) => PageEndpoint::new_safe_to_embed(T::default(), port),
Embeddable::No => PageEndpoint::new(T::default()),
}));
} }
// TODO [ToDr] Temporary wrapper until we get rid of old built-ins. enum Embeddable {
use std::collections::HashMap; Yes(Option<u16>),
#[allow(dead_code)]
struct NewUi { No,
app: parity_ui::App,
files: HashMap<&'static str, parity_dapps::File>,
}
impl Default for NewUi {
fn default() -> Self {
let app = parity_ui::App::default();
let files = {
let mut files = HashMap::new();
for (k, v) in &app.files {
files.insert(*k, parity_dapps::File {
path: v.path,
content: v.content,
content_type: v.content_type,
});
}
files
};
NewUi {
app: app,
files: files,
}
}
}
impl WebApp for NewUi {
fn file(&self, path: &str) -> Option<&parity_dapps::File> {
self.files.get(path)
}
fn info(&self) -> parity_dapps::Info {
let info = self.app.info();
parity_dapps::Info {
name: info.name,
version: info.version,
author: info.author,
description: info.description,
icon_url: info.icon_url,
}
}
} }

View File

@ -17,76 +17,19 @@
//! Echo Handler //! Echo Handler
use std::io::Read; use std::io::Read;
use hyper::{header, server, Decoder, Encoder, Next}; use hyper::{server, Decoder, Encoder, Next};
use hyper::method::Method;
use hyper::net::HttpStream; use hyper::net::HttpStream;
use unicase::UniCase;
use super::ContentHandler; use super::ContentHandler;
#[derive(Debug, PartialEq)] #[derive(Default)]
/// 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 { pub struct EchoHandler {
safe_origins: Vec<String>,
content: String, content: String,
cors: Cors,
handler: Option<ContentHandler>, 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 { impl server::Handler<HttpStream> for EchoHandler {
fn on_request(&mut self, request: server::Request<HttpStream>) -> Next { fn on_request(&mut self, _: server::Request<HttpStream>) -> Next {
let origin = request.headers().get_raw("origin") Next::read()
.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(), mime!(Text/Plain)));
Next::write()
} else {
Next::read()
}
} }
fn on_request_readable(&mut self, decoder: &mut Decoder<HttpStream>) -> Next { fn on_request_readable(&mut self, decoder: &mut Decoder<HttpStream>) -> Next {
@ -104,16 +47,6 @@ impl server::Handler<HttpStream> for EchoHandler {
} }
fn on_response(&mut self, res: &mut server::Response) -> Next { 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() self.handler.as_mut()
.expect("handler always set in on_request, which is before now; qed") .expect("handler always set in on_request, which is before now; qed")
.on_response(res) .on_response(res)
@ -125,28 +58,3 @@ impl server::Handler<HttpStream> for EchoHandler {
.on_response_writable(encoder) .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

@ -30,6 +30,7 @@ pub use self::fetch::{ContentFetcherHandler, ContentValidator, FetchControl};
use url::Url; use url::Url;
use hyper::{server, header, net, uri}; use hyper::{server, header, net, uri};
use signer_address;
/// Adds security-related headers to the Response. /// Adds security-related headers to the Response.
pub fn add_security_headers(headers: &mut header::Headers, embeddable_at: Option<u16>) { pub fn add_security_headers(headers: &mut header::Headers, embeddable_at: Option<u16>) {
@ -40,7 +41,7 @@ pub fn add_security_headers(headers: &mut header::Headers, embeddable_at: Option
if let Some(port) = embeddable_at { if let Some(port) = embeddable_at {
headers.set_raw( headers.set_raw(
"X-Frame-Options", "X-Frame-Options",
vec![format!("ALLOW-FROM http://127.0.0.1:{}", port).into_bytes()] vec![format!("ALLOW-FROM http://{}", signer_address(port)).into_bytes()]
); );
} else { } else {
// TODO [ToDr] Should we be more strict here (DENY?)? // TODO [ToDr] Should we be more strict here (DENY?)?

View File

@ -59,19 +59,17 @@ extern crate ethcore_rpc;
extern crate ethcore_util as util; extern crate ethcore_util as util;
extern crate linked_hash_map; extern crate linked_hash_map;
extern crate fetch; extern crate fetch;
extern crate parity_dapps_glue as parity_dapps;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
#[macro_use] #[macro_use]
extern crate mime; extern crate mime;
#[cfg(test)] #[cfg(test)]
extern crate ethcore_devtools as devtools; extern crate ethcore_devtools as devtools;
#[cfg(test)]
extern crate env_logger;
extern crate parity_dapps_glue;
// TODO [ToDr] - Deprecate when we get rid of old dapps.
extern crate parity_dapps;
mod endpoint; mod endpoint;
mod apps; mod apps;
@ -95,7 +93,7 @@ use jsonrpc_core::{IoHandler, IoDelegate};
use router::auth::{Authorization, NoAuth, HttpBasicAuth}; use router::auth::{Authorization, NoAuth, HttpBasicAuth};
use ethcore_rpc::Extendable; use ethcore_rpc::Extendable;
static DAPPS_DOMAIN : &'static str = ".parity"; use self::apps::{HOME_PAGE, DAPPS_DOMAIN};
/// Indicates sync status /// Indicates sync status
pub trait SyncStatus: Send + Sync { pub trait SyncStatus: Send + Sync {
@ -197,6 +195,17 @@ impl Server {
Some(allowed) Some(allowed)
} }
/// Returns a list of CORS domains for API endpoint.
fn cors_domains(signer_port: Option<u16>) -> Vec<String> {
match signer_port {
Some(port) => vec![
format!("http://{}{}", HOME_PAGE, DAPPS_DOMAIN),
format!("http://{}", signer_address(port)),
],
None => vec![],
}
}
fn start_http<A: Authorization + 'static>( fn start_http<A: Authorization + 'static>(
addr: &SocketAddr, addr: &SocketAddr,
hosts: Option<Vec<String>>, hosts: Option<Vec<String>>,
@ -210,14 +219,16 @@ impl Server {
let panic_handler = Arc::new(Mutex::new(None)); let panic_handler = Arc::new(Mutex::new(None));
let authorization = Arc::new(authorization); let authorization = Arc::new(authorization);
let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(apps::urlhint::URLHintContract::new(registrar), sync_status)); let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(apps::urlhint::URLHintContract::new(registrar), sync_status));
let endpoints = Arc::new(apps::all_endpoints(dapps_path, signer_port)); let endpoints = Arc::new(apps::all_endpoints(dapps_path, signer_port.clone()));
let cors_domains = Self::cors_domains(signer_port);
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::Utils, apps::utils()); special.insert(router::SpecialEndpoint::Utils, apps::utils());
special.insert( special.insert(
router::SpecialEndpoint::Api, router::SpecialEndpoint::Api,
api::RestApi::new(format!("{}", addr), endpoints.clone(), content_fetcher.clone()) api::RestApi::new(cors_domains, endpoints.clone(), content_fetcher.clone())
); );
special special
}); });
@ -226,7 +237,7 @@ impl Server {
try!(hyper::Server::http(addr)) try!(hyper::Server::http(addr))
.handle(move |ctrl| router::Router::new( .handle(move |ctrl| router::Router::new(
ctrl, ctrl,
apps::main_page(), signer_port.clone(),
content_fetcher.clone(), content_fetcher.clone(),
endpoints.clone(), endpoints.clone(),
special.clone(), special.clone(),
@ -290,6 +301,10 @@ pub fn random_filename() -> String {
rng.gen_ascii_chars().take(12).collect() rng.gen_ascii_chars().take(12).collect()
} }
fn signer_address(port: u16) -> String {
format!("127.0.0.1:{}", port)
}
#[cfg(test)] #[cfg(test)]
mod util_tests { mod util_tests {
use super::Server; use super::Server;
@ -309,4 +324,17 @@ mod util_tests {
assert_eq!(address, Some(vec!["localhost".into(), "127.0.0.1".into()])); assert_eq!(address, Some(vec!["localhost".into(), "127.0.0.1".into()]));
assert_eq!(some, Some(vec!["ethcore.io".into(), "localhost".into(), "127.0.0.1".into()])); assert_eq!(some, Some(vec!["ethcore.io".into(), "localhost".into(), "127.0.0.1".into()]));
} }
#[test]
fn should_return_cors_domains() {
// given
// when
let none = Server::cors_domains(None);
let some = Server::cors_domains(Some(18180));
// then
assert_eq!(none, Vec::<String>::new());
assert_eq!(some, vec!["http://home.parity".to_owned(), "http://127.0.0.1:18180".into()]);
}
} }

View File

@ -18,30 +18,45 @@
use endpoint::{Endpoint, Handler, EndpointPath}; use endpoint::{Endpoint, Handler, EndpointPath};
use handlers::ContentHandler; use handlers::ContentHandler;
use apps::DAPPS_DOMAIN; use apps::{HOME_PAGE, DAPPS_DOMAIN};
use signer_address;
pub struct ProxyPac; pub struct ProxyPac {
signer_port: Option<u16>,
}
impl ProxyPac { impl ProxyPac {
pub fn boxed() -> Box<Endpoint> { pub fn boxed(signer_port: Option<u16>) -> Box<Endpoint> {
Box::new(ProxyPac) Box::new(ProxyPac {
signer_port: signer_port
})
} }
} }
impl Endpoint for ProxyPac { impl Endpoint for ProxyPac {
fn to_handler(&self, path: EndpointPath) -> Box<Handler> { fn to_handler(&self, path: EndpointPath) -> Box<Handler> {
let signer = self.signer_port
.map(signer_address)
.unwrap_or_else(|| format!("{}:{}", path.host, path.port));
let content = format!( let content = format!(
r#" r#"
function FindProxyForURL(url, host) {{ function FindProxyForURL(url, host) {{
if (shExpMatch(host, "*{0}")) if (shExpMatch(host, "{0}{1}"))
{{ {{
return "PROXY {1}:{2}"; return "PROXY {4}";
}}
if (shExpMatch(host, "*{1}"))
{{
return "PROXY {2}:{3}";
}} }}
return "DIRECT"; return "DIRECT";
}} }}
"#, "#,
DAPPS_DOMAIN, path.host, path.port); HOME_PAGE, DAPPS_DOMAIN, path.host, path.port, signer);
Box::new(ContentHandler::ok(content, mime!(Application/Javascript))) Box::new(ContentHandler::ok(content, mime!(Application/Javascript)))
} }
} }

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use DAPPS_DOMAIN; use apps::DAPPS_DOMAIN;
use hyper::{server, header, StatusCode}; use hyper::{server, header, StatusCode};
use hyper::net::HttpStream; use hyper::net::HttpStream;

View File

@ -20,13 +20,13 @@
pub mod auth; pub mod auth;
mod host_validation; mod host_validation;
use DAPPS_DOMAIN; use signer_address;
use std::sync::Arc; use std::sync::Arc;
use std::collections::HashMap; use std::collections::HashMap;
use url::{Url, Host}; use url::{Url, Host};
use hyper::{self, server, Next, Encoder, Decoder, Control, StatusCode}; use hyper::{self, server, Next, Encoder, Decoder, Control, StatusCode};
use hyper::net::HttpStream; use hyper::net::HttpStream;
use apps; use apps::{self, DAPPS_DOMAIN};
use apps::fetcher::ContentFetcher; use apps::fetcher::ContentFetcher;
use endpoint::{Endpoint, Endpoints, EndpointPath}; use endpoint::{Endpoint, Endpoints, EndpointPath};
use handlers::{Redirection, extract_url, ContentHandler}; use handlers::{Redirection, extract_url, ContentHandler};
@ -43,7 +43,7 @@ pub enum SpecialEndpoint {
pub struct Router<A: Authorization + 'static> { pub struct Router<A: Authorization + 'static> {
control: Option<Control>, control: Option<Control>,
main_page: &'static str, signer_port: Option<u16>,
endpoints: Arc<Endpoints>, endpoints: Arc<Endpoints>,
fetch: Arc<ContentFetcher>, fetch: Arc<ContentFetcher>,
special: Arc<HashMap<SpecialEndpoint, Box<Endpoint>>>, special: Arc<HashMap<SpecialEndpoint, Box<Endpoint>>>,
@ -61,57 +61,78 @@ impl<A: Authorization + 'static> server::Handler<HttpStream> for Router<A> {
let endpoint = extract_endpoint(&url); let endpoint = extract_endpoint(&url);
let is_utils = endpoint.1 == SpecialEndpoint::Utils; let is_utils = endpoint.1 == SpecialEndpoint::Utils;
trace!(target: "dapps", "Routing request to {:?}. Details: {:?}", url, req);
// Validate Host header // Validate Host header
if let Some(ref hosts) = self.allowed_hosts { if let Some(ref hosts) = self.allowed_hosts {
trace!(target: "dapps", "Validating host headers against: {:?}", hosts);
let is_valid = is_utils || host_validation::is_valid(&req, hosts, self.endpoints.keys().cloned().collect()); let is_valid = is_utils || host_validation::is_valid(&req, hosts, self.endpoints.keys().cloned().collect());
if !is_valid { if !is_valid {
debug!(target: "dapps", "Rejecting invalid host header.");
self.handler = host_validation::host_invalid_response(); self.handler = host_validation::host_invalid_response();
return self.handler.on_request(req); return self.handler.on_request(req);
} }
} }
trace!(target: "dapps", "Checking authorization.");
// Check authorization // Check authorization
let auth = self.authorization.is_authorized(&req); let auth = self.authorization.is_authorized(&req);
if let Authorized::No(handler) = auth { if let Authorized::No(handler) = auth {
debug!(target: "dapps", "Authorization denied.");
self.handler = handler; self.handler = handler;
return self.handler.on_request(req); return self.handler.on_request(req);
} }
let control = self.control.take().expect("on_request is called only once; control is always defined at start; qed"); let control = self.control.take().expect("on_request is called only once; control is always defined at start; qed");
debug!(target: "dapps", "Handling endpoint request: {:?}", endpoint);
self.handler = match endpoint { self.handler = match endpoint {
// First check special endpoints // First check special endpoints
(ref path, ref endpoint) if self.special.contains_key(endpoint) => { (ref path, ref endpoint) if self.special.contains_key(endpoint) => {
trace!(target: "dapps", "Resolving to special endpoint.");
self.special.get(endpoint) self.special.get(endpoint)
.expect("special known to contain key; qed") .expect("special known to contain key; qed")
.to_async_handler(path.clone().unwrap_or_default(), control) .to_async_handler(path.clone().unwrap_or_default(), control)
}, },
// Then delegate to dapp // Then delegate to dapp
(Some(ref path), _) if self.endpoints.contains_key(&path.app_id) => { (Some(ref path), _) if self.endpoints.contains_key(&path.app_id) => {
trace!(target: "dapps", "Resolving to local/builtin dapp.");
self.endpoints.get(&path.app_id) self.endpoints.get(&path.app_id)
.expect("special known to contain key; qed") .expect("special known to contain key; qed")
.to_async_handler(path.clone(), control) .to_async_handler(path.clone(), control)
}, },
// Try to resolve and fetch the dapp // Try to resolve and fetch the dapp
(Some(ref path), _) if self.fetch.contains(&path.app_id) => { (Some(ref path), _) if self.fetch.contains(&path.app_id) => {
trace!(target: "dapps", "Resolving to fetchable content.");
self.fetch.to_async_handler(path.clone(), control) self.fetch.to_async_handler(path.clone(), control)
}, },
// 404 for non-existent content // 404 for non-existent content
(Some(ref path), _) if *req.method() == hyper::method::Method::Get => { (Some(_), _) if *req.method() == hyper::method::Method::Get => {
let address = apps::redirection_address(path.using_dapps_domains, self.main_page); trace!(target: "dapps", "Resolving to 404.");
Box::new(ContentHandler::error( Box::new(ContentHandler::error(
StatusCode::NotFound, StatusCode::NotFound,
"404 Not Found", "404 Not Found",
"Requested content was not found.", "Requested content was not found.",
Some(&format!("Go back to the <a href=\"{}\">Home Page</a>.", address)) None,
)) ))
}, },
// Redirect any GET request to home. // Redirect any other GET request to signer.
_ if *req.method() == hyper::method::Method::Get => { _ if *req.method() == hyper::method::Method::Get => {
let address = apps::redirection_address(false, self.main_page); if let Some(port) = self.signer_port {
Redirection::boxed(address.as_str()) trace!(target: "dapps", "Redirecting to signer interface.");
Redirection::boxed(&format!("http://{}", signer_address(port)))
} else {
trace!(target: "dapps", "Signer disabled, returning 404.");
Box::new(ContentHandler::error(
StatusCode::NotFound,
"404 Not Found",
"Your homepage is not available when Trusted Signer is disabled.",
Some("You can still access dapps by writing a correct address, though. Re-enabled Signer to get your homepage back."),
))
}
}, },
// RPC by default // RPC by default
_ => { _ => {
trace!(target: "dapps", "Resolving to RPC call.");
self.special.get(&SpecialEndpoint::Rpc) self.special.get(&SpecialEndpoint::Rpc)
.expect("RPC endpoint always stored; qed") .expect("RPC endpoint always stored; qed")
.to_async_handler(EndpointPath::default(), control) .to_async_handler(EndpointPath::default(), control)
@ -141,7 +162,7 @@ impl<A: Authorization + 'static> server::Handler<HttpStream> for Router<A> {
impl<A: Authorization> Router<A> { impl<A: Authorization> Router<A> {
pub fn new( pub fn new(
control: Control, control: Control,
main_page: &'static str, signer_port: Option<u16>,
content_fetcher: Arc<ContentFetcher>, content_fetcher: Arc<ContentFetcher>,
endpoints: Arc<Endpoints>, endpoints: Arc<Endpoints>,
special: Arc<HashMap<SpecialEndpoint, Box<Endpoint>>>, special: Arc<HashMap<SpecialEndpoint, Box<Endpoint>>>,
@ -154,7 +175,7 @@ impl<A: Authorization> Router<A> {
.to_handler(EndpointPath::default()); .to_handler(EndpointPath::default());
Router { Router {
control: Some(control), control: Some(control),
main_page: main_page, signer_port: signer_port,
endpoints: endpoints, endpoints: endpoints,
fetch: content_fetcher, fetch: content_fetcher,
special: special, special: special,

View File

@ -24,7 +24,7 @@ pub fn rpc(handler: Arc<IoHandler>, panic_handler: Arc<Mutex<Option<Box<Fn() ->
Box::new(RpcEndpoint { Box::new(RpcEndpoint {
handler: handler, handler: handler,
panic_handler: panic_handler, panic_handler: panic_handler,
cors_domain: Some(vec![AccessControlAllowOrigin::Null]), cors_domain: None,
// NOTE [ToDr] We don't need to do any hosts validation here. It's already done in router. // NOTE [ToDr] We don't need to do any hosts validation here. It's already done in router.
allowed_hosts: None, allowed_hosts: None,
}) })

View File

@ -34,7 +34,7 @@ fn should_return_error() {
// then // then
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned()); assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
assert_eq!(response.headers.get(0).unwrap(), "Content-Type: application/json"); assert_eq!(response.headers.get(3).unwrap(), "Content-Type: application/json");
assert_eq!(response.body, format!("58\n{}\n0\n\n", r#"{"code":"404","title":"Not Found","detail":"Resource you requested has not been found."}"#)); assert_eq!(response.body, format!("58\n{}\n0\n\n", r#"{"code":"404","title":"Not Found","detail":"Resource you requested has not been found."}"#));
assert_security_headers(&response.headers); assert_security_headers(&response.headers);
} }
@ -57,8 +57,8 @@ fn should_serve_apps() {
// then // then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.headers.get(0).unwrap(), "Content-Type: application/json"); assert_eq!(response.headers.get(3).unwrap(), "Content-Type: application/json");
assert!(response.body.contains("Parity Home Screen"), response.body); assert!(response.body.contains("Parity UI"), response.body);
assert_security_headers(&response.headers); assert_security_headers(&response.headers);
} }
@ -80,7 +80,7 @@ fn should_handle_ping() {
// then // then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.headers.get(0).unwrap(), "Content-Type: application/json"); assert_eq!(response.headers.get(3).unwrap(), "Content-Type: application/json");
assert_eq!(response.body, "0\n\n".to_owned()); assert_eq!(response.body, "0\n\n".to_owned());
assert_security_headers(&response.headers); assert_security_headers(&response.headers);
} }
@ -107,3 +107,54 @@ fn should_try_to_resolve_dapp() {
assert_security_headers(&response.headers); assert_security_headers(&response.headers);
} }
#[test]
fn should_return_signer_port_cors_headers() {
// given
let server = serve();
// when
let response = request(server,
"\
POST /api/ping HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Origin: http://127.0.0.1:18180\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert!(
response.headers_raw.contains("Access-Control-Allow-Origin: http://127.0.0.1:18180"),
"CORS header for signer missing: {:?}",
response.headers
);
}
#[test]
fn should_return_signer_port_cors_headers_for_home_parity() {
// given
let server = serve();
// when
let response = request(server,
"\
POST /api/ping HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Origin: http://home.parity\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert!(
response.headers_raw.contains("Access-Control-Allow-Origin: http://home.parity"),
"CORS header for home.parity missing: {:?}",
response.headers
);
}

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use tests::helpers::{serve_with_auth, request, assert_security_headers}; use tests::helpers::{serve_with_auth, request, assert_security_headers_for_embed};
#[test] #[test]
fn should_require_authorization() { fn should_require_authorization() {
@ -66,7 +66,7 @@ fn should_allow_on_valid_auth() {
// when // when
let response = request(server, let response = request(server,
"\ "\
GET /home/ HTTP/1.1\r\n\ GET /ui/ HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\ Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\ Connection: close\r\n\
Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l\r\n Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l\r\n
@ -76,5 +76,5 @@ fn should_allow_on_valid_auth() {
// then // then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_security_headers(&response.headers); assert_security_headers_for_embed(&response.headers);
} }

View File

@ -18,6 +18,7 @@ use std::env;
use std::str; use std::str;
use std::sync::Arc; use std::sync::Arc;
use rustc_serialize::hex::FromHex; use rustc_serialize::hex::FromHex;
use env_logger::LogBuilder;
use ServerBuilder; use ServerBuilder;
use Server; use Server;
@ -27,6 +28,7 @@ use devtools::http_client;
const REGISTRAR: &'static str = "8e4e9b13d4b45cb0befc93c3061b1408f67316b2"; const REGISTRAR: &'static str = "8e4e9b13d4b45cb0befc93c3061b1408f67316b2";
const URLHINT: &'static str = "deadbeefcafe0000000000000000000000000000"; const URLHINT: &'static str = "deadbeefcafe0000000000000000000000000000";
const SIGNER_PORT: u16 = 18180;
pub struct FakeRegistrar { pub struct FakeRegistrar {
pub calls: Arc<Mutex<Vec<(String, String)>>>, pub calls: Arc<Mutex<Vec<(String, String)>>>,
@ -58,11 +60,22 @@ impl ContractClient for FakeRegistrar {
} }
} }
fn init_logger() {
// Initialize logger
if let Ok(log) = env::var("RUST_LOG") {
let mut builder = LogBuilder::new();
builder.parse(&log);
builder.init().expect("Logger is initialized only once.");
}
}
pub fn init_server(hosts: Option<Vec<String>>) -> (Server, Arc<FakeRegistrar>) { pub fn init_server(hosts: Option<Vec<String>>) -> (Server, Arc<FakeRegistrar>) {
init_logger();
let registrar = Arc::new(FakeRegistrar::new()); let registrar = Arc::new(FakeRegistrar::new());
let mut dapps_path = env::temp_dir(); let mut dapps_path = env::temp_dir();
dapps_path.push("non-existent-dir-to-prevent-fs-files-from-loading"); dapps_path.push("non-existent-dir-to-prevent-fs-files-from-loading");
let builder = ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar.clone()); let mut builder = ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar.clone());
builder.with_signer_port(Some(SIGNER_PORT));
( (
builder.start_unsecured_http(&"127.0.0.1:0".parse().unwrap(), hosts).unwrap(), builder.start_unsecured_http(&"127.0.0.1:0".parse().unwrap(), hosts).unwrap(),
registrar, registrar,
@ -70,10 +83,12 @@ pub fn init_server(hosts: Option<Vec<String>>) -> (Server, Arc<FakeRegistrar>) {
} }
pub fn serve_with_auth(user: &str, pass: &str) -> Server { pub fn serve_with_auth(user: &str, pass: &str) -> Server {
init_logger();
let registrar = Arc::new(FakeRegistrar::new()); let registrar = Arc::new(FakeRegistrar::new());
let mut dapps_path = env::temp_dir(); let mut dapps_path = env::temp_dir();
dapps_path.push("non-existent-dir-to-prevent-fs-files-from-loading"); dapps_path.push("non-existent-dir-to-prevent-fs-files-from-loading");
let builder = ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar); let mut builder = ServerBuilder::new(dapps_path.to_str().unwrap().into(), registrar);
builder.with_signer_port(Some(SIGNER_PORT));
builder.start_basic_auth_http(&"127.0.0.1:0".parse().unwrap(), None, user, pass).unwrap() builder.start_basic_auth_http(&"127.0.0.1:0".parse().unwrap(), None, user, pass).unwrap()
} }
@ -94,5 +109,8 @@ pub fn request(server: Server, request: &str) -> http_client::Response {
} }
pub fn assert_security_headers(headers: &[String]) { pub fn assert_security_headers(headers: &[String]) {
http_client::assert_security_headers_present(headers) http_client::assert_security_headers_present(headers, None)
}
pub fn assert_security_headers_for_embed(headers: &[String]) {
http_client::assert_security_headers_present(headers, Some(SIGNER_PORT))
} }

View File

@ -33,7 +33,7 @@ fn should_redirect_to_home() {
// then // then
assert_eq!(response.status, "HTTP/1.1 302 Found".to_owned()); assert_eq!(response.status, "HTTP/1.1 302 Found".to_owned());
assert_eq!(response.headers.get(0).unwrap(), "Location: /home/"); assert_eq!(response.headers.get(0).unwrap(), "Location: http://127.0.0.1:18180");
} }
#[test] #[test]
@ -53,7 +53,7 @@ fn should_redirect_to_home_when_trailing_slash_is_missing() {
// then // then
assert_eq!(response.status, "HTTP/1.1 302 Found".to_owned()); assert_eq!(response.status, "HTTP/1.1 302 Found".to_owned());
assert_eq!(response.headers.get(0).unwrap(), "Location: /home/"); assert_eq!(response.headers.get(0).unwrap(), "Location: http://127.0.0.1:18180");
} }
#[test] #[test]
@ -73,7 +73,6 @@ fn should_display_404_on_invalid_dapp() {
// then // then
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned()); assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
assert!(response.body.contains("href=\"/home/"));
assert_security_headers(&response.headers); assert_security_headers(&response.headers);
} }
@ -94,7 +93,6 @@ fn should_display_404_on_invalid_dapp_with_domain() {
// then // then
assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned()); assert_eq!(response.status, "HTTP/1.1 404 Not Found".to_owned());
assert!(response.body.contains("href=\"http://home.parity/"));
assert_security_headers(&response.headers); assert_security_headers(&response.headers);
} }
@ -161,7 +159,7 @@ fn should_serve_proxy_pac() {
// then // then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, "86\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"*.parity\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:8080\";\n\t}\n\n\treturn \"DIRECT\";\n}\n\n0\n\n".to_owned()); assert_eq!(response.body, "D5\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"home.parity\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:18180\";\n\t}\n\n\tif (shExpMatch(host, \"*.parity\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:8080\";\n\t}\n\n\treturn \"DIRECT\";\n}\n\n0\n\n".to_owned());
assert_security_headers(&response.headers); assert_security_headers(&response.headers);
} }

View File

@ -45,7 +45,7 @@ fn should_allow_valid_host() {
// when // when
let response = request(server, let response = request(server,
"\ "\
GET /home/ HTTP/1.1\r\n\ GET /ui/ HTTP/1.1\r\n\
Host: localhost:8080\r\n\ Host: localhost:8080\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
@ -66,7 +66,7 @@ fn should_serve_dapps_domains() {
let response = request(server, let response = request(server,
"\ "\
GET / HTTP/1.1\r\n\ GET / HTTP/1.1\r\n\
Host: home.parity\r\n\ Host: ui.parity\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
{} {}
@ -98,3 +98,30 @@ fn should_allow_parity_utils_even_on_invalid_domain() {
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
} }
#[test]
fn should_not_return_cors_headers_for_rpc() {
// given
let server = serve_hosts(Some(vec!["localhost:8080".into()]));
// when
let response = request(server,
"\
POST /rpc HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Origin: null\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert!(
!response.headers_raw.contains("Access-Control-Allow-Origin"),
"CORS headers were not expected: {:?}",
response.headers
);
}

View File

@ -65,11 +65,18 @@ pub fn request(address: &SocketAddr, request: &str) -> Response {
} }
/// Check if all required security headers are present /// Check if all required security headers are present
pub fn assert_security_headers_present(headers: &[String]) { pub fn assert_security_headers_present(headers: &[String], port: Option<u16>) {
assert!( if let Some(port) = port {
headers.iter().find(|header| header.as_str() == "X-Frame-Options: SAMEORIGIN").is_some(), assert!(
"X-Frame-Options missing: {:?}", headers headers.iter().find(|header| header.as_str() == &format!("X-Frame-Options: ALLOW-FROM http://127.0.0.1:{}", port)).is_some(),
); "X-Frame-Options: ALLOW-FROM missing: {:?}", headers
);
} else {
assert!(
headers.iter().find(|header| header.as_str() == "X-Frame-Options: SAMEORIGIN").is_some(),
"X-Frame-Options: SAMEORIGIN missing: {:?}", headers
);
}
assert!( assert!(
headers.iter().find(|header| header.as_str() == "X-XSS-Protection: 1; mode=block").is_some(), headers.iter().find(|header| header.as_str() == "X-XSS-Protection: 1; mode=block").is_some(),
"X-XSS-Protection missing: {:?}", headers "X-XSS-Protection missing: {:?}", headers

View File

@ -65,11 +65,10 @@ pub fn setup_log(config: &Config) -> Result<Arc<RotatingLogger>, String> {
builder.filter(Some("rustls"), LogLevelFilter::Warn); builder.filter(Some("rustls"), LogLevelFilter::Warn);
builder.filter(None, LogLevelFilter::Info); builder.filter(None, LogLevelFilter::Info);
if env::var("RUST_LOG").is_ok() { if let Ok(lvl) = env::var("RUST_LOG") {
let lvl = &env::var("RUST_LOG").unwrap(); levels.push_str(&lvl);
levels.push_str(lvl);
levels.push_str(","); levels.push_str(",");
builder.parse(lvl); builder.parse(&lvl);
} }
if let Some(ref s) = config.mode { if let Some(ref s) = config.mode {
@ -119,7 +118,7 @@ pub fn setup_log(config: &Config) -> Result<Arc<RotatingLogger>, String> {
}; };
builder.format(format); builder.format(format);
builder.init().unwrap(); builder.init().expect("Logger initialized only once.");
Ok(logs) Ok(logs)
} }

View File

@ -81,7 +81,28 @@ fn should_reject_invalid_host() {
// then // then
assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned()); assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned());
assert!(response.body.contains("URL Blocked")); assert!(response.body.contains("URL Blocked"));
http_client::assert_security_headers_present(&response.headers); http_client::assert_security_headers_present(&response.headers, None);
}
#[test]
fn should_allow_home_parity_host() {
// given
let server = serve().0;
// when
let response = request(server,
"\
GET http://home.parity/ HTTP/1.1\r\n\
Host: home.parity\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
http_client::assert_security_headers_present(&response.headers, None);
} }
#[test] #[test]
@ -102,7 +123,27 @@ fn should_serve_styles_even_on_disallowed_domain() {
// then // then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
http_client::assert_security_headers_present(&response.headers); http_client::assert_security_headers_present(&response.headers, None);
}
#[test]
fn should_return_200_ok_for_connect_requests() {
// given
let server = serve().0;
// when
let response = request(server,
"\
CONNECT home.parity:8080 HTTP/1.1\r\n\
Host: home.parity\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
} }
#[test] #[test]
@ -126,7 +167,7 @@ fn should_block_if_authorization_is_incorrect() {
// then // then
assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned()); assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned());
http_client::assert_security_headers_present(&response.headers); http_client::assert_security_headers_present(&response.headers, None);
} }
#[test] #[test]
@ -205,5 +246,6 @@ fn should_allow_initial_connection_but_only_once() {
// then // then
assert_eq!(response1.status, "HTTP/1.1 101 Switching Protocols".to_owned()); assert_eq!(response1.status, "HTTP/1.1 101 Switching Protocols".to_owned());
assert_eq!(response2.status, "HTTP/1.1 403 FORBIDDEN".to_owned()); assert_eq!(response2.status, "HTTP/1.1 403 FORBIDDEN".to_owned());
http_client::assert_security_headers_present(&response2.headers); http_client::assert_security_headers_present(&response2.headers, None);
} }

View File

@ -63,6 +63,8 @@ mod ui {
} }
} }
const HOME_DOMAIN: &'static str = "home.parity";
fn origin_is_allowed(self_origin: &str, header: Option<&[u8]>) -> bool { fn origin_is_allowed(self_origin: &str, header: Option<&[u8]>) -> bool {
match header { match header {
None => false, None => false,
@ -72,6 +74,8 @@ fn origin_is_allowed(self_origin: &str, header: Option<&[u8]>) -> bool {
Some(ref origin) if origin.starts_with("chrome-extension://") => true, Some(ref origin) if origin.starts_with("chrome-extension://") => true,
Some(ref origin) if origin.starts_with(self_origin) => true, Some(ref origin) if origin.starts_with(self_origin) => true,
Some(ref origin) if origin.starts_with(&format!("http://{}", self_origin)) => true, Some(ref origin) if origin.starts_with(&format!("http://{}", self_origin)) => true,
Some(ref origin) if origin.starts_with(HOME_DOMAIN) => true,
Some(ref origin) if origin.starts_with(&format!("http://{}", HOME_DOMAIN)) => true,
_ => false _ => false
} }
} }
@ -134,13 +138,20 @@ pub struct Session {
impl ws::Handler for Session { impl ws::Handler for Session {
#[cfg_attr(feature="dev", allow(collapsible_if))] #[cfg_attr(feature="dev", allow(collapsible_if))]
fn on_request(&mut self, req: &ws::Request) -> ws::Result<(ws::Response)> { fn on_request(&mut self, req: &ws::Request) -> ws::Result<(ws::Response)> {
let origin = req.header("origin").or_else(|| req.header("Origin")).map(|x| &x[..]); trace!(target: "signer", "Handling request: {:?}", req);
let host = req.header("host").or_else(|| req.header("Host")).map(|x| &x[..]);
// TODO [ToDr] ws server is not handling proxied requests correctly:
// Trim domain name from resource part:
let resource = req.resource().trim_left_matches(&format!("http://{}", HOME_DOMAIN));
// Styles file is allowed for error pages to display nicely. // Styles file is allowed for error pages to display nicely.
let is_styles_file = req.resource() == "/styles.css"; let is_styles_file = resource == "/styles.css";
// Check request origin and host header. // Check request origin and host header.
if !self.skip_origin_validation { if !self.skip_origin_validation {
let origin = req.header("origin").or_else(|| req.header("Origin")).map(|x| &x[..]);
let host = req.header("host").or_else(|| req.header("Host")).map(|x| &x[..]);
let is_valid = origin_is_allowed(&self.self_origin, origin) || (origin.is_none() && origin_is_allowed(&self.self_origin, host)); let is_valid = origin_is_allowed(&self.self_origin, origin) || (origin.is_none() && origin_is_allowed(&self.self_origin, host));
let is_valid = is_styles_file || is_valid; let is_valid = is_styles_file || is_valid;
@ -155,6 +166,14 @@ impl ws::Handler for Session {
} }
} }
// PROXY requests when running behind home.parity
if req.method() == "CONNECT" {
let mut res = ws::Response::ok("".into());
res.headers_mut().push(("Content-Length".into(), b"0".to_vec()));
res.headers_mut().push(("Connection".into(), b"keep-alive".to_vec()));
return Ok(res);
}
// Detect if it's a websocket request // Detect if it's a websocket request
// (styles file skips origin validation, so make sure to prevent WS connections on this resource) // (styles file skips origin validation, so make sure to prevent WS connections on this resource)
if req.header("sec-websocket-key").is_some() && !is_styles_file { if req.header("sec-websocket-key").is_some() && !is_styles_file {
@ -173,8 +192,9 @@ impl ws::Handler for Session {
}); });
} }
debug!(target: "signer", "Requesting resource: {:?}", resource);
// Otherwise try to serve a page. // Otherwise try to serve a page.
Ok(self.file_handler.handle(req.resource()) Ok(self.file_handler.handle(resource)
.map_or_else( .map_or_else(
// return 404 not found // return 404 not found
|| error(ErrorType::NotFound, "Not found", "Requested file was not found.", None), || error(ErrorType::NotFound, "Not found", "Requested file was not found.", None),