UI server refactoring (#5580)

* Full API in Authenticated WS server.

* Replacing UI server with Hyper.

* Solving CLI, RPCs and tests.

* Porting signer tests.

* Fixing origin recognition for dapps/rpc.

* Fixing tests. Adding parity-rpc-client to test.

* Dapps exposed as RPC method.

* JS code to support new connection scheme.

* Fixing dapps tests.

* Updating allowed origins/hosts to support web3.site.

* Fixing tests, fixing UI.

* Fixing tests.

* Removing invalid tests.

* Fixing merge.

* 404 fallback for UI

* Improve ContentFetcher constructor readability.

* Naming.

* Update .gitlab-ci.yml

fix CI lint error

* Fixing tests and linting issues.

* Fixing new tests.

* UI hosts.

* Submodules fix.
This commit is contained in:
Tomasz Drwięga 2017-05-24 12:24:07 +02:00 committed by Arkadiy Paronyan
parent 7499efecf6
commit cbcc369a2d
91 changed files with 2171 additions and 2591 deletions

View File

@ -564,7 +564,7 @@ test-windows:
- git submodule update --init --recursive - git submodule update --init --recursive
script: script:
- set RUST_BACKTRACE=1 - set RUST_BACKTRACE=1
- echo cargo test --features json-tests -p rlp -p ethash -p ethcore -p ethcore-bigint -p ethcore-dapps -p parity-rpc -p ethcore-signer -p ethcore-util -p ethcore-network -p ethcore-io -p ethkey -p ethstore -p ethsync -p ethcore-ipc -p ethcore-ipc-tests -p ethcore-ipc-nano -p parity %CARGOFLAGS% --verbose --release - echo cargo test --features json-tests -p rlp -p ethash -p ethcore -p ethcore-bigint -p parity-dapps -p parity-rpc -p ethcore-util -p ethcore-network -p ethcore-io -p ethkey -p ethstore -p ethsync -p ethcore-ipc -p ethcore-ipc-tests -p ethcore-ipc-nano -p parity-rpc-client -p parity %CARGOFLAGS% --verbose --release
tags: tags:
- rust-windows - rust-windows
allow_failure: true allow_failure: true

49
Cargo.lock generated
View File

@ -609,26 +609,6 @@ dependencies = [
"url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "ethcore-signer"
version = "1.7.0"
dependencies = [
"clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)",
"env_logger 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"ethcore-devtools 1.7.0",
"ethcore-io 1.7.0",
"ethcore-util 1.7.0",
"jsonrpc-core 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)",
"jsonrpc-server-utils 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)",
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-dapps-glue 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-rpc 1.7.0",
"parity-ui 1.7.0",
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"ws 0.5.3 (git+https://github.com/paritytech/ws-rs.git?branch=parity-1.7)",
]
[[package]] [[package]]
name = "ethcore-stratum" name = "ethcore-stratum"
version = "1.7.0" version = "1.7.0"
@ -1608,7 +1588,6 @@ dependencies = [
"ethcore-light 1.7.0", "ethcore-light 1.7.0",
"ethcore-logger 1.7.0", "ethcore-logger 1.7.0",
"ethcore-secretstore 1.0.0", "ethcore-secretstore 1.0.0",
"ethcore-signer 1.7.0",
"ethcore-stratum 1.7.0", "ethcore-stratum 1.7.0",
"ethcore-util 1.7.0", "ethcore-util 1.7.0",
"ethkey 0.2.0", "ethkey 0.2.0",
@ -1797,18 +1776,18 @@ dependencies = [
name = "parity-rpc-client" name = "parity-rpc-client"
version = "1.4.0" version = "1.4.0"
dependencies = [ dependencies = [
"ethcore-signer 1.7.0",
"ethcore-util 1.7.0", "ethcore-util 1.7.0",
"futures 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)", "jsonrpc-core 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)",
"jsonrpc-ws-server 7.0.0 (git+https://github.com/paritytech/jsonrpc.git?branch=parity-1.7)",
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
"matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-rpc 1.7.0", "parity-rpc 1.7.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)",
"serde 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)",
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"ws 0.5.3 (git+https://github.com/paritytech/ws-rs.git?branch=parity-1.7)",
] ]
[[package]] [[package]]
@ -2385,11 +2364,6 @@ name = "siphasher"
version = "0.1.1" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "slab"
version = "0.2.0"
source = "git+https://github.com/carllerche/slab?rev=5476efcafb#5476efcafbc5ef4d7315b1bea3f756d8a1fe975e"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.2.0" version = "0.2.0"
@ -2823,25 +2797,10 @@ name = "winapi-build"
version = "0.1.1" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ws"
version = "0.5.3"
source = "git+https://github.com/paritytech/ws-rs.git?branch=parity-1.7#30415c17f1bec53b2dcabae5b8b887df75dcbe34"
dependencies = [
"bytes 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
"mio 0.6.1 (git+https://github.com/paritytech/mio)",
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
"sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"slab 0.2.0 (git+https://github.com/carllerche/slab?rev=5476efcafb)",
"url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "ws" name = "ws"
version = "0.6.0" version = "0.6.0"
source = "git+https://github.com/tomusdrw/ws-rs#3259e7ca906c848beae109eb32e492871f8f397d" source = "git+https://github.com/tomusdrw/ws-rs#7f8e416b7f048880228005457e117128be38bf0f"
dependencies = [ dependencies = [
"bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"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)",
@ -3082,7 +3041,6 @@ dependencies = [
"checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c" "checksum sha1 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cc30b1e1e8c40c121ca33b86c23308a090d19974ef001b4bf6e61fd1a0fb095c"
"checksum shell32-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "72f20b8f3c060374edb8046591ba28f62448c369ccbdc7b02075103fb3a9e38d" "checksum shell32-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "72f20b8f3c060374edb8046591ba28f62448c369ccbdc7b02075103fb3a9e38d"
"checksum siphasher 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5c44e42fa187b5a8782489cf7740cc27c3125806be2bf33563cf5e02e9533fcd" "checksum siphasher 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5c44e42fa187b5a8782489cf7740cc27c3125806be2bf33563cf5e02e9533fcd"
"checksum slab 0.2.0 (git+https://github.com/carllerche/slab?rev=5476efcafb)" = "<none>"
"checksum slab 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6dbdd334bd28d328dad1c41b0ea662517883d8880d8533895ef96c8003dec9c4" "checksum slab 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6dbdd334bd28d328dad1c41b0ea662517883d8880d8533895ef96c8003dec9c4"
"checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23" "checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23"
"checksum smallvec 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "fcc8d19212aacecf95e4a7a2179b26f7aeb9732a915cf01f05b0d3e044865410" "checksum smallvec 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "fcc8d19212aacecf95e4a7a2179b26f7aeb9732a915cf01f05b0d3e044865410"
@ -3135,7 +3093,6 @@ dependencies = [
"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
"checksum ws 0.5.3 (git+https://github.com/paritytech/ws-rs.git?branch=parity-1.7)" = "<none>"
"checksum ws 0.6.0 (git+https://github.com/tomusdrw/ws-rs)" = "<none>" "checksum ws 0.6.0 (git+https://github.com/tomusdrw/ws-rs)" = "<none>"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
"checksum xdg 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "77b831a5ba77110f438f0ac5583aafeb087f70432998ba6b7dcb1d32185db453" "checksum xdg 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "77b831a5ba77110f438f0ac5583aafeb087f70432998ba6b7dcb1d32185db453"

View File

@ -33,7 +33,6 @@ ethcore = { path = "ethcore" }
ethcore-util = { path = "util" } ethcore-util = { path = "util" }
ethcore-io = { path = "util/io" } ethcore-io = { path = "util/io" }
ethcore-devtools = { path = "devtools" } ethcore-devtools = { path = "devtools" }
ethcore-signer = { path = "signer" }
ethcore-ipc = { path = "ipc/rpc" } ethcore-ipc = { path = "ipc/rpc" }
ethcore-ipc-nano = { path = "ipc/nano" } ethcore-ipc-nano = { path = "ipc/nano" }
ethcore-ipc-hypervisor = { path = "ipc/hypervisor" } ethcore-ipc-hypervisor = { path = "ipc/hypervisor" }
@ -75,17 +74,15 @@ default = ["ui-precompiled"]
ui = [ ui = [
"dapps", "dapps",
"parity-dapps/ui", "parity-dapps/ui",
"ethcore-signer/ui",
] ]
ui-precompiled = [ ui-precompiled = [
"dapps", "dapps",
"ethcore-signer/ui-precompiled",
"parity-dapps/ui-precompiled", "parity-dapps/ui-precompiled",
] ]
dapps = ["parity-dapps"] dapps = ["parity-dapps"]
ipc = ["ethcore/ipc", "ethsync/ipc"] ipc = ["ethcore/ipc", "ethsync/ipc"]
jit = ["ethcore/jit"] jit = ["ethcore/jit"]
dev = ["clippy", "ethcore/dev", "ethcore-util/dev", "ethsync/dev", "parity-rpc/dev", "parity-dapps/dev", "ethcore-signer/dev"] dev = ["clippy", "ethcore/dev", "ethcore-util/dev", "ethsync/dev", "parity-rpc/dev", "parity-dapps/dev"]
json-tests = ["ethcore/json-tests"] json-tests = ["ethcore/json-tests"]
test-heavy = ["ethcore/test-heavy"] test-heavy = ["ethcore/test-heavy"]
ethkey-cli = ["ethcore/ethkey-cli"] ethkey-cli = ["ethcore/ethkey-cli"]

View File

@ -16,42 +16,27 @@
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::method::Method;
use api::types::{App, ApiError}; use api::types::ApiError;
use api::response; use api::response;
use apps::fetcher::Fetcher; use apps::fetcher::Fetcher;
use handlers::extract_url; use handlers::extract_url;
use endpoint::{Endpoint, Endpoints, Handler, EndpointPath}; use endpoint::{Endpoint, Handler, EndpointPath};
use jsonrpc_http_server::{self, AccessControlAllowOrigin};
#[derive(Clone)] #[derive(Clone)]
pub struct RestApi { pub struct RestApi {
// TODO [ToDr] cors_domains should be handled by the server to avoid duplicated logic.
// RequestMiddleware should be able to tell that cors headers should be included.
cors_domains: Option<Vec<AccessControlAllowOrigin>>,
apps: Vec<App>,
fetcher: Arc<Fetcher>, fetcher: Arc<Fetcher>,
} }
impl RestApi { impl RestApi {
pub fn new(cors_domains: Vec<AccessControlAllowOrigin>, endpoints: &Endpoints, fetcher: Arc<Fetcher>) -> Box<Endpoint> { pub fn new(fetcher: Arc<Fetcher>) -> Box<Endpoint> {
Box::new(RestApi { Box::new(RestApi {
cors_domains: Some(cors_domains),
apps: Self::list_apps(endpoints),
fetcher: fetcher, fetcher: fetcher,
}) })
} }
fn list_apps(endpoints: &Endpoints) -> Vec<App> {
endpoints.iter().filter_map(|(ref k, ref e)| {
e.info().map(|ref info| App::from_info(k, info))
}).collect()
}
} }
impl Endpoint for RestApi { impl Endpoint for RestApi {
@ -62,7 +47,6 @@ impl Endpoint for RestApi {
struct RestApiRouter { struct RestApiRouter {
api: RestApi, api: RestApi,
cors_header: Option<header::AccessControlAllowOrigin>,
path: Option<EndpointPath>, path: Option<EndpointPath>,
control: Option<Control>, control: Option<Control>,
handler: Box<Handler>, handler: Box<Handler>,
@ -72,7 +56,6 @@ 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),
cors_header: None,
control: Some(control), control: Some(control),
api: api, api: api,
handler: response::as_json_error(&ApiError { handler: response::as_json_error(&ApiError {
@ -92,35 +75,10 @@ impl RestApiRouter {
_ => None _ => None
} }
} }
/// Returns basic headers for a response (it may be overwritten by the handler)
fn response_headers(cors_header: Option<header::AccessControlAllowOrigin>) -> header::Headers {
let mut headers = header::Headers::new();
if let Some(cors_header) = cors_header {
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()),
]));
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.cors_header = jsonrpc_http_server::cors_header(&request, &self.api.cors_domains).into();
if let Method::Options = *request.method() { if let Method::Options = *request.method() {
self.handler = response::empty(); self.handler = response::empty();
return Next::write(); return Next::write();
@ -144,7 +102,6 @@ impl server::Handler<net::HttpStream> for RestApiRouter {
if let Some(ref hash) = hash { path.app_id = hash.clone().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(response::as_json(&self.api.apps)),
"ping" => Some(response::ping()), "ping" => Some(response::ping()),
"content" => self.resolve_content(hash, path, control), "content" => self.resolve_content(hash, path, control),
_ => None _ => None
@ -163,7 +120,6 @@ 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.cors_header.take());
self.handler.on_response(res) self.handler.on_response(res)
} }

View File

@ -21,4 +21,3 @@ mod response;
mod types; mod types;
pub use self::api::RestApi; pub use self::api::RestApi;
pub use self::types::App;

View File

@ -23,12 +23,6 @@ pub fn empty() -> Box<Handler> {
Box::new(ContentHandler::ok("".into(), mime!(Text/Plain))) Box::new(ContentHandler::ok("".into(), mime!(Text/Plain)))
} }
pub fn as_json<T: Serialize>(val: &T) -> Box<Handler> {
let json = serde_json::to_string(val)
.expect("serialization to string is infallible; qed");
Box::new(ContentHandler::ok(json, mime!(Application/Json)))
}
pub fn as_json_error<T: Serialize>(val: &T) -> Box<Handler> { pub fn as_json_error<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");

View File

@ -14,46 +14,6 @@
// 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 endpoint::EndpointInfo;
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct App {
pub id: String,
pub name: String,
pub description: String,
pub version: String,
pub author: String,
#[serde(rename="iconUrl")]
pub icon_url: String,
}
impl App {
/// Creates `App` instance from `EndpointInfo` and `id`.
pub fn from_info(id: &str, info: &EndpointInfo) -> Self {
App {
id: id.to_owned(),
name: info.name.to_owned(),
description: info.description.to_owned(),
version: info.version.to_owned(),
author: info.author.to_owned(),
icon_url: info.icon_url.to_owned(),
}
}
}
impl Into<EndpointInfo> for App {
fn into(self) -> EndpointInfo {
EndpointInfo {
name: self.name,
description: self.description,
version: self.version,
author: self.author,
icon_url: self.icon_url,
}
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct ApiError { pub struct ApiError {

55
dapps/src/apps/app.rs Normal file
View File

@ -0,0 +1,55 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use endpoint::EndpointInfo;
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct App {
pub id: String,
pub name: String,
pub description: String,
pub version: String,
pub author: String,
#[serde(rename="iconUrl")]
pub icon_url: String,
}
impl App {
/// Creates `App` instance from `EndpointInfo` and `id`.
pub fn from_info(id: &str, info: &EndpointInfo) -> Self {
App {
id: id.to_owned(),
name: info.name.to_owned(),
description: info.description.to_owned(),
version: info.version.to_owned(),
author: info.author.to_owned(),
icon_url: info.icon_url.to_owned(),
}
}
}
impl Into<EndpointInfo> for App {
fn into(self) -> EndpointInfo {
EndpointInfo {
name: self.name,
description: self.description,
version: self.version,
author: self.author,
icon_url: self.icon_url,
}
}
}

View File

@ -55,6 +55,7 @@ pub struct ContentFetcher<F: Fetch = FetchClient, R: URLHint + 'static = URLHint
embeddable_on: Option<(String, u16)>, embeddable_on: Option<(String, u16)>,
remote: Remote, remote: Remote,
fetch: F, fetch: F,
only_content: bool,
} }
impl<R: URLHint + 'static, F: Fetch> Drop for ContentFetcher<F, R> { impl<R: URLHint + 'static, F: Fetch> Drop for ContentFetcher<F, R> {
@ -66,7 +67,12 @@ impl<R: URLHint + 'static, F: Fetch> Drop for ContentFetcher<F, R> {
impl<R: URLHint + 'static, F: Fetch> ContentFetcher<F, R> { impl<R: URLHint + 'static, F: Fetch> ContentFetcher<F, R> {
pub fn new(resolver: R, sync_status: Arc<SyncStatus>, embeddable_on: Option<(String, u16)>, remote: Remote, fetch: F) -> Self { pub fn new(
resolver: R,
sync_status: Arc<SyncStatus>,
remote: Remote,
fetch: F,
) -> Self {
let mut dapps_path = env::temp_dir(); let mut dapps_path = env::temp_dir();
dapps_path.push(random_filename()); dapps_path.push(random_filename());
@ -75,12 +81,23 @@ impl<R: URLHint + 'static, F: Fetch> ContentFetcher<F, R> {
resolver: resolver, resolver: resolver,
sync: sync_status, sync: sync_status,
cache: Arc::new(Mutex::new(ContentCache::default())), cache: Arc::new(Mutex::new(ContentCache::default())),
embeddable_on: embeddable_on, embeddable_on: None,
remote: remote, remote: remote,
fetch: fetch, fetch: fetch,
only_content: true,
} }
} }
pub fn allow_dapps(mut self, dapps: bool) -> Self {
self.only_content = !dapps;
self
}
pub fn embeddable_on(mut self, embeddable_on: Option<(String, u16)>) -> Self {
self.embeddable_on = embeddable_on;
self
}
fn still_syncing(address: Option<(String, u16)>) -> Box<Handler> { fn still_syncing(address: Option<(String, u16)>) -> Box<Handler> {
Box::new(ContentHandler::error( Box::new(ContentHandler::error(
StatusCode::ServiceUnavailable, StatusCode::ServiceUnavailable,
@ -91,6 +108,16 @@ impl<R: URLHint + 'static, F: Fetch> ContentFetcher<F, R> {
)) ))
} }
fn dapps_disabled(address: Option<(String, u16)>) -> Box<Handler> {
Box::new(ContentHandler::error(
StatusCode::ServiceUnavailable,
"Network Dapps Not Available",
"This interface doesn't support network dapps for security reasons.",
None,
address,
))
}
#[cfg(test)] #[cfg(test)]
fn set_status(&self, content_id: &str, status: ContentStatus) { fn set_status(&self, content_id: &str, status: ContentStatus) {
self.cache.lock().insert(content_id.to_owned(), status); self.cache.lock().insert(content_id.to_owned(), status);
@ -163,6 +190,9 @@ impl<R: URLHint + 'static, F: Fetch> Fetcher for ContentFetcher<F, R> {
Some(URLHintResult::Dapp(_)) if self.sync.is_major_importing() => { Some(URLHintResult::Dapp(_)) if self.sync.is_major_importing() => {
(None, Self::still_syncing(self.embeddable_on.clone())) (None, Self::still_syncing(self.embeddable_on.clone()))
}, },
Some(URLHintResult::Dapp(_)) if self.only_content => {
(None, Self::dapps_disabled(self.embeddable_on.clone()))
},
Some(URLHintResult::Dapp(dapp)) => { Some(URLHintResult::Dapp(dapp)) => {
let handler = ContentFetcherHandler::new( let handler = ContentFetcherHandler::new(
dapp.url(), dapp.url(),
@ -254,7 +284,8 @@ mod tests {
fn should_true_if_contains_the_app() { fn should_true_if_contains_the_app() {
// given // given
let path = env::temp_dir(); let path = env::temp_dir();
let fetcher = ContentFetcher::new(FakeResolver, Arc::new(|| false), None, Remote::new_sync(), Client::new().unwrap()); let fetcher = ContentFetcher::new(FakeResolver, Arc::new(|| false), Remote::new_sync(), Client::new().unwrap())
.allow_dapps(true);
let handler = LocalPageEndpoint::new(path, EndpointInfo { let handler = LocalPageEndpoint::new(path, EndpointInfo {
name: "fake".into(), name: "fake".into(),
description: "".into(), description: "".into(),

View File

@ -14,12 +14,13 @@
// 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 std::collections::BTreeMap;
use std::io; use std::io;
use std::io::Read; use std::io::Read;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use page::{LocalPageEndpoint, PageCache}; use page::{LocalPageEndpoint, PageCache};
use endpoint::{Endpoints, EndpointInfo}; use endpoint::{Endpoint, EndpointInfo};
use apps::manifest::{MANIFEST_FILENAME, deserialize_manifest}; use apps::manifest::{MANIFEST_FILENAME, deserialize_manifest};
struct LocalDapp { struct LocalDapp {
@ -85,8 +86,8 @@ fn local_dapp(name: String, path: PathBuf) -> LocalDapp {
/// Returns endpoints for Local Dapps found for given filesystem path. /// Returns endpoints for Local Dapps found for given filesystem path.
/// Scans the directory and collects `LocalPageEndpoints`. /// Scans the directory and collects `LocalPageEndpoints`.
pub fn local_endpoints<P: AsRef<Path>>(dapps_path: P, signer_address: Option<(String, u16)>) -> Endpoints { pub fn local_endpoints<P: AsRef<Path>>(dapps_path: P, signer_address: Option<(String, u16)>) -> BTreeMap<String, Box<Endpoint>> {
let mut pages = Endpoints::new(); let mut pages = BTreeMap::<String, Box<Endpoint>>::new();
for dapp in local_dapps(dapps_path.as_ref()) { for dapp in local_dapps(dapps_path.as_ref()) {
pages.insert( pages.insert(
dapp.id, dapp.id,

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 serde_json; use serde_json;
pub use api::App as Manifest; pub use apps::App as Manifest;
pub const MANIFEST_FILENAME: &'static str = "manifest.json"; pub const MANIFEST_FILENAME: &'static str = "manifest.json";

View File

@ -14,8 +14,10 @@
// 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 std::collections::BTreeMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use endpoint::{Endpoints, Endpoint}; use endpoint::{Endpoints, Endpoint};
use page::PageEndpoint; use page::PageEndpoint;
use proxypac::ProxyPac; use proxypac::ProxyPac;
@ -23,17 +25,19 @@ use web::Web;
use fetch::Fetch; use fetch::Fetch;
use parity_dapps::WebApp; use parity_dapps::WebApp;
use parity_reactor::Remote; use parity_reactor::Remote;
use parity_ui;
use {WebProxyTokens}; use {WebProxyTokens};
mod app;
mod cache; mod cache;
mod fs; mod fs;
mod ui;
pub mod fetcher; pub mod fetcher;
pub mod manifest; pub mod manifest;
extern crate parity_ui; pub use self::app::App;
pub const HOME_PAGE: &'static str = "parity"; pub const HOME_PAGE: &'static str = "home";
pub const DAPPS_DOMAIN: &'static str = ".web3.site";
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";
@ -44,18 +48,27 @@ pub fn utils() -> Box<Endpoint> {
Box::new(PageEndpoint::with_prefix(parity_ui::App::default(), UTILS_PATH.to_owned())) Box::new(PageEndpoint::with_prefix(parity_ui::App::default(), UTILS_PATH.to_owned()))
} }
pub fn ui() -> Box<Endpoint> {
Box::new(PageEndpoint::with_fallback_to_index(parity_ui::App::default()))
}
pub fn ui_redirection(ui_address: Option<(String, u16)>) -> Box<Endpoint> {
Box::new(ui::Redirection::new(ui_address))
}
pub fn all_endpoints<F: Fetch>( pub fn all_endpoints<F: Fetch>(
dapps_path: PathBuf, dapps_path: PathBuf,
extra_dapps: Vec<PathBuf>, extra_dapps: Vec<PathBuf>,
signer_address: Option<(String, u16)>, dapps_domain: String,
ui_address: Option<(String, u16)>,
web_proxy_tokens: Arc<WebProxyTokens>, web_proxy_tokens: Arc<WebProxyTokens>,
remote: Remote, remote: Remote,
fetch: F, fetch: F,
) -> Endpoints { ) -> Endpoints {
// fetch fs dapps at first to avoid overwriting builtins // fetch fs dapps at first to avoid overwriting builtins
let mut pages = fs::local_endpoints(dapps_path, signer_address.clone()); let mut pages = fs::local_endpoints(dapps_path, ui_address.clone());
for path in extra_dapps { for path in extra_dapps {
if let Some((id, endpoint)) = fs::local_endpoint(path.clone(), signer_address.clone()) { if let Some((id, endpoint)) = fs::local_endpoint(path.clone(), ui_address.clone()) {
pages.insert(id, endpoint); pages.insert(id, endpoint);
} else { } else {
warn!(target: "dapps", "Ignoring invalid dapp at {}", path.display()); warn!(target: "dapps", "Ignoring invalid dapp at {}", path.display());
@ -63,14 +76,14 @@ pub fn all_endpoints<F: Fetch>(
} }
// NOTE [ToDr] Dapps will be currently embeded on 8180 // NOTE [ToDr] Dapps will be currently embeded on 8180
insert::<parity_ui::App>(&mut pages, "ui", Embeddable::Yes(signer_address.clone())); insert::<parity_ui::App>(&mut pages, "ui", Embeddable::Yes(ui_address.clone()));
pages.insert("proxy".into(), ProxyPac::boxed(signer_address.clone())); pages.insert("proxy".into(), ProxyPac::boxed(ui_address.clone(), dapps_domain));
pages.insert(WEB_PATH.into(), Web::boxed(signer_address.clone(), web_proxy_tokens.clone(), remote.clone(), fetch.clone())); pages.insert(WEB_PATH.into(), Web::boxed(ui_address.clone(), web_proxy_tokens.clone(), remote.clone(), fetch.clone()));
pages Arc::new(pages)
} }
fn insert<T : WebApp + Default + 'static>(pages: &mut Endpoints, id: &str, embed_at: Embeddable) { fn insert<T : WebApp + Default + 'static>(pages: &mut BTreeMap<String, Box<Endpoint>>, id: &str, embed_at: Embeddable) {
pages.insert(id.to_owned(), Box::new(match embed_at { pages.insert(id.to_owned(), Box::new(match embed_at {
Embeddable::Yes(address) => PageEndpoint::new_safe_to_embed(T::default(), address), Embeddable::Yes(address) => PageEndpoint::new_safe_to_embed(T::default(), address),
Embeddable::No => PageEndpoint::new(T::default()), Embeddable::No => PageEndpoint::new(T::default()),

55
dapps/src/apps/ui.rs Normal file
View File

@ -0,0 +1,55 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! UI redirections
use hyper::{Control, StatusCode};
use endpoint::{Endpoint, Handler, EndpointPath};
use {address, handlers};
/// Redirection to UI server.
pub struct Redirection {
signer_address: Option<(String, u16)>,
}
impl Redirection {
pub fn new(
signer_address: Option<(String, u16)>,
) -> Self {
Redirection {
signer_address: signer_address,
}
}
}
impl Endpoint for Redirection {
fn to_async_handler(&self, _path: EndpointPath, _control: Control) -> Box<Handler> {
if let Some(ref signer_address) = self.signer_address {
trace!(target: "dapps", "Redirecting to signer interface.");
handlers::Redirection::boxed(&format!("http://{}", address(signer_address)))
} else {
trace!(target: "dapps", "Signer disabled, returning 404.");
Box::new(handlers::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-enable Signer to get your homepage back."),
self.signer_address.clone(),
))
}
}
}

View File

@ -16,6 +16,7 @@
//! URL Endpoint traits //! URL Endpoint traits
use std::sync::Arc;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use hyper::{self, server, net}; use hyper::{self, server, net};
@ -38,7 +39,7 @@ pub struct EndpointInfo {
pub icon_url: String, pub icon_url: String,
} }
pub type Endpoints = BTreeMap<String, Box<Endpoint>>; pub type Endpoints = Arc<BTreeMap<String, Box<Endpoint>>>;
pub type Handler = server::Handler<net::HttpStream> + Send; pub type Handler = server::Handler<net::HttpStream> + Send;
pub trait Endpoint : Send + Sync { pub trait Endpoint : Send + Sync {

View File

@ -40,6 +40,7 @@ extern crate fetch;
extern crate parity_dapps_glue as parity_dapps; extern crate parity_dapps_glue as parity_dapps;
extern crate parity_hash_fetch as hash_fetch; extern crate parity_hash_fetch as hash_fetch;
extern crate parity_reactor; extern crate parity_reactor;
extern crate parity_ui;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
@ -70,7 +71,7 @@ use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::collections::HashMap; use std::collections::HashMap;
use jsonrpc_http_server::{self as http, hyper, AccessControlAllowOrigin}; use jsonrpc_http_server::{self as http, hyper};
use fetch::Fetch; use fetch::Fetch;
use parity_reactor::Remote; use parity_reactor::Remote;
@ -97,18 +98,74 @@ impl<F> WebProxyTokens for F where F: Fn(String) -> bool + Send + Sync {
fn is_web_proxy_token_valid(&self, token: &str) -> bool { self(token.to_owned()) } fn is_web_proxy_token_valid(&self, token: &str) -> bool { self(token.to_owned()) }
} }
/// Current supported endpoints.
pub struct Endpoints {
endpoints: endpoint::Endpoints,
}
impl Endpoints {
/// Returns a current list of app endpoints.
pub fn list(&self) -> Vec<apps::App> {
self.endpoints.iter().filter_map(|(ref k, ref e)| {
e.info().map(|ref info| apps::App::from_info(k, info))
}).collect()
}
}
/// Dapps server as `jsonrpc-http-server` request middleware. /// Dapps server as `jsonrpc-http-server` request middleware.
pub struct Middleware { pub struct Middleware {
router: router::Router, router: router::Router,
endpoints: endpoint::Endpoints,
} }
impl Middleware { impl Middleware {
/// Creates new Dapps server middleware. /// Get local endpoints handle.
pub fn new<F: Fetch + Clone>( pub fn endpoints(&self) -> Endpoints {
Endpoints {
endpoints: self.endpoints.clone(),
}
}
/// Creates new middleware for UI server.
pub fn ui<F: Fetch + Clone>(
remote: Remote, remote: Remote,
signer_address: Option<(String, u16)>, registrar: Arc<ContractClient>,
sync_status: Arc<SyncStatus>,
fetch: F,
dapps_domain: String,
) -> Self {
let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(
hash_fetch::urlhint::URLHintContract::new(registrar),
sync_status,
remote.clone(),
fetch.clone(),
).embeddable_on(None).allow_dapps(false));
let special = {
let mut special = special_endpoints(content_fetcher.clone());
special.insert(router::SpecialEndpoint::Home, Some(apps::ui()));
special
};
let router = router::Router::new(
content_fetcher,
None,
special,
None,
dapps_domain,
);
Middleware {
router: router,
endpoints: Default::default(),
}
}
/// Creates new Dapps server middleware.
pub fn dapps<F: Fetch + Clone>(
remote: Remote,
ui_address: Option<(String, u16)>,
dapps_path: PathBuf, dapps_path: PathBuf,
extra_dapps: Vec<PathBuf>, extra_dapps: Vec<PathBuf>,
dapps_domain: String,
registrar: Arc<ContractClient>, registrar: Arc<ContractClient>,
sync_status: Arc<SyncStatus>, sync_status: Arc<SyncStatus>,
web_proxy_tokens: Arc<WebProxyTokens>, web_proxy_tokens: Arc<WebProxyTokens>,
@ -117,45 +174,36 @@ impl Middleware {
let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new( let content_fetcher = Arc::new(apps::fetcher::ContentFetcher::new(
hash_fetch::urlhint::URLHintContract::new(registrar), hash_fetch::urlhint::URLHintContract::new(registrar),
sync_status, sync_status,
signer_address.clone(),
remote.clone(), remote.clone(),
fetch.clone(), fetch.clone(),
)); ).embeddable_on(ui_address.clone()).allow_dapps(true));
let endpoints = apps::all_endpoints( let endpoints = apps::all_endpoints(
dapps_path, dapps_path,
extra_dapps, extra_dapps,
signer_address.clone(), dapps_domain.clone(),
ui_address.clone(),
web_proxy_tokens, web_proxy_tokens,
remote.clone(), remote.clone(),
fetch.clone(), fetch.clone(),
); );
let cors_domains = cors_domains(signer_address.clone());
let special = { let special = {
let mut special = HashMap::new(); let mut special = special_endpoints(content_fetcher.clone());
special.insert(router::SpecialEndpoint::Rpc, None); special.insert(router::SpecialEndpoint::Home, Some(apps::ui_redirection(ui_address.clone())));
special.insert(router::SpecialEndpoint::Utils, Some(apps::utils()));
special.insert(
router::SpecialEndpoint::Api,
Some(api::RestApi::new(
cors_domains.clone(),
&endpoints,
content_fetcher.clone()
)),
);
special special
}; };
let router = router::Router::new( let router = router::Router::new(
signer_address,
content_fetcher, content_fetcher,
endpoints, Some(endpoints.clone()),
special, special,
ui_address,
dapps_domain,
); );
Middleware { Middleware {
router: router, router: router,
endpoints: endpoints,
} }
} }
} }
@ -166,21 +214,12 @@ impl http::RequestMiddleware for Middleware {
} }
} }
/// Returns a list of CORS domains for API endpoint. fn special_endpoints(content_fetcher: Arc<apps::fetcher::Fetcher>) -> HashMap<router::SpecialEndpoint, Option<Box<endpoint::Endpoint>>> {
fn cors_domains(signer_address: Option<(String, u16)>) -> Vec<AccessControlAllowOrigin> { let mut special = HashMap::new();
use self::apps::{HOME_PAGE, DAPPS_DOMAIN}; special.insert(router::SpecialEndpoint::Rpc, None);
special.insert(router::SpecialEndpoint::Utils, Some(apps::utils()));
match signer_address { special.insert(router::SpecialEndpoint::Api, Some(api::RestApi::new(content_fetcher)));
Some(signer_address) => [ special
format!("http://{}{}", HOME_PAGE, DAPPS_DOMAIN),
format!("http://{}{}:{}", HOME_PAGE, DAPPS_DOMAIN, signer_address.1),
format!("http://{}", address(&signer_address)),
format!("https://{}{}", HOME_PAGE, DAPPS_DOMAIN),
format!("https://{}{}:{}", HOME_PAGE, DAPPS_DOMAIN, signer_address.1),
format!("https://{}", address(&signer_address)),
].into_iter().map(|val| AccessControlAllowOrigin::Value(val.into())).collect(),
None => vec![],
}
} }
fn address(address: &(String, u16)) -> String { fn address(address: &(String, u16)) -> String {
@ -193,29 +232,3 @@ fn random_filename() -> String {
let mut rng = ::rand::OsRng::new().unwrap(); let mut rng = ::rand::OsRng::new().unwrap();
rng.gen_ascii_chars().take(12).collect() rng.gen_ascii_chars().take(12).collect()
} }
#[cfg(test)]
mod util_tests {
use super::cors_domains;
use jsonrpc_http_server::AccessControlAllowOrigin;
#[test]
fn should_return_cors_domains() {
// given
// when
let none = cors_domains(None);
let some = cors_domains(Some(("127.0.0.1".into(), 18180)));
// then
assert_eq!(none, Vec::<AccessControlAllowOrigin>::new());
assert_eq!(some, vec![
"http://parity.web3.site".into(),
"http://parity.web3.site:18180".into(),
"http://127.0.0.1:18180".into(),
"https://parity.web3.site".into(),
"https://parity.web3.site:18180".into(),
"https://127.0.0.1:18180".into(),
]);
}
}

View File

@ -27,6 +27,7 @@ pub struct PageEndpoint<T : WebApp + 'static> {
/// Safe to be loaded in frame by other origin. (use wisely!) /// Safe to be loaded in frame by other origin. (use wisely!)
safe_to_embed_on: Option<(String, u16)>, safe_to_embed_on: Option<(String, u16)>,
info: EndpointInfo, info: EndpointInfo,
fallback_to_index_html: bool,
} }
impl<T: WebApp + 'static> PageEndpoint<T> { impl<T: WebApp + 'static> PageEndpoint<T> {
@ -38,6 +39,20 @@ impl<T: WebApp + 'static> PageEndpoint<T> {
prefix: None, prefix: None,
safe_to_embed_on: None, safe_to_embed_on: None,
info: EndpointInfo::from(info), info: EndpointInfo::from(info),
fallback_to_index_html: false,
}
}
/// Creates a new `PageEndpoint` for builtin (compile time) Dapp.
/// Instead of returning 404 this endpoint will always server index.html.
pub fn with_fallback_to_index(app: T) -> Self {
let info = app.info();
PageEndpoint {
app: Arc::new(app),
prefix: None,
safe_to_embed_on: None,
info: EndpointInfo::from(info),
fallback_to_index_html: true,
} }
} }
@ -51,6 +66,7 @@ impl<T: WebApp + 'static> PageEndpoint<T> {
prefix: Some(prefix), prefix: Some(prefix),
safe_to_embed_on: None, safe_to_embed_on: None,
info: EndpointInfo::from(info), info: EndpointInfo::from(info),
fallback_to_index_html: false,
} }
} }
@ -64,6 +80,7 @@ impl<T: WebApp + 'static> PageEndpoint<T> {
prefix: None, prefix: None,
safe_to_embed_on: address, safe_to_embed_on: address,
info: EndpointInfo::from(info), info: EndpointInfo::from(info),
fallback_to_index_html: false,
} }
} }
} }
@ -76,7 +93,7 @@ impl<T: WebApp> Endpoint for PageEndpoint<T> {
fn to_handler(&self, path: EndpointPath) -> Box<Handler> { fn to_handler(&self, path: EndpointPath) -> Box<Handler> {
Box::new(handler::PageHandler { Box::new(handler::PageHandler {
app: BuiltinDapp::new(self.app.clone()), app: BuiltinDapp::new(self.app.clone(), self.fallback_to_index_html),
prefix: self.prefix.clone(), prefix: self.prefix.clone(),
path: path, path: path,
file: handler::ServedFile::new(self.safe_to_embed_on.clone()), file: handler::ServedFile::new(self.safe_to_embed_on.clone()),
@ -100,12 +117,14 @@ impl From<Info> for EndpointInfo {
struct BuiltinDapp<T: WebApp + 'static> { struct BuiltinDapp<T: WebApp + 'static> {
app: Arc<T>, app: Arc<T>,
fallback_to_index_html: bool,
} }
impl<T: WebApp + 'static> BuiltinDapp<T> { impl<T: WebApp + 'static> BuiltinDapp<T> {
fn new(app: Arc<T>) -> Self { fn new(app: Arc<T>, fallback_to_index_html: bool) -> Self {
BuiltinDapp { BuiltinDapp {
app: app, app: app,
fallback_to_index_html: fallback_to_index_html,
} }
} }
} }
@ -114,13 +133,19 @@ impl<T: WebApp + 'static> handler::Dapp for BuiltinDapp<T> {
type DappFile = BuiltinDappFile<T>; type DappFile = BuiltinDappFile<T>;
fn file(&self, path: &str) -> Option<Self::DappFile> { fn file(&self, path: &str) -> Option<Self::DappFile> {
self.app.file(path).map(|_| { let file = |path| self.app.file(path).map(|_| {
BuiltinDappFile { BuiltinDappFile {
app: self.app.clone(), app: self.app.clone(),
path: path.into(), path: path.into(),
write_pos: 0, write_pos: 0,
} }
}) });
let res = file(path);
if self.fallback_to_index_html {
res.or_else(|| file("index.html"))
} else {
res
}
} }
} }

View File

@ -18,17 +18,19 @@
use endpoint::{Endpoint, Handler, EndpointPath}; use endpoint::{Endpoint, Handler, EndpointPath};
use handlers::ContentHandler; use handlers::ContentHandler;
use apps::{HOME_PAGE, DAPPS_DOMAIN}; use apps::HOME_PAGE;
use address; use address;
pub struct ProxyPac { pub struct ProxyPac {
signer_address: Option<(String, u16)>, signer_address: Option<(String, u16)>,
dapps_domain: String,
} }
impl ProxyPac { impl ProxyPac {
pub fn boxed(signer_address: Option<(String, u16)>) -> Box<Endpoint> { pub fn boxed(signer_address: Option<(String, u16)>, dapps_domain: String) -> Box<Endpoint> {
Box::new(ProxyPac { Box::new(ProxyPac {
signer_address: signer_address signer_address: signer_address,
dapps_domain: dapps_domain,
}) })
} }
} }
@ -43,12 +45,12 @@ impl Endpoint for ProxyPac {
let content = format!( let content = format!(
r#" r#"
function FindProxyForURL(url, host) {{ function FindProxyForURL(url, host) {{
if (shExpMatch(host, "{0}{1}")) if (shExpMatch(host, "{0}.{1}"))
{{ {{
return "PROXY {4}"; return "PROXY {4}";
}} }}
if (shExpMatch(host, "*{1}")) if (shExpMatch(host, "*.{1}"))
{{ {{
return "PROXY {2}:{3}"; return "PROXY {2}:{3}";
}} }}
@ -56,7 +58,7 @@ function FindProxyForURL(url, host) {{
return "DIRECT"; return "DIRECT";
}} }}
"#, "#,
HOME_PAGE, DAPPS_DOMAIN, path.host, path.port, signer); HOME_PAGE, self.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

@ -17,20 +17,19 @@
//! Router implementation //! Router implementation
//! Dispatch requests to proper application. //! Dispatch requests to proper application.
use address;
use std::cmp; use std::cmp;
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, header, Control, StatusCode}; use hyper::{self, server, header, Control};
use hyper::net::HttpStream; use hyper::net::HttpStream;
use jsonrpc_http_server as http; use jsonrpc_http_server as http;
use apps::{self, DAPPS_DOMAIN}; use apps;
use apps::fetcher::Fetcher; use apps::fetcher::Fetcher;
use endpoint::{Endpoint, Endpoints, EndpointPath, Handler}; use endpoint::{Endpoint, Endpoints, EndpointPath, Handler};
use handlers::{self, Redirection, ContentHandler}; use handlers;
/// Special endpoints are accessible on every domain (every dapp) /// Special endpoints are accessible on every domain (every dapp)
#[derive(Debug, PartialEq, Hash, Eq)] #[derive(Debug, PartialEq, Hash, Eq)]
@ -38,26 +37,28 @@ pub enum SpecialEndpoint {
Rpc, Rpc,
Api, Api,
Utils, Utils,
Home,
None, None,
} }
pub struct Router { pub struct Router {
signer_address: Option<(String, u16)>, endpoints: Option<Endpoints>,
endpoints: Endpoints,
fetch: Arc<Fetcher>, fetch: Arc<Fetcher>,
special: HashMap<SpecialEndpoint, Option<Box<Endpoint>>>, special: HashMap<SpecialEndpoint, Option<Box<Endpoint>>>,
embeddable_on: Option<(String, u16)>,
dapps_domain: String,
} }
impl http::RequestMiddleware for Router { impl http::RequestMiddleware for Router {
fn on_request(&self, req: &server::Request<HttpStream>, control: &Control) -> http::RequestMiddlewareAction { fn on_request(&self, req: &server::Request<HttpStream>, control: &Control) -> http::RequestMiddlewareAction {
// Choose proper handler depending on path / domain // Choose proper handler depending on path / domain
let url = handlers::extract_url(req); let url = handlers::extract_url(req);
let endpoint = extract_endpoint(&url); let endpoint = extract_endpoint(&url, &self.dapps_domain);
let referer = extract_referer_endpoint(req); let referer = extract_referer_endpoint(req, &self.dapps_domain);
let is_utils = endpoint.1 == SpecialEndpoint::Utils; let is_utils = endpoint.1 == SpecialEndpoint::Utils;
let is_dapps_domain = endpoint.0.as_ref().map(|endpoint| endpoint.using_dapps_domains).unwrap_or(false); let is_origin_set = req.headers().get::<header::Origin>().is_some();
let is_origin_set = req.headers().get::<http::hyper::header::Origin>().is_some();
let is_get_request = *req.method() == hyper::Method::Get; let is_get_request = *req.method() == hyper::Method::Get;
let is_head_request = *req.method() == hyper::Method::Head;
trace!(target: "dapps", "Routing request to {:?}. Details: {:?}", url, req); trace!(target: "dapps", "Routing request to {:?}. Details: {:?}", url, req);
@ -67,7 +68,7 @@ impl http::RequestMiddleware for Router {
// Handle invalid web requests that we can recover from // Handle invalid web requests that we can recover from
(ref path, SpecialEndpoint::None, Some((ref referer, ref referer_url))) (ref path, SpecialEndpoint::None, Some((ref referer, ref referer_url)))
if referer.app_id == apps::WEB_PATH if referer.app_id == apps::WEB_PATH
&& self.endpoints.contains_key(apps::WEB_PATH) && self.endpoints.as_ref().map(|ep| ep.contains_key(apps::WEB_PATH)).unwrap_or(false)
&& !is_web_endpoint(path) && !is_web_endpoint(path)
=> =>
{ {
@ -75,7 +76,7 @@ impl http::RequestMiddleware for Router {
let len = cmp::min(referer_url.path.len(), 2); // /web/<encoded>/ let len = cmp::min(referer_url.path.len(), 2); // /web/<encoded>/
let base = referer_url.path[..len].join("/"); let base = referer_url.path[..len].join("/");
let requested = url.map(|u| u.path.join("/")).unwrap_or_default(); let requested = url.map(|u| u.path.join("/")).unwrap_or_default();
Some(Redirection::boxed(&format!("/{}/{}", base, requested))) Some(handlers::Redirection::boxed(&format!("/{}/{}", base, requested)))
}, },
// 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) => {
@ -86,9 +87,12 @@ impl http::RequestMiddleware for Router {
.map(|special| special.to_async_handler(path.clone().unwrap_or_default(), control)) .map(|special| special.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.as_ref().map(|ep| ep.contains_key(&path.app_id)).unwrap_or(false) => {
trace!(target: "dapps", "Resolving to local/builtin dapp."); trace!(target: "dapps", "Resolving to local/builtin dapp.");
Some(self.endpoints.get(&path.app_id) Some(self.endpoints
.as_ref()
.expect("endpoints known to be set; qed")
.get(&path.app_id)
.expect("endpoints known to contain key; qed") .expect("endpoints known to contain key; qed")
.to_async_handler(path.clone(), control)) .to_async_handler(path.clone(), control))
}, },
@ -97,36 +101,28 @@ impl http::RequestMiddleware for Router {
trace!(target: "dapps", "Resolving to fetchable content."); trace!(target: "dapps", "Resolving to fetchable content.");
Some(self.fetch.to_async_handler(path.clone(), control)) Some(self.fetch.to_async_handler(path.clone(), control))
}, },
// NOTE [todr] /home is redirected to home page since some users may have the redirection cached // 404 for non-existent content (only if serving endpoints and not homepage)
// (in the past we used 301 instead of 302) (Some(ref path), _, _)
// It should be safe to remove it in (near) future. if (is_get_request || is_head_request)
// && self.endpoints.is_some()
// 404 for non-existent content && path.app_id != apps::HOME_PAGE
(Some(ref path), _, _) if is_get_request && path.app_id != "home" => { =>
{
trace!(target: "dapps", "Resolving to 404."); trace!(target: "dapps", "Resolving to 404.");
Some(Box::new(ContentHandler::error( Some(Box::new(handlers::ContentHandler::error(
StatusCode::NotFound, hyper::StatusCode::NotFound,
"404 Not Found", "404 Not Found",
"Requested content was not found.", "Requested content was not found.",
None, None,
self.signer_address.clone(), self.embeddable_on.clone(),
))) )))
}, },
// Redirect any other GET request to signer. // Any other GET|HEAD requests to home page.
_ if is_get_request => { _ if (is_get_request || is_head_request) && self.special.contains_key(&SpecialEndpoint::Home) => {
if let Some(ref signer_address) = self.signer_address { self.special.get(&SpecialEndpoint::Home)
trace!(target: "dapps", "Redirecting to signer interface."); .expect("special known to contain key; qed")
Some(Redirection::boxed(&format!("http://{}", address(signer_address)))) .as_ref()
} else { .map(|special| special.to_async_handler(Default::default(), control))
trace!(target: "dapps", "Signer disabled, returning 404.");
Some(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-enable Signer to get your homepage back."),
self.signer_address.clone(),
)))
}
}, },
// RPC by default // RPC by default
_ => { _ => {
@ -137,7 +133,7 @@ impl http::RequestMiddleware for Router {
match handler { match handler {
Some(handler) => http::RequestMiddlewareAction::Respond { Some(handler) => http::RequestMiddlewareAction::Respond {
should_validate_hosts: !(is_utils || is_dapps_domain), should_validate_hosts: !is_utils,
handler: handler, handler: handler,
}, },
None => http::RequestMiddlewareAction::Proceed { None => http::RequestMiddlewareAction::Proceed {
@ -149,16 +145,18 @@ impl http::RequestMiddleware for Router {
impl Router { impl Router {
pub fn new( pub fn new(
signer_address: Option<(String, u16)>,
content_fetcher: Arc<Fetcher>, content_fetcher: Arc<Fetcher>,
endpoints: Endpoints, endpoints: Option<Endpoints>,
special: HashMap<SpecialEndpoint, Option<Box<Endpoint>>>, special: HashMap<SpecialEndpoint, Option<Box<Endpoint>>>,
embeddable_on: Option<(String, u16)>,
dapps_domain: String,
) -> Self { ) -> Self {
Router { Router {
signer_address: signer_address,
endpoints: endpoints, endpoints: endpoints,
fetch: content_fetcher, fetch: content_fetcher,
special: special, special: special,
embeddable_on: embeddable_on,
dapps_domain: format!(".{}", dapps_domain),
} }
} }
} }
@ -170,19 +168,19 @@ fn is_web_endpoint(path: &Option<EndpointPath>) -> bool {
} }
} }
fn extract_referer_endpoint(req: &server::Request<HttpStream>) -> Option<(EndpointPath, Url)> { fn extract_referer_endpoint(req: &server::Request<HttpStream>, dapps_domain: &str) -> Option<(EndpointPath, Url)> {
let referer = req.headers().get::<header::Referer>(); let referer = req.headers().get::<header::Referer>();
let url = referer.and_then(|referer| Url::parse(&referer.0).ok()); let url = referer.and_then(|referer| Url::parse(&referer.0).ok());
url.and_then(|url| { url.and_then(|url| {
let option = Some(url); let option = Some(url);
extract_url_referer_endpoint(&option).or_else(|| { extract_url_referer_endpoint(&option, dapps_domain).or_else(|| {
extract_endpoint(&option).0.map(|endpoint| (endpoint, option.expect("Just wrapped; qed"))) extract_endpoint(&option, dapps_domain).0.map(|endpoint| (endpoint, option.expect("Just wrapped; qed")))
}) })
}) })
} }
fn extract_url_referer_endpoint(url: &Option<Url>) -> Option<(EndpointPath, Url)> { fn extract_url_referer_endpoint(url: &Option<Url>, dapps_domain: &str) -> Option<(EndpointPath, Url)> {
let query = url.as_ref().and_then(|url| url.query.as_ref()); let query = url.as_ref().and_then(|url| url.query.as_ref());
match (url, query) { match (url, query) {
(&Some(ref url), Some(ref query)) if query.starts_with(apps::URL_REFERER) => { (&Some(ref url), Some(ref query)) if query.starts_with(apps::URL_REFERER) => {
@ -190,7 +188,7 @@ fn extract_url_referer_endpoint(url: &Option<Url>) -> Option<(EndpointPath, Url)
debug!(target: "dapps", "Recovering referer from query parameter: {}", referer_url); debug!(target: "dapps", "Recovering referer from query parameter: {}", referer_url);
let referer_url = Url::parse(&referer_url).ok(); let referer_url = Url::parse(&referer_url).ok();
extract_endpoint(&referer_url).0.map(|endpoint| { extract_endpoint(&referer_url, dapps_domain).0.map(|endpoint| {
(endpoint, referer_url.expect("Endpoint returned only when url `is_some`").clone()) (endpoint, referer_url.expect("Endpoint returned only when url `is_some`").clone())
}) })
}, },
@ -198,7 +196,7 @@ fn extract_url_referer_endpoint(url: &Option<Url>) -> Option<(EndpointPath, Url)
} }
} }
fn extract_endpoint(url: &Option<Url>) -> (Option<EndpointPath>, SpecialEndpoint) { fn extract_endpoint(url: &Option<Url>, dapps_domain: &str) -> (Option<EndpointPath>, SpecialEndpoint) {
fn special_endpoint(url: &Url) -> SpecialEndpoint { fn special_endpoint(url: &Url) -> SpecialEndpoint {
if url.path.len() <= 1 { if url.path.len() <= 1 {
return SpecialEndpoint::None; return SpecialEndpoint::None;
@ -208,14 +206,15 @@ fn extract_endpoint(url: &Option<Url>) -> (Option<EndpointPath>, SpecialEndpoint
apps::RPC_PATH => SpecialEndpoint::Rpc, apps::RPC_PATH => SpecialEndpoint::Rpc,
apps::API_PATH => SpecialEndpoint::Api, apps::API_PATH => SpecialEndpoint::Api,
apps::UTILS_PATH => SpecialEndpoint::Utils, apps::UTILS_PATH => SpecialEndpoint::Utils,
apps::HOME_PAGE => SpecialEndpoint::Home,
_ => SpecialEndpoint::None, _ => SpecialEndpoint::None,
} }
} }
match *url { match *url {
Some(ref url) => match url.host { Some(ref url) => match url.host {
Host::Domain(ref domain) if domain.ends_with(DAPPS_DOMAIN) => { Host::Domain(ref domain) if domain.ends_with(dapps_domain) => {
let id = &domain[0..(domain.len() - DAPPS_DOMAIN.len())]; let id = &domain[0..(domain.len() - dapps_domain.len())];
let (id, params) = if let Some(split) = id.rfind('.') { let (id, params) = if let Some(split) = id.rfind('.') {
let (params, id) = id.split_at(split); let (params, id) = id.split_at(split);
(id[1..].to_owned(), [params.to_owned()].into_iter().chain(&url.path).cloned().collect()) (id[1..].to_owned(), [params.to_owned()].into_iter().chain(&url.path).cloned().collect())
@ -249,11 +248,12 @@ fn extract_endpoint(url: &Option<Url>) -> (Option<EndpointPath>, SpecialEndpoint
#[test] #[test]
fn should_extract_endpoint() { fn should_extract_endpoint() {
assert_eq!(extract_endpoint(&None), (None, SpecialEndpoint::None)); let dapps_domain = ".web3.site";
assert_eq!(extract_endpoint(&None, dapps_domain), (None, SpecialEndpoint::None));
// With path prefix // With path prefix
assert_eq!( assert_eq!(
extract_endpoint(&Url::parse("http://localhost:8080/status/index.html").ok()), extract_endpoint(&Url::parse("http://localhost:8080/status/index.html").ok(), dapps_domain),
(Some(EndpointPath { (Some(EndpointPath {
app_id: "status".to_owned(), app_id: "status".to_owned(),
app_params: vec!["index.html".to_owned()], app_params: vec!["index.html".to_owned()],
@ -265,7 +265,7 @@ fn should_extract_endpoint() {
// With path prefix // With path prefix
assert_eq!( assert_eq!(
extract_endpoint(&Url::parse("http://localhost:8080/rpc/").ok()), extract_endpoint(&Url::parse("http://localhost:8080/rpc/").ok(), dapps_domain),
(Some(EndpointPath { (Some(EndpointPath {
app_id: "rpc".to_owned(), app_id: "rpc".to_owned(),
app_params: vec!["".to_owned()], app_params: vec!["".to_owned()],
@ -276,7 +276,7 @@ fn should_extract_endpoint() {
); );
assert_eq!( assert_eq!(
extract_endpoint(&Url::parse("http://my.status.web3.site/parity-utils/inject.js").ok()), extract_endpoint(&Url::parse("http://my.status.web3.site/parity-utils/inject.js").ok(), dapps_domain),
(Some(EndpointPath { (Some(EndpointPath {
app_id: "status".to_owned(), app_id: "status".to_owned(),
app_params: vec!["my".to_owned(), "parity-utils".into(), "inject.js".into()], app_params: vec!["my".to_owned(), "parity-utils".into(), "inject.js".into()],
@ -288,7 +288,7 @@ fn should_extract_endpoint() {
// By Subdomain // By Subdomain
assert_eq!( assert_eq!(
extract_endpoint(&Url::parse("http://status.web3.site/test.html").ok()), extract_endpoint(&Url::parse("http://status.web3.site/test.html").ok(), dapps_domain),
(Some(EndpointPath { (Some(EndpointPath {
app_id: "status".to_owned(), app_id: "status".to_owned(),
app_params: vec!["test.html".to_owned()], app_params: vec!["test.html".to_owned()],
@ -300,7 +300,7 @@ fn should_extract_endpoint() {
// RPC by subdomain // RPC by subdomain
assert_eq!( assert_eq!(
extract_endpoint(&Url::parse("http://my.status.web3.site/rpc/").ok()), extract_endpoint(&Url::parse("http://my.status.web3.site/rpc/").ok(), dapps_domain),
(Some(EndpointPath { (Some(EndpointPath {
app_id: "status".to_owned(), app_id: "status".to_owned(),
app_params: vec!["my".to_owned(), "rpc".into(), "".into()], app_params: vec!["my".to_owned(), "rpc".into(), "".into()],
@ -312,7 +312,7 @@ fn should_extract_endpoint() {
// API by subdomain // API by subdomain
assert_eq!( assert_eq!(
extract_endpoint(&Url::parse("http://my.status.web3.site/api/").ok()), extract_endpoint(&Url::parse("http://my.status.web3.site/api/").ok(), dapps_domain),
(Some(EndpointPath { (Some(EndpointPath {
app_id: "status".to_owned(), app_id: "status".to_owned(),
app_params: vec!["my".to_owned(), "api".into(), "".into()], app_params: vec!["my".to_owned(), "api".into(), "".into()],

View File

@ -1,97 +0,0 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::sync::Arc;
use hyper;
use parity_rpc::{Metadata, Origin};
use jsonrpc_core::{Middleware, MetaIoHandler};
use jsonrpc_http_server::{self as http, AccessControlAllowOrigin, HttpMetaExtractor};
use jsonrpc_http_server::tokio_core::reactor::Remote;
use endpoint::{Endpoint, EndpointPath, Handler};
pub fn rpc<T: Middleware<Metadata>>(
handler: MetaIoHandler<Metadata, T>,
remote: Remote,
cors_domains: Vec<AccessControlAllowOrigin>,
) -> Box<Endpoint> {
Box::new(RpcEndpoint {
handler: Arc::new(handler),
remote: remote,
meta_extractor: Arc::new(MetadataExtractor),
cors_domain: Some(cors_domains),
// NOTE [ToDr] We don't need to do any hosts validation here. It's already done in router.
allowed_hosts: None,
})
}
struct RpcEndpoint<T: Middleware<Metadata>> {
handler: Arc<MetaIoHandler<Metadata, T>>,
remote: Remote,
meta_extractor: Arc<HttpMetaExtractor<Metadata>>,
cors_domain: Option<Vec<AccessControlAllowOrigin>>,
allowed_hosts: Option<Vec<http::Host>>,
}
impl<T: Middleware<Metadata>> Endpoint for RpcEndpoint<T> {
fn to_async_handler(&self, _path: EndpointPath, control: hyper::Control) -> Box<Handler> {
Box::new(http::ServerHandler::new(
http::Rpc {
handler: self.handler.clone(),
remote: self.remote.clone(),
extractor: self.meta_extractor.clone(),
},
self.cors_domain.clone(),
self.allowed_hosts.clone(),
Arc::new(NoopMiddleware),
control,
))
}
}
#[derive(Default)]
struct NoopMiddleware;
impl http::RequestMiddleware for NoopMiddleware {
fn on_request(&self, request: &http::hyper::server::Request<http::hyper::net::HttpStream>, _control: &http::hyper::Control) -> http::RequestMiddlewareAction {
http::RequestMiddlewareAction::Proceed {
should_continue_on_invalid_cors: request.headers().get::<http::hyper::header::Origin>().is_none(),
}
}
}
pub struct MetadataExtractor;
impl HttpMetaExtractor<Metadata> for MetadataExtractor {
fn read_metadata(&self, request: &http::hyper::server::Request<http::hyper::net::HttpStream>) -> Metadata {
let dapp_id = request.headers().get::<http::hyper::header::Origin>()
.map(|origin| format!("{}://{}", origin.scheme, origin.host))
.or_else(|| {
// fallback to custom header, but only if origin is null
request.headers().get_raw("origin")
.and_then(|raw| raw.one())
.and_then(|raw| if raw == "null".as_bytes() {
request.headers().get_raw("x-parity-origin")
.and_then(|raw| raw.one())
.map(|raw| String::from_utf8_lossy(raw).into_owned())
} else {
None
})
});
Metadata {
origin: Origin::Dapps(dapp_id.map(Into::into).unwrap_or_default()),
}
}
}

View File

@ -39,29 +39,6 @@ fn should_return_error() {
assert_security_headers(&response.headers); assert_security_headers(&response.headers);
} }
#[test]
fn should_serve_apps() {
// given
let server = serve();
// when
let response = request(server,
"\
GET /api/apps HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
response.assert_header("Content-Type", "application/json");
assert!(response.body.contains("Parity UI"), response.body);
assert_security_headers(&response.headers);
}
#[test] #[test]
fn should_handle_ping() { fn should_handle_ping() {
// given // given
@ -106,92 +83,3 @@ fn should_try_to_resolve_dapp() {
assert_eq!(registrar.calls.lock().len(), 2); assert_eq!(registrar.calls.lock().len(), 2);
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
response.assert_status("HTTP/1.1 200 OK");
response.assert_header("Access-Control-Allow-Origin", "http://127.0.0.1:18180");
}
#[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://parity.web3.site\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
response.assert_header("Access-Control-Allow-Origin", "http://parity.web3.site");
}
#[test]
fn should_return_signer_port_cors_headers_for_home_parity_with_https() {
// given
let server = serve();
// when
let response = request(server,
"\
POST /api/ping HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Origin: https://parity.web3.site\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
response.assert_header("Access-Control-Allow-Origin", "https://parity.web3.site");
}
#[test]
fn should_return_signer_port_cors_headers_for_home_parity_with_port() {
// given
let server = serve();
// when
let response = request(server,
"\
POST /api/ping HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Origin: http://parity.web3.site:18180\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
response.assert_status("HTTP/1.1 200 OK");
response.assert_header("Access-Control-Allow-Origin", "http://parity.web3.site:18180");
}

View File

@ -26,7 +26,7 @@ use jsonrpc_http_server::{self as http, Host, DomainsValidation};
use devtools::http_client; use devtools::http_client;
use hash_fetch::urlhint::ContractClient; use hash_fetch::urlhint::ContractClient;
use fetch::{Fetch, Client as FetchClient}; use fetch::{Fetch, Client as FetchClient};
use parity_reactor::{EventLoop, Remote}; use parity_reactor::Remote;
use {Middleware, SyncStatus, WebProxyTokens}; use {Middleware, SyncStatus, WebProxyTokens};
@ -47,20 +47,7 @@ fn init_logger() {
} }
} }
pub struct ServerLoop { pub fn init_server<F, B>(process: F, io: IoHandler, remote: Remote) -> (Server, Arc<FakeRegistrar>) where
pub server: Server,
pub event_loop: EventLoop,
}
impl ::std::ops::Deref for ServerLoop {
type Target = Server;
fn deref(&self) -> &Self::Target {
&self.server
}
}
pub fn init_server<F, B>(process: F, io: IoHandler, remote: Remote) -> (ServerLoop, Arc<FakeRegistrar>) where
F: FnOnce(ServerBuilder) -> ServerBuilder<B>, F: FnOnce(ServerBuilder) -> ServerBuilder<B>,
B: Fetch, B: Fetch,
{ {
@ -69,44 +56,41 @@ pub fn init_server<F, B>(process: F, io: IoHandler, remote: Remote) -> (ServerLo
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");
// TODO [ToDr] When https://github.com/paritytech/jsonrpc/issues/26 is resolved
// this additional EventLoop wouldn't be needed, we should be able to re-use remote.
let event_loop = EventLoop::spawn();
let server = process(ServerBuilder::new( let server = process(ServerBuilder::new(
&dapps_path, registrar.clone(), remote, &dapps_path, registrar.clone(), remote,
)) ))
.signer_address(Some(("127.0.0.1".into(), SIGNER_PORT))) .signer_address(Some(("127.0.0.1".into(), SIGNER_PORT)))
.start_unsecured_http(&"127.0.0.1:0".parse().unwrap(), io).unwrap(); .start_unsecured_http(&"127.0.0.1:0".parse().unwrap(), io).unwrap();
( (
ServerLoop { server: server, event_loop: event_loop }, server,
registrar, registrar,
) )
} }
pub fn serve_with_rpc(io: IoHandler) -> ServerLoop { pub fn serve_with_rpc(io: IoHandler) -> Server {
init_server(|builder| builder, io, Remote::new_sync()).0 init_server(|builder| builder, io, Remote::new_sync()).0
} }
pub fn serve_hosts(hosts: Option<Vec<String>>) -> ServerLoop { pub fn serve_hosts(hosts: Option<Vec<String>>) -> Server {
let hosts = hosts.map(|hosts| hosts.into_iter().map(Into::into).collect()); let hosts = hosts.map(|hosts| hosts.into_iter().map(Into::into).collect());
init_server(|builder| builder.allowed_hosts(hosts.into()), Default::default(), Remote::new_sync()).0 init_server(|builder| builder.allowed_hosts(hosts.into()), Default::default(), Remote::new_sync()).0
} }
pub fn serve_with_registrar() -> (ServerLoop, Arc<FakeRegistrar>) { pub fn serve_with_registrar() -> (Server, Arc<FakeRegistrar>) {
init_server(|builder| builder, Default::default(), Remote::new_sync()) init_server(|builder| builder, Default::default(), Remote::new_sync())
} }
pub fn serve_with_registrar_and_sync() -> (ServerLoop, Arc<FakeRegistrar>) { pub fn serve_with_registrar_and_sync() -> (Server, Arc<FakeRegistrar>) {
init_server(|builder| { init_server(|builder| {
builder.sync_status(Arc::new(|| true)) builder.sync_status(Arc::new(|| true))
}, Default::default(), Remote::new_sync()) }, Default::default(), Remote::new_sync())
} }
pub fn serve_with_registrar_and_fetch() -> (ServerLoop, FakeFetch, Arc<FakeRegistrar>) { pub fn serve_with_registrar_and_fetch() -> (Server, FakeFetch, Arc<FakeRegistrar>) {
serve_with_registrar_and_fetch_and_threads(false) serve_with_registrar_and_fetch_and_threads(false)
} }
pub fn serve_with_registrar_and_fetch_and_threads(multi_threaded: bool) -> (ServerLoop, FakeFetch, Arc<FakeRegistrar>) { pub fn serve_with_registrar_and_fetch_and_threads(multi_threaded: bool) -> (Server, FakeFetch, Arc<FakeRegistrar>) {
let fetch = FakeFetch::default(); let fetch = FakeFetch::default();
let f = fetch.clone(); let f = fetch.clone();
let (server, reg) = init_server(move |builder| { let (server, reg) = init_server(move |builder| {
@ -116,7 +100,7 @@ pub fn serve_with_registrar_and_fetch_and_threads(multi_threaded: bool) -> (Serv
(server, fetch, reg) (server, fetch, reg)
} }
pub fn serve_with_fetch(web_token: &'static str) -> (ServerLoop, FakeFetch) { pub fn serve_with_fetch(web_token: &'static str) -> (Server, FakeFetch) {
let fetch = FakeFetch::default(); let fetch = FakeFetch::default();
let f = fetch.clone(); let f = fetch.clone();
let (server, _) = init_server(move |builder| { let (server, _) = init_server(move |builder| {
@ -128,11 +112,11 @@ pub fn serve_with_fetch(web_token: &'static str) -> (ServerLoop, FakeFetch) {
(server, fetch) (server, fetch)
} }
pub fn serve() -> ServerLoop { pub fn serve() -> Server {
init_server(|builder| builder, Default::default(), Remote::new_sync()).0 init_server(|builder| builder, Default::default(), Remote::new_sync()).0
} }
pub fn request(server: ServerLoop, request: &str) -> http_client::Response { pub fn request(server: Server, request: &str) -> http_client::Response {
http_client::request(server.addr(), request) http_client::request(server.addr(), request)
} }
@ -240,6 +224,7 @@ impl<T: Fetch> ServerBuilder<T> {
} }
} }
const DAPPS_DOMAIN: &'static str = "web3.site";
/// Webapps HTTP server. /// Webapps HTTP server.
pub struct Server { pub struct Server {
@ -260,19 +245,27 @@ impl Server {
remote: Remote, remote: Remote,
fetch: F, fetch: F,
) -> Result<Server, http::Error> { ) -> Result<Server, http::Error> {
let middleware = Middleware::new( let middleware = Middleware::dapps(
remote, remote,
signer_address, signer_address,
dapps_path, dapps_path,
extra_dapps, extra_dapps,
DAPPS_DOMAIN.into(),
registrar, registrar,
sync_status, sync_status,
web_proxy_tokens, web_proxy_tokens,
fetch, fetch,
); );
let mut allowed_hosts: Option<Vec<Host>> = allowed_hosts.into();
allowed_hosts.as_mut().map(|mut hosts| {
hosts.push(format!("http://*.{}:*", DAPPS_DOMAIN).into());
hosts.push(format!("http://*.{}", DAPPS_DOMAIN).into());
});
http::ServerBuilder::new(io) http::ServerBuilder::new(io)
.request_middleware(middleware) .request_middleware(middleware)
.allowed_hosts(allowed_hosts) .allowed_hosts(allowed_hosts.into())
.cors(http::DomainsValidation::Disabled) .cors(http::DomainsValidation::Disabled)
.start_http(addr) .start_http(addr)
.map(|server| Server { .map(|server| Server {

View File

@ -37,15 +37,15 @@ fn should_redirect_to_home() {
} }
#[test] #[test]
fn should_redirect_to_home_when_trailing_slash_is_missing() { fn should_redirect_to_home_with_domain() {
// given // given
let server = serve(); let server = serve();
// when // when
let response = request(server, let response = request(server,
"\ "\
GET /app HTTP/1.1\r\n\ GET / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\ Host: home.web3.site\r\n\
Connection: close\r\n\ Connection: close\r\n\
\r\n\ \r\n\
" "
@ -57,14 +57,14 @@ fn should_redirect_to_home_when_trailing_slash_is_missing() {
} }
#[test] #[test]
fn should_redirect_to_home_for_users_with_cached_redirection() { fn should_redirect_to_home_when_trailing_slash_is_missing() {
// given // given
let server = serve(); let server = serve();
// when // when
let response = request(server, let response = request(server,
"\ "\
GET /home/ HTTP/1.1\r\n\ GET /app 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\
\r\n\ \r\n\
@ -179,7 +179,7 @@ fn should_serve_proxy_pac() {
// then // then
response.assert_status("HTTP/1.1 200 OK"); response.assert_status("HTTP/1.1 200 OK");
assert_eq!(response.body, "DD\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"parity.web3.site\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:18180\";\n\t}\n\n\tif (shExpMatch(host, \"*.web3.site\"))\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, "DB\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"home.web3.site\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:18180\";\n\t}\n\n\tif (shExpMatch(host, \"*.web3.site\"))\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

@ -90,15 +90,14 @@ export default class Parity {
.execute('parity_consensusCapability'); .execute('parity_consensusCapability');
} }
dappsPort () { dappsList () {
return this._transport return this._transport
.execute('parity_dappsPort') .execute('parity_dappsList');
.then(outNumber);
} }
dappsInterface () { dappsUrl () {
return this._transport return this._transport
.execute('parity_dappsInterface'); .execute('parity_dappsUrl');
} }
decryptMessage (address, data) { decryptMessage (address, data) {
@ -530,12 +529,6 @@ export default class Parity {
.execute('parity_setVaultMeta', vaultName, JSON.stringify(meta)); .execute('parity_setVaultMeta', vaultName, JSON.stringify(meta));
} }
signerPort () {
return this._transport
.execute('parity_signerPort')
.then(outNumber);
}
signMessage (address, password, messageHash) { signMessage (address, password, messageHash) {
return this._transport return this._transport
.execute('parity_signMessage', inAddress(address), password, inHex(messageHash)); .execute('parity_signMessage', inAddress(address), password, inHex(messageHash));
@ -567,4 +560,9 @@ export default class Parity {
return this._transport return this._transport
.execute('parity_versionInfo'); .execute('parity_versionInfo');
} }
wsUrl () {
return this._transport
.execute('parity_wsUrl');
}
} }

View File

@ -19,12 +19,8 @@ import Web3 from 'web3';
const api = window.parent.secureApi; const api = window.parent.secureApi;
let web3; let web3;
Promise api.parity.dappsUrl().then(url => {
.all([ web3 = new Web3(new Web3.providers.HttpProvider(`${window.location.protocol}//${url}/rpc/`));
api.parity.dappsInterface(),
api.parity.dappsPort()
]).then((res) => {
web3 = new Web3(new Web3.providers.HttpProvider(`http://${res.join(':')}/rpc/`));
window.web3 = web3; window.web3 = web3;
// Usage example: // Usage example:

View File

@ -16,8 +16,6 @@
import React from 'react'; import React from 'react';
import { parityNode } from '../../../environment';
const styles = { const styles = {
padding: '.5em', padding: '.5em',
border: '1px solid #777' border: '1px solid #777'
@ -34,7 +32,7 @@ export default (address) => {
return ( return (
<img <img
src={ `${parityNode}/api/content/${address.replace(/^0x/, '')}` } src={ `/api/content/${address.replace(/^0x/, '')}` }
alt={ address } alt={ address }
style={ styles } style={ styles }
/> />

View File

@ -30,7 +30,6 @@ import styles from './token.css';
import { metaDataKeys } from '../../constants'; import { metaDataKeys } from '../../constants';
import { api } from '../../parity'; import { api } from '../../parity';
import { parityNode } from '../../../../environment';
export default class Token extends Component { export default class Token extends Component {
static propTypes = { static propTypes = {
@ -312,7 +311,7 @@ export default class Token extends Component {
</span> meta-data: </span> meta-data:
</p> </p>
<div className={ styles['meta-image'] }> <div className={ styles['meta-image'] }>
<img src={ `${parityNode}/api/content/${imageHash}/` } /> <img src={ `/api/content/${imageHash}/` } />
</div> </div>
</div> </div>
); );

View File

@ -55,6 +55,9 @@ class FakeTransport {
return Promise.reject('not connected'); return Promise.reject('not connected');
} }
addMiddleware () {
}
on () { on () {
} }
} }

View File

@ -19,14 +19,4 @@
import './tests'; import './tests';
const parityNode = ( export {};
process.env.PARITY_URL && `http://${process.env.PARITY_URL}`
) || (
process.env.NODE_ENV === 'production'
? 'http://127.0.0.1:8545'
: ''
);
export {
parityNode
};

View File

@ -53,8 +53,7 @@ if (process.env.NODE_ENV === 'development') {
} }
const AUTH_HASH = '#/auth?'; const AUTH_HASH = '#/auth?';
const parityUrl = process.env.PARITY_URL || window.location.host; const parityUrl = process.env.PARITY_URL || '127.0.0.1:8546';
const urlScheme = window.location.href.match(/^https/) ? 'wss://' : 'ws://';
let token = null; let token = null;
@ -62,7 +61,7 @@ if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) {
token = qs.parse(window.location.hash.substr(AUTH_HASH.length)).token; token = qs.parse(window.location.hash.substr(AUTH_HASH.length)).token;
} }
const api = new SecureApi(`${urlScheme}${parityUrl}`, token); const api = new SecureApi(parityUrl, token);
patchApi(api); patchApi(api);
loadSender(api); loadSender(api);

View File

@ -143,25 +143,34 @@ export default {
} }
}, },
dappsPort: { dappsList: {
section: SECTION_NODE, subdoc: SUBDOC_SET,
desc: 'Returns the port the dapps are running on, error if not enabled.', desc: 'Returns a list of available local dapps.',
params: [], params: [],
returns: { returns: {
type: Quantity, type: Array,
desc: 'The port number', desc: 'The list of dapps',
example: 8080 example: [
{
author: 'Parity Technologies Ltd',
description: 'A skeleton dapp',
iconUrl: 'title.png',
id: 'skeleton',
name: 'Skeleton',
version: '0.1'
}
]
} }
}, },
dappsInterface: { dappsUrl: {
section: SECTION_NODE, section: SECTION_NODE,
desc: 'Returns the interface the dapps are running on, error if not enabled.', desc: 'Returns the hostname and the port of dapps/rpc server, error if not enabled.',
params: [], params: [],
returns: { returns: {
type: String, type: String,
desc: 'The interface', desc: 'The hostname and port number',
example: '127.0.0.1' example: 'localhost:8545'
} }
}, },
@ -788,17 +797,6 @@ export default {
} }
}, },
signerPort: {
section: SECTION_NODE,
desc: 'Returns the port the signer is running on, error if not enabled',
params: [],
returns: {
type: Quantity,
desc: 'The port number',
example: 8180
}
},
transactionsLimit: { transactionsLimit: {
section: SECTION_MINING, section: SECTION_MINING,
desc: 'Changes limit for transactions in queue.', desc: 'Changes limit for transactions in queue.',
@ -1916,6 +1914,17 @@ export default {
} }
}, },
wsUrl: {
section: SECTION_NODE,
desc: 'Returns the hostname and the port of WebSockets/Signer server, error if not enabled.',
params: [],
returns: {
type: String,
desc: 'The hostname and port number',
example: 'localhost:8546'
}
},
composeTransaction: { composeTransaction: {
desc: 'Given partial transaction request produces transaction with all fields filled in. Such transaction can be then signed externally.', desc: 'Given partial transaction request produces transaction with all fields filled in. Such transaction can be then signed externally.',
params: [ params: [
@ -1997,4 +2006,5 @@ export default {
example: 'QmSbFjqjd6nFwNHqsBCC7SK8GShGcayLUEtysJjNGhZAnC' example: 'QmSbFjqjd6nFwNHqsBCC7SK8GShGcayLUEtysJjNGhZAnC'
} }
} }
}; };

View File

@ -27,21 +27,28 @@ export default class SecureApi extends Api {
_needsToken = false; _needsToken = false;
_tokens = []; _tokens = [];
_dappsInterface = null; _dappsUrl = null;
_dappsPort = 8545; _wsUrl = null;
_signerPort = 8180;
static getTransport (url, sysuiToken) { static getTransport (url, sysuiToken, protocol) {
return new Api.Transport.Ws(url, sysuiToken, false); const proto = protocol() === 'https:' ? 'wss:' : 'ws:';
return new Api.Transport.Ws(`${proto}//${url}`, sysuiToken, false);
} }
constructor (url, nextToken, getTransport = SecureApi.getTransport) { // Returns a protocol with `:` at the end.
static protocol () {
return window.location.protocol;
}
constructor (url, nextToken, getTransport = SecureApi.getTransport, protocol = SecureApi.protocol) {
const sysuiToken = store.get('sysuiToken'); const sysuiToken = store.get('sysuiToken');
const transport = getTransport(url, sysuiToken); const transport = getTransport(url, sysuiToken, protocol);
super(transport); super(transport);
this._url = url; this._wsUrl = url;
this.protocol = protocol;
// Try tokens from localStorage, from hash and 'initial' // Try tokens from localStorage, from hash and 'initial'
this._tokens = uniq([sysuiToken, nextToken, 'initial']) this._tokens = uniq([sysuiToken, nextToken, 'initial'])
.filter((token) => token) .filter((token) => token)
@ -53,12 +60,30 @@ export default class SecureApi extends Api {
this.connect(); this.connect();
} }
get _dappsAddress () {
if (!this._dappsUrl) {
return {
host: null,
port: 8545
};
}
const [host, port] = this._dappsUrl.split(':');
return {
host,
port: parseInt(port, 10)
};
}
get dappsPort () { get dappsPort () {
return this._dappsPort; return this._dappsAddress.port;
} }
get dappsUrl () { get dappsUrl () {
return `http://${this.hostname}:${this.dappsPort}`; const { port } = this._dappsAddress;
return `${this.protocol()}//${this.hostname}:${port}`;
} }
get hostname () { get hostname () {
@ -66,15 +91,13 @@ export default class SecureApi extends Api {
return 'dapps.parity'; return 'dapps.parity';
} }
if (!this._dappsInterface || this._dappsInterface === '0.0.0.0') { const { host } = this._dappsAddress;
if (!host || host === '0.0.0.0') {
return window.location.hostname; return window.location.hostname;
} }
return this._dappsInterface; return host;
}
get signerPort () {
return this._signerPort;
} }
get isConnecting () { get isConnecting () {
@ -98,18 +121,18 @@ export default class SecureApi extends Api {
* (`signerPort`, `dappsInterface`, `dappsPort`, ...) * (`signerPort`, `dappsInterface`, `dappsPort`, ...)
*/ */
configure (configuration) { configure (configuration) {
const { dappsInterface, dappsPort, signerPort } = configuration; const { dappsInterface, dappsPort, signerPort, wsPort } = configuration;
if (dappsInterface) { if (dappsInterface) {
this._dappsInterface = dappsInterface; this._dappsUrl = `${dappsInterface}:${this._dappsAddress.port}`;
} }
if (dappsPort) { if (dappsPort) {
this._dappsPort = dappsPort; this._dappsUrl = `${this.hostname}:${dappsPort}`;
} }
if (signerPort) { if (signerPort || wsPort) {
this._signerPort = signerPort; this._wsUrl = `${this.hostname}:${signerPort || wsPort}`;
} }
} }
@ -166,9 +189,7 @@ export default class SecureApi extends Api {
* otherwise (HEAD request to the Node) * otherwise (HEAD request to the Node)
*/ */
isNodeUp () { isNodeUp () {
const url = this._url.replace(/wss?/, 'http'); return fetch(`${this.protocol()}//${this._wsUrl}`, { method: 'HEAD', mode: 'no-cors' })
return fetch(url, { method: 'HEAD' })
.then( .then(
(r) => r.status === 200, (r) => r.status === 200,
() => false () => false
@ -297,14 +318,12 @@ export default class SecureApi extends Api {
_fetchSettings () { _fetchSettings () {
return Promise return Promise
.all([ .all([
this.parity.dappsPort(), this.parity.dappsUrl(),
this.parity.dappsInterface(), this.parity.wsUrl()
this.parity.signerPort()
]) ])
.then(([dappsPort, dappsInterface, signerPort]) => { .then(([dappsUrl, wsUrl]) => {
this._dappsPort = dappsPort.toNumber(); this._dappsUrl = dappsUrl;
this._dappsInterface = dappsInterface; this._wsUrl = dappsUrl;
this._signerPort = signerPort.toNumber();
}); });
} }

View File

@ -25,21 +25,6 @@ import builtinJson from '~/views/Dapps/builtin.json';
const builtinApps = builtinJson.filter((app) => app.id); const builtinApps = builtinJson.filter((app) => app.id);
function getHost (api) {
const host = process.env.DAPPS_URL ||
(
process.env.NODE_ENV === 'production'
? api.dappsUrl
: ''
);
if (host === '/') {
return '';
}
return host;
}
export function subscribeToChanges (api, dappReg, callback) { export function subscribeToChanges (api, dappReg, callback) {
return dappReg return dappReg
.getContract() .getContract()
@ -105,12 +90,7 @@ export function fetchBuiltinApps () {
} }
export function fetchLocalApps (api) { export function fetchLocalApps (api) {
return fetch(`${getHost(api)}/api/apps`) return api.parity.dappsList()
.then((response) => {
return response.ok
? response.json()
: [];
})
.then((apps) => { .then((apps) => {
return apps return apps
.map((app) => { .map((app) => {
@ -195,7 +175,7 @@ export function fetchManifest (api, manifestHash) {
} }
return fetch( return fetch(
`${getHost(api)}/api/content/${manifestHash}/`, `/api/content/${manifestHash}/`,
{ redirect: 'follow', mode: 'cors' } { redirect: 'follow', mode: 'cors' }
) )
.then((response) => { .then((response) => {

View File

@ -26,17 +26,11 @@ const APPID_DAPPREG = '0x7bbc4f1a27628781b96213e781a1b8eec6982c1db8fac739af6e4c5
const APPID_GHH = '0x058740ee9a5a3fb9f1cfa10752baec87e09cc45cd7027fd54708271aca300c75'; const APPID_GHH = '0x058740ee9a5a3fb9f1cfa10752baec87e09cc45cd7027fd54708271aca300c75';
const APPID_LOCALTX = '0xae74ad174b95cdbd01c88ac5b73a296d33e9088fc2a200e76bcedf3a94a7815d'; const APPID_LOCALTX = '0xae74ad174b95cdbd01c88ac5b73a296d33e9088fc2a200e76bcedf3a94a7815d';
const APPID_TOKENDEPLOY = '0xf9f2d620c2e08f83e45555247146c62185e4ab7cf82a4b9002a265a0d020348f'; const APPID_TOKENDEPLOY = '0xf9f2d620c2e08f83e45555247146c62185e4ab7cf82a4b9002a265a0d020348f';
const FETCH_OK = {
ok: true,
status: 200
};
let globalContractsGet; let globalContractsGet;
let globalFetch;
function stubGlobals () { function stubGlobals () {
globalContractsGet = Contracts.get; globalContractsGet = Contracts.get;
globalFetch = global.fetch;
Contracts.get = () => { Contracts.get = () => {
return { return {
@ -50,31 +44,21 @@ function stubGlobals () {
} }
}; };
}; };
global.fetch = (url) => {
switch (url) {
case '/api/apps':
return Promise.resolve(Object.assign({}, FETCH_OK, {
json: sinon.stub().resolves([]) // TODO: Local stubs in here
}));
default:
console.log('Unknown fetch stub endpoint', url);
return Promise.reject();
}
};
} }
function restoreGlobals () { function restoreGlobals () {
Contracts.get = globalContractsGet; Contracts.get = globalContractsGet;
global.fetch = globalFetch;
} }
let api; let api;
let store; let store;
function create () { function create () {
api = {}; api = {
parity: {
dappsList: () => Promise.resolve([])
}
};
store = new Store(api); store = new Store(api);
return store; return store;

View File

@ -34,8 +34,8 @@ let store;
function createApi () { function createApi () {
api = { api = {
dappsPort: 8080, dappsPort: 8545,
dappsUrl: 'http://home.web3.site:8080', dappsUrl: 'http://home.web3.site:8545',
parity: { parity: {
listRecentDapps: sinon.stub().resolves(TEST_HISTORY) listRecentDapps: sinon.stub().resolves(TEST_HISTORY)
}, },
@ -159,7 +159,7 @@ describe('views/Web/Store', () => {
it('encodes current', () => { it('encodes current', () => {
store.setCurrentUrl(TEST_URL1); store.setCurrentUrl(TEST_URL1);
expect(store.encodedPath).to.match( expect(store.encodedPath).to.match(
/http:\/\/home\.web3\.site:8080\/web\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\?t=[0-9]*$/ /http:\/\/home\.web3\.site:8545\/web\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\?t=[0-9]*$/
); );
}); });
}); });
@ -167,7 +167,7 @@ describe('views/Web/Store', () => {
it('encodes current', () => { it('encodes current', () => {
store.setCurrentUrl(TEST_URL1); store.setCurrentUrl(TEST_URL1);
expect(store.encodedUrl).to.match( expect(store.encodedUrl).to.match(
/^http:\/\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\.web\.web3\.site:8080\?t=[0-9]*$/ /^http:\/\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\.web\.web3\.site:8545\?t=[0-9]*$/
); );
}); });
}); });

View File

@ -15,26 +15,21 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
// test only // test only
/** /**
* Run `DAPPS_URL="/" PARITY_URL="127.0.0.1:8180" NODE_ENV="production" npm run build` * Run `DAPPS_URL="/" PARITY_URL="127.0.0.1:8546" NODE_ENV="production" npm run build`
* to build the project ; use this server to test that the minifed * to build the project ; use this server to test that the minifed
* version is working (this is a simple proxy server) * version is working (this is a simple proxy server)
*/ */
var express = require('express'); var express = require('express');
var proxy = require('http-proxy-middleware');
var Shared = require('./shared'); var Shared = require('./shared');
var app = express(); var app = express();
var wsProxy = proxy('ws://127.0.0.1:8180', { changeOrigin: true });
Shared.addProxies(app); Shared.addProxies(app);
app.use(express.static('.build')); app.use(express.static('.build'));
app.use(wsProxy);
var server = app.listen(process.env.PORT || 3000, function () { var server = app.listen(process.env.PORT || 3000, function () {
console.log('Listening on port', server.address().port); console.log('Listening on port', server.address().port);
}); });
server.on('upgrade', wsProxy.upgrade);

View File

@ -22,7 +22,6 @@ const webpackHotMiddleware = require('webpack-hot-middleware');
const http = require('http'); const http = require('http');
const express = require('express'); const express = require('express');
const ProgressBar = require('progress'); const ProgressBar = require('progress');
const proxy = require('http-proxy-middleware');
const webpackConfig = require('./app'); const webpackConfig = require('./app');
const Shared = require('./shared'); const Shared = require('./shared');
@ -84,18 +83,13 @@ app.use(webpackDevMiddleware(compiler, {
} }
})); }));
var wsProxy = proxy('ws://127.0.0.1:8180', { changeOrigin: true });
// Add the dev proxies in the express App // Add the dev proxies in the express App
Shared.addProxies(app); Shared.addProxies(app);
app.use(express.static(webpackConfig.output.path)); app.use(express.static(webpackConfig.output.path));
app.use(wsProxy);
const server = http.createServer(app); const server = http.createServer(app);
server.listen(process.env.PORT || 3000, function () { server.listen(process.env.PORT || 3000, function () {
console.log('Listening on port', server.address().port); console.log('Listening on port', server.address().port);
progressBar = new ProgressBar('[:bar] :percent :etas', { total: 50 }); progressBar = new ProgressBar('[:bar] :percent :etas', { total: 50 });
}); });
server.on('upgrade', wsProxy.upgrade);

View File

@ -162,16 +162,8 @@ function getDappsEntry () {
function addProxies (app) { function addProxies (app) {
const proxy = require('http-proxy-middleware'); const proxy = require('http-proxy-middleware');
app.use(proxy((pathname, req) => {
return pathname === '/' && req.method === 'HEAD';
}, {
target: 'http://127.0.0.1:8180',
changeOrigin: true,
autoRewrite: true
}));
app.use('/api', proxy({ app.use('/api', proxy({
target: 'http://127.0.0.1:8545', target: 'http://127.0.0.1:8180',
changeOrigin: true, changeOrigin: true,
autoRewrite: true autoRewrite: true
})); }));

View File

@ -124,6 +124,8 @@ usage! {
or |c: &Config| otry!(c.ui).port.clone(), or |c: &Config| otry!(c.ui).port.clone(),
flag_ui_interface: String = "local", flag_ui_interface: String = "local",
or |c: &Config| otry!(c.ui).interface.clone(), or |c: &Config| otry!(c.ui).interface.clone(),
flag_ui_hosts: String = "none",
or |c: &Config| otry!(c.ui).hosts.as_ref().map(|vec| vec.join(",")),
flag_ui_path: String = "$BASE/signer", flag_ui_path: String = "$BASE/signer",
or |c: &Config| otry!(c.ui).path.clone(), or |c: &Config| otry!(c.ui).path.clone(),
// NOTE [todr] For security reasons don't put this to config files // NOTE [todr] For security reasons don't put this to config files
@ -188,7 +190,7 @@ usage! {
or |c: &Config| otry!(c.websockets).interface.clone(), or |c: &Config| otry!(c.websockets).interface.clone(),
flag_ws_apis: String = "web3,eth,pubsub,net,parity,parity_pubsub,traces,rpc,secretstore", flag_ws_apis: String = "web3,eth,pubsub,net,parity,parity_pubsub,traces,rpc,secretstore",
or |c: &Config| otry!(c.websockets).apis.as_ref().map(|vec| vec.join(",")), or |c: &Config| otry!(c.websockets).apis.as_ref().map(|vec| vec.join(",")),
flag_ws_origins: String = "none", flag_ws_origins: String = "chrome-extension://*",
or |c: &Config| otry!(c.websockets).origins.as_ref().map(|vec| vec.join(",")), or |c: &Config| otry!(c.websockets).origins.as_ref().map(|vec| vec.join(",")),
flag_ws_hosts: String = "none", flag_ws_hosts: String = "none",
or |c: &Config| otry!(c.websockets).hosts.as_ref().map(|vec| vec.join(",")), or |c: &Config| otry!(c.websockets).hosts.as_ref().map(|vec| vec.join(",")),
@ -430,6 +432,7 @@ struct Ui {
disable: Option<bool>, disable: Option<bool>,
port: Option<u16>, port: Option<u16>,
interface: Option<String>, interface: Option<String>,
hosts: Option<Vec<String>>,
path: Option<String>, path: Option<String>,
} }
@ -709,6 +712,7 @@ mod tests {
flag_no_ui: false, flag_no_ui: false,
flag_ui_port: 8180u16, flag_ui_port: 8180u16,
flag_ui_interface: "127.0.0.1".into(), flag_ui_interface: "127.0.0.1".into(),
flag_ui_hosts: "none".into(),
flag_ui_path: "$HOME/.parity/signer".into(), flag_ui_path: "$HOME/.parity/signer".into(),
flag_ui_no_validation: false, flag_ui_no_validation: false,
@ -929,6 +933,7 @@ mod tests {
disable: Some(true), disable: Some(true),
port: None, port: None,
interface: None, interface: None,
hosts: None,
path: None, path: None,
}), }),
network: Some(Network { network: Some(Network {

View File

@ -110,6 +110,11 @@ UI Options:
--ui-interface IP Specify the hostname portion of the Trusted UI --ui-interface IP Specify the hostname portion of the Trusted UI
server, IP should be an interface's IP address, server, IP should be an interface's IP address,
or local (default: {flag_ui_interface}). or local (default: {flag_ui_interface}).
--ui-hosts HOSTS List of allowed Host header values. This option will
validate the Host header sent by the browser, it
is additional security against some attack
vectors. Special options: "all", "none",
(default: {flag_ui_hosts}).
--ui-path PATH Specify directory where Trusted UIs tokens should --ui-path PATH Specify directory where Trusted UIs tokens should
be stored. (default: {flag_ui_path}) be stored. (default: {flag_ui_path})
--ui-no-validation Disable Origin and Host headers validation for --ui-no-validation Disable Origin and Host headers validation for

View File

@ -30,7 +30,7 @@ use ethcore::client::{VMType};
use ethcore::miner::{MinerOptions, Banning, StratumOptions}; use ethcore::miner::{MinerOptions, Banning, StratumOptions};
use ethcore::verification::queue::VerifierSettings; use ethcore::verification::queue::VerifierSettings;
use rpc::{IpcConfiguration, HttpConfiguration, WsConfiguration}; use rpc::{IpcConfiguration, HttpConfiguration, WsConfiguration, UiConfiguration};
use rpc_apis::ApiSet; use rpc_apis::ApiSet;
use parity_rpc::NetworkSettings; use parity_rpc::NetworkSettings;
use cache::CacheConfig; use cache::CacheConfig;
@ -41,7 +41,6 @@ use ethcore_logger::Config as LogConfig;
use dir::{self, Directories, default_hypervisor_path, default_local_path, default_data_path}; use dir::{self, Directories, default_hypervisor_path, default_local_path, default_data_path};
use dapps::Configuration as DappsConfiguration; use dapps::Configuration as DappsConfiguration;
use ipfs::Configuration as IpfsConfiguration; use ipfs::Configuration as IpfsConfiguration;
use signer::{Configuration as SignerConfiguration};
use secretstore::Configuration as SecretStoreConfiguration; use secretstore::Configuration as SecretStoreConfiguration;
use updater::{UpdatePolicy, UpdateFilter, ReleaseTrack}; use updater::{UpdatePolicy, UpdateFilter, ReleaseTrack};
use run::RunCmd; use run::RunCmd;
@ -50,8 +49,6 @@ use presale::ImportWallet;
use account::{AccountCmd, NewAccount, ListAccounts, ImportAccounts, ImportFromGethAccounts}; use account::{AccountCmd, NewAccount, ListAccounts, ImportAccounts, ImportFromGethAccounts};
use snapshot::{self, SnapshotCommand}; use snapshot::{self, SnapshotCommand};
const AUTHCODE_FILENAME: &'static str = "authcodes";
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Cmd { pub enum Cmd {
Run(RunCmd), Run(RunCmd),
@ -59,7 +56,7 @@ pub enum Cmd {
Account(AccountCmd), Account(AccountCmd),
ImportPresaleWallet(ImportWallet), ImportPresaleWallet(ImportWallet),
Blockchain(BlockchainCmd), Blockchain(BlockchainCmd),
SignerToken(SignerConfiguration), SignerToken(WsConfiguration, UiConfiguration),
SignerSign { SignerSign {
id: Option<usize>, id: Option<usize>,
pwfile: Option<PathBuf>, pwfile: Option<PathBuf>,
@ -118,6 +115,7 @@ impl Configuration {
let http_conf = self.http_config()?; let http_conf = self.http_config()?;
let ipc_conf = self.ipc_config()?; let ipc_conf = self.ipc_config()?;
let net_conf = self.net_config()?; let net_conf = self.net_config()?;
let ui_conf = self.ui_config();
let network_id = self.network_id(); let network_id = self.network_id();
let cache_config = self.cache_config(); let cache_config = self.cache_config();
let tracing = self.args.flag_tracing.parse()?; let tracing = self.args.flag_tracing.parse()?;
@ -134,10 +132,8 @@ impl Configuration {
let public_node = self.args.flag_public_node; let public_node = self.args.flag_public_node;
let warp_sync = !self.args.flag_no_warp && fat_db != Switch::On && tracing != Switch::On && pruning != Pruning::Specific(Algorithm::Archive); let warp_sync = !self.args.flag_no_warp && fat_db != Switch::On && tracing != Switch::On && pruning != Pruning::Specific(Algorithm::Archive);
let geth_compatibility = self.args.flag_geth; let geth_compatibility = self.args.flag_geth;
let ui_address = self.ui_port().map(|port| (self.ui_interface(), port));
let mut dapps_conf = self.dapps_config(); let mut dapps_conf = self.dapps_config();
let ipfs_conf = self.ipfs_config(); let ipfs_conf = self.ipfs_config();
let signer_conf = self.signer_config();
let secretstore_conf = self.secretstore_config()?; let secretstore_conf = self.secretstore_config()?;
let format = self.format()?; let format = self.format()?;
@ -149,11 +145,10 @@ impl Configuration {
let cmd = if self.args.flag_version { let cmd = if self.args.flag_version {
Cmd::Version Cmd::Version
} else if self.args.cmd_signer { } else if self.args.cmd_signer {
let mut authfile = PathBuf::from(signer_conf.signer_path.clone()); let authfile = ::signer::codes_path(&ws_conf.signer_path);
authfile.push(AUTHCODE_FILENAME);
if self.args.cmd_new_token { if self.args.cmd_new_token {
Cmd::SignerToken(signer_conf) Cmd::SignerToken(ws_conf, ui_conf)
} else if self.args.cmd_sign { } else if self.args.cmd_sign {
let pwfile = self.args.flag_password.get(0).map(|pwfile| { let pwfile = self.args.flag_password.get(0).map(|pwfile| {
PathBuf::from(pwfile) PathBuf::from(pwfile)
@ -161,18 +156,18 @@ impl Configuration {
Cmd::SignerSign { Cmd::SignerSign {
id: self.args.arg_id, id: self.args.arg_id,
pwfile: pwfile, pwfile: pwfile,
port: signer_conf.port, port: ws_conf.port,
authfile: authfile, authfile: authfile,
} }
} else if self.args.cmd_reject { } else if self.args.cmd_reject {
Cmd::SignerReject { Cmd::SignerReject {
id: self.args.arg_id, id: self.args.arg_id,
port: signer_conf.port, port: ws_conf.port,
authfile: authfile, authfile: authfile,
} }
} else if self.args.cmd_list { } else if self.args.cmd_list {
Cmd::SignerList { Cmd::SignerList {
port: signer_conf.port, port: ws_conf.port,
authfile: authfile, authfile: authfile,
} }
} else { } else {
@ -372,11 +367,10 @@ impl Configuration {
warp_sync: warp_sync, warp_sync: warp_sync,
public_node: public_node, public_node: public_node,
geth_compatibility: geth_compatibility, geth_compatibility: geth_compatibility,
ui_address: ui_address,
net_settings: self.network_settings()?, net_settings: self.network_settings()?,
dapps_conf: dapps_conf, dapps_conf: dapps_conf,
ipfs_conf: ipfs_conf, ipfs_conf: ipfs_conf,
signer_conf: signer_conf, ui_conf: ui_conf,
secretstore_conf: secretstore_conf, secretstore_conf: secretstore_conf,
dapp: self.dapp_to_open()?, dapp: self.dapp_to_open()?,
ui: self.args.cmd_ui, ui: self.args.cmd_ui,
@ -553,13 +547,12 @@ impl Configuration {
Ok(options) Ok(options)
} }
fn signer_config(&self) -> SignerConfiguration { fn ui_config(&self) -> UiConfiguration {
SignerConfiguration { UiConfiguration {
enabled: self.ui_enabled(), enabled: self.ui_enabled(),
port: self.args.flag_ports_shift + self.args.flag_ui_port,
interface: self.ui_interface(), interface: self.ui_interface(),
signer_path: self.directories().signer, port: self.args.flag_ports_shift + self.args.flag_ui_port,
skip_origin_validation: self.args.flag_unsafe_expose || self.args.flag_ui_no_validation, hosts: self.ui_hosts(),
} }
} }
@ -768,6 +761,14 @@ impl Configuration {
Some(hosts) Some(hosts)
} }
fn ui_hosts(&self) -> Option<Vec<String>> {
if self.args.flag_ui_no_validation {
return None;
}
self.hosts(&self.args.flag_ui_hosts, &self.ui_interface())
}
fn rpc_hosts(&self) -> Option<Vec<String>> { fn rpc_hosts(&self) -> Option<Vec<String>> {
self.hosts(&self.args.flag_jsonrpc_hosts, &self.rpc_interface()) self.hosts(&self.args.flag_jsonrpc_hosts, &self.rpc_interface())
} }
@ -825,13 +826,17 @@ impl Configuration {
} }
fn ws_config(&self) -> Result<WsConfiguration, String> { fn ws_config(&self) -> Result<WsConfiguration, String> {
let ui = self.ui_config();
let conf = WsConfiguration { let conf = WsConfiguration {
enabled: self.ws_enabled(), enabled: self.ws_enabled(),
interface: self.ws_interface(), interface: self.ws_interface(),
port: self.args.flag_ports_shift + self.args.flag_ws_port, port: self.args.flag_ports_shift + self.args.flag_ws_port,
apis: self.args.flag_ws_apis.parse()?, apis: self.args.flag_ws_apis.parse()?,
hosts: self.ws_hosts(), hosts: self.ws_hosts(),
origins: self.ws_origins() origins: self.ws_origins(),
signer_path: self.directories().signer.into(),
ui_address: ui.address(),
}; };
Ok(conf) Ok(conf)
@ -928,18 +933,6 @@ impl Configuration {
} }
} }
fn ui_port(&self) -> Option<u16> {
if !self.ui_enabled() {
None
} else {
Some(self.args.flag_ui_port)
}
}
fn ui_interface(&self) -> String {
self.interface(&self.args.flag_ui_interface)
}
fn interface(&self, interface: &str) -> String { fn interface(&self, interface: &str) -> String {
if self.args.flag_unsafe_expose { if self.args.flag_unsafe_expose {
return "0.0.0.0".into(); return "0.0.0.0".into();
@ -952,6 +945,11 @@ impl Configuration {
}.into() }.into()
} }
fn ui_interface(&self) -> String {
self.interface(&self.args.flag_ui_interface)
}
fn rpc_interface(&self) -> String { fn rpc_interface(&self) -> String {
let rpc_interface = self.args.flag_rpcaddr.clone().unwrap_or(self.args.flag_jsonrpc_interface.clone()); let rpc_interface = self.args.flag_rpcaddr.clone().unwrap_or(self.args.flag_jsonrpc_interface.clone());
self.interface(&rpc_interface) self.interface(&rpc_interface)
@ -1050,24 +1048,27 @@ impl Configuration {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use cli::Args;
use parity_rpc::NetworkSettings;
use ethcore::client::{VMType, BlockId};
use ethcore::miner::{MinerOptions, PrioritizationStrategy};
use helpers::{default_network_config};
use run::RunCmd;
use dir::{Directories, default_hypervisor_path};
use signer::{Configuration as SignerConfiguration};
use blockchain::{BlockchainCmd, ImportBlockchain, ExportBlockchain, DataFormat, ExportState};
use presale::ImportWallet;
use params::SpecType;
use account::{AccountCmd, NewAccount, ImportAccounts, ListAccounts};
use devtools::{RandomTempPath};
use updater::{UpdatePolicy, UpdateFilter, ReleaseTrack};
use std::io::Write; use std::io::Write;
use std::fs::{File, create_dir}; use std::fs::{File, create_dir};
use devtools::{RandomTempPath};
use ethcore::client::{VMType, BlockId};
use ethcore::miner::{MinerOptions, PrioritizationStrategy};
use parity_rpc::NetworkSettings;
use updater::{UpdatePolicy, UpdateFilter, ReleaseTrack};
use account::{AccountCmd, NewAccount, ImportAccounts, ListAccounts};
use blockchain::{BlockchainCmd, ImportBlockchain, ExportBlockchain, DataFormat, ExportState};
use cli::Args;
use dir::{Directories, default_hypervisor_path};
use helpers::{default_network_config};
use params::SpecType;
use presale::ImportWallet;
use rpc::{WsConfiguration, UiConfiguration};
use run::RunCmd;
use super::*;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
struct TestPasswordReader(&'static str); struct TestPasswordReader(&'static str);
@ -1233,12 +1234,20 @@ mod tests {
let args = vec!["parity", "signer", "new-token"]; let args = vec!["parity", "signer", "new-token"];
let conf = parse(&args); let conf = parse(&args);
let expected = Directories::default().signer; let expected = Directories::default().signer;
assert_eq!(conf.into_command().unwrap().cmd, Cmd::SignerToken(SignerConfiguration { assert_eq!(conf.into_command().unwrap().cmd, Cmd::SignerToken(WsConfiguration {
enabled: true,
interface: "127.0.0.1".into(),
port: 8546,
apis: ApiSet::UnsafeContext,
origins: Some(vec!["chrome-extension://*".into()]),
hosts: Some(vec![]),
signer_path: expected.into(),
ui_address: Some(("127.0.0.1".to_owned(), 8180)),
}, UiConfiguration {
enabled: true, enabled: true,
signer_path: expected,
interface: "127.0.0.1".into(), interface: "127.0.0.1".into(),
port: 8180, port: 8180,
skip_origin_validation: false, hosts: Some(vec![]),
})); }));
} }
@ -1273,11 +1282,10 @@ mod tests {
wal: true, wal: true,
vm_type: Default::default(), vm_type: Default::default(),
geth_compatibility: false, geth_compatibility: false,
ui_address: Some(("127.0.0.1".into(), 8180)),
net_settings: Default::default(), net_settings: Default::default(),
dapps_conf: Default::default(), dapps_conf: Default::default(),
ipfs_conf: Default::default(), ipfs_conf: Default::default(),
signer_conf: Default::default(), ui_conf: Default::default(),
secretstore_conf: Default::default(), secretstore_conf: Default::default(),
ui: false, ui: false,
dapp: None, dapp: None,
@ -1457,7 +1465,7 @@ mod tests {
} }
#[test] #[test]
fn should_parse_signer_configration() { fn should_parse_ui_configuration() {
// given // given
// when // when
@ -1467,33 +1475,33 @@ mod tests {
let conf3 = parse(&["parity", "--ui-path", "signer", "--ui-interface", "test"]); let conf3 = parse(&["parity", "--ui-path", "signer", "--ui-interface", "test"]);
// then // then
assert_eq!(conf0.signer_config(), SignerConfiguration { assert_eq!(conf0.directories().signer, "signer".to_owned());
assert_eq!(conf0.ui_config(), UiConfiguration {
enabled: true, enabled: true,
port: 8180,
interface: "127.0.0.1".into(), interface: "127.0.0.1".into(),
signer_path: "signer".into(),
skip_origin_validation: false,
});
assert_eq!(conf1.signer_config(), SignerConfiguration {
enabled: true,
port: 8180, port: 8180,
interface: "127.0.0.1".into(), hosts: Some(vec![]),
signer_path: "signer".into(),
skip_origin_validation: true,
}); });
assert_eq!(conf2.signer_config(), SignerConfiguration { assert_eq!(conf1.directories().signer, "signer".to_owned());
assert_eq!(conf1.ui_config(), UiConfiguration {
enabled: true, enabled: true,
interface: "127.0.0.1".into(),
port: 8180,
hosts: None,
});
assert_eq!(conf2.directories().signer, "signer".to_owned());
assert_eq!(conf2.ui_config(), UiConfiguration {
enabled: true,
interface: "127.0.0.1".into(),
port: 3123, port: 3123,
interface: "127.0.0.1".into(), hosts: Some(vec![]),
signer_path: "signer".into(),
skip_origin_validation: false,
}); });
assert_eq!(conf3.signer_config(), SignerConfiguration { assert_eq!(conf3.directories().signer, "signer".to_owned());
assert_eq!(conf3.ui_config(), UiConfiguration {
enabled: true, enabled: true,
port: 8180,
interface: "test".into(), interface: "test".into(),
signer_path: "signer".into(), port: 8180,
skip_origin_validation: false, hosts: Some(vec![]),
}); });
} }
@ -1551,7 +1559,7 @@ mod tests {
assert_eq!(conf0.network_settings().unwrap().rpc_port, 8546); assert_eq!(conf0.network_settings().unwrap().rpc_port, 8546);
assert_eq!(conf0.http_config().unwrap().port, 8546); assert_eq!(conf0.http_config().unwrap().port, 8546);
assert_eq!(conf0.ws_config().unwrap().port, 8547); assert_eq!(conf0.ws_config().unwrap().port, 8547);
assert_eq!(conf0.signer_config().port, 8181); assert_eq!(conf0.ui_config().port, 8181);
assert_eq!(conf0.secretstore_config().unwrap().port, 8084); assert_eq!(conf0.secretstore_config().unwrap().port, 8084);
assert_eq!(conf0.secretstore_config().unwrap().http_port, 8083); assert_eq!(conf0.secretstore_config().unwrap().http_port, 8083);
assert_eq!(conf0.ipfs_config().port, 5002); assert_eq!(conf0.ipfs_config().port, 5002);
@ -1563,7 +1571,7 @@ mod tests {
assert_eq!(conf1.network_settings().unwrap().rpc_port, 8545); assert_eq!(conf1.network_settings().unwrap().rpc_port, 8545);
assert_eq!(conf1.http_config().unwrap().port, 8545); assert_eq!(conf1.http_config().unwrap().port, 8545);
assert_eq!(conf1.ws_config().unwrap().port, 8547); assert_eq!(conf1.ws_config().unwrap().port, 8547);
assert_eq!(conf1.signer_config().port, 8181); assert_eq!(conf1.ui_config().port, 8181);
assert_eq!(conf1.secretstore_config().unwrap().port, 8084); assert_eq!(conf1.secretstore_config().unwrap().port, 8084);
assert_eq!(conf1.secretstore_config().unwrap().http_port, 8083); assert_eq!(conf1.secretstore_config().unwrap().http_port, 8083);
assert_eq!(conf1.ipfs_config().port, 5002); assert_eq!(conf1.ipfs_config().port, 5002);
@ -1582,8 +1590,8 @@ mod tests {
assert_eq!(conf0.http_config().unwrap().hosts, None); assert_eq!(conf0.http_config().unwrap().hosts, None);
assert_eq!(&conf0.ws_config().unwrap().interface, "0.0.0.0"); assert_eq!(&conf0.ws_config().unwrap().interface, "0.0.0.0");
assert_eq!(conf0.ws_config().unwrap().hosts, None); assert_eq!(conf0.ws_config().unwrap().hosts, None);
assert_eq!(&conf0.signer_config().interface, "0.0.0.0"); assert_eq!(&conf0.ui_config().interface, "0.0.0.0");
assert_eq!(conf0.signer_config().skip_origin_validation, true); assert_eq!(conf0.ui_config().hosts, None);
assert_eq!(&conf0.secretstore_config().unwrap().interface, "0.0.0.0"); assert_eq!(&conf0.secretstore_config().unwrap().interface, "0.0.0.0");
assert_eq!(&conf0.secretstore_config().unwrap().http_interface, "0.0.0.0"); assert_eq!(&conf0.secretstore_config().unwrap().http_interface, "0.0.0.0");
assert_eq!(&conf0.ipfs_config().interface, "0.0.0.0"); assert_eq!(&conf0.ipfs_config().interface, "0.0.0.0");

View File

@ -27,6 +27,7 @@ use hash_fetch::urlhint::ContractClient;
use helpers::replace_home; use helpers::replace_home;
use light::client::Client as LightClient; use light::client::Client as LightClient;
use light::on_demand::{self, OnDemand}; use light::on_demand::{self, OnDemand};
use rpc;
use rpc_apis::SignerService; use rpc_apis::SignerService;
use parity_reactor; use parity_reactor;
use util::{Bytes, Address}; use util::{Bytes, Address};
@ -49,6 +50,15 @@ impl Default for Configuration {
} }
} }
impl Configuration {
pub fn address(&self, address: Option<(String, u16)>) -> Option<(String, u16)> {
match self.enabled {
true => address,
false => None,
}
}
}
/// Registrar implementation of the full client. /// Registrar implementation of the full client.
pub struct FullRegistrar { pub struct FullRegistrar {
/// Handle to the full client. /// Handle to the full client.
@ -125,35 +135,49 @@ impl ContractClient for LightRegistrar {
// TODO: light client implementation forwarding to OnDemand and waiting for future // TODO: light client implementation forwarding to OnDemand and waiting for future
// to resolve. // to resolve.
#[derive(Clone)]
pub struct Dependencies { pub struct Dependencies {
pub sync_status: Arc<SyncStatus>, pub sync_status: Arc<SyncStatus>,
pub contract_client: Arc<ContractClient>, pub contract_client: Arc<ContractClient>,
pub remote: parity_reactor::TokioRemote, pub remote: parity_reactor::TokioRemote,
pub fetch: FetchClient, pub fetch: FetchClient,
pub signer: Arc<SignerService>, pub signer: Arc<SignerService>,
pub ui_address: Option<(String, u16)>,
} }
pub fn new(configuration: Configuration, deps: Dependencies) pub fn new(configuration: Configuration, deps: Dependencies) -> Result<Option<Middleware>, String> {
-> Result<Option<Middleware>, String>
{
if !configuration.enabled { if !configuration.enabled {
return Ok(None); return Ok(None);
} }
dapps_middleware( server::dapps_middleware(
deps, deps,
configuration.dapps_path, configuration.dapps_path,
configuration.extra_dapps, configuration.extra_dapps,
rpc::DAPPS_DOMAIN.into(),
).map(Some) ).map(Some)
} }
pub use self::server::{SyncStatus, Middleware, dapps_middleware}; pub fn new_ui(enabled: bool, deps: Dependencies) -> Result<Option<Middleware>, String> {
if !enabled {
return Ok(None);
}
server::ui_middleware(
deps,
rpc::DAPPS_DOMAIN.into(),
).map(Some)
}
pub use self::server::{SyncStatus, Middleware, service};
#[cfg(not(feature = "dapps"))] #[cfg(not(feature = "dapps"))]
mod server { mod server {
use super::Dependencies; use super::Dependencies;
use std::sync::Arc;
use std::path::PathBuf; use std::path::PathBuf;
use parity_rpc::{hyper, RequestMiddleware, RequestMiddlewareAction}; use parity_rpc::{hyper, RequestMiddleware, RequestMiddlewareAction};
use rpc_apis;
pub type SyncStatus = Fn() -> bool; pub type SyncStatus = Fn() -> bool;
@ -170,9 +194,21 @@ mod server {
_deps: Dependencies, _deps: Dependencies,
_dapps_path: PathBuf, _dapps_path: PathBuf,
_extra_dapps: Vec<PathBuf>, _extra_dapps: Vec<PathBuf>,
_dapps_domain: String,
) -> Result<Middleware, String> { ) -> Result<Middleware, String> {
Err("Your Parity version has been compiled without WebApps support.".into()) Err("Your Parity version has been compiled without WebApps support.".into())
} }
pub fn ui_middleware(
_deps: Dependencies,
_dapps_domain: String,
) -> Result<Middleware, String> {
Err("Your Parity version has been compiled without UI support.".into())
}
pub fn service(_: &Option<Middleware>) -> Option<Arc<rpc_apis::DappsService>> {
None
}
} }
#[cfg(feature = "dapps")] #[cfg(feature = "dapps")]
@ -180,6 +216,7 @@ mod server {
use super::Dependencies; use super::Dependencies;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use rpc_apis;
use parity_dapps; use parity_dapps;
use parity_reactor; use parity_reactor;
@ -191,20 +228,62 @@ mod server {
deps: Dependencies, deps: Dependencies,
dapps_path: PathBuf, dapps_path: PathBuf,
extra_dapps: Vec<PathBuf>, extra_dapps: Vec<PathBuf>,
dapps_domain: String,
) -> Result<Middleware, String> { ) -> Result<Middleware, String> {
let signer = deps.signer.clone(); let signer = deps.signer;
let parity_remote = parity_reactor::Remote::new(deps.remote.clone()); let parity_remote = parity_reactor::Remote::new(deps.remote.clone());
let web_proxy_tokens = Arc::new(move |token| signer.is_valid_web_proxy_access_token(&token)); let web_proxy_tokens = Arc::new(move |token| signer.is_valid_web_proxy_access_token(&token));
Ok(parity_dapps::Middleware::new( Ok(parity_dapps::Middleware::dapps(
parity_remote, parity_remote,
deps.signer.address(), deps.ui_address,
dapps_path, dapps_path,
extra_dapps, extra_dapps,
dapps_domain,
deps.contract_client, deps.contract_client,
deps.sync_status, deps.sync_status,
web_proxy_tokens, web_proxy_tokens,
deps.fetch.clone(), deps.fetch,
)) ))
} }
pub fn ui_middleware(
deps: Dependencies,
dapps_domain: String,
) -> Result<Middleware, String> {
let parity_remote = parity_reactor::Remote::new(deps.remote.clone());
Ok(parity_dapps::Middleware::ui(
parity_remote,
deps.contract_client,
deps.sync_status,
deps.fetch,
dapps_domain,
))
}
pub fn service(middleware: &Option<Middleware>) -> Option<Arc<rpc_apis::DappsService>> {
middleware.as_ref().map(|m| Arc::new(DappsServiceWrapper {
endpoints: m.endpoints()
}) as Arc<rpc_apis::DappsService>)
}
pub struct DappsServiceWrapper {
endpoints: parity_dapps::Endpoints,
}
impl rpc_apis::DappsService for DappsServiceWrapper {
fn list_dapps(&self) -> Vec<rpc_apis::LocalDapp> {
self.endpoints.list()
.into_iter()
.map(|app| rpc_apis::LocalDapp {
id: app.id,
name: app.name,
description: app.description,
version: app.version,
author: app.author,
icon_url: app.icon_url,
})
.collect()
}
}
} }

View File

@ -51,7 +51,6 @@ extern crate ethcore_ipc_hypervisor as hypervisor;
extern crate ethcore_ipc_nano as nanoipc; extern crate ethcore_ipc_nano as nanoipc;
extern crate ethcore_light as light; extern crate ethcore_light as light;
extern crate ethcore_logger; extern crate ethcore_logger;
extern crate ethcore_signer;
extern crate ethcore_util as util; extern crate ethcore_util as util;
extern crate ethkey; extern crate ethkey;
extern crate ethsync; extern crate ethsync;
@ -114,9 +113,9 @@ mod presale;
mod rpc; mod rpc;
mod rpc_apis; mod rpc_apis;
mod run; mod run;
mod secretstore;
mod signer; mod signer;
mod snapshot; mod snapshot;
mod secretstore;
mod upgrade; mod upgrade;
mod url; mod url;
mod user_defaults; mod user_defaults;
@ -170,7 +169,7 @@ fn execute(command: Execute, can_restart: bool) -> Result<PostExecutionAction, S
Cmd::Account(account_cmd) => account::execute(account_cmd).map(|s| PostExecutionAction::Print(s)), Cmd::Account(account_cmd) => account::execute(account_cmd).map(|s| PostExecutionAction::Print(s)),
Cmd::ImportPresaleWallet(presale_cmd) => presale::execute(presale_cmd).map(|s| PostExecutionAction::Print(s)), Cmd::ImportPresaleWallet(presale_cmd) => presale::execute(presale_cmd).map(|s| PostExecutionAction::Print(s)),
Cmd::Blockchain(blockchain_cmd) => blockchain::execute(blockchain_cmd).map(|_| PostExecutionAction::Quit), Cmd::Blockchain(blockchain_cmd) => blockchain::execute(blockchain_cmd).map(|_| PostExecutionAction::Quit),
Cmd::SignerToken(signer_cmd) => signer::execute(signer_cmd).map(|s| PostExecutionAction::Print(s)), Cmd::SignerToken(ws_conf, ui_conf) => signer::execute(ws_conf, ui_conf).map(|s| PostExecutionAction::Print(s)),
Cmd::SignerSign { id, pwfile, port, authfile } => rpc_cli::signer_sign(id, pwfile, port, authfile).map(|s| PostExecutionAction::Print(s)), Cmd::SignerSign { id, pwfile, port, authfile } => rpc_cli::signer_sign(id, pwfile, port, authfile).map(|s| PostExecutionAction::Print(s)),
Cmd::SignerList { port, authfile } => rpc_cli::signer_list(port, authfile).map(|s| PostExecutionAction::Print(s)), Cmd::SignerList { port, authfile } => rpc_cli::signer_list(port, authfile).map(|s| PostExecutionAction::Print(s)),
Cmd::SignerReject { id, port, authfile } => rpc_cli::signer_reject(id, port, authfile).map(|s| PostExecutionAction::Print(s)), Cmd::SignerReject { id, port, authfile } => rpc_cli::signer_reject(id, port, authfile).map(|s| PostExecutionAction::Print(s)),

View File

@ -16,18 +16,24 @@
use std::io; use std::io;
use std::sync::Arc; use std::sync::Arc;
use std::path::PathBuf;
use std::collections::HashSet;
use dapps; use dapps;
use parity_rpc::informant::{RpcStats, Middleware}; use dir::default_data_path;
use parity_rpc::{self as rpc, HttpServerError, Metadata, Origin, DomainsValidation}; use helpers::{parity_ipc_path, replace_home};
use helpers::parity_ipc_path;
use jsonrpc_core::MetaIoHandler; use jsonrpc_core::MetaIoHandler;
use parity_reactor::TokioRemote; use parity_reactor::TokioRemote;
use parity_rpc::informant::{RpcStats, Middleware};
use parity_rpc::{self as rpc, Metadata, DomainsValidation};
use rpc_apis::{self, ApiSet}; use rpc_apis::{self, ApiSet};
pub use parity_rpc::{IpcServer, HttpServer, RequestMiddleware}; pub use parity_rpc::{IpcServer, HttpServer, RequestMiddleware};
pub use parity_rpc::ws::Server as WsServer; pub use parity_rpc::ws::Server as WsServer;
pub const DAPPS_DOMAIN: &'static str = "web3.site";
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct HttpConfiguration { pub struct HttpConfiguration {
pub enabled: bool, pub enabled: bool,
@ -39,6 +45,15 @@ pub struct HttpConfiguration {
pub threads: Option<usize>, pub threads: Option<usize>,
} }
impl HttpConfiguration {
pub fn address(&self) -> Option<(String, u16)> {
match self.enabled {
true => Some((self.interface.clone(), self.port)),
false => None,
}
}
}
impl Default for HttpConfiguration { impl Default for HttpConfiguration {
fn default() -> Self { fn default() -> Self {
HttpConfiguration { HttpConfiguration {
@ -53,6 +68,48 @@ impl Default for HttpConfiguration {
} }
} }
#[derive(Debug, PartialEq, Clone)]
pub struct UiConfiguration {
pub enabled: bool,
pub interface: String,
pub port: u16,
pub hosts: Option<Vec<String>>,
}
impl UiConfiguration {
pub fn address(&self) -> Option<(String, u16)> {
match self.enabled {
true => Some((self.interface.clone(), self.port)),
false => None,
}
}
}
impl From<UiConfiguration> for HttpConfiguration {
fn from(conf: UiConfiguration) -> Self {
HttpConfiguration {
enabled: conf.enabled,
interface: conf.interface,
port: conf.port,
apis: rpc_apis::ApiSet::SafeContext,
cors: None,
hosts: conf.hosts,
threads: None,
}
}
}
impl Default for UiConfiguration {
fn default() -> Self {
UiConfiguration {
enabled: true,
port: 8180,
interface: "127.0.0.1".into(),
hosts: Some(vec![]),
}
}
}
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub struct IpcConfiguration { pub struct IpcConfiguration {
pub enabled: bool, pub enabled: bool,
@ -75,7 +132,7 @@ impl Default for IpcConfiguration {
} }
} }
#[derive(Debug, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct WsConfiguration { pub struct WsConfiguration {
pub enabled: bool, pub enabled: bool,
pub interface: String, pub interface: String,
@ -83,17 +140,32 @@ pub struct WsConfiguration {
pub apis: ApiSet, pub apis: ApiSet,
pub origins: Option<Vec<String>>, pub origins: Option<Vec<String>>,
pub hosts: Option<Vec<String>>, pub hosts: Option<Vec<String>>,
pub signer_path: PathBuf,
pub ui_address: Option<(String, u16)>,
} }
impl Default for WsConfiguration { impl Default for WsConfiguration {
fn default() -> Self { fn default() -> Self {
let data_dir = default_data_path();
WsConfiguration { WsConfiguration {
enabled: true, enabled: true,
interface: "127.0.0.1".into(), interface: "127.0.0.1".into(),
port: 8546, port: 8546,
apis: ApiSet::UnsafeContext, apis: ApiSet::UnsafeContext,
origins: Some(Vec::new()), origins: Some(vec!["chrome-extension://*".into()]),
hosts: Some(Vec::new()), hosts: Some(Vec::new()),
signer_path: replace_home(&data_dir, "$BASE/signer").into(),
ui_address: Some(("127.0.0.1".to_owned(), 8180)),
}
}
}
impl WsConfiguration {
pub fn address(&self) -> Option<(String, u16)> {
match self.enabled {
true => Some((self.interface.clone(), self.port)),
false => None,
} }
} }
} }
@ -104,62 +176,6 @@ pub struct Dependencies<D: rpc_apis::Dependencies> {
pub stats: Arc<RpcStats>, pub stats: Arc<RpcStats>,
} }
pub struct RpcExtractor;
impl rpc::HttpMetaExtractor for RpcExtractor {
type Metadata = Metadata;
fn read_metadata(&self, origin: String, dapps_origin: Option<String>) -> Metadata {
let mut metadata = Metadata::default();
metadata.origin = match (origin.as_str(), dapps_origin) {
("null", Some(dapp)) => Origin::Dapps(dapp.into()),
_ => Origin::Rpc(origin),
};
metadata
}
}
impl rpc::IpcMetaExtractor<Metadata> for RpcExtractor {
fn extract(&self, _req: &rpc::IpcRequestContext) -> Metadata {
let mut metadata = Metadata::default();
// TODO [ToDr] Extract proper session id when it's available in context.
metadata.origin = Origin::Ipc(1.into());
metadata
}
}
struct WsRpcExtractor;
impl rpc::ws::MetaExtractor<Metadata> for WsRpcExtractor {
fn extract(&self, req: &rpc::ws::RequestContext) -> Metadata {
let mut metadata = Metadata::default();
let id = req.session_id as u64;
metadata.origin = Origin::Ws(id.into());
metadata.session = Some(Arc::new(rpc::PubSubSession::new(req.sender())));
metadata
}
}
struct WsStats {
stats: Arc<RpcStats>,
}
impl rpc::ws::SessionStats for WsStats {
fn open_session(&self, _id: rpc::ws::SessionId) {
self.stats.open_session()
}
fn close_session(&self, _id: rpc::ws::SessionId) {
self.stats.close_session()
}
}
fn setup_apis<D>(apis: ApiSet, deps: &Dependencies<D>) -> MetaIoHandler<Metadata, Middleware<D::Notifier>>
where D: rpc_apis::Dependencies
{
rpc_apis::setup_rpc(deps.stats.clone(), &*deps.apis, apis)
}
pub fn new_ws<D: rpc_apis::Dependencies>( pub fn new_ws<D: rpc_apis::Dependencies>(
conf: WsConfiguration, conf: WsConfiguration,
deps: &Dependencies<D>, deps: &Dependencies<D>,
@ -168,23 +184,41 @@ pub fn new_ws<D: rpc_apis::Dependencies>(
return Ok(None); return Ok(None);
} }
let url = format!("{}:{}", conf.interface, conf.port); let domain = DAPPS_DOMAIN;
let ws_address = (conf.interface, conf.port);
let url = format!("{}:{}", ws_address.0, ws_address.1);
let addr = url.parse().map_err(|_| format!("Invalid WebSockets listen host/port given: {}", url))?; let addr = url.parse().map_err(|_| format!("Invalid WebSockets listen host/port given: {}", url))?;
let handler = setup_apis(conf.apis, deps);
let remote = deps.remote.clone();
let allowed_origins = into_domains(conf.origins);
let allowed_hosts = into_domains(conf.hosts);
let full_handler = setup_apis(rpc_apis::ApiSet::SafeContext, deps);
let handler = {
let mut handler = MetaIoHandler::with_middleware((
rpc::WsDispatcher::new(full_handler),
Middleware::new(deps.stats.clone(), deps.apis.activity_notifier())
));
let apis = conf.apis.list_apis().into_iter().collect::<Vec<_>>();
deps.apis.extend_with_set(&mut handler, &apis);
handler
};
let remote = deps.remote.clone();
let ui_address = conf.ui_address.clone();
let allowed_origins = into_domains(with_domain(conf.origins, domain, &[ui_address]));
let allowed_hosts = into_domains(with_domain(conf.hosts, domain, &[Some(ws_address)]));
let signer_path = conf.signer_path;
let signer_path = conf.ui_address.map(move |_| ::signer::codes_path(&signer_path));
let path = signer_path.as_ref().map(|p| p.as_path());
let start_result = rpc::start_ws( let start_result = rpc::start_ws(
&addr, &addr,
handler, handler,
remote.clone(), remote.clone(),
allowed_origins, allowed_origins,
allowed_hosts, allowed_hosts,
WsRpcExtractor, rpc::WsExtractor::new(path.clone()),
WsStats { rpc::WsExtractor::new(path.clone()),
stats: deps.stats.clone(), rpc::WsStats::new(deps.stats.clone()),
},
); );
match start_result { match start_result {
@ -197,21 +231,25 @@ pub fn new_ws<D: rpc_apis::Dependencies>(
} }
pub fn new_http<D: rpc_apis::Dependencies>( pub fn new_http<D: rpc_apis::Dependencies>(
id: &str,
options: &str,
conf: HttpConfiguration, conf: HttpConfiguration,
deps: &Dependencies<D>, deps: &Dependencies<D>,
middleware: Option<dapps::Middleware> middleware: Option<dapps::Middleware>,
) -> Result<Option<HttpServer>, String> { ) -> Result<Option<HttpServer>, String> {
if !conf.enabled { if !conf.enabled {
return Ok(None); return Ok(None);
} }
let url = format!("{}:{}", conf.interface, conf.port); let domain = DAPPS_DOMAIN;
let addr = url.parse().map_err(|_| format!("Invalid HTTP JSON-RPC listen host/port given: {}", url))?; let http_address = (conf.interface, conf.port);
let url = format!("{}:{}", http_address.0, http_address.1);
let addr = url.parse().map_err(|_| format!("Invalid {} listen host/port given: {}", id, url))?;
let handler = setup_apis(conf.apis, deps); let handler = setup_apis(conf.apis, deps);
let remote = deps.remote.clone(); let remote = deps.remote.clone();
let cors_domains = into_domains(conf.cors); let cors_domains = into_domains(conf.cors);
let allowed_hosts = into_domains(conf.hosts); let allowed_hosts = into_domains(with_domain(conf.hosts, domain, &[Some(http_address)]));
let start_result = rpc::start_http( let start_result = rpc::start_http(
&addr, &addr,
@ -219,7 +257,7 @@ pub fn new_http<D: rpc_apis::Dependencies>(
allowed_hosts, allowed_hosts,
handler, handler,
remote, remote,
RpcExtractor, rpc::RpcExtractor,
match (conf.threads, middleware) { match (conf.threads, middleware) {
(Some(threads), None) => rpc::HttpSettings::Threads(threads), (Some(threads), None) => rpc::HttpSettings::Threads(threads),
(None, middleware) => rpc::HttpSettings::Dapps(middleware), (None, middleware) => rpc::HttpSettings::Dapps(middleware),
@ -231,17 +269,13 @@ pub fn new_http<D: rpc_apis::Dependencies>(
match start_result { match start_result {
Ok(server) => Ok(Some(server)), Ok(server) => Ok(Some(server)),
Err(HttpServerError::Io(ref err)) if err.kind() == io::ErrorKind::AddrInUse => Err( Err(rpc::HttpServerError::Io(ref err)) if err.kind() == io::ErrorKind::AddrInUse => Err(
format!("HTTP address {} is already in use, make sure that another instance of an Ethereum client is not running or change the address using the --jsonrpc-port and --jsonrpc-interface options.", url) format!("{} address {} is already in use, make sure that another instance of an Ethereum client is not running or change the address using the --{}-port and --{}-interface options.", id, url, options, options)
), ),
Err(e) => Err(format!("HTTP error: {:?}", e)), Err(e) => Err(format!("{} error: {:?}", id, e)),
} }
} }
fn into_domains<T: From<String>>(items: Option<Vec<String>>) -> DomainsValidation<T> {
items.map(|vals| vals.into_iter().map(T::from).collect()).into()
}
pub fn new_ipc<D: rpc_apis::Dependencies>( pub fn new_ipc<D: rpc_apis::Dependencies>(
conf: IpcConfiguration, conf: IpcConfiguration,
dependencies: &Dependencies<D> dependencies: &Dependencies<D>
@ -252,48 +286,39 @@ pub fn new_ipc<D: rpc_apis::Dependencies>(
let handler = setup_apis(conf.apis, dependencies); let handler = setup_apis(conf.apis, dependencies);
let remote = dependencies.remote.clone(); let remote = dependencies.remote.clone();
let ipc = rpc::start_ipc( match rpc::start_ipc(&conf.socket_addr, handler, remote, rpc::RpcExtractor) {
&conf.socket_addr,
handler,
remote,
RpcExtractor,
);
match ipc {
Ok(server) => Ok(Some(server)), Ok(server) => Ok(Some(server)),
Err(io_error) => Err(format!("IPC error: {}", io_error)), Err(io_error) => Err(format!("IPC error: {}", io_error)),
} }
} }
#[cfg(test)] fn into_domains<T: From<String>>(items: Option<Vec<String>>) -> DomainsValidation<T> {
mod tests { items.map(|vals| vals.into_iter().map(T::from).collect()).into()
use super::RpcExtractor;
use parity_rpc::{HttpMetaExtractor, Origin};
#[test]
fn should_extract_rpc_origin() {
// given
let extractor = RpcExtractor;
// when
let meta = extractor.read_metadata("http://parity.io".into(), None);
let meta1 = extractor.read_metadata("http://parity.io".into(), Some("ignored".into()));
// then
assert_eq!(meta.origin, Origin::Rpc("http://parity.io".into()));
assert_eq!(meta1.origin, Origin::Rpc("http://parity.io".into()));
} }
#[test] fn with_domain(items: Option<Vec<String>>, domain: &str, addresses: &[Option<(String, u16)>]) -> Option<Vec<String>> {
fn should_dapps_origin() { items.map(move |items| {
// given let mut items = items.into_iter().collect::<HashSet<_>>();
let extractor = RpcExtractor; for address in addresses {
let dapp = "https://wallet.ethereum.org".to_owned(); if let Some((host, port)) = address.clone() {
items.insert(format!("{}:{}", host, port));
// when items.insert(format!("{}:{}", host.replace("127.0.0.1", "localhost"), port));
let meta = extractor.read_metadata("null".into(), Some(dapp.clone())); items.insert(format!("http://*.{}:{}", domain, port));
items.insert(format!("http://*.{}", domain)); //proxypac
// then
assert_eq!(meta.origin, Origin::Dapps(dapp.into()));
} }
} }
items.into_iter().collect()
})
}
fn setup_apis<D>(apis: ApiSet, deps: &Dependencies<D>) -> MetaIoHandler<Metadata, Middleware<D::Notifier>>
where D: rpc_apis::Dependencies
{
let mut handler = MetaIoHandler::with_middleware(
Middleware::new(deps.stats.clone(), deps.apis.activity_notifier())
);
let apis = apis.list_apis().into_iter().collect::<Vec<_>>();
deps.apis.extend_with_set(&mut handler, &apis);
handler
}

View File

@ -20,14 +20,15 @@ use std::collections::HashSet;
use std::str::FromStr; use std::str::FromStr;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
pub use parity_rpc::SignerService; pub use parity_rpc::signer::SignerService;
pub use parity_rpc::dapps::{DappsService, LocalDapp};
use ethcore::account_provider::AccountProvider; use ethcore::account_provider::AccountProvider;
use ethcore::client::Client; use ethcore::client::Client;
use ethcore::miner::{Miner, ExternalMiner}; use ethcore::miner::{Miner, ExternalMiner};
use ethcore::snapshot::SnapshotService; use ethcore::snapshot::SnapshotService;
use parity_rpc::{Metadata, NetworkSettings}; use parity_rpc::{Metadata, NetworkSettings};
use parity_rpc::informant::{ActivityNotifier, Middleware, RpcStats, ClientNotifier}; use parity_rpc::informant::{ActivityNotifier, ClientNotifier};
use parity_rpc::dispatch::{FullDispatcher, LightDispatcher}; use parity_rpc::dispatch::{FullDispatcher, LightDispatcher};
use ethsync::{ManageNetwork, SyncProvider, LightSync}; use ethsync::{ManageNetwork, SyncProvider, LightSync};
use hash_fetch::fetch::Client as FetchClient; use hash_fetch::fetch::Client as FetchClient;
@ -183,7 +184,11 @@ pub trait Dependencies {
fn activity_notifier(&self) -> Self::Notifier; fn activity_notifier(&self) -> Self::Notifier;
/// Extend the given I/O handler with endpoints for each API. /// Extend the given I/O handler with endpoints for each API.
fn extend_with_set(&self, handler: &mut MetaIoHandler<Metadata, Middleware<Self::Notifier>>, apis: &[Api]); fn extend_with_set<S>(
&self,
handler: &mut MetaIoHandler<Metadata, S>,
apis: &[Api],
) where S: core::Middleware<Metadata>;
} }
/// RPC dependencies for a full node. /// RPC dependencies for a full node.
@ -201,19 +206,20 @@ pub struct FullDependencies {
pub net_service: Arc<ManageNetwork>, pub net_service: Arc<ManageNetwork>,
pub updater: Arc<Updater>, pub updater: Arc<Updater>,
pub geth_compatibility: bool, pub geth_compatibility: bool,
pub dapps_interface: Option<String>, pub dapps_service: Option<Arc<DappsService>>,
pub dapps_port: Option<u16>, pub dapps_address: Option<(String, u16)>,
pub ws_address: Option<(String, u16)>,
pub fetch: FetchClient, pub fetch: FetchClient,
pub remote: parity_reactor::Remote, pub remote: parity_reactor::Remote,
} }
impl FullDependencies { impl FullDependencies {
fn extend_api<T: core::Middleware<Metadata>>( fn extend_api<S>(
&self, &self,
handler: &mut MetaIoHandler<Metadata, T>, handler: &mut MetaIoHandler<Metadata, S>,
apis: &[Api], apis: &[Api],
for_generic_pubsub: bool, for_generic_pubsub: bool,
) { ) where S: core::Middleware<Metadata> {
use parity_rpc::v1::*; use parity_rpc::v1::*;
macro_rules! add_signing_methods { macro_rules! add_signing_methods {
@ -288,8 +294,8 @@ impl FullDependencies {
self.logger.clone(), self.logger.clone(),
self.settings.clone(), self.settings.clone(),
signer, signer,
self.dapps_interface.clone(), self.dapps_address.clone(),
self.dapps_port, self.ws_address.clone(),
).to_delegate()); ).to_delegate());
if !for_generic_pubsub { if !for_generic_pubsub {
@ -312,6 +318,7 @@ impl FullDependencies {
&self.miner, &self.miner,
&self.updater, &self.updater,
&self.net_service, &self.net_service,
self.dapps_service.clone(),
self.fetch.clone(), self.fetch.clone(),
).to_delegate()) ).to_delegate())
}, },
@ -339,7 +346,11 @@ impl Dependencies for FullDependencies {
} }
} }
fn extend_with_set(&self, handler: &mut MetaIoHandler<Metadata, Middleware<Self::Notifier>>, apis: &[Api]) { fn extend_with_set<S>(
&self,
handler: &mut MetaIoHandler<Metadata, S>,
apis: &[Api],
) where S: core::Middleware<Metadata> {
self.extend_api(handler, apis, false) self.extend_api(handler, apis, false)
} }
} }
@ -363,8 +374,9 @@ pub struct LightDependencies {
pub on_demand: Arc<::light::on_demand::OnDemand>, pub on_demand: Arc<::light::on_demand::OnDemand>,
pub cache: Arc<Mutex<LightDataCache>>, pub cache: Arc<Mutex<LightDataCache>>,
pub transaction_queue: Arc<RwLock<LightTransactionQueue>>, pub transaction_queue: Arc<RwLock<LightTransactionQueue>>,
pub dapps_interface: Option<String>, pub dapps_service: Option<Arc<DappsService>>,
pub dapps_port: Option<u16>, pub dapps_address: Option<(String, u16)>,
pub ws_address: Option<(String, u16)>,
pub fetch: FetchClient, pub fetch: FetchClient,
pub geth_compatibility: bool, pub geth_compatibility: bool,
pub remote: parity_reactor::Remote, pub remote: parity_reactor::Remote,
@ -457,8 +469,8 @@ impl LightDependencies {
self.logger.clone(), self.logger.clone(),
self.settings.clone(), self.settings.clone(),
signer, signer,
self.dapps_interface.clone(), self.dapps_address.clone(),
self.dapps_port, self.ws_address.clone(),
).to_delegate()); ).to_delegate());
if !for_generic_pubsub { if !for_generic_pubsub {
@ -479,6 +491,7 @@ impl LightDependencies {
Api::ParitySet => { Api::ParitySet => {
handler.extend_with(light::ParitySetClient::new( handler.extend_with(light::ParitySetClient::new(
self.sync.clone(), self.sync.clone(),
self.dapps_service.clone(),
self.fetch.clone(), self.fetch.clone(),
).to_delegate()) ).to_delegate())
}, },
@ -502,7 +515,12 @@ impl Dependencies for LightDependencies {
type Notifier = LightClientNotifier; type Notifier = LightClientNotifier;
fn activity_notifier(&self) -> Self::Notifier { LightClientNotifier } fn activity_notifier(&self) -> Self::Notifier { LightClientNotifier }
fn extend_with_set(&self, handler: &mut MetaIoHandler<Metadata, Middleware<Self::Notifier>>, apis: &[Api]) {
fn extend_with_set<S>(
&self,
handler: &mut MetaIoHandler<Metadata, S>,
apis: &[Api],
) where S: core::Middleware<Metadata> {
self.extend_api(handler, apis, false) self.extend_api(handler, apis, false)
} }
} }
@ -552,15 +570,6 @@ impl ApiSet {
} }
} }
pub fn setup_rpc<D: Dependencies>(stats: Arc<RpcStats>, deps: &D, apis: ApiSet) -> MetaIoHandler<Metadata, Middleware<D::Notifier>> {
let mut handler = MetaIoHandler::with_middleware(Middleware::new(stats, deps.activity_notifier()));
// it's turned into vector, cause ont of the cases requires &[]
let apis = apis.list_apis().into_iter().collect::<Vec<_>>();
deps.extend_with_set(&mut handler, &apis[..]);
handler
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{Api, ApiSet}; use super::{Api, ApiSet};

View File

@ -49,11 +49,11 @@ use cache::CacheConfig;
use user_defaults::UserDefaults; use user_defaults::UserDefaults;
use dapps; use dapps;
use ipfs; use ipfs;
use signer;
use secretstore;
use modules; use modules;
use rpc_apis;
use rpc; use rpc;
use rpc_apis;
use secretstore;
use signer;
use url; use url;
// how often to take periodic snapshots. // how often to take periodic snapshots.
@ -99,11 +99,10 @@ pub struct RunCmd {
pub wal: bool, pub wal: bool,
pub vm_type: VMType, pub vm_type: VMType,
pub geth_compatibility: bool, pub geth_compatibility: bool,
pub ui_address: Option<(String, u16)>,
pub net_settings: NetworkSettings, pub net_settings: NetworkSettings,
pub dapps_conf: dapps::Configuration, pub dapps_conf: dapps::Configuration,
pub ipfs_conf: ipfs::Configuration, pub ipfs_conf: ipfs::Configuration,
pub signer_conf: signer::Configuration, pub ui_conf: rpc::UiConfiguration,
pub secretstore_conf: secretstore::Configuration, pub secretstore_conf: secretstore::Configuration,
pub dapp: Option<String>, pub dapp: Option<String>,
pub ui: bool, pub ui: bool,
@ -119,12 +118,12 @@ pub struct RunCmd {
pub no_persistent_txqueue: bool, pub no_persistent_txqueue: bool,
} }
pub fn open_ui(signer_conf: &signer::Configuration) -> Result<(), String> { pub fn open_ui(ws_conf: &rpc::WsConfiguration, ui_conf: &rpc::UiConfiguration) -> Result<(), String> {
if !signer_conf.enabled { if !ui_conf.enabled {
return Err("Cannot use UI command with UI turned off.".into()) return Err("Cannot use UI command with UI turned off.".into())
} }
let token = signer::generate_token_and_url(signer_conf)?; let token = signer::generate_token_and_url(ws_conf, ui_conf)?;
// Open a browser // Open a browser
url::open(&token.url); url::open(&token.url);
// Print a message // Print a message
@ -195,7 +194,7 @@ fn execute_light(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) ->
execute_upgrades(&cmd.dirs.base, &db_dirs, algorithm, compaction.clone())?; execute_upgrades(&cmd.dirs.base, &db_dirs, algorithm, compaction.clone())?;
// create dirs used by parity // create dirs used by parity
cmd.dirs.create_dirs(cmd.dapps_conf.enabled, cmd.signer_conf.enabled, cmd.secretstore_conf.enabled)?; cmd.dirs.create_dirs(cmd.dapps_conf.enabled, cmd.ui_conf.enabled, cmd.secretstore_conf.enabled)?;
info!("Starting {}", Colour::White.bold().paint(version())); info!("Starting {}", Colour::White.bold().paint(version()));
info!("Running in experimental {} mode.", Colour::Blue.bold().paint("Light Client")); info!("Running in experimental {} mode.", Colour::Blue.bold().paint("Light Client"));
@ -267,31 +266,47 @@ fn execute_light(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) ->
// prepare account provider // prepare account provider
let account_provider = Arc::new(prepare_account_provider(&cmd.spec, &cmd.dirs, &spec.data_dir, cmd.acc_conf, &passwords)?); let account_provider = Arc::new(prepare_account_provider(&cmd.spec, &cmd.dirs, &spec.data_dir, cmd.acc_conf, &passwords)?);
let rpc_stats = Arc::new(informant::RpcStats::default()); let rpc_stats = Arc::new(informant::RpcStats::default());
let signer_path = cmd.signer_conf.signer_path.clone();
// the dapps server
let signer_service = Arc::new(signer::new_service(&cmd.ws_conf, &cmd.ui_conf));
let dapps_deps = {
let contract_client = Arc::new(::dapps::LightRegistrar {
client: service.client().clone(),
sync: light_sync.clone(),
on_demand: on_demand.clone(),
});
let sync = light_sync.clone();
dapps::Dependencies {
sync_status: Arc::new(move || sync.is_major_importing()),
contract_client: contract_client,
remote: event_loop.raw_remote(),
fetch: fetch.clone(),
signer: signer_service.clone(),
ui_address: cmd.ui_conf.address(),
}
};
let dapps_middleware = dapps::new(cmd.dapps_conf.clone(), dapps_deps.clone())?;
let ui_middleware = dapps::new_ui(cmd.ui_conf.enabled, dapps_deps)?;
// start RPCs // start RPCs
let dapps_service = dapps::service(&dapps_middleware);
let deps_for_rpc_apis = Arc::new(rpc_apis::LightDependencies { let deps_for_rpc_apis = Arc::new(rpc_apis::LightDependencies {
signer_service: Arc::new(rpc_apis::SignerService::new(move || { signer_service: signer_service,
signer::generate_new_token(signer_path.clone()).map_err(|e| format!("{:?}", e))
}, cmd.ui_address)),
client: service.client().clone(), client: service.client().clone(),
sync: light_sync.clone(), sync: light_sync.clone(),
net: light_sync.clone(), net: light_sync.clone(),
secret_store: account_provider, secret_store: account_provider,
logger: logger, logger: logger,
settings: Arc::new(cmd.net_settings), settings: Arc::new(cmd.net_settings),
on_demand: on_demand.clone(), on_demand: on_demand,
cache: cache, cache: cache,
transaction_queue: txq, transaction_queue: txq,
dapps_interface: match cmd.dapps_conf.enabled { dapps_service: dapps_service,
true => Some(cmd.http_conf.interface.clone()), dapps_address: cmd.dapps_conf.address(cmd.http_conf.address()),
false => None, ws_address: cmd.ws_conf.address(),
}, fetch: fetch,
dapps_port: match cmd.dapps_conf.enabled {
true => Some(cmd.http_conf.port),
false => None,
},
fetch: fetch.clone(),
geth_compatibility: cmd.geth_compatibility, geth_compatibility: cmd.geth_compatibility,
remote: event_loop.remote(), remote: event_loop.remote(),
}); });
@ -302,39 +317,11 @@ fn execute_light(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) ->
stats: rpc_stats.clone(), stats: rpc_stats.clone(),
}; };
// the dapps server
let dapps_deps = {
let contract_client = Arc::new(::dapps::LightRegistrar {
client: service.client().clone(),
sync: light_sync.clone(),
on_demand: on_demand,
});
let sync = light_sync.clone();
dapps::Dependencies {
sync_status: Arc::new(move || sync.is_major_importing()),
contract_client: contract_client,
remote: event_loop.raw_remote(),
fetch: fetch,
signer: deps_for_rpc_apis.signer_service.clone(),
}
};
let dapps_middleware = dapps::new(cmd.dapps_conf.clone(), dapps_deps)?;
// start rpc servers // start rpc servers
let _ws_server = rpc::new_ws(cmd.ws_conf, &dependencies)?; let _ws_server = rpc::new_ws(cmd.ws_conf, &dependencies)?;
let _http_server = rpc::new_http(cmd.http_conf.clone(), &dependencies, dapps_middleware)?; let _http_server = rpc::new_http("HTTP JSON-RPC", "jsonrpc", cmd.http_conf.clone(), &dependencies, dapps_middleware)?;
let _ipc_server = rpc::new_ipc(cmd.ipc_conf, &dependencies)?; let _ipc_server = rpc::new_ipc(cmd.ipc_conf, &dependencies)?;
let _ui_server = rpc::new_http("Parity Wallet (UI)", "ui", cmd.ui_conf.clone().into(), &dependencies, ui_middleware)?;
// the signer server
let signer_deps = signer::Dependencies {
apis: deps_for_rpc_apis.clone(),
remote: event_loop.raw_remote(),
rpc_stats: rpc_stats.clone(),
};
let signing_queue = deps_for_rpc_apis.signer_service.queue();
let _signer_server = signer::start(cmd.signer_conf.clone(), signing_queue, signer_deps)?;
// minimal informant thread. Just prints block number every 5 seconds. // minimal informant thread. Just prints block number every 5 seconds.
// TODO: integrate with informant.rs // TODO: integrate with informant.rs
@ -351,9 +338,9 @@ fn execute_light(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) ->
pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> Result<(bool, Option<String>), String> { pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> Result<(bool, Option<String>), String> {
if cmd.ui && cmd.dapps_conf.enabled { if cmd.ui && cmd.dapps_conf.enabled {
// Check if Parity is already running // Check if Parity is already running
let addr = format!("{}:{}", cmd.signer_conf.interface, cmd.signer_conf.port); let addr = format!("{}:{}", cmd.ui_conf.interface, cmd.ui_conf.port);
if !TcpListener::bind(&addr as &str).is_ok() { if !TcpListener::bind(&addr as &str).is_ok() {
return open_ui(&cmd.signer_conf).map(|_| (false, None)); return open_ui(&cmd.ws_conf, &cmd.ui_conf).map(|_| (false, None));
} }
} }
@ -408,7 +395,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
execute_upgrades(&cmd.dirs.base, &db_dirs, algorithm, cmd.compaction.compaction_profile(db_dirs.db_root_path().as_path()))?; execute_upgrades(&cmd.dirs.base, &db_dirs, algorithm, cmd.compaction.compaction_profile(db_dirs.db_root_path().as_path()))?;
// create dirs used by parity // create dirs used by parity
cmd.dirs.create_dirs(cmd.dapps_conf.enabled, cmd.signer_conf.enabled, cmd.secretstore_conf.enabled)?; cmd.dirs.create_dirs(cmd.dapps_conf.enabled, cmd.ui_conf.enabled, cmd.secretstore_conf.enabled)?;
// run in daemon mode // run in daemon mode
if let Some(pid_file) = cmd.daemon { if let Some(pid_file) = cmd.daemon {
@ -620,16 +607,33 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
// set up dependencies for rpc servers // set up dependencies for rpc servers
let rpc_stats = Arc::new(informant::RpcStats::default()); let rpc_stats = Arc::new(informant::RpcStats::default());
let signer_path = cmd.signer_conf.signer_path.clone();
let secret_store = match cmd.public_node { let secret_store = match cmd.public_node {
true => None, true => None,
false => Some(account_provider.clone()) false => Some(account_provider.clone())
}; };
let signer_service = Arc::new(signer::new_service(&cmd.ws_conf, &cmd.ui_conf));
// the dapps server
let dapps_deps = {
let (sync, client) = (sync_provider.clone(), client.clone());
let contract_client = Arc::new(::dapps::FullRegistrar { client: client.clone() });
dapps::Dependencies {
sync_status: Arc::new(move || is_major_importing(Some(sync.status().state), client.queue_info())),
contract_client: contract_client,
remote: event_loop.raw_remote(),
fetch: fetch.clone(),
signer: signer_service.clone(),
ui_address: cmd.ui_conf.address(),
}
};
let dapps_middleware = dapps::new(cmd.dapps_conf.clone(), dapps_deps.clone())?;
let ui_middleware = dapps::new_ui(cmd.ui_conf.enabled, dapps_deps)?;
let dapps_service = dapps::service(&dapps_middleware);
let deps_for_rpc_apis = Arc::new(rpc_apis::FullDependencies { let deps_for_rpc_apis = Arc::new(rpc_apis::FullDependencies {
signer_service: Arc::new(rpc_apis::SignerService::new(move || { signer_service: signer_service,
signer::generate_new_token(signer_path.clone()).map_err(|e| format!("{:?}", e))
}, cmd.ui_address)),
snapshot: snapshot_service.clone(), snapshot: snapshot_service.clone(),
client: client.clone(), client: client.clone(),
sync: sync_provider.clone(), sync: sync_provider.clone(),
@ -642,14 +646,9 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
net_service: manage_network.clone(), net_service: manage_network.clone(),
updater: updater.clone(), updater: updater.clone(),
geth_compatibility: cmd.geth_compatibility, geth_compatibility: cmd.geth_compatibility,
dapps_interface: match cmd.dapps_conf.enabled { dapps_service: dapps_service,
true => Some(cmd.http_conf.interface.clone()), dapps_address: cmd.dapps_conf.address(cmd.http_conf.address()),
false => None, ws_address: cmd.ws_conf.address(),
},
dapps_port: match cmd.dapps_conf.enabled {
true => Some(cmd.http_conf.port),
false => None,
},
fetch: fetch.clone(), fetch: fetch.clone(),
remote: event_loop.remote(), remote: event_loop.remote(),
}); });
@ -660,34 +659,12 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
stats: rpc_stats.clone(), stats: rpc_stats.clone(),
}; };
// the dapps server
let dapps_deps = {
let (sync, client) = (sync_provider.clone(), client.clone());
let contract_client = Arc::new(::dapps::FullRegistrar { client: client.clone() });
dapps::Dependencies {
sync_status: Arc::new(move || is_major_importing(Some(sync.status().state), client.queue_info())),
contract_client: contract_client,
remote: event_loop.raw_remote(),
fetch: fetch.clone(),
signer: deps_for_rpc_apis.signer_service.clone(),
}
};
let dapps_middleware = dapps::new(cmd.dapps_conf.clone(), dapps_deps)?;
// start rpc servers // start rpc servers
let ws_server = rpc::new_ws(cmd.ws_conf, &dependencies)?; let ws_server = rpc::new_ws(cmd.ws_conf.clone(), &dependencies)?;
let http_server = rpc::new_http(cmd.http_conf.clone(), &dependencies, dapps_middleware)?;
let ipc_server = rpc::new_ipc(cmd.ipc_conf, &dependencies)?; let ipc_server = rpc::new_ipc(cmd.ipc_conf, &dependencies)?;
let http_server = rpc::new_http("HTTP JSON-RPC", "jsonrpc", cmd.http_conf.clone(), &dependencies, dapps_middleware)?;
// the signer server // the ui server
let signer_deps = signer::Dependencies { let ui_server = rpc::new_http("UI WALLET", "ui", cmd.ui_conf.clone().into(), &dependencies, ui_middleware)?;
apis: deps_for_rpc_apis.clone(),
remote: event_loop.raw_remote(),
rpc_stats: rpc_stats.clone(),
};
let signing_queue = deps_for_rpc_apis.signer_service.queue();
let signer_server = signer::start(cmd.signer_conf.clone(), signing_queue, signer_deps)?;
// secret store key server // secret store key server
let secretstore_deps = secretstore::Dependencies { let secretstore_deps = secretstore::Dependencies {
@ -746,7 +723,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
// start ui // start ui
if cmd.ui { if cmd.ui {
open_ui(&cmd.signer_conf)?; open_ui(&cmd.ws_conf, &cmd.ui_conf)?;
} }
if let Some(dapp) = cmd.dapp { if let Some(dapp) = cmd.dapp {
@ -756,11 +733,11 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R
// Handle exit // Handle exit
let restart = wait_for_exit(panic_handler, Some(updater), Some(client), can_restart); let restart = wait_for_exit(panic_handler, Some(updater), Some(client), can_restart);
// drop this stuff as soon as exit detected.
drop((ws_server, http_server, ipc_server, signer_server, secretstore_key_server, ipfs_server, event_loop));
info!("Finishing work, please wait..."); info!("Finishing work, please wait...");
// drop this stuff as soon as exit detected.
drop((ws_server, http_server, ipc_server, ui_server, secretstore_key_server, ipfs_server, event_loop));
// to make sure timer does not spawn requests while shutdown is in progress // to make sure timer does not spawn requests while shutdown is in progress
informant.shutdown(); informant.shutdown();
// just Arc is dropping here, to allow other reference release in its default time // just Arc is dropping here, to allow other reference release in its default time

View File

@ -15,51 +15,16 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::io; use std::io;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use std::sync::Arc;
pub use ethcore_signer::Server as SignerServer;
use ansi_term::Colour; use ansi_term::Colour;
use dir::default_data_path; use rpc;
use parity_rpc::informant::RpcStats;
use parity_rpc::{self, ConfirmationsQueue};
use ethcore_signer as signer;
use helpers::replace_home;
use parity_reactor::TokioRemote;
use rpc_apis; use rpc_apis;
use parity_rpc;
use path::restrict_permissions_owner; use path::restrict_permissions_owner;
use util::H256;
const CODES_FILENAME: &'static str = "authcodes";
#[derive(Debug, PartialEq, Clone)] pub const CODES_FILENAME: &'static str = "authcodes";
pub struct Configuration {
pub enabled: bool,
pub port: u16,
pub interface: String,
pub signer_path: String,
pub skip_origin_validation: bool,
}
impl Default for Configuration {
fn default() -> Self {
let data_dir = default_data_path();
Configuration {
enabled: true,
port: 8180,
interface: "127.0.0.1".into(),
signer_path: replace_home(&data_dir, "$BASE/signer"),
skip_origin_validation: false,
}
}
}
pub struct Dependencies<D: rpc_apis::Dependencies> {
pub apis: Arc<D>,
pub remote: TokioRemote,
pub rpc_stats: Arc<RpcStats>,
}
pub struct NewToken { pub struct NewToken {
pub token: String, pub token: String,
@ -67,42 +32,29 @@ pub struct NewToken {
pub message: String, pub message: String,
} }
#[derive(Debug, Default, Clone)] pub fn new_service(ws_conf: &rpc::WsConfiguration, ui_conf: &rpc::UiConfiguration) -> rpc_apis::SignerService {
pub struct StandardExtractor; let signer_path = ws_conf.signer_path.clone();
impl signer::MetaExtractor<parity_rpc::Metadata> for StandardExtractor { let signer_enabled = ui_conf.enabled;
fn extract_metadata(&self, session: &H256) -> parity_rpc::Metadata {
let mut metadata = parity_rpc::Metadata::default(); rpc_apis::SignerService::new(move || {
metadata.origin = parity_rpc::Origin::Signer((*session).into()); generate_new_token(&signer_path).map_err(|e| format!("{:?}", e))
metadata }, signer_enabled)
}
} }
pub fn start<D: rpc_apis::Dependencies>( pub fn codes_path(path: &Path) -> PathBuf {
conf: Configuration, let mut p = path.to_owned();
queue: Arc<ConfirmationsQueue>,
deps: Dependencies<D>,
) -> Result<Option<SignerServer>, String> {
if !conf.enabled {
Ok(None)
} else {
Ok(Some(do_start(conf, queue, deps)?))
}
}
fn codes_path(path: String) -> PathBuf {
let mut p = PathBuf::from(path);
p.push(CODES_FILENAME); p.push(CODES_FILENAME);
let _ = restrict_permissions_owner(&p, true, false); let _ = restrict_permissions_owner(&p, true, false);
p p
} }
pub fn execute(cmd: Configuration) -> Result<String, String> { pub fn execute(ws_conf: rpc::WsConfiguration, ui_conf: rpc::UiConfiguration) -> Result<String, String> {
Ok(generate_token_and_url(&cmd)?.message) Ok(generate_token_and_url(&ws_conf, &ui_conf)?.message)
} }
pub fn generate_token_and_url(conf: &Configuration) -> Result<NewToken, String> { pub fn generate_token_and_url(ws_conf: &rpc::WsConfiguration, ui_conf: &rpc::UiConfiguration) -> Result<NewToken, String> {
let code = generate_new_token(conf.signer_path.clone()).map_err(|err| format!("Error generating token: {}", err))?; let code = generate_new_token(&ws_conf.signer_path).map_err(|err| format!("Error generating token: {:?}", err))?;
let auth_url = format!("http://{}:{}/#/auth?token={}", conf.interface, conf.port, code); let auth_url = format!("http://{}:{}/#/auth?token={}", ui_conf.interface, ui_conf.port, code);
// And print in to the console // And print in to the console
Ok(NewToken { Ok(NewToken {
token: code.clone(), token: code.clone(),
@ -119,49 +71,12 @@ Or use the generated token:
}) })
} }
pub fn generate_new_token(path: String) -> io::Result<String> { fn generate_new_token(path: &Path) -> io::Result<String> {
let path = codes_path(path); let path = codes_path(path);
let mut codes = signer::AuthCodes::from_file(&path)?; let mut codes = parity_rpc::AuthCodes::from_file(&path)?;
codes.clear_garbage(); codes.clear_garbage();
let code = codes.generate_new()?; let code = codes.generate_new()?;
codes.to_file(&path)?; codes.to_file(&path)?;
trace!("New key code created: {}", Colour::White.bold().paint(&code[..])); trace!("New key code created: {}", Colour::White.bold().paint(&code[..]));
Ok(code) Ok(code)
} }
fn do_start<D: rpc_apis::Dependencies>(
conf: Configuration,
queue: Arc<ConfirmationsQueue>,
deps: Dependencies<D>
) -> Result<SignerServer, String> {
let addr = format!("{}:{}", conf.interface, conf.port)
.parse()
.map_err(|_| format!("Invalid port specified: {}", conf.port))?;
let start_result = {
let server = signer::ServerBuilder::new(
queue,
codes_path(conf.signer_path),
);
if conf.skip_origin_validation {
warn!("{}", Colour::Red.bold().paint("*** INSECURE *** Running Trusted Signer with no origin validation."));
info!("If you do not intend this, exit now.");
}
let server = server.skip_origin_validation(conf.skip_origin_validation);
let server = server.stats(deps.rpc_stats.clone());
let handler = rpc_apis::setup_rpc(deps.rpc_stats, &*deps.apis, rpc_apis::ApiSet::SafeContext);
let remote = deps.remote.clone();
server.start_with_extractor(addr, handler, remote, StandardExtractor)
};
match start_result {
Err(signer::ServerError::IoError(err)) => match err.kind() {
io::ErrorKind::AddrInUse => Err(format!("Trusted UI address {} is already in use, make sure that another instance of an Ethereum client is not running or change the address using the --ui-port and --ui-interface options.", addr)),
_ => Err(format!("Trusted Signer io error: {}", err)),
},
Err(e) => Err(format!("Trusted Signer Error: {:?}", e)),
Ok(server) => Ok(server),
}
}

View File

@ -8,9 +8,13 @@ authors = ["Parity Technologies <admin@parity.io>"]
[lib] [lib]
[dependencies] [dependencies]
cid = "0.2"
futures = "0.1" futures = "0.1"
log = "0.3" log = "0.3"
multihash = "0.5"
order-stat = "0.1" order-stat = "0.1"
rand = "0.3"
rust-crypto = "0.2"
rustc-serialize = "0.3" rustc-serialize = "0.3"
semver = "0.6" semver = "0.6"
serde = "0.9" serde = "0.9"
@ -19,10 +23,6 @@ serde_json = "0.9"
time = "0.1" time = "0.1"
tokio-timer = "0.1" tokio-timer = "0.1"
transient-hashmap = "0.4" transient-hashmap = "0.4"
cid = "0.2.1"
multihash = "0.5"
rust-crypto = "0.2.36"
rand = "0.3"
jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc.git", branch = "parity-1.7" } jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc.git", branch = "parity-1.7" }
jsonrpc-http-server = { git = "https://github.com/paritytech/jsonrpc.git", branch = "parity-1.7" } jsonrpc-http-server = { git = "https://github.com/paritytech/jsonrpc.git", branch = "parity-1.7" }

View File

@ -1,17 +0,0 @@
[package]
description = "Rpc test client."
name = "rpctest"
version = "1.7.0"
license = "GPL-3.0"
authors = ["Parity Technologies <admin@parity.io>"]
[dependencies]
ctrlc = { git = "https://github.com/paritytech/rust-ctrlc.git" }
docopt = "0.7"
ethcore = { path = "../../ethcore" }
ethcore-devtools = { path = "../../devtools" }
ethcore-util = { path = "../../util" }
ethjson = { path = "../../json" }
parity-rpc = { path = ".." }
rustc-serialize = "0.3"
serde_json = "0.8"

View File

@ -1,148 +0,0 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
extern crate ctrlc;
extern crate docopt;
extern crate ethcore;
extern crate ethcore_devtools as devtools;
extern crate ethcore_util as util;
extern crate ethjson;
extern crate parity_rpc as rpc;
extern crate rustc_serialize;
extern crate serde_json;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, Condvar};
use std::process;
use std::fs::File;
use std::path::Path;
use docopt::Docopt;
use ctrlc::CtrlC;
use ethcore::spec::Genesis;
use ethcore::pod_state::PodState;
use ethcore::ethereum;
use ethcore::client::{BlockChainClient, Client, ClientConfig};
use devtools::RandomTempPath;
use util::IoChannel;
use rpc::v1::tests::helpers::{TestSyncProvider, Config as SyncConfig, TestMinerService, TestAccountProvider, TestAccount};
use rpc::v1::{Eth, EthClient, EthFilter, EthFilterClient};
use util::panics::MayPanic;
use util::hash::Address;
const USAGE: &'static str = r#"
Parity rpctest client.
By Wood/Paronyan/Kotewicz/Drwięga/Volf.
Copyright 2015, 2016, 2017 Parity Technologies (UK) Ltd
Usage:
rpctest --json <test-file> --name <test-name> [options]
rpctest --help
Options:
--jsonrpc-addr HOST Specify the hostname portion of the JSONRPC API
server [default: 127.0.0.1].
--jsonrpc-port PORT Specify the port portion of the JSONRPC API server
[default: 8545].
"#;
#[derive(Debug, RustcDecodable)]
struct Args {
arg_test_file: String,
arg_test_name: String,
flag_jsonrpc_addr: String,
flag_jsonrpc_port: u16,
}
struct Configuration {
args: Args,
}
impl Configuration {
fn parse() -> Self {
Configuration {
args: Docopt::new(USAGE).and_then(|d| d.decode()).unwrap_or_else(|e| e.exit())
}
}
fn execute(&self) {
println!("file path: {:?}", self.args.arg_test_file);
println!("test name: {:?}", self.args.arg_test_name);
let path = Path::new(&self.args.arg_test_file);
let file = File::open(path).unwrap_or_else(|_| {
println!("Cannot open file.");
process::exit(1);
});
let tests: ethjson::blockchain::Test = serde_json::from_reader(file).unwrap_or_else(|err| {
println!("Invalid json file.");
println!("{:?}", err);
process::exit(2);
});
let blockchain = tests.get(&self.args.arg_test_name).unwrap_or_else(|| {
println!("Invalid test name.");
process::exit(3);
});
let genesis = Genesis::from(blockchain.genesis());
let state = PodState::from(blockchain.pre_state.clone());
let mut spec = ethereum::new_frontier_test();
spec.set_genesis_state(state);
spec.overwrite_genesis_params(genesis);
assert!(spec.is_state_root_valid());
let temp = RandomTempPath::new();
{
let client: Arc<Client> = Client::new(ClientConfig::default(), spec, temp.as_path(), IoChannel::disconnected()).unwrap();
for b in &blockchain.blocks_rlp() {
let _ = client.import_block(b.clone());
client.flush_queue();
client.import_verified_blocks();
}
let sync = Arc::new(TestSyncProvider::new(SyncConfig {
protocol_version: 65,
num_peers: 120
}));
let miner = Arc::new(TestMinerService::default());
let mut accs = HashMap::new();
accs.insert(Address::from(1), TestAccount::new("test"));
let accounts = Arc::new(TestAccountProvider::new(accs));
let server = rpc::RpcServer::new();
server.add_delegate(EthClient::new(&client, &sync, &accounts, &miner, true).to_delegate());
server.add_delegate(EthFilterClient::new(&client, &miner).to_delegate());
let url = format!("{}:{}", self.args.flag_jsonrpc_addr, self.args.flag_jsonrpc_port);
let panic_handler = server.start_http(url.as_ref(), "*", 1);
let exit = Arc::new(Condvar::new());
let e = exit.clone();
CtrlC::set_handler(move || { e.notify_all(); });
let e = exit.clone();
panic_handler.on_panic(move |_reason| { e.notify_all(); });
let mutex = Mutex::new(());
let _ = exit.wait(mutex.lock()).unwrap();
}
}
}
fn main() {
Configuration::parse().execute();
}

View File

@ -14,11 +14,12 @@
// 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 rand::Rng;
use rand::os::OsRng;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::path::Path; use std::path::Path;
use std::{fs, time, mem}; use std::{fs, time, mem};
use rand::Rng;
use rand::os::OsRng;
use util::{H256, Hashable, Itertools}; use util::{H256, Hashable, Itertools};
/// Providing current time in seconds /// Providing current time in seconds
@ -347,5 +348,3 @@ mod tests {
} }
} }

View File

@ -14,11 +14,20 @@
// 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/>.
//! Transport-specific metadata extractors.
use jsonrpc_core; use jsonrpc_core;
use http; use http;
use hyper; use hyper;
use minihttp; use minihttp;
use HttpMetaExtractor;
/// HTTP RPC server impl-independent metadata extractor
pub trait HttpMetaExtractor: Send + Sync + 'static {
/// Type of Metadata
type Metadata: jsonrpc_core::Metadata;
/// Extracts metadata from given params.
fn read_metadata(&self, origin: Option<String>, user_agent: Option<String>, dapps_origin: Option<String>) -> Self::Metadata;
}
pub struct HyperMetaExtractor<T> { pub struct HyperMetaExtractor<T> {
extractor: T, extractor: T,
@ -37,13 +46,14 @@ impl<M, T> http::MetaExtractor<M> for HyperMetaExtractor<T> where
M: jsonrpc_core::Metadata, M: jsonrpc_core::Metadata,
{ {
fn read_metadata(&self, req: &hyper::server::Request<hyper::net::HttpStream>) -> M { fn read_metadata(&self, req: &hyper::server::Request<hyper::net::HttpStream>) -> M {
let origin = req.headers().get::<hyper::header::Origin>() let as_string = |header: Option<&http::request_response::header::Raw>| header
.map(|origin| format!("{}://{}", origin.scheme, origin.host))
.unwrap_or_else(|| "unknown".into());
let dapps_origin = req.headers().get_raw("x-parity-origin")
.and_then(|raw| raw.one()) .and_then(|raw| raw.one())
.map(|raw| String::from_utf8_lossy(raw).into_owned()); .map(|raw| String::from_utf8_lossy(raw).into_owned());
self.extractor.read_metadata(origin, dapps_origin)
let origin = as_string(req.headers().get_raw("origin"));
let user_agent = as_string(req.headers().get_raw("user-agent"));
let dapps_origin = as_string(req.headers().get_raw("x-parity-origin"));
self.extractor.read_metadata(origin, user_agent, dapps_origin)
} }
} }
@ -64,11 +74,10 @@ impl<M, T> minihttp::MetaExtractor<M> for MiniMetaExtractor<T> where
M: jsonrpc_core::Metadata, M: jsonrpc_core::Metadata,
{ {
fn read_metadata(&self, req: &minihttp::Req) -> M { fn read_metadata(&self, req: &minihttp::Req) -> M {
let origin = req.header("origin") let origin = req.header("origin").map(|h| h.to_owned());
.unwrap_or_else(|| "unknown") let user_agent = req.header("user-agent").map(|h| h.to_owned());
.to_owned();
let dapps_origin = req.header("x-parity-origin").map(|h| h.to_owned()); let dapps_origin = req.header("x-parity-origin").map(|h| h.to_owned());
self.extractor.read_metadata(origin, dapps_origin) self.extractor.read_metadata(origin, user_agent, dapps_origin)
} }
} }

View File

@ -14,13 +14,18 @@
// 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/>.
//! Ethcore rpc. //! Parity RPC.
#![warn(missing_docs)]
#![cfg_attr(feature="nightly", feature(plugin))]
#![cfg_attr(feature="nightly", plugin(clippy))]
#![warn(missing_docs)]
#![cfg_attr(feature="dev", feature(plugin))]
#![cfg_attr(feature="dev", plugin(clippy))]
extern crate cid;
extern crate crypto as rust_crypto;
extern crate futures; extern crate futures;
extern crate multihash;
extern crate order_stat; extern crate order_stat;
extern crate rand;
extern crate rustc_serialize; extern crate rustc_serialize;
extern crate semver; extern crate semver;
extern crate serde; extern crate serde;
@ -28,10 +33,6 @@ extern crate serde_json;
extern crate time; extern crate time;
extern crate tokio_timer; extern crate tokio_timer;
extern crate transient_hashmap; extern crate transient_hashmap;
extern crate cid;
extern crate multihash;
extern crate crypto as rust_crypto;
extern crate rand;
extern crate jsonrpc_core; extern crate jsonrpc_core;
extern crate jsonrpc_http_server as http; extern crate jsonrpc_http_server as http;
@ -41,6 +42,7 @@ extern crate jsonrpc_pubsub;
extern crate ethash; extern crate ethash;
extern crate ethcore; extern crate ethcore;
extern crate ethcore_devtools as devtools;
extern crate ethcore_io as io; extern crate ethcore_io as io;
extern crate ethcore_ipc; extern crate ethcore_ipc;
extern crate ethcore_light as light; extern crate ethcore_light as light;
@ -66,8 +68,6 @@ extern crate serde_derive;
#[cfg(test)] #[cfg(test)]
extern crate ethjson; extern crate ethjson;
#[cfg(test)]
extern crate ethcore_devtools as devtools;
#[cfg(test)] #[cfg(test)]
#[macro_use] #[macro_use]
@ -75,9 +75,12 @@ extern crate pretty_assertions;
pub extern crate jsonrpc_ws_server as ws; pub extern crate jsonrpc_ws_server as ws;
mod metadata; mod authcodes;
mod http_common;
pub mod v1; pub mod v1;
pub mod tests;
pub use jsonrpc_pubsub::Session as PubSubSession; pub use jsonrpc_pubsub::Session as PubSubSession;
pub use ipc::{Server as IpcServer, MetaExtractor as IpcMetaExtractor, RequestContext as IpcRequestContext}; pub use ipc::{Server as IpcServer, MetaExtractor as IpcMetaExtractor, RequestContext as IpcRequestContext};
pub use http::{ pub use http::{
@ -86,8 +89,11 @@ pub use http::{
AccessControlAllowOrigin, Host, DomainsValidation AccessControlAllowOrigin, Host, DomainsValidation
}; };
pub use v1::{SigningQueue, SignerService, ConfirmationsQueue, NetworkSettings, Metadata, Origin, informant, dispatch}; pub use v1::{NetworkSettings, Metadata, Origin, informant, dispatch, signer, dapps};
pub use v1::block_import::is_major_importing; pub use v1::block_import::is_major_importing;
pub use v1::extractors::{RpcExtractor, WsExtractor, WsStats, WsDispatcher};
pub use authcodes::{AuthCodes, TimeProvider};
pub use http_common::HttpMetaExtractor;
use std::net::SocketAddr; use std::net::SocketAddr;
use http::tokio_core; use http::tokio_core;
@ -100,6 +106,16 @@ pub enum HttpServer {
Hyper(http::Server), Hyper(http::Server),
} }
impl HttpServer {
/// Returns current listening address.
pub fn address(&self) -> &SocketAddr {
match *self {
HttpServer::Mini(ref s) => s.address(),
HttpServer::Hyper(ref s) => &s.addrs()[0],
}
}
}
/// RPC HTTP Server error /// RPC HTTP Server error
#[derive(Debug)] #[derive(Debug)]
pub enum HttpServerError { pub enum HttpServerError {
@ -128,14 +144,6 @@ impl From<minihttp::Error> for HttpServerError {
} }
} }
/// HTTP RPC server impl-independent metadata extractor
pub trait HttpMetaExtractor: Send + Sync + 'static {
/// Type of Metadata
type Metadata: jsonrpc_core::Metadata;
/// Extracts metadata from given params.
fn read_metadata(&self, origin: String, dapps_origin: Option<String>) -> Self::Metadata;
}
/// HTTP server implementation-specific settings. /// HTTP server implementation-specific settings.
pub enum HttpSettings<R: RequestMiddleware> { pub enum HttpSettings<R: RequestMiddleware> {
/// Enable fast minihttp server with given number of threads. /// Enable fast minihttp server with given number of threads.
@ -164,7 +172,7 @@ pub fn start_http<M, S, H, T, R>(
HttpSettings::Dapps(middleware) => { HttpSettings::Dapps(middleware) => {
let mut builder = http::ServerBuilder::new(handler) let mut builder = http::ServerBuilder::new(handler)
.event_loop_remote(remote) .event_loop_remote(remote)
.meta_extractor(metadata::HyperMetaExtractor::new(extractor)) .meta_extractor(http_common::HyperMetaExtractor::new(extractor))
.cors(cors_domains.into()) .cors(cors_domains.into())
.allowed_hosts(allowed_hosts.into()); .allowed_hosts(allowed_hosts.into());
@ -177,7 +185,7 @@ pub fn start_http<M, S, H, T, R>(
HttpSettings::Threads(threads) => { HttpSettings::Threads(threads) => {
minihttp::ServerBuilder::new(handler) minihttp::ServerBuilder::new(handler)
.threads(threads) .threads(threads)
.meta_extractor(metadata::MiniMetaExtractor::new(extractor)) .meta_extractor(http_common::MiniMetaExtractor::new(extractor))
.cors(cors_domains.into()) .cors(cors_domains.into())
.allowed_hosts(allowed_hosts.into()) .allowed_hosts(allowed_hosts.into())
.start_http(addr) .start_http(addr)
@ -205,13 +213,14 @@ pub fn start_ipc<M, S, H, T>(
} }
/// Start WS server and return `Server` handle. /// Start WS server and return `Server` handle.
pub fn start_ws<M, S, H, T, U>( pub fn start_ws<M, S, H, T, U, V>(
addr: &SocketAddr, addr: &SocketAddr,
handler: H, handler: H,
remote: tokio_core::reactor::Remote, remote: tokio_core::reactor::Remote,
allowed_origins: ws::DomainsValidation<ws::Origin>, allowed_origins: ws::DomainsValidation<ws::Origin>,
allowed_hosts: ws::DomainsValidation<ws::Host>, allowed_hosts: ws::DomainsValidation<ws::Host>,
extractor: T, extractor: T,
middleware: V,
stats: U, stats: U,
) -> Result<ws::Server, ws::Error> where ) -> Result<ws::Server, ws::Error> where
M: jsonrpc_core::Metadata, M: jsonrpc_core::Metadata,
@ -219,9 +228,11 @@ pub fn start_ws<M, S, H, T, U>(
H: Into<jsonrpc_core::MetaIoHandler<M, S>>, H: Into<jsonrpc_core::MetaIoHandler<M, S>>,
T: ws::MetaExtractor<M>, T: ws::MetaExtractor<M>,
U: ws::SessionStats, U: ws::SessionStats,
V: ws::RequestMiddleware,
{ {
ws::ServerBuilder::new(handler) ws::ServerBuilder::new(handler)
.event_loop_remote(remote) .event_loop_remote(remote)
.request_middleware(middleware)
.allowed_origins(allowed_origins) .allowed_origins(allowed_origins)
.allowed_hosts(allowed_hosts) .allowed_hosts(allowed_hosts)
.session_meta_extractor(extractor) .session_meta_extractor(extractor)

84
rpc/src/tests/helpers.rs Normal file
View File

@ -0,0 +1,84 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use std::ops::{Deref, DerefMut};
use devtools::RandomTempPath;
use parity_reactor::{EventLoop, TokioRemote};
use authcodes::AuthCodes;
/// Server with event loop
pub struct Server<T> {
/// Server
pub server: T,
/// RPC Event Loop
pub event_loop: EventLoop,
}
impl<T> Server<T> {
pub fn new<F>(f: F) -> Server<T> where
F: FnOnce(TokioRemote) -> T,
{
let event_loop = EventLoop::spawn();
let remote = event_loop.raw_remote();
Server {
server: f(remote),
event_loop: event_loop,
}
}
}
impl<T> Deref for Server<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.server
}
}
/// Struct representing authcodes
pub struct GuardedAuthCodes {
authcodes: AuthCodes,
/// The path to the mock authcodes
pub path: RandomTempPath,
}
impl GuardedAuthCodes {
pub fn new() -> Self {
let mut path = RandomTempPath::new();
path.panic_on_drop_failure = false;
GuardedAuthCodes {
authcodes: AuthCodes::from_file(&path).unwrap(),
path: path,
}
}
}
impl Deref for GuardedAuthCodes {
type Target = AuthCodes;
fn deref(&self) -> &Self::Target {
&self.authcodes
}
}
impl DerefMut for GuardedAuthCodes {
fn deref_mut(&mut self) -> &mut AuthCodes {
&mut self.authcodes
}
}

View File

@ -14,12 +14,8 @@
// 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/>.
extern crate rustc_version; //! RPC integration tests.
use rustc_version::{version_meta, Channel}; mod helpers;
#[cfg(test)] mod rpc;
fn main() { pub mod ws;
if let Channel::Nightly = version_meta().channel {
println!("cargo:rustc-cfg=nightly");
}
}

172
rpc/src/tests/rpc.rs Normal file
View File

@ -0,0 +1,172 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use devtools::http_client;
use jsonrpc_core::MetaIoHandler;
use http::{self, hyper};
use {HttpSettings, HttpServer};
use tests::helpers::Server;
use v1::{extractors, Metadata};
fn serve(handler: Option<MetaIoHandler<Metadata>>) -> Server<HttpServer> {
let address = "127.0.0.1:0".parse().unwrap();
let handler = handler.unwrap_or_default();
Server::new(|remote| ::start_http(
&address,
http::DomainsValidation::Disabled,
http::DomainsValidation::Disabled,
handler,
remote,
extractors::RpcExtractor,
HttpSettings::Dapps(Some(|_req: &hyper::server::Request<hyper::net::HttpStream>, _control: &hyper::Control| {
http::RequestMiddlewareAction::Proceed {
should_continue_on_invalid_cors: false
}
})),
).unwrap())
}
/// Test a single request to running server
fn request(server: Server<HttpServer>, request: &str) -> http_client::Response {
http_client::request(server.server.address(), request)
}
#[cfg(test)]
mod testsing {
use jsonrpc_core::{MetaIoHandler, Value};
use jsonrpc_core::futures::{Future, future};
use v1::Metadata;
use super::{request, Server};
fn serve() -> (Server<::HttpServer>, ::std::net::SocketAddr) {
let mut io = MetaIoHandler::default();
io.add_method_with_meta("hello", |_, meta: Metadata| {
future::ok(Value::String(format!("{}", meta.origin))).boxed()
});
let server = super::serve(Some(io));
let address = server.server.address().to_owned();
(server, address)
}
#[test]
fn should_extract_rpc_origin() {
// given
let (server, address) = serve();
// when
let req = r#"{"method":"hello","params":[],"jsonrpc":"2.0","id":1}"#;
let expected = "34\n{\"jsonrpc\":\"2.0\",\"result\":\"unknown via RPC\",\"id\":1}\n\n0\n\n";
let res = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: {}\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\
\r\n\
{}
", address, req.len(), req)
);
// then
res.assert_status("HTTP/1.1 200 OK");
assert_eq!(res.body, expected);
}
#[test]
fn should_extract_rpc_origin_with_service() {
// given
let (server, address) = serve();
// when
let req = r#"{"method":"hello","params":[],"jsonrpc":"2.0","id":1}"#;
let expected = "38\n{\"jsonrpc\":\"2.0\",\"result\":\"curl/7.16.3 via RPC\",\"id\":1}\n\n0\n\n";
let res = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: {}\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\
User-Agent: curl/7.16.3\r\n\
\r\n\
{}
", address, req.len(), req)
);
// then
res.assert_status("HTTP/1.1 200 OK");
assert_eq!(res.body, expected);
}
#[test]
fn should_extract_dapp_origin() {
// given
let (server, address) = serve();
// when
let req = r#"{"method":"hello","params":[],"jsonrpc":"2.0","id":1}"#;
let expected = "3A\n{\"jsonrpc\":\"2.0\",\"result\":\"Dapp http://parity.io\",\"id\":1}\n\n0\n\n";
let res = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: {}\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
Origin: http://parity.io\r\n\
Connection: close\r\n\
User-Agent: curl/7.16.3\r\n\
\r\n\
{}
", address, req.len(), req)
);
// then
res.assert_status("HTTP/1.1 200 OK");
assert_eq!(res.body, expected);
}
#[test]
fn should_extract_dapp_origin_from_extension() {
// given
let (server, address) = serve();
// when
let req = r#"{"method":"hello","params":[],"jsonrpc":"2.0","id":1}"#;
let expected = "44\n{\"jsonrpc\":\"2.0\",\"result\":\"Dapp http://wallet.ethereum.org\",\"id\":1}\n\n0\n\n";
let res = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: {}\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
Origin: null\r\n\
X-Parity-Origin: http://wallet.ethereum.org\r\n\
Connection: close\r\n\
User-Agent: curl/7.16.3\r\n\
\r\n\
{}
", address, req.len(), req)
);
// then
res.assert_status("HTTP/1.1 200 OK");
assert_eq!(res.body, expected);
}
}

View File

@ -14,79 +14,42 @@
// 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 std::ops::{Deref, DerefMut}; //! WebSockets server tests.
use std::sync::Arc; use std::sync::Arc;
use devtools::http_client; use devtools::http_client;
use devtools::RandomTempPath; use jsonrpc_core::MetaIoHandler;
use rpc::ConfirmationsQueue;
use jsonrpc_core::IoHandler;
use jsonrpc_server_utils::reactor::RpcEventLoop;
use rand; use rand;
use ws;
use ServerBuilder; use v1::{extractors, informant};
use Server; use tests::helpers::{GuardedAuthCodes, Server};
use AuthCodes;
/// Struct representing authcodes
pub struct GuardedAuthCodes {
authcodes: AuthCodes,
/// The path to the mock authcodes
pub path: RandomTempPath,
}
impl Deref for GuardedAuthCodes {
type Target = AuthCodes;
fn deref(&self) -> &Self::Target {
&self.authcodes
}
}
impl DerefMut for GuardedAuthCodes {
fn deref_mut(&mut self) -> &mut AuthCodes {
&mut self.authcodes
}
}
/// Server with event loop
pub struct ServerLoop {
/// Signer Server
pub server: Server,
/// RPC Event Loop
pub event_loop: RpcEventLoop,
}
impl Deref for ServerLoop {
type Target = Server;
fn deref(&self) -> &Self::Target {
&self.server
}
}
/// Setup a mock signer for tests /// Setup a mock signer for tests
pub fn serve() -> (ServerLoop, usize, GuardedAuthCodes) { pub fn serve() -> (Server<ws::Server>, usize, GuardedAuthCodes) {
let mut path = RandomTempPath::new();
path.panic_on_drop_failure = false;
let queue = Arc::new(ConfirmationsQueue::default());
let builder = ServerBuilder::new(queue, path.to_path_buf());
let port = 35000 + rand::random::<usize>() % 10000; let port = 35000 + rand::random::<usize>() % 10000;
let event_loop = RpcEventLoop::spawn().unwrap(); let address = format!("127.0.0.1:{}", port).parse().unwrap();
let io = IoHandler::default(); let io = MetaIoHandler::default();
let remote = event_loop.remote(); let authcodes = GuardedAuthCodes::new();
let server = builder.start(format!("127.0.0.1:{}", port).parse().unwrap(), io, remote).unwrap(); let stats = Arc::new(informant::RpcStats::default());
let res = ServerLoop {
server: server,
event_loop: event_loop,
};
(res, port, GuardedAuthCodes { let res = Server::new(|remote| ::start_ws(
authcodes: AuthCodes::from_file(&path).unwrap(), &address,
path: path, io,
}) remote,
ws::DomainsValidation::Disabled,
ws::DomainsValidation::Disabled,
extractors::WsExtractor::new(Some(&authcodes.path)),
extractors::WsExtractor::new(Some(&authcodes.path)),
extractors::WsStats::new(stats),
).unwrap());
(res, port, authcodes)
} }
/// Test a single request to running server /// Test a single request to running server
pub fn request(server: ServerLoop, request: &str) -> http_client::Response { pub fn request(server: Server<ws::Server>, request: &str) -> http_client::Response {
http_client::request(server.server.addr(), request) http_client::request(server.server.addr(), request)
} }
@ -97,49 +60,6 @@ mod testing {
use devtools::http_client; use devtools::http_client;
use super::{serve, request}; use super::{serve, request};
#[test]
fn should_reject_invalid_host() {
// given
let server = serve().0;
// when
let response = request(server,
"\
GET / HTTP/1.1\r\n\
Host: test:8180\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned());
assert!(response.body.contains("URL Blocked"));
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://parity.web3.site/ HTTP/1.1\r\n\
Host: parity.web3.site\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]
fn should_not_redirect_to_parity_host() { fn should_not_redirect_to_parity_host() {
// given // given
@ -157,48 +77,7 @@ mod testing {
); );
// then // then
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_serve_styles_even_on_disallowed_domain() {
// given
let server = serve().0;
// when
let response = request(server,
"\
GET /styles.css HTTP/1.1\r\n\
Host: test:8180\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]
fn should_return_200_ok_for_connect_requests() {
// given
let server = serve().0;
// when
let response = request(server,
"\
CONNECT parity.web3.site:8080 HTTP/1.1\r\n\
Host: parity.web3.site\r\n\
Connection: close\r\n\
\r\n\
{}
"
);
// then
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
} }
#[test] #[test]
@ -221,7 +100,7 @@ mod testing {
); );
// 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, None); http_client::assert_security_headers_present(&response.headers, None);
} }
@ -300,7 +179,7 @@ mod testing {
// 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, None); http_client::assert_security_headers_present(&response2.headers, None);
} }
} }

263
rpc/src/v1/extractors.rs Normal file
View File

@ -0,0 +1,263 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Parity-specific metadata extractors.
use std::path::{Path, PathBuf};
use std::sync::Arc;
use authcodes;
use http_common::HttpMetaExtractor;
use ipc;
use jsonrpc_core as core;
use jsonrpc_pubsub::Session;
use ws;
use util::H256;
use v1::{Metadata, Origin};
use v1::informant::RpcStats;
/// Common HTTP & IPC metadata extractor.
pub struct RpcExtractor;
impl HttpMetaExtractor for RpcExtractor {
type Metadata = Metadata;
fn read_metadata(&self, origin: Option<String>, user_agent: Option<String>, dapps_origin: Option<String>) -> Metadata {
let mut metadata = Metadata::default();
metadata.origin = match (origin.as_ref().map(|s| s.as_str()), user_agent, dapps_origin) {
(Some("null"), _, Some(dapp)) => Origin::Dapps(dapp.into()),
(Some(dapp), _, _) => Origin::Dapps(dapp.to_owned().into()),
(None, Some(service), _) => Origin::Rpc(service.into()),
(None, _, _) => Origin::Rpc("unknown".into()),
};
metadata
}
}
impl ipc::MetaExtractor<Metadata> for RpcExtractor {
fn extract(&self, _req: &ipc::RequestContext) -> Metadata {
let mut metadata = Metadata::default();
// TODO [ToDr] Extract proper session id when it's available in context.
metadata.origin = Origin::Ipc(1.into());
metadata
}
}
/// WebSockets server metadata extractor and request middleware.
pub struct WsExtractor {
authcodes_path: Option<PathBuf>,
}
impl WsExtractor {
/// Creates new `WsExtractor` with given authcodes path.
pub fn new(path: Option<&Path>) -> Self {
WsExtractor {
authcodes_path: path.map(|p| p.to_owned()),
}
}
}
impl ws::MetaExtractor<Metadata> for WsExtractor {
fn extract(&self, req: &ws::RequestContext) -> Metadata {
let mut metadata = Metadata::default();
let id = req.session_id as u64;
// TODO [ToDr] Extract dapp from Origin
let dapp = "".into();
metadata.origin = match self.authcodes_path {
Some(ref path) => {
let authorization = req.protocols.get(0).and_then(|p| auth_token_hash(&path, p));
match authorization {
Some(id) => Origin::Signer { session: id.into(), dapp: dapp },
None => Origin::Ws { session: id.into(), dapp: dapp },
}
},
None => Origin::Ws { session: id.into(), dapp: dapp },
};
metadata.session = Some(Arc::new(Session::new(req.sender())));
metadata
}
}
impl ws::RequestMiddleware for WsExtractor {
fn process(&self, req: &ws::ws::Request) -> ws::MiddlewareAction {
use self::ws::ws::Response;
// Reply with 200 Ok to HEAD requests.
if req.method() == "HEAD" {
let mut response = Response::new(200, "Ok");
add_security_headers(&mut response);
return Some(response).into();
}
// Display WS info.
if req.header("sec-websocket-key").is_none() {
let mut response = Response::new(200, "Ok");
response.set_body("WebSocket interface is active. Open WS connection to access RPC.");
add_security_headers(&mut response);
return Some(response).into();
}
// If protocol is provided it needs to be valid.
let protocols = req.protocols().ok().unwrap_or_else(Vec::new);
if let Some(ref path) = self.authcodes_path {
if protocols.len() == 1 {
let authorization = auth_token_hash(&path, protocols[0]);
if authorization.is_none() {
warn!(
"Blocked connection from {} using invalid token.",
req.header("origin").and_then(|e| ::std::str::from_utf8(e).ok()).unwrap_or("Unknown Origin")
);
let mut response = Response::new(403, "Forbidden");
add_security_headers(&mut response);
return Some(response).into();
}
}
}
// Otherwise just proceed.
ws::MiddlewareAction::Proceed
}
}
fn add_security_headers(res: &mut ws::ws::Response) {
let mut headers = res.headers_mut();
headers.push(("X-Frame-Options".into(), b"SAMEORIGIN".to_vec()));
headers.push(("X-XSS-Protection".into(), b"1; mode=block".to_vec()));
headers.push(("X-Content-Type-Options".into(), b"nosniff".to_vec()));
}
fn auth_token_hash(codes_path: &Path, protocol: &str) -> Option<H256> {
let mut split = protocol.split('_');
let auth = split.next().and_then(|v| v.parse().ok());
let time = split.next().and_then(|v| u64::from_str_radix(v, 10).ok());
if let (Some(auth), Some(time)) = (auth, time) {
// Check if the code is valid
return authcodes::AuthCodes::from_file(codes_path)
.ok()
.and_then(|mut codes| {
// remove old tokens
codes.clear_garbage();
let res = codes.is_valid(&auth, time);
// make sure to save back authcodes - it might have been modified
if codes.to_file(codes_path).is_err() {
warn!(target: "signer", "Couldn't save authorization codes to file.");
}
if res {
Some(auth)
} else {
None
}
})
}
None
}
/// WebSockets RPC usage statistics.
pub struct WsStats {
stats: Arc<RpcStats>,
}
impl WsStats {
/// Creates new WS usage tracker.
pub fn new(stats: Arc<RpcStats>) -> Self {
WsStats {
stats: stats,
}
}
}
impl ws::SessionStats for WsStats {
fn open_session(&self, _id: ws::SessionId) {
self.stats.open_session()
}
fn close_session(&self, _id: ws::SessionId) {
self.stats.close_session()
}
}
/// WebSockets middleware dispatching requests to different handles dependning on metadata.
pub struct WsDispatcher<M: core::Middleware<Metadata>> {
full_handler: core::MetaIoHandler<Metadata, M>,
}
impl<M: core::Middleware<Metadata>> WsDispatcher<M> {
/// Create new `WsDispatcher` with given full handler.
pub fn new(full_handler: core::MetaIoHandler<Metadata, M>) -> Self {
WsDispatcher {
full_handler: full_handler,
}
}
}
impl<M: core::Middleware<Metadata>> core::Middleware<Metadata> for WsDispatcher<M> {
fn on_request<F>(&self, request: core::Request, meta: Metadata, process: F) -> core::FutureResponse where
F: FnOnce(core::Request, Metadata) -> core::FutureResponse,
{
let use_full = match &meta.origin {
&Origin::Signer { .. } => true,
_ => false,
};
if use_full {
self.full_handler.handle_rpc_request(request, meta)
} else {
process(request, meta)
}
}
}
#[cfg(test)]
mod tests {
use super::RpcExtractor;
use {HttpMetaExtractor, Origin};
#[test]
fn should_extract_rpc_origin() {
// given
let extractor = RpcExtractor;
// when
let meta1 = extractor.read_metadata(None, None, None);
let meta2 = extractor.read_metadata(None, Some("http://parity.io".to_owned()), None);
let meta3 = extractor.read_metadata(None, Some("http://parity.io".to_owned()), Some("ignored".into()));
// then
assert_eq!(meta1.origin, Origin::Rpc("unknown".into()));
assert_eq!(meta2.origin, Origin::Rpc("http://parity.io".into()));
assert_eq!(meta3.origin, Origin::Rpc("http://parity.io".into()));
}
#[test]
fn should_dapps_origin() {
// given
let extractor = RpcExtractor;
let dapp = "https://wallet.ethereum.org".to_owned();
// when
let meta = extractor.read_metadata(Some("null".into()), None, Some(dapp.clone()));
// then
assert_eq!(meta.origin, Origin::Dapps(dapp.into()));
}
}

View File

@ -0,0 +1,33 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Dapps Service
use v1::types::LocalDapp;
/// Dapps Server service.
pub trait DappsService: Send + Sync + 'static {
/// List available local dapps.
fn list_dapps(&self) -> Vec<LocalDapp>;
}
impl<F> DappsService for F where
F: Fn() -> Vec<LocalDapp> + Send + Sync + 'static
{
fn list_dapps(&self) -> Vec<LocalDapp> {
(*self)()
}
}

View File

@ -209,6 +209,14 @@ pub fn dapps_disabled() -> Error {
} }
} }
pub fn ws_disabled() -> Error {
Error {
code: ErrorCode::ServerError(codes::UNSUPPORTED_REQUEST),
message: "WebSockets Server is disabled. This API is not available.".into(),
data: None,
}
}
pub fn network_disabled() -> Error { pub fn network_disabled() -> Error {
Error { Error {
code: ErrorCode::ServerError(codes::UNSUPPORTED_REQUEST), code: ErrorCode::ServerError(codes::UNSUPPORTED_REQUEST),

View File

@ -19,10 +19,10 @@ pub mod errors;
pub mod accounts; pub mod accounts;
pub mod block_import; pub mod block_import;
pub mod dapps;
pub mod dispatch; pub mod dispatch;
pub mod fake_sign; pub mod fake_sign;
pub mod light_fetch; pub mod light_fetch;
pub mod informant;
pub mod oneshot; pub mod oneshot;
pub mod ipfs; pub mod ipfs;
pub mod secretstore; pub mod secretstore;
@ -50,3 +50,7 @@ pub use self::signing_queue::{
pub use self::signer::SignerService; pub use self::signer::SignerService;
pub use self::subscribers::Subscribers; pub use self::subscribers::Subscribers;
pub use self::subscription_manager::GenericPollManager; pub use self::subscription_manager::GenericPollManager;
pub fn to_url(address: &Option<(String, u16)>) -> Option<String> {
address.as_ref().map(|&(ref iface, ref port)| format!("{}:{}", iface, port))
}

View File

@ -27,21 +27,21 @@ const TOKEN_LIFETIME_SECS: u32 = 3600;
/// Manages communication with Signer crate /// Manages communication with Signer crate
pub struct SignerService { pub struct SignerService {
is_enabled: bool,
queue: Arc<ConfirmationsQueue>, queue: Arc<ConfirmationsQueue>,
web_proxy_tokens: Mutex<TransientHashMap<String, ()>>, web_proxy_tokens: Mutex<TransientHashMap<String, ()>>,
generate_new_token: Box<Fn() -> Result<String, String> + Send + Sync + 'static>, generate_new_token: Box<Fn() -> Result<String, String> + Send + Sync + 'static>,
address: Option<(String, u16)>,
} }
impl SignerService { impl SignerService {
/// Creates new Signer Service given function to generate new tokens. /// Creates new Signer Service given function to generate new tokens.
pub fn new<F>(new_token: F, address: Option<(String, u16)>) -> Self pub fn new<F>(new_token: F, is_enabled: bool) -> Self
where F: Fn() -> Result<String, String> + Send + Sync + 'static { where F: Fn() -> Result<String, String> + Send + Sync + 'static {
SignerService { SignerService {
queue: Arc::new(ConfirmationsQueue::default()), queue: Arc::new(ConfirmationsQueue::default()),
web_proxy_tokens: Mutex::new(TransientHashMap::new(TOKEN_LIFETIME_SECS)), web_proxy_tokens: Mutex::new(TransientHashMap::new(TOKEN_LIFETIME_SECS)),
generate_new_token: Box::new(new_token), generate_new_token: Box::new(new_token),
address: address, is_enabled: is_enabled,
} }
} }
@ -69,20 +69,15 @@ impl SignerService {
self.queue.clone() self.queue.clone()
} }
/// Returns signer address (if signer enabled) or `None` otherwise
pub fn address(&self) -> Option<(String, u16)> {
self.address.clone()
}
/// Returns true if Signer is enabled. /// Returns true if Signer is enabled.
pub fn is_enabled(&self) -> bool { pub fn is_enabled(&self) -> bool {
self.address.is_some() self.is_enabled
} }
#[cfg(test)] #[cfg(test)]
/// Creates new Signer Service for tests. /// Creates new Signer Service for tests.
pub fn new_test(address: Option<(String, u16)>) -> Self { pub fn new_test(is_enabled: bool) -> Self {
SignerService::new(|| Ok("new_token".into()), address) SignerService::new(|| Ok("new_token".into()), is_enabled)
} }
} }

View File

@ -33,7 +33,7 @@ use light::client::LightChainClient;
use jsonrpc_core::Error; use jsonrpc_core::Error;
use jsonrpc_macros::Trailing; use jsonrpc_macros::Trailing;
use v1::helpers::{errors, ipfs, SigningQueue, SignerService, NetworkSettings}; use v1::helpers::{self, errors, ipfs, SigningQueue, SignerService, NetworkSettings};
use v1::helpers::dispatch::LightDispatcher; use v1::helpers::dispatch::LightDispatcher;
use v1::helpers::light_fetch::LightFetch; use v1::helpers::light_fetch::LightFetch;
use v1::metadata::Metadata; use v1::metadata::Metadata;
@ -54,8 +54,8 @@ pub struct ParityClient {
logger: Arc<RotatingLogger>, logger: Arc<RotatingLogger>,
settings: Arc<NetworkSettings>, settings: Arc<NetworkSettings>,
signer: Option<Arc<SignerService>>, signer: Option<Arc<SignerService>>,
dapps_interface: Option<String>, dapps_address: Option<(String, u16)>,
dapps_port: Option<u16>, ws_address: Option<(String, u16)>,
eip86_transition: u64, eip86_transition: u64,
} }
@ -68,8 +68,8 @@ impl ParityClient {
logger: Arc<RotatingLogger>, logger: Arc<RotatingLogger>,
settings: Arc<NetworkSettings>, settings: Arc<NetworkSettings>,
signer: Option<Arc<SignerService>>, signer: Option<Arc<SignerService>>,
dapps_interface: Option<String>, dapps_address: Option<(String, u16)>,
dapps_port: Option<u16>, ws_address: Option<(String, u16)>,
) -> Self { ) -> Self {
ParityClient { ParityClient {
light_dispatch: light_dispatch, light_dispatch: light_dispatch,
@ -77,8 +77,8 @@ impl ParityClient {
logger: logger, logger: logger,
settings: settings, settings: settings,
signer: signer, signer: signer,
dapps_interface: dapps_interface, dapps_address: dapps_address,
dapps_port: dapps_port, ws_address: ws_address,
eip86_transition: client.eip86_transition(), eip86_transition: client.eip86_transition(),
} }
} }
@ -294,22 +294,14 @@ impl Parity for ParityClient {
Ok(map) Ok(map)
} }
fn signer_port(&self) -> Result<u16, Error> { fn dapps_url(&self) -> Result<String, Error> {
self.signer helpers::to_url(&self.dapps_address)
.clone()
.and_then(|signer| signer.address())
.map(|address| address.1)
.ok_or_else(|| errors::signer_disabled())
}
fn dapps_port(&self) -> Result<u16, Error> {
self.dapps_port
.ok_or_else(|| errors::dapps_disabled()) .ok_or_else(|| errors::dapps_disabled())
} }
fn dapps_interface(&self) -> Result<String, Error> { fn ws_url(&self) -> Result<String, Error> {
self.dapps_interface.clone() helpers::to_url(&self.ws_address)
.ok_or_else(|| errors::dapps_disabled()) .ok_or_else(|| errors::ws_disabled())
} }
fn next_nonce(&self, address: H160) -> BoxFuture<U256, Error> { fn next_nonce(&self, address: H160) -> BoxFuture<U256, Error> {

View File

@ -26,21 +26,24 @@ use futures::{BoxFuture, Future};
use util::sha3; use util::sha3;
use jsonrpc_core::Error; use jsonrpc_core::Error;
use v1::helpers::dapps::DappsService;
use v1::helpers::errors; use v1::helpers::errors;
use v1::traits::ParitySet; use v1::traits::ParitySet;
use v1::types::{Bytes, H160, H256, U256, ReleaseInfo, Transaction}; use v1::types::{Bytes, H160, H256, U256, ReleaseInfo, Transaction, LocalDapp};
/// Parity-specific rpc interface for operations altering the settings. /// Parity-specific rpc interface for operations altering the settings.
pub struct ParitySetClient<F> { pub struct ParitySetClient<F> {
net: Arc<ManageNetwork>, net: Arc<ManageNetwork>,
dapps: Option<Arc<DappsService>>,
fetch: F, fetch: F,
} }
impl<F: Fetch> ParitySetClient<F> { impl<F: Fetch> ParitySetClient<F> {
/// Creates new `ParitySetClient` with given `Fetch`. /// Creates new `ParitySetClient` with given `Fetch`.
pub fn new(net: Arc<ManageNetwork>, fetch: F) -> Self { pub fn new(net: Arc<ManageNetwork>, dapps: Option<Arc<DappsService>>, fetch: F) -> Self {
ParitySetClient { ParitySetClient {
net: net, net: net,
dapps: dapps,
fetch: fetch, fetch: fetch,
} }
} }
@ -132,6 +135,10 @@ impl<F: Fetch> ParitySet for ParitySetClient<F> {
})) }))
} }
fn dapps_list(&self) -> Result<Vec<LocalDapp>, Error> {
self.dapps.as_ref().map(|dapps| dapps.list_dapps()).ok_or_else(errors::dapps_disabled)
}
fn upgrade_ready(&self) -> Result<Option<ReleaseInfo>, Error> { fn upgrade_ready(&self) -> Result<Option<ReleaseInfo>, Error> {
Err(errors::light_unimplemented(None)) Err(errors::light_unimplemented(None))
} }

View File

@ -38,7 +38,7 @@ use crypto::DEFAULT_MAC;
use jsonrpc_core::Error; use jsonrpc_core::Error;
use jsonrpc_macros::Trailing; use jsonrpc_macros::Trailing;
use v1::helpers::{errors, ipfs, SigningQueue, SignerService, NetworkSettings}; use v1::helpers::{self, errors, ipfs, SigningQueue, SignerService, NetworkSettings};
use v1::helpers::accounts::unwrap_provider; use v1::helpers::accounts::unwrap_provider;
use v1::metadata::Metadata; use v1::metadata::Metadata;
use v1::traits::Parity; use v1::traits::Parity;
@ -67,8 +67,8 @@ pub struct ParityClient<C, M, S: ?Sized, U> where
logger: Arc<RotatingLogger>, logger: Arc<RotatingLogger>,
settings: Arc<NetworkSettings>, settings: Arc<NetworkSettings>,
signer: Option<Arc<SignerService>>, signer: Option<Arc<SignerService>>,
dapps_interface: Option<String>, dapps_address: Option<(String, u16)>,
dapps_port: Option<u16>, ws_address: Option<(String, u16)>,
eip86_transition: u64, eip86_transition: u64,
} }
@ -89,8 +89,8 @@ impl<C, M, S: ?Sized, U> ParityClient<C, M, S, U> where
logger: Arc<RotatingLogger>, logger: Arc<RotatingLogger>,
settings: Arc<NetworkSettings>, settings: Arc<NetworkSettings>,
signer: Option<Arc<SignerService>>, signer: Option<Arc<SignerService>>,
dapps_interface: Option<String>, dapps_address: Option<(String, u16)>,
dapps_port: Option<u16>, ws_address: Option<(String, u16)>,
) -> Self { ) -> Self {
ParityClient { ParityClient {
client: Arc::downgrade(client), client: Arc::downgrade(client),
@ -102,8 +102,8 @@ impl<C, M, S: ?Sized, U> ParityClient<C, M, S, U> where
logger: logger, logger: logger,
settings: settings, settings: settings,
signer: signer, signer: signer,
dapps_interface: dapps_interface, dapps_address: dapps_address,
dapps_port: dapps_port, ws_address: ws_address,
eip86_transition: client.eip86_transition(), eip86_transition: client.eip86_transition(),
} }
} }
@ -317,22 +317,14 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
) )
} }
fn signer_port(&self) -> Result<u16, Error> { fn dapps_url(&self) -> Result<String, Error> {
self.signer helpers::to_url(&self.dapps_address)
.clone()
.and_then(|signer| signer.address())
.map(|address| address.1)
.ok_or_else(|| errors::signer_disabled())
}
fn dapps_port(&self) -> Result<u16, Error> {
self.dapps_port
.ok_or_else(|| errors::dapps_disabled()) .ok_or_else(|| errors::dapps_disabled())
} }
fn dapps_interface(&self) -> Result<String, Error> { fn ws_url(&self) -> Result<String, Error> {
self.dapps_interface.clone() helpers::to_url(&self.ws_address)
.ok_or_else(|| errors::dapps_disabled()) .ok_or_else(|| errors::ws_disabled())
} }
fn next_nonce(&self, address: H160) -> BoxFuture<U256, Error> { fn next_nonce(&self, address: H160) -> BoxFuture<U256, Error> {

View File

@ -28,9 +28,10 @@ use util::sha3;
use updater::{Service as UpdateService}; use updater::{Service as UpdateService};
use jsonrpc_core::Error; use jsonrpc_core::Error;
use v1::helpers::dapps::DappsService;
use v1::helpers::errors; use v1::helpers::errors;
use v1::traits::ParitySet; use v1::traits::ParitySet;
use v1::types::{Bytes, H160, H256, U256, ReleaseInfo, Transaction}; use v1::types::{Bytes, H160, H256, U256, ReleaseInfo, Transaction, LocalDapp};
/// Parity-specific rpc interface for operations altering the settings. /// Parity-specific rpc interface for operations altering the settings.
pub struct ParitySetClient<C, M, U, F = fetch::Client> { pub struct ParitySetClient<C, M, U, F = fetch::Client> {
@ -38,6 +39,7 @@ pub struct ParitySetClient<C, M, U, F = fetch::Client> {
miner: Weak<M>, miner: Weak<M>,
updater: Weak<U>, updater: Weak<U>,
net: Weak<ManageNetwork>, net: Weak<ManageNetwork>,
dapps: Option<Arc<DappsService>>,
fetch: F, fetch: F,
eip86_transition: u64, eip86_transition: u64,
} }
@ -46,12 +48,20 @@ impl<C, M, U, F> ParitySetClient<C, M, U, F>
where C: MiningBlockChainClient + 'static, where C: MiningBlockChainClient + 'static,
{ {
/// Creates new `ParitySetClient` with given `Fetch`. /// Creates new `ParitySetClient` with given `Fetch`.
pub fn new(client: &Arc<C>, miner: &Arc<M>, updater: &Arc<U>, net: &Arc<ManageNetwork>, fetch: F) -> Self { pub fn new(
client: &Arc<C>,
miner: &Arc<M>,
updater: &Arc<U>,
net: &Arc<ManageNetwork>,
dapps: Option<Arc<DappsService>>,
fetch: F,
) -> Self {
ParitySetClient { ParitySetClient {
client: Arc::downgrade(client), client: Arc::downgrade(client),
miner: Arc::downgrade(miner), miner: Arc::downgrade(miner),
updater: Arc::downgrade(updater), updater: Arc::downgrade(updater),
net: Arc::downgrade(net), net: Arc::downgrade(net),
dapps: dapps,
fetch: fetch, fetch: fetch,
eip86_transition: client.eip86_transition(), eip86_transition: client.eip86_transition(),
} }
@ -166,6 +176,10 @@ impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where
})) }))
} }
fn dapps_list(&self) -> Result<Vec<LocalDapp>, Error> {
self.dapps.as_ref().map(|dapps| dapps.list_dapps()).ok_or_else(errors::dapps_disabled)
}
fn upgrade_ready(&self) -> Result<Option<ReleaseInfo>, Error> { fn upgrade_ready(&self) -> Result<Option<ReleaseInfo>, Error> {
let updater = take_weak!(self.updater); let updater = take_weak!(self.updater);
Ok(updater.upgrade_ready().map(Into::into)) Ok(updater.upgrade_ready().map(Into::into))

View File

@ -14,6 +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/>.
//! Parity RPC requests Metadata.
use std::sync::Arc; use std::sync::Arc;
use jsonrpc_core; use jsonrpc_core;
@ -35,7 +36,9 @@ impl Metadata {
pub fn dapp_id(&self) -> DappId { pub fn dapp_id(&self) -> DappId {
// TODO [ToDr] Extract dapp info from Ws connections. // TODO [ToDr] Extract dapp info from Ws connections.
match self.origin { match self.origin {
Origin::Dapps(ref dapp_id) => dapp_id.clone(), Origin::Dapps(ref dapp) => dapp.clone(),
Origin::Ws { ref dapp, .. } => dapp.clone(),
Origin::Signer { ref dapp, .. } => dapp.clone(),
_ => DappId::default(), _ => DappId::default(),
} }
} }

View File

@ -52,14 +52,30 @@ macro_rules! try_bf {
#[macro_use] #[macro_use]
mod helpers; mod helpers;
mod impls; mod impls;
mod metadata; mod types;
#[cfg(test)]
mod tests;
pub mod extractors;
pub mod informant;
pub mod metadata;
pub mod traits; pub mod traits;
pub mod tests;
pub mod types;
pub use self::traits::{Web3, Eth, EthFilter, EthPubSub, EthSigning, Net, Parity, ParityAccounts, ParitySet, ParitySigning, PubSub, Signer, Personal, Traces, Rpc, SecretStore}; pub use self::traits::{Web3, Eth, EthFilter, EthPubSub, EthSigning, Net, Parity, ParityAccounts, ParitySet, ParitySigning, PubSub, Signer, Personal, Traces, Rpc, SecretStore};
pub use self::impls::*; pub use self::impls::*;
pub use self::helpers::{SigningQueue, SignerService, ConfirmationsQueue, NetworkSettings, block_import, informant, dispatch}; pub use self::helpers::{NetworkSettings, block_import, dispatch};
pub use self::metadata::Metadata; pub use self::metadata::Metadata;
pub use self::types::Origin; pub use self::types::Origin;
pub use self::extractors::{RpcExtractor, WsExtractor, WsStats, WsDispatcher};
/// Signer utilities
pub mod signer {
pub use super::helpers::{SigningQueue, SignerService, ConfirmationsQueue};
pub use super::types::{ConfirmationRequest, TransactionModification, U256, TransactionCondition};
}
/// Dapps integration utilities
pub mod dapps {
pub use super::helpers::dapps::DappsService;
pub use super::types::LocalDapp;
}

View File

@ -0,0 +1,37 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Test implementation of dapps service.
use v1::types::LocalDapp;
use v1::helpers::dapps::DappsService;
/// Test implementation of dapps service. Will always return the same list of dapps.
#[derive(Default, Clone)]
pub struct TestDappsService;
impl DappsService for TestDappsService {
fn list_dapps(&self) -> Vec<LocalDapp> {
vec![LocalDapp {
id: "skeleton".into(),
name: "Skeleton".into(),
description: "A skeleton dapp".into(),
version: "0.1".into(),
author: "Parity Technologies Ltd".into(),
icon_url: "title.png".into(),
}]
}
}

View File

@ -16,14 +16,16 @@
//! Test rpc services. //! Test rpc services.
mod sync_provider; mod dapps;
mod miner_service;
mod fetch; mod fetch;
mod miner_service;
mod snapshot_service; mod snapshot_service;
mod sync_provider;
mod update_service; mod update_service;
pub use self::sync_provider::{Config, TestSyncProvider}; pub use self::dapps::TestDappsService;
pub use self::miner_service::TestMinerService;
pub use self::fetch::TestFetch; pub use self::fetch::TestFetch;
pub use self::miner_service::TestMinerService;
pub use self::snapshot_service::TestSnapshotService; pub use self::snapshot_service::TestSnapshotService;
pub use self::sync_provider::{Config, TestSyncProvider};
pub use self::update_service::TestUpdater; pub use self::update_service::TestUpdater;

View File

@ -41,8 +41,8 @@ pub struct Dependencies {
pub settings: Arc<NetworkSettings>, pub settings: Arc<NetworkSettings>,
pub network: Arc<ManageNetwork>, pub network: Arc<ManageNetwork>,
pub accounts: Arc<AccountProvider>, pub accounts: Arc<AccountProvider>,
pub dapps_interface: Option<String>, pub dapps_address: Option<(String, u16)>,
pub dapps_port: Option<u16>, pub ws_address: Option<(String, u16)>,
} }
impl Dependencies { impl Dependencies {
@ -66,8 +66,8 @@ impl Dependencies {
}), }),
network: Arc::new(TestManageNetwork), network: Arc::new(TestManageNetwork),
accounts: Arc::new(AccountProvider::transient_provider()), accounts: Arc::new(AccountProvider::transient_provider()),
dapps_interface: Some("127.0.0.1".into()), dapps_address: Some(("127.0.0.1".into(), 18080)),
dapps_port: Some(18080), ws_address: Some(("127.0.0.1".into(), 18546)),
} }
} }
@ -84,8 +84,8 @@ impl Dependencies {
self.logger.clone(), self.logger.clone(),
self.settings.clone(), self.settings.clone(),
signer, signer,
self.dapps_interface.clone(), self.dapps_address.clone(),
self.dapps_port, self.ws_address.clone(),
) )
} }
@ -345,7 +345,7 @@ fn rpc_parity_node_name() {
#[test] #[test]
fn rpc_parity_unsigned_transactions_count() { fn rpc_parity_unsigned_transactions_count() {
let deps = Dependencies::new(); let deps = Dependencies::new();
let io = deps.with_signer(SignerService::new_test(Some(("127.0.0.1".into(), 18180)))); let io = deps.with_signer(SignerService::new_test(true));
let request = r#"{"jsonrpc": "2.0", "method": "parity_unsignedTransactionsCount", "params":[], "id": 1}"#; let request = r#"{"jsonrpc": "2.0", "method": "parity_unsignedTransactionsCount", "params":[], "id": 1}"#;
let response = r#"{"jsonrpc":"2.0","result":0,"id":1}"#; let response = r#"{"jsonrpc":"2.0","result":0,"id":1}"#;
@ -386,16 +386,17 @@ fn rpc_parity_encrypt() {
} }
#[test] #[test]
fn rpc_parity_signer_port() { fn rpc_parity_ws_address() {
// given // given
let deps = Dependencies::new(); let mut deps = Dependencies::new();
let io1 = deps.with_signer(SignerService::new_test(Some(("127.0.0.1".into(), 18180)))); let io1 = deps.default_client();
deps.ws_address = None;
let io2 = deps.default_client(); let io2 = deps.default_client();
// when // when
let request = r#"{"jsonrpc": "2.0", "method": "parity_signerPort", "params": [], "id": 1}"#; let request = r#"{"jsonrpc": "2.0", "method": "parity_wsUrl", "params": [], "id": 1}"#;
let response1 = r#"{"jsonrpc":"2.0","result":18180,"id":1}"#; let response1 = r#"{"jsonrpc":"2.0","result":"127.0.0.1:18546","id":1}"#;
let response2 = r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"Trusted Signer is disabled. This API is not available."},"id":1}"#; let response2 = r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"WebSockets Server is disabled. This API is not available."},"id":1}"#;
// then // then
assert_eq!(io1.handle_request_sync(request), Some(response1.to_owned())); assert_eq!(io1.handle_request_sync(request), Some(response1.to_owned()));
@ -403,34 +404,16 @@ fn rpc_parity_signer_port() {
} }
#[test] #[test]
fn rpc_parity_dapps_port() { fn rpc_parity_dapps_address() {
// given // given
let mut deps = Dependencies::new(); let mut deps = Dependencies::new();
let io1 = deps.default_client(); let io1 = deps.default_client();
deps.dapps_port = None; deps.dapps_address = None;
let io2 = deps.default_client(); let io2 = deps.default_client();
// when // when
let request = r#"{"jsonrpc": "2.0", "method": "parity_dappsPort", "params": [], "id": 1}"#; let request = r#"{"jsonrpc": "2.0", "method": "parity_dappsUrl", "params": [], "id": 1}"#;
let response1 = r#"{"jsonrpc":"2.0","result":18080,"id":1}"#; let response1 = r#"{"jsonrpc":"2.0","result":"127.0.0.1:18080","id":1}"#;
let response2 = r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"Dapps Server is disabled. This API is not available."},"id":1}"#;
// then
assert_eq!(io1.handle_request_sync(request), Some(response1.to_owned()));
assert_eq!(io2.handle_request_sync(request), Some(response2.to_owned()));
}
#[test]
fn rpc_parity_dapps_interface() {
// given
let mut deps = Dependencies::new();
let io1 = deps.default_client();
deps.dapps_interface = None;
let io2 = deps.default_client();
// when
let request = r#"{"jsonrpc": "2.0", "method": "parity_dappsInterface", "params": [], "id": 1}"#;
let response1 = r#"{"jsonrpc":"2.0","result":"127.0.0.1","id":1}"#;
let response2 = r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"Dapps Server is disabled. This API is not available."},"id":1}"#; let response2 = r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"Dapps Server is disabled. This API is not available."},"id":1}"#;
// then // then

View File

@ -25,7 +25,7 @@ use ethsync::ManageNetwork;
use jsonrpc_core::IoHandler; use jsonrpc_core::IoHandler;
use v1::{ParitySet, ParitySetClient}; use v1::{ParitySet, ParitySetClient};
use v1::tests::helpers::{TestMinerService, TestFetch, TestUpdater}; use v1::tests::helpers::{TestMinerService, TestFetch, TestUpdater, TestDappsService};
use super::manage_network::TestManageNetwork; use super::manage_network::TestManageNetwork;
fn miner_service() -> Arc<TestMinerService> { fn miner_service() -> Arc<TestMinerService> {
@ -46,8 +46,14 @@ fn updater_service() -> Arc<TestUpdater> {
pub type TestParitySetClient = ParitySetClient<TestBlockChainClient, TestMinerService, TestUpdater, TestFetch>; pub type TestParitySetClient = ParitySetClient<TestBlockChainClient, TestMinerService, TestUpdater, TestFetch>;
fn parity_set_client(client: &Arc<TestBlockChainClient>, miner: &Arc<TestMinerService>, updater: &Arc<TestUpdater>, net: &Arc<TestManageNetwork>) -> TestParitySetClient { fn parity_set_client(
ParitySetClient::new(client, miner, updater, &(net.clone() as Arc<ManageNetwork>), TestFetch::default()) client: &Arc<TestBlockChainClient>,
miner: &Arc<TestMinerService>,
updater: &Arc<TestUpdater>,
net: &Arc<TestManageNetwork>,
) -> TestParitySetClient {
let dapps_service = Arc::new(TestDappsService);
ParitySetClient::new(client, miner, updater, &(net.clone() as Arc<ManageNetwork>), Some(dapps_service), TestFetch::default())
} }
#[test] #[test]
@ -232,3 +238,18 @@ fn rpc_parity_remove_transaction() {
miner.pending_transactions.lock().insert(hash, signed); miner.pending_transactions.lock().insert(hash, signed);
assert_eq!(io.handle_request_sync(&request), Some(response.to_owned())); assert_eq!(io.handle_request_sync(&request), Some(response.to_owned()));
} }
#[test]
fn rpc_parity_set_dapps_list() {
let miner = miner_service();
let client = client_service();
let network = network_service();
let updater = updater_service();
let mut io = IoHandler::new();
io.extend_with(parity_set_client(&client, &miner, &updater, &network).to_delegate());
let request = r#"{"jsonrpc": "2.0", "method": "parity_dappsList", "params":[], "id": 1}"#;
let response = r#"{"jsonrpc":"2.0","result":[{"author":"Parity Technologies Ltd","description":"A skeleton dapp","iconUrl":"title.png","id":"skeleton","name":"Skeleton","version":"0.1"}],"id":1}"#;
assert_eq!(io.handle_request_sync(request), Some(response.to_owned()));
}

View File

@ -58,7 +58,7 @@ fn miner_service() -> Arc<TestMinerService> {
} }
fn signer_tester() -> SignerTester { fn signer_tester() -> SignerTester {
let signer = Arc::new(SignerService::new_test(None)); let signer = Arc::new(SignerService::new_test(false));
let accounts = accounts_provider(); let accounts = accounts_provider();
let opt_accounts = Some(accounts.clone()); let opt_accounts = Some(accounts.clone());
let client = blockchain_client(); let client = blockchain_client();

View File

@ -47,7 +47,7 @@ struct SigningTester {
impl Default for SigningTester { impl Default for SigningTester {
fn default() -> Self { fn default() -> Self {
let signer = Arc::new(SignerService::new_test(None)); let signer = Arc::new(SignerService::new_test(false));
let client = Arc::new(TestBlockChainClient::default()); let client = Arc::new(TestBlockChainClient::default());
let miner = Arc::new(TestMinerService::default()); let miner = Arc::new(TestMinerService::default());
let accounts = Arc::new(AccountProvider::transient_provider()); let accounts = Arc::new(AccountProvider::transient_provider());

View File

@ -151,17 +151,13 @@ build_rpc_trait! {
#[rpc(name = "parity_localTransactions")] #[rpc(name = "parity_localTransactions")]
fn local_transactions(&self) -> Result<BTreeMap<H256, LocalTransactionStatus>, Error>; fn local_transactions(&self) -> Result<BTreeMap<H256, LocalTransactionStatus>, Error>;
/// Returns current Trusted Signer port or an error if signer is disabled. /// Returns current Dapps Server interface and port or an error if dapps server is disabled.
#[rpc(name = "parity_signerPort")] #[rpc(name = "parity_dappsUrl")]
fn signer_port(&self) -> Result<u16, Error>; fn dapps_url(&self) -> Result<String, Error>;
/// Returns current Dapps Server port or an error if dapps server is disabled. /// Returns current WS Server interface and port or an error if ws server is disabled.
#[rpc(name = "parity_dappsPort")] #[rpc(name = "parity_wsUrl")]
fn dapps_port(&self) -> Result<u16, Error>; fn ws_url(&self) -> Result<String, Error>;
/// Returns current Dapps Server interface address or an error if dapps server is disabled.
#[rpc(name = "parity_dappsInterface")]
fn dapps_interface(&self) -> Result<String, Error>;
/// Returns next nonce for particular sender. Should include all transactions in the queue. /// Returns next nonce for particular sender. Should include all transactions in the queue.
#[rpc(async, name = "parity_nextNonce")] #[rpc(async, name = "parity_nextNonce")]

View File

@ -19,7 +19,7 @@
use jsonrpc_core::Error; use jsonrpc_core::Error;
use futures::BoxFuture; use futures::BoxFuture;
use v1::types::{Bytes, H160, H256, U256, ReleaseInfo, Transaction}; use v1::types::{Bytes, H160, H256, U256, ReleaseInfo, Transaction, LocalDapp};
build_rpc_trait! { build_rpc_trait! {
/// Parity-specific rpc interface for operations altering the settings. /// Parity-specific rpc interface for operations altering the settings.
@ -96,6 +96,10 @@ build_rpc_trait! {
#[rpc(async, name = "parity_hashContent")] #[rpc(async, name = "parity_hashContent")]
fn hash_content(&self, String) -> BoxFuture<H256, Error>; fn hash_content(&self, String) -> BoxFuture<H256, Error>;
/// Returns a list of local dapps
#[rpc(name = "parity_dappsList")]
fn dapps_list(&self) -> Result<Vec<LocalDapp>, Error>;
/// Is there a release ready for install? /// Is there a release ready for install?
#[rpc(name = "parity_upgradeReady")] #[rpc(name = "parity_upgradeReady")]
fn upgrade_ready(&self) -> Result<Option<ReleaseInfo>, Error>; fn upgrade_ready(&self) -> Result<Option<ReleaseInfo>, Error>;

View File

@ -283,12 +283,15 @@ mod tests {
nonce: Some(1.into()), nonce: Some(1.into()),
condition: None, condition: None,
}), }),
origin: Origin::Signer(5.into()), origin: Origin::Signer {
dapp: "http://parity.io".into(),
session: 5.into(),
}
}; };
// when // when
let res = serde_json::to_string(&ConfirmationRequest::from(request)); let res = serde_json::to_string(&ConfirmationRequest::from(request));
let expected = r#"{"id":"0xf","payload":{"sendTransaction":{"from":"0x0000000000000000000000000000000000000000","to":null,"gasPrice":"0x2710","gas":"0x3a98","value":"0x186a0","data":"0x010203","nonce":"0x1","condition":null}},"origin":{"signer":"0x0000000000000000000000000000000000000000000000000000000000000005"}}"#; let expected = r#"{"id":"0xf","payload":{"sendTransaction":{"from":"0x0000000000000000000000000000000000000000","to":null,"gasPrice":"0x2710","gas":"0x3a98","value":"0x186a0","data":"0x010203","nonce":"0x1","condition":null}},"origin":{"signer":{"dapp":"http://parity.io","session":"0x0000000000000000000000000000000000000000000000000000000000000005"}}}"#;
// then // then
assert_eq!(res.unwrap(), expected.to_owned()); assert_eq!(res.unwrap(), expected.to_owned());

57
rpc/src/v1/types/dapps.rs Normal file
View File

@ -0,0 +1,57 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
/// Local Dapp
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LocalDapp {
/// ID of local dapp
pub id: String,
/// Dapp name
pub name: String,
/// Dapp description
pub description: String,
/// Dapp version string
pub version: String,
/// Dapp author
pub author: String,
/// Dapp icon
#[serde(rename="iconUrl")]
pub icon_url: String,
}
#[cfg(test)]
mod tests {
use serde_json;
use super::LocalDapp;
#[test]
fn dapp_serialization() {
let s = r#"{"id":"skeleton","name":"Skeleton","description":"A skeleton dapp","version":"0.1","author":"Parity Technologies Ltd","iconUrl":"title.png"}"#;
let dapp = LocalDapp {
id: "skeleton".into(),
name: "Skeleton".into(),
description: "A skeleton dapp".into(),
version: "0.1".into(),
author: "Parity Technologies Ltd".into(),
icon_url: "title.png".into(),
};
let serialized = serde_json::to_string(&dapp).unwrap();
assert_eq!(serialized, s);
}
}

View File

@ -24,6 +24,7 @@ mod bytes;
mod call_request; mod call_request;
mod confirmations; mod confirmations;
mod consensus_status; mod consensus_status;
mod dapps;
mod derivation; mod derivation;
mod filter; mod filter;
mod hash; mod hash;
@ -55,6 +56,7 @@ pub use self::confirmations::{
TransactionModification, SignRequest, DecryptRequest, Either TransactionModification, SignRequest, DecryptRequest, Either
}; };
pub use self::consensus_status::*; pub use self::consensus_status::*;
pub use self::dapps::LocalDapp;
pub use self::derivation::{DeriveHash, DeriveHierarchical, Derive}; pub use self::derivation::{DeriveHash, DeriveHierarchical, Derive};
pub use self::filter::{Filter, FilterChanges}; pub use self::filter::{Filter, FilterChanges};
pub use self::hash::{H64, H160, H256, H512, H520, H2048}; pub use self::hash::{H64, H160, H256, H512, H520, H2048};

View File

@ -33,12 +33,22 @@ pub enum Origin {
/// IPC server (includes session hash) /// IPC server (includes session hash)
#[serde(rename="ipc")] #[serde(rename="ipc")]
Ipc(H256), Ipc(H256),
/// WS server (includes session hash) /// WS server
#[serde(rename="ws")] #[serde(rename="ws")]
Ws(H256), Ws {
/// Signer (includes session hash) /// Dapp id
dapp: DappId,
/// Session id
session: H256,
},
/// Signer (authorized WS server)
#[serde(rename="signer")] #[serde(rename="signer")]
Signer(H256), Signer {
/// Dapp id
dapp: DappId,
/// Session id
session: H256
},
/// Unknown /// Unknown
#[serde(rename="unknown")] #[serde(rename="unknown")]
Unknown, Unknown,
@ -53,11 +63,11 @@ impl Default for Origin {
impl fmt::Display for Origin { impl fmt::Display for Origin {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self { match *self {
Origin::Rpc(ref origin) => write!(f, "RPC (service: {})", origin), Origin::Rpc(ref origin) => write!(f, "{} via RPC", origin),
Origin::Dapps(ref origin) => write!(f, "Dapp {}", origin), Origin::Dapps(ref origin) => write!(f, "Dapp {}", origin),
Origin::Ipc(ref session) => write!(f, "IPC (session: {})", session), Origin::Ipc(ref session) => write!(f, "IPC (session: {})", session),
Origin::Ws(ref session) => write!(f, "WebSocket (session: {})", session), Origin::Ws { ref session, ref dapp } => write!(f, "{} via WebSocket (session: {})", dapp, session),
Origin::Signer(ref session) => write!(f, "UI (session: {})", session), Origin::Signer { ref session, ref dapp } => write!(f, "{} via UI (session: {})", dapp, session),
Origin::Unknown => write!(f, "unknown origin"), Origin::Unknown => write!(f, "unknown origin"),
} }
} }
@ -114,9 +124,15 @@ mod tests {
let o1 = Origin::Rpc("test service".into()); let o1 = Origin::Rpc("test service".into());
let o2 = Origin::Dapps("http://parity.io".into()); let o2 = Origin::Dapps("http://parity.io".into());
let o3 = Origin::Ipc(5.into()); let o3 = Origin::Ipc(5.into());
let o4 = Origin::Signer(10.into()); let o4 = Origin::Signer {
dapp: "http://parity.io".into(),
session: 10.into(),
};
let o5 = Origin::Unknown; let o5 = Origin::Unknown;
let o6 = Origin::Ws(5.into()); let o6 = Origin::Ws {
dapp: "http://parity.io".into(),
session: 5.into(),
};
// when // when
let res1 = serde_json::to_string(&o1).unwrap(); let res1 = serde_json::to_string(&o1).unwrap();
@ -130,9 +146,9 @@ mod tests {
assert_eq!(res1, r#"{"rpc":"test service"}"#); assert_eq!(res1, r#"{"rpc":"test service"}"#);
assert_eq!(res2, r#"{"dapp":"http://parity.io"}"#); assert_eq!(res2, r#"{"dapp":"http://parity.io"}"#);
assert_eq!(res3, r#"{"ipc":"0x0000000000000000000000000000000000000000000000000000000000000005"}"#); assert_eq!(res3, r#"{"ipc":"0x0000000000000000000000000000000000000000000000000000000000000005"}"#);
assert_eq!(res4, r#"{"signer":"0x000000000000000000000000000000000000000000000000000000000000000a"}"#); assert_eq!(res4, r#"{"signer":{"dapp":"http://parity.io","session":"0x000000000000000000000000000000000000000000000000000000000000000a"}}"#);
assert_eq!(res5, r#""unknown""#); assert_eq!(res5, r#""unknown""#);
assert_eq!(res6, r#"{"ws":"0x0000000000000000000000000000000000000000000000000000000000000005"}"#); assert_eq!(res6, r#"{"ws":{"dapp":"http://parity.io","session":"0x0000000000000000000000000000000000000000000000000000000000000005"}}"#);
} }
#[test] #[test]

View File

@ -7,7 +7,7 @@ extern crate ethcore_bigint as bigint;
extern crate parity_rpc as rpc; extern crate parity_rpc as rpc;
extern crate parity_rpc_client as client; extern crate parity_rpc_client as client;
use rpc::v1::types::{U256, ConfirmationRequest}; use rpc::signer::{U256, ConfirmationRequest};
use client::signer_client::SignerRpc; use client::signer_client::SignerRpc;
use std::io::{Write, BufRead, BufReader, stdout, stdin}; use std::io::{Write, BufRead, BufReader, stdout, stdin};
use std::path::PathBuf; use std::path::PathBuf;

View File

@ -14,8 +14,8 @@ serde = "0.9"
serde_json = "0.9" serde_json = "0.9"
tempdir = "0.3.5" tempdir = "0.3.5"
url = "1.2.0" url = "1.2.0"
matches = "0.1"
jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc.git", branch = "parity-1.7" } jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc.git", branch = "parity-1.7" }
ws = { git = "https://github.com/paritytech/ws-rs.git", branch = "parity-1.7" } jsonrpc-ws-server = { git = "https://github.com/paritytech/jsonrpc.git", branch = "parity-1.7" }
parity-rpc = { path = "../rpc" } parity-rpc = { path = "../rpc" }
ethcore-signer = { path = "../signer" }
ethcore-util = { path = "../util" } ethcore-util = { path = "../util" }

View File

@ -13,7 +13,7 @@ use util::{Hashable, Mutex};
use url::Url; use url::Url;
use std::fs::File; use std::fs::File;
use ws::{ use ws::ws::{
self, self,
Request, Request,
Handler, Handler,
@ -204,6 +204,7 @@ impl Rpc {
let rpc = Self::connect(url, authpath).map(|rpc| rpc).wait()?; let rpc = Self::connect(url, authpath).map(|rpc| rpc).wait()?;
rpc rpc
} }
/// Non-blocking, returns a future /// Non-blocking, returns a future
pub fn connect( pub fn connect(
url: &str, authpath: &PathBuf url: &str, authpath: &PathBuf
@ -241,6 +242,7 @@ impl Rpc {
} }
} }
} }
/// Non-blocking, returns a future of the request response /// Non-blocking, returns a future of the request response
pub fn request<T>( pub fn request<T>(
&mut self, method: &'static str, params: Vec<JsonValue> &mut self, method: &'static str, params: Vec<JsonValue>

View File

@ -1,34 +1,36 @@
pub mod client; pub mod client;
pub mod signer_client; pub mod signer_client;
extern crate ethcore_signer;
extern crate ethcore_util as util; extern crate ethcore_util as util;
extern crate futures; extern crate futures;
extern crate jsonrpc_core; extern crate jsonrpc_core;
extern crate jsonrpc_ws_server as ws;
extern crate parity_rpc as rpc; extern crate parity_rpc as rpc;
extern crate rand; extern crate rand;
extern crate serde; extern crate serde;
extern crate serde_json; extern crate serde_json;
extern crate tempdir; extern crate tempdir;
extern crate url; extern crate url;
extern crate ws;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
#[cfg(test)] #[cfg(test)]
mod tests {
#[macro_use] #[macro_use]
extern crate matches; extern crate matches;
#[cfg(test)]
mod tests {
use futures::Future; use futures::Future;
use std::path::PathBuf; use std::path::PathBuf;
use client::{Rpc, RpcError}; use client::{Rpc, RpcError};
use ethcore_signer; use rpc;
#[test] #[test]
fn test_connection_refused() { fn test_connection_refused() {
let (_srv, port, mut authcodes) = ethcore_signer::tests::serve(); let (_srv, port, mut authcodes) = rpc::tests::ws::serve();
let _ = authcodes.generate_new(); let _ = authcodes.generate_new();
authcodes.to_file(&authcodes.path).unwrap(); authcodes.to_file(&authcodes.path).unwrap();
@ -43,7 +45,7 @@ mod tests {
#[test] #[test]
fn test_authcode_fail() { fn test_authcode_fail() {
let (_srv, port, _) = ethcore_signer::tests::serve(); let (_srv, port, _) = rpc::tests::ws::serve();
let path = PathBuf::from("nonexist"); let path = PathBuf::from("nonexist");
let connect = Rpc::connect(&format!("ws://127.0.0.1:{}", port), &path); let connect = Rpc::connect(&format!("ws://127.0.0.1:{}", port), &path);
@ -55,7 +57,7 @@ mod tests {
#[test] #[test]
fn test_authcode_correct() { fn test_authcode_correct() {
let (_srv, port, mut authcodes) = ethcore_signer::tests::serve(); let (_srv, port, mut authcodes) = rpc::tests::ws::serve();
let _ = authcodes.generate_new(); let _ = authcodes.generate_new();
authcodes.to_file(&authcodes.path).unwrap(); authcodes.to_file(&authcodes.path).unwrap();

View File

@ -1,5 +1,5 @@
use client::{Rpc, RpcError}; use client::{Rpc, RpcError};
use rpc::v1::types::{ConfirmationRequest, TransactionModification, U256, TransactionCondition}; use rpc::signer::{ConfirmationRequest, TransactionModification, U256, TransactionCondition};
use serde; use serde;
use serde_json::{Value as JsonValue, to_value}; use serde_json::{Value as JsonValue, to_value};
use std::path::PathBuf; use std::path::PathBuf;
@ -13,11 +13,11 @@ impl SignerRpc {
pub fn new(url: &str, authfile: &PathBuf) -> Result<Self, RpcError> { pub fn new(url: &str, authfile: &PathBuf) -> Result<Self, RpcError> {
Ok(SignerRpc { rpc: Rpc::new(&url, authfile)? }) Ok(SignerRpc { rpc: Rpc::new(&url, authfile)? })
} }
pub fn requests_to_confirm(&mut self) ->
BoxFuture<Result<Vec<ConfirmationRequest>, RpcError>, Canceled> pub fn requests_to_confirm(&mut self) -> BoxFuture<Result<Vec<ConfirmationRequest>, RpcError>, Canceled> {
{
self.rpc.request("signer_requestsToConfirm", vec![]) self.rpc.request("signer_requestsToConfirm", vec![])
} }
pub fn confirm_request( pub fn confirm_request(
&mut self, &mut self,
id: U256, id: U256,
@ -25,17 +25,15 @@ impl SignerRpc {
new_gas_price: Option<U256>, new_gas_price: Option<U256>,
new_condition: Option<Option<TransactionCondition>>, new_condition: Option<Option<TransactionCondition>>,
pwd: &str pwd: &str
) -> BoxFuture<Result<U256, RpcError>, Canceled> ) -> BoxFuture<Result<U256, RpcError>, Canceled> {
{
self.rpc.request("signer_confirmRequest", vec![ self.rpc.request("signer_confirmRequest", vec![
Self::to_value(&format!("{:#x}", id)), Self::to_value(&format!("{:#x}", id)),
Self::to_value(&TransactionModification { sender: None, gas_price: new_gas_price, gas: new_gas, condition: new_condition }), Self::to_value(&TransactionModification { sender: None, gas_price: new_gas_price, gas: new_gas, condition: new_condition }),
Self::to_value(&pwd), Self::to_value(&pwd),
]) ])
} }
pub fn reject_request(&mut self, id: U256) ->
BoxFuture<Result<bool, RpcError>, Canceled> pub fn reject_request(&mut self, id: U256) -> BoxFuture<Result<bool, RpcError>, Canceled> {
{
self.rpc.request("signer_rejectRequest", vec![ self.rpc.request("signer_rejectRequest", vec![
JsonValue::String(format!("{:#x}", id)) JsonValue::String(format!("{:#x}", id))
]) ])

View File

@ -7,7 +7,8 @@ export TARGETS="
-p ethcore-bigint\ -p ethcore-bigint\
-p parity-dapps \ -p parity-dapps \
-p parity-rpc \ -p parity-rpc \
-p ethcore-signer \ -p parity-rpc-client \
-p rpc-cli \
-p ethcore-util \ -p ethcore-util \
-p ethcore-network \ -p ethcore-network \
-p ethcore-io \ -p ethcore-io \

View File

@ -1,32 +0,0 @@
[package]
description = "Ethcore Trusted Signer"
homepage = "http://parity.io"
license = "GPL-3.0"
name = "ethcore-signer"
version = "1.7.0"
authors = ["Parity Technologies <admin@parity.io>"]
build = "build.rs"
[build-dependencies]
rustc_version = "0.1"
[dependencies]
rand = "0.3.14"
jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc.git", branch = "parity-1.7" }
jsonrpc-server-utils = { git = "https://github.com/paritytech/jsonrpc.git", branch = "parity-1.7" }
log = "0.3"
env_logger = "0.4"
ws = { git = "https://github.com/paritytech/ws-rs.git", branch = "parity-1.7" }
parity-dapps-glue = { version = "1.7", optional = true }
ethcore-util = { path = "../util" }
ethcore-io = { path = "../util/io" }
parity-rpc = { path = "../rpc" }
ethcore-devtools = { path = "../devtools" }
parity-ui = { path = "../dapps/ui", version = "1.4", optional = true }
clippy = { version = "0.0.103", optional = true}
[features]
dev = ["clippy"]
ui = ["parity-dapps-glue", "parity-ui", "parity-ui/no-precompiled-js"]
ui-precompiled = ["parity-dapps-glue", "parity-ui", "parity-ui/use-precompiled-js"]

View File

@ -1,73 +0,0 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
#![warn(missing_docs)]
#![cfg_attr(all(nightly, feature="dev"), feature(plugin))]
#![cfg_attr(all(nightly, feature="dev"), plugin(clippy))]
//! Signer module
//!
//! This module manages your private keys and accounts/identities
//! that can be used within Dapps.
//!
//! It exposes API (over `WebSockets`) accessed by Signer UIs.
//! Each transaction sent by Dapp is broadcasted to Signer UIs
//! and their responsibility is to confirm (or confirm and sign)
//! the transaction for you.
//!
//! ```
//! extern crate jsonrpc_core;
//! extern crate jsonrpc_server_utils;
//! extern crate ethcore_signer;
//! extern crate parity_rpc;
//!
//! use std::sync::Arc;
//! use jsonrpc_core::IoHandler;
//! use jsonrpc_server_utils::reactor::RpcEventLoop;
//! use ethcore_signer::ServerBuilder;
//! use parity_rpc::ConfirmationsQueue;
//!
//! fn main() {
//! let queue = Arc::new(ConfirmationsQueue::default());
//! let io = IoHandler::default();
//! let event_loop = RpcEventLoop::spawn().unwrap();
//! let remote = event_loop.remote();
//! let _server = ServerBuilder::new(queue, "/tmp/authcodes".into())
//! .start("127.0.0.1:8084".parse().unwrap(), io, remote);
//! }
//! ```
#[macro_use]
extern crate log;
extern crate env_logger;
extern crate rand;
extern crate ethcore_io as io;
extern crate ethcore_util as util;
extern crate jsonrpc_core;
extern crate jsonrpc_server_utils;
extern crate parity_rpc as rpc;
extern crate ws;
extern crate ethcore_devtools as devtools;
mod authcode_store;
mod ws_server;
/// Exported tests for use in signer RPC client testing
pub mod tests;
pub use authcode_store::*;
pub use ws_server::*;

View File

@ -1,21 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
{meta}
<title>{title}</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div class="parity-navbar"></div>
<div class="parity-box">
<h1>{title}</h1>
<h3>{message}</h3>
<p><code>{details}</code></p>
</div>
<div class="parity-status">
<small>{version}</small>
</div>
</body>
</html>

View File

@ -1,219 +0,0 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! `WebSockets` server.
use ws;
use std::default::Default;
use std::net::SocketAddr;
use std::ops::Drop;
use std::path::PathBuf;
use std::sync::Arc;
use std::thread;
use std;
use io::{PanicHandler, OnPanicListener, MayPanic};
use jsonrpc_core::{Metadata, Middleware, MetaIoHandler};
use jsonrpc_server_utils::tokio_core::reactor::Remote;
use rpc::{ConfirmationsQueue};
use rpc::informant::RpcStats;
mod session;
pub use self::session::MetaExtractor;
/// Signer startup error
#[derive(Debug)]
pub enum ServerError {
/// Wrapped `std::io::Error`
IoError(std::io::Error),
/// Other `ws-rs` error
WebSocket(ws::Error)
}
impl From<ws::Error> for ServerError {
fn from(err: ws::Error) -> Self {
match err.kind {
ws::ErrorKind::Io(e) => ServerError::IoError(e),
_ => ServerError::WebSocket(err),
}
}
}
/// Dummy metadata extractor
#[derive(Clone)]
pub struct NoopExtractor;
impl<M: Metadata> session::MetaExtractor<M> for NoopExtractor {}
/// Builder for `WebSockets` server
pub struct ServerBuilder {
queue: Arc<ConfirmationsQueue>,
authcodes_path: PathBuf,
skip_origin_validation: bool,
stats: Option<Arc<RpcStats>>,
}
impl ServerBuilder {
/// Creates new `ServerBuilder`
pub fn new(queue: Arc<ConfirmationsQueue>, authcodes_path: PathBuf) -> Self {
ServerBuilder {
queue: queue,
authcodes_path: authcodes_path,
skip_origin_validation: false,
stats: None,
}
}
/// If set to `true` server will not verify Origin of incoming requests.
/// Not recommended. Use only for development.
pub fn skip_origin_validation(mut self, skip: bool) -> Self {
self.skip_origin_validation = skip;
self
}
/// Configure statistic collection
pub fn stats(mut self, stats: Arc<RpcStats>) -> Self {
self.stats = Some(stats);
self
}
/// Starts a new `WebSocket` server in separate thread.
/// Returns a `Server` handle which closes the server when droped.
pub fn start<M: Metadata, S: Middleware<M>, H: Into<MetaIoHandler<M, S>>>(
self,
addr: SocketAddr,
handler: H,
remote: Remote,
) -> Result<Server, ServerError> {
self.start_with_extractor(addr, handler, remote, NoopExtractor)
}
/// Starts a new `WebSocket` server in separate thread.
/// Returns a `Server` handle which closes the server when droped.
pub fn start_with_extractor<M: Metadata, S: Middleware<M>, H: Into<MetaIoHandler<M, S>>, T: session::MetaExtractor<M>>(
self,
addr: SocketAddr,
handler: H,
remote: Remote,
meta_extractor: T,
) -> Result<Server, ServerError> {
Server::start(
addr,
handler.into(),
remote,
self.queue,
self.authcodes_path,
self.skip_origin_validation,
self.stats,
meta_extractor,
)
}
}
/// `WebSockets` server implementation.
pub struct Server {
handle: Option<thread::JoinHandle<()>>,
broadcaster: ws::Sender,
queue: Arc<ConfirmationsQueue>,
panic_handler: Arc<PanicHandler>,
addr: SocketAddr,
}
impl Server {
/// Returns the address this server is listening on
pub fn addr(&self) -> &SocketAddr {
&self.addr
}
/// Starts a new `WebSocket` server in separate thread.
/// Returns a `Server` handle which closes the server when droped.
fn start<M: Metadata, S: Middleware<M>, T: session::MetaExtractor<M>>(
addr: SocketAddr,
handler: MetaIoHandler<M, S>,
remote: Remote,
queue: Arc<ConfirmationsQueue>,
authcodes_path: PathBuf,
skip_origin_validation: bool,
stats: Option<Arc<RpcStats>>,
meta_extractor: T,
) -> Result<Server, ServerError> {
let config = {
let mut config = ws::Settings::default();
// accept only handshakes beginning with GET
config.method_strict = true;
// Was shutting down server when suspending on linux:
config.shutdown_on_interrupt = false;
config
};
// Create WebSocket
let origin = format!("{}", addr);
let port = addr.port();
let ws = ws::Builder::new().with_settings(config).build(
session::Factory::new(handler, remote, origin, port, authcodes_path, skip_origin_validation, stats, meta_extractor)
)?;
let panic_handler = PanicHandler::new_in_arc();
let ph = panic_handler.clone();
let broadcaster = ws.broadcaster();
// Spawn a thread with event loop
let handle = thread::spawn(move || {
ph.catch_panic(move || {
match ws.listen(addr).map_err(ServerError::from) {
Err(ServerError::IoError(io)) => die(format!(
"Signer: Could not start listening on specified address. Make sure that no other instance is running on Signer's port. Details: {:?}",
io
)),
Err(any_error) => die(format!(
"Signer: Unknown error occurred when starting Signer. Details: {:?}",
any_error
)),
Ok(server) => server,
}
}).unwrap();
});
// Return a handle
Ok(Server {
handle: Some(handle),
broadcaster: broadcaster,
queue: queue,
panic_handler: panic_handler,
addr: addr,
})
}
}
impl MayPanic for Server {
fn on_panic<F>(&self, closure: F) where F: OnPanicListener {
self.panic_handler.on_panic(closure);
}
}
impl Drop for Server {
fn drop(&mut self) {
self.queue.finish();
self.broadcaster.shutdown().unwrap();
self.handle.take().unwrap().join().unwrap();
}
}
fn die(msg: String) -> ! {
println!("ERROR: {}", msg);
std::process::exit(1);
}

View File

@ -1,333 +0,0 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
//! Session handlers factory.
use std::path::{PathBuf, Path};
use std::sync::Arc;
use std::str::FromStr;
use authcode_store::AuthCodes;
use jsonrpc_core::{Metadata, Middleware, MetaIoHandler};
use jsonrpc_core::futures::Future;
use jsonrpc_server_utils::tokio_core::reactor::Remote;
use rpc::informant::RpcStats;
use util::{H256, version};
use ws;
#[cfg(feature = "parity-ui")]
mod ui {
extern crate parity_ui as ui;
extern crate parity_dapps_glue as dapps;
use self::dapps::WebApp;
#[derive(Default)]
pub struct Handler {
ui: ui::App,
}
impl Handler {
pub fn handle(&self, req: &str) -> Option<&dapps::File> {
let file = match req {
"" | "/" => "index.html",
path => &path[1..],
};
self.ui.file(file)
}
}
}
#[cfg(not(feature = "parity-ui"))]
mod ui {
pub struct File {
pub content: &'static [u8],
pub content_type: &'static str,
}
#[derive(Default)]
pub struct Handler;
impl Handler {
pub fn handle(&self, _req: &str) -> Option<&File> {
None
}
}
}
const HOME_DOMAIN: &'static str = "parity.web3.site";
fn origin_is_allowed(self_origin: &str, header: Option<&[u8]>) -> bool {
match header.map(|h| String::from_utf8_lossy(h).into_owned()) {
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(&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,
}
}
fn auth_token_hash(codes_path: &Path, protocols: ws::Result<Vec<&str>>) -> Option<H256> {
match protocols {
Ok(ref protocols) if protocols.len() == 1 => {
let protocol = protocols[0];
let mut split = protocol.split('_');
let auth = split.next().and_then(|v| H256::from_str(v).ok());
let time = split.next().and_then(|v| u64::from_str_radix(v, 10).ok());
if let (Some(auth), Some(time)) = (auth, time) {
// Check if the code is valid
AuthCodes::from_file(codes_path)
.ok()
.and_then(|mut codes| {
// remove old tokens
codes.clear_garbage();
let res = codes.is_valid(&auth, time);
// make sure to save back authcodes - it might have been modified
if codes.to_file(codes_path).is_err() {
warn!(target: "signer", "Couldn't save authorization codes to file.");
}
if res {
Some(auth)
} else {
None
}
})
} else {
None
}
},
_ => None,
}
}
fn add_headers(mut response: ws::Response, mime: &str) -> ws::Response {
let content_len = format!("{}", response.len());
{
let mut headers = response.headers_mut();
headers.push(("X-Frame-Options".into(), b"SAMEORIGIN".to_vec()));
headers.push(("X-XSS-Protection".into(), b"1; mode=block".to_vec()));
headers.push(("X-Content-Type-Options".into(), b"nosniff".to_vec()));
headers.push(("Server".into(), b"Parity/SignerUI".to_vec()));
headers.push(("Content-Length".into(), content_len.as_bytes().to_vec()));
headers.push(("Content-Type".into(), mime.as_bytes().to_vec()));
headers.push(("Connection".into(), b"close".to_vec()));
}
response
}
/// Metadata extractor from session data.
pub trait MetaExtractor<M: Metadata>: Send + Clone + 'static {
/// Extract metadata for given session
fn extract_metadata(&self, _session_id: &H256) -> M {
Default::default()
}
}
pub struct Session<M: Metadata, S: Middleware<M>, T> {
session_id: H256,
out: ws::Sender,
skip_origin_validation: bool,
self_origin: String,
self_port: u16,
authcodes_path: PathBuf,
handler: Arc<MetaIoHandler<M, S>>,
remote: Remote,
file_handler: Arc<ui::Handler>,
stats: Option<Arc<RpcStats>>,
meta_extractor: T,
}
impl<M: Metadata, S: Middleware<M>, T> Drop for Session<M, S, T> {
fn drop(&mut self) {
self.stats.as_ref().map(|stats| stats.close_session());
}
}
impl<M: Metadata, S: Middleware<M>, T: MetaExtractor<M>> ws::Handler for Session<M, S, T> {
fn on_request(&mut self, req: &ws::Request) -> ws::Result<(ws::Response)> {
trace!(target: "signer", "Handling request: {:?}", req);
// 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, self.self_port));
let resource = resource.trim_left_matches(&format!("http://{}", HOME_DOMAIN));
// Styles file is allowed for error pages to display nicely.
let is_styles_file = resource == "/styles.css";
// Check request origin and host header.
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 = is_styles_file || is_valid;
if !is_valid {
warn!(target: "signer", "Blocked connection to Signer API from untrusted origin.");
return Ok(error(
ErrorType::Forbidden,
"URL Blocked",
"You are not allowed to access Trusted Signer using this URL.",
Some(&format!("Use: http://{}", self.self_origin)),
));
}
}
// 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
// (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 {
// Check authorization
let auth_token_hash = auth_token_hash(&self.authcodes_path, req.protocols());
match auth_token_hash {
None => {
info!(target: "signer", "Unauthorized connection to Signer API blocked.");
return Ok(error(ErrorType::Forbidden, "Not Authorized", "Request to this API was not authorized.", None));
},
Some(auth) => {
self.session_id = auth;
},
}
let protocols = req.protocols().expect("Existence checked by authorization.");
let protocol = protocols.get(0).expect("Proved by authorization.");
return ws::Response::from_request(req).map(|mut res| {
// To make WebSockets connection successful we need to send back the protocol header.
res.set_protocol(protocol);
res
});
}
debug!(target: "signer", "Requesting resource: {:?}", resource);
// Otherwise try to serve a page.
Ok(self.file_handler.handle(resource)
.map_or_else(
// return 404 not found
|| error(ErrorType::NotFound, "Not found", "Requested file was not found.", None),
// or serve the file
|f| add_headers(ws::Response::ok_raw(f.content.to_vec()), f.content_type)
))
}
fn on_message(&mut self, msg: ws::Message) -> ws::Result<()> {
let req = msg.as_text()?;
let out = self.out.clone();
// TODO [ToDr] Move to on_connect
let metadata = self.meta_extractor.extract_metadata(&self.session_id);
let future = self.handler.handle_request(req, metadata).map(move |response| {
if let Some(result) = response {
let res = out.send(result);
if let Err(e) = res {
warn!(target: "signer", "Error while sending response: {:?}", e);
}
}
});
self.remote.spawn(move |_| future);
Ok(())
}
}
pub struct Factory<M: Metadata, S: Middleware<M>, T> {
handler: Arc<MetaIoHandler<M, S>>,
remote: Remote,
skip_origin_validation: bool,
self_origin: String,
self_port: u16,
authcodes_path: PathBuf,
meta_extractor: T,
file_handler: Arc<ui::Handler>,
stats: Option<Arc<RpcStats>>,
}
impl<M: Metadata, S: Middleware<M>, T> Factory<M, S, T> {
pub fn new(
handler: MetaIoHandler<M, S>,
remote: Remote,
self_origin: String,
self_port: u16,
authcodes_path: PathBuf,
skip_origin_validation: bool,
stats: Option<Arc<RpcStats>>,
meta_extractor: T,
) -> Self {
Factory {
handler: Arc::new(handler),
remote: remote,
skip_origin_validation: skip_origin_validation,
self_origin: self_origin,
self_port: self_port,
authcodes_path: authcodes_path,
meta_extractor: meta_extractor,
file_handler: Arc::new(ui::Handler::default()),
stats: stats,
}
}
}
impl<M: Metadata, S: Middleware<M>, T: MetaExtractor<M>> ws::Factory for Factory<M, S, T> {
type Handler = Session<M, S, T>;
fn connection_made(&mut self, sender: ws::Sender) -> Self::Handler {
self.stats.as_ref().map(|stats| stats.open_session());
Session {
session_id: 0.into(),
out: sender,
handler: self.handler.clone(),
remote: self.remote.clone(),
skip_origin_validation: self.skip_origin_validation,
self_origin: self.self_origin.clone(),
self_port: self.self_port,
authcodes_path: self.authcodes_path.clone(),
meta_extractor: self.meta_extractor.clone(),
file_handler: self.file_handler.clone(),
stats: self.stats.clone(),
}
}
}
enum ErrorType {
NotFound,
Forbidden,
}
fn error(error: ErrorType, title: &str, message: &str, details: Option<&str>) -> ws::Response {
let content = format!(
include_str!("./error_tpl.html"),
title=title,
meta="",
message=message,
details=details.unwrap_or(""),
version=version(),
);
let res = match error {
ErrorType::NotFound => ws::Response::not_found(content),
ErrorType::Forbidden => ws::Response::forbidden(content),
};
add_headers(res, "text/html")
}