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:
parent
9150fce2f1
commit
3ff1ca81f4
31
Cargo.lock
generated
31
Cargo.lock
generated
@ -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"
|
||||||
|
@ -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",
|
|
||||||
]
|
|
||||||
|
@ -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
0
dapps/src/api/cors.rs
Normal 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")),
|
|
||||||
]))
|
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
|
@ -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?)?
|
||||||
|
@ -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()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
Loading…
Reference in New Issue
Block a user