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:
committed by
Arkadiy Paronyan
parent
7499efecf6
commit
cbcc369a2d
@@ -8,9 +8,13 @@ authors = ["Parity Technologies <admin@parity.io>"]
|
||||
[lib]
|
||||
|
||||
[dependencies]
|
||||
cid = "0.2"
|
||||
futures = "0.1"
|
||||
log = "0.3"
|
||||
multihash = "0.5"
|
||||
order-stat = "0.1"
|
||||
rand = "0.3"
|
||||
rust-crypto = "0.2"
|
||||
rustc-serialize = "0.3"
|
||||
semver = "0.6"
|
||||
serde = "0.9"
|
||||
@@ -19,10 +23,6 @@ serde_json = "0.9"
|
||||
time = "0.1"
|
||||
tokio-timer = "0.1"
|
||||
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-http-server = { git = "https://github.com/paritytech/jsonrpc.git", branch = "parity-1.7" }
|
||||
|
||||
@@ -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"
|
||||
@@ -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();
|
||||
}
|
||||
350
rpc/src/authcodes.rs
Normal file
350
rpc/src/authcodes.rs
Normal file
@@ -0,0 +1,350 @@
|
||||
// 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::io::{self, Read, Write};
|
||||
use std::path::Path;
|
||||
use std::{fs, time, mem};
|
||||
|
||||
use rand::Rng;
|
||||
use rand::os::OsRng;
|
||||
use util::{H256, Hashable, Itertools};
|
||||
|
||||
/// Providing current time in seconds
|
||||
pub trait TimeProvider {
|
||||
/// Returns timestamp (in seconds since epoch)
|
||||
fn now(&self) -> u64;
|
||||
}
|
||||
|
||||
impl<F : Fn() -> u64> TimeProvider for F {
|
||||
fn now(&self) -> u64 {
|
||||
self()
|
||||
}
|
||||
}
|
||||
|
||||
/// Default implementation of `TimeProvider` using system time.
|
||||
#[derive(Default)]
|
||||
pub struct DefaultTimeProvider;
|
||||
|
||||
impl TimeProvider for DefaultTimeProvider {
|
||||
fn now(&self) -> u64 {
|
||||
time::UNIX_EPOCH.elapsed().expect("Valid time has to be set in your system.").as_secs()
|
||||
}
|
||||
}
|
||||
|
||||
/// No of seconds the hash is valid
|
||||
const TIME_THRESHOLD: u64 = 7;
|
||||
/// minimal length of hash
|
||||
const TOKEN_LENGTH: usize = 16;
|
||||
/// special "initial" token used for authorization when there are no tokens yet.
|
||||
const INITIAL_TOKEN: &'static str = "initial";
|
||||
/// Separator between fields in serialized tokens file.
|
||||
const SEPARATOR: &'static str = ";";
|
||||
/// Number of seconds to keep unused tokens.
|
||||
const UNUSED_TOKEN_TIMEOUT: u64 = 3600 * 24; // a day
|
||||
|
||||
struct Code {
|
||||
code: String,
|
||||
/// Duration since unix_epoch
|
||||
created_at: time::Duration,
|
||||
/// Duration since unix_epoch
|
||||
last_used_at: Option<time::Duration>,
|
||||
}
|
||||
|
||||
fn decode_time(val: &str) -> Option<time::Duration> {
|
||||
let time = val.parse::<u64>().ok();
|
||||
time.map(time::Duration::from_secs)
|
||||
}
|
||||
|
||||
fn encode_time(time: time::Duration) -> String {
|
||||
format!("{}", time.as_secs())
|
||||
}
|
||||
|
||||
/// Manages authorization codes for `SignerUIs`
|
||||
pub struct AuthCodes<T: TimeProvider = DefaultTimeProvider> {
|
||||
codes: Vec<Code>,
|
||||
now: T,
|
||||
}
|
||||
|
||||
impl AuthCodes<DefaultTimeProvider> {
|
||||
|
||||
/// Reads `AuthCodes` from file and creates new instance using `DefaultTimeProvider`.
|
||||
#[cfg_attr(feature="dev", allow(single_char_pattern))]
|
||||
pub fn from_file(file: &Path) -> io::Result<AuthCodes> {
|
||||
let content = {
|
||||
if let Ok(mut file) = fs::File::open(file) {
|
||||
let mut s = String::new();
|
||||
let _ = file.read_to_string(&mut s)?;
|
||||
s
|
||||
} else {
|
||||
"".into()
|
||||
}
|
||||
};
|
||||
let time_provider = DefaultTimeProvider::default();
|
||||
|
||||
let codes = content.lines()
|
||||
.filter_map(|line| {
|
||||
let mut parts = line.split(SEPARATOR);
|
||||
let token = parts.next();
|
||||
let created = parts.next();
|
||||
let used = parts.next();
|
||||
|
||||
match token {
|
||||
None => None,
|
||||
Some(token) if token.len() < TOKEN_LENGTH => None,
|
||||
Some(token) => {
|
||||
Some(Code {
|
||||
code: token.into(),
|
||||
last_used_at: used.and_then(decode_time),
|
||||
created_at: created.and_then(decode_time)
|
||||
.unwrap_or_else(|| time::Duration::from_secs(time_provider.now())),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(AuthCodes {
|
||||
codes: codes,
|
||||
now: time_provider,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<T: TimeProvider> AuthCodes<T> {
|
||||
|
||||
/// Writes all `AuthCodes` to a disk.
|
||||
pub fn to_file(&self, file: &Path) -> io::Result<()> {
|
||||
let mut file = fs::File::create(file)?;
|
||||
let content = self.codes.iter().map(|code| {
|
||||
let mut data = vec![code.code.clone(), encode_time(code.created_at.clone())];
|
||||
if let Some(used_at) = code.last_used_at {
|
||||
data.push(encode_time(used_at));
|
||||
}
|
||||
data.join(SEPARATOR)
|
||||
}).join("\n");
|
||||
file.write_all(content.as_bytes())
|
||||
}
|
||||
|
||||
/// Creates a new `AuthCodes` store with given `TimeProvider`.
|
||||
pub fn new(codes: Vec<String>, now: T) -> Self {
|
||||
AuthCodes {
|
||||
codes: codes.into_iter().map(|code| Code {
|
||||
code: code,
|
||||
created_at: time::Duration::from_secs(now.now()),
|
||||
last_used_at: None,
|
||||
}).collect(),
|
||||
now: now,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if given hash is correct authcode of `SignerUI`
|
||||
/// Updates this hash last used field in case it's valid.
|
||||
#[cfg_attr(feature="dev", allow(wrong_self_convention))]
|
||||
pub fn is_valid(&mut self, hash: &H256, time: u64) -> bool {
|
||||
let now = self.now.now();
|
||||
// check time
|
||||
if time >= now + TIME_THRESHOLD || time <= now - TIME_THRESHOLD {
|
||||
warn!(target: "signer", "Received old authentication request. ({} vs {})", now, time);
|
||||
return false;
|
||||
}
|
||||
|
||||
let as_token = |code| format!("{}:{}", code, time).sha3();
|
||||
|
||||
// Check if it's the initial token.
|
||||
if self.is_empty() {
|
||||
let initial = &as_token(INITIAL_TOKEN) == hash;
|
||||
// Initial token can be used only once.
|
||||
if initial {
|
||||
let _ = self.generate_new();
|
||||
}
|
||||
return initial;
|
||||
}
|
||||
|
||||
// look for code
|
||||
for mut code in &mut self.codes {
|
||||
if &as_token(&code.code) == hash {
|
||||
code.last_used_at = Some(time::Duration::from_secs(now));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Generates and returns a new code that can be used by `SignerUIs`
|
||||
pub fn generate_new(&mut self) -> io::Result<String> {
|
||||
let mut rng = OsRng::new()?;
|
||||
let code = rng.gen_ascii_chars().take(TOKEN_LENGTH).collect::<String>();
|
||||
let readable_code = code.as_bytes()
|
||||
.chunks(4)
|
||||
.filter_map(|f| String::from_utf8(f.to_vec()).ok())
|
||||
.collect::<Vec<String>>()
|
||||
.join("-");
|
||||
trace!(target: "signer", "New authentication token generated.");
|
||||
self.codes.push(Code {
|
||||
code: code,
|
||||
created_at: time::Duration::from_secs(self.now.now()),
|
||||
last_used_at: None,
|
||||
});
|
||||
Ok(readable_code)
|
||||
}
|
||||
|
||||
/// Returns true if there are no tokens in this store
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.codes.is_empty()
|
||||
}
|
||||
|
||||
/// Removes old tokens that have not been used since creation.
|
||||
pub fn clear_garbage(&mut self) {
|
||||
let now = self.now.now();
|
||||
let threshold = time::Duration::from_secs(now.saturating_sub(UNUSED_TOKEN_TIMEOUT));
|
||||
|
||||
let codes = mem::replace(&mut self.codes, Vec::new());
|
||||
for code in codes {
|
||||
// Skip codes that are old and were never used.
|
||||
if code.last_used_at.is_none() && code.created_at <= threshold {
|
||||
continue;
|
||||
}
|
||||
self.codes.push(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use devtools;
|
||||
use std::io::{Read, Write};
|
||||
use std::{time, fs};
|
||||
use std::cell::Cell;
|
||||
|
||||
use util::{H256, Hashable};
|
||||
use super::*;
|
||||
|
||||
fn generate_hash(val: &str, time: u64) -> H256 {
|
||||
format!("{}:{}", val, time).sha3()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_true_if_code_is_initial_and_store_is_empty() {
|
||||
// given
|
||||
let code = "initial";
|
||||
let time = 99;
|
||||
let mut codes = AuthCodes::new(vec![], || 100);
|
||||
|
||||
// when
|
||||
let res1 = codes.is_valid(&generate_hash(code, time), time);
|
||||
let res2 = codes.is_valid(&generate_hash(code, time), time);
|
||||
|
||||
// then
|
||||
assert_eq!(res1, true);
|
||||
assert_eq!(res2, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_true_if_hash_is_valid() {
|
||||
// given
|
||||
let code = "23521352asdfasdfadf";
|
||||
let time = 99;
|
||||
let mut codes = AuthCodes::new(vec![code.into()], || 100);
|
||||
|
||||
// when
|
||||
let res = codes.is_valid(&generate_hash(code, time), time);
|
||||
|
||||
// then
|
||||
assert_eq!(res, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_false_if_code_is_unknown() {
|
||||
// given
|
||||
let code = "23521352asdfasdfadf";
|
||||
let time = 99;
|
||||
let mut codes = AuthCodes::new(vec!["1".into()], || 100);
|
||||
|
||||
// when
|
||||
let res = codes.is_valid(&generate_hash(code, time), time);
|
||||
|
||||
// then
|
||||
assert_eq!(res, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_false_if_hash_is_valid_but_time_is_invalid() {
|
||||
// given
|
||||
let code = "23521352asdfasdfadf";
|
||||
let time = 107;
|
||||
let time2 = 93;
|
||||
let mut codes = AuthCodes::new(vec![code.into()], || 100);
|
||||
|
||||
// when
|
||||
let res1 = codes.is_valid(&generate_hash(code, time), time);
|
||||
let res2 = codes.is_valid(&generate_hash(code, time2), time2);
|
||||
|
||||
// then
|
||||
assert_eq!(res1, false);
|
||||
assert_eq!(res2, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_read_old_format_from_file() {
|
||||
// given
|
||||
let path = devtools::RandomTempPath::new();
|
||||
let code = "23521352asdfasdfadf";
|
||||
{
|
||||
let mut file = fs::File::create(&path).unwrap();
|
||||
file.write_all(b"a\n23521352asdfasdfadf\nb\n").unwrap();
|
||||
}
|
||||
|
||||
// when
|
||||
let mut authcodes = AuthCodes::from_file(&path).unwrap();
|
||||
let time = time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
|
||||
// then
|
||||
assert!(authcodes.is_valid(&generate_hash(code, time), time), "Code should be read from file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_remove_old_unused_tokens() {
|
||||
// given
|
||||
let path = devtools::RandomTempPath::new();
|
||||
let code1 = "11111111asdfasdf111";
|
||||
let code2 = "22222222asdfasdf222";
|
||||
let code3 = "33333333asdfasdf333";
|
||||
|
||||
let time = Cell::new(100);
|
||||
let mut codes = AuthCodes::new(vec![code1.into(), code2.into(), code3.into()], || time.get());
|
||||
// `code2` should not be removed (we never remove tokens that were used)
|
||||
codes.is_valid(&generate_hash(code2, time.get()), time.get());
|
||||
|
||||
// when
|
||||
time.set(100 + 10_000_000);
|
||||
// mark `code1` as used now
|
||||
codes.is_valid(&generate_hash(code1, time.get()), time.get());
|
||||
|
||||
let new_code = codes.generate_new().unwrap().replace('-', "");
|
||||
codes.clear_garbage();
|
||||
codes.to_file(&path).unwrap();
|
||||
|
||||
// then
|
||||
let mut content = String::new();
|
||||
let mut file = fs::File::open(&path).unwrap();
|
||||
file.read_to_string(&mut content).unwrap();
|
||||
|
||||
assert_eq!(content, format!("{};100;10000100\n{};100;100\n{};10000100", code1, code2, new_code));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,11 +14,20 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Transport-specific metadata extractors.
|
||||
|
||||
use jsonrpc_core;
|
||||
use http;
|
||||
use hyper;
|
||||
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> {
|
||||
extractor: T,
|
||||
@@ -37,13 +46,14 @@ impl<M, T> http::MetaExtractor<M> for HyperMetaExtractor<T> where
|
||||
M: jsonrpc_core::Metadata,
|
||||
{
|
||||
fn read_metadata(&self, req: &hyper::server::Request<hyper::net::HttpStream>) -> M {
|
||||
let origin = req.headers().get::<hyper::header::Origin>()
|
||||
.map(|origin| format!("{}://{}", origin.scheme, origin.host))
|
||||
.unwrap_or_else(|| "unknown".into());
|
||||
let dapps_origin = req.headers().get_raw("x-parity-origin")
|
||||
let as_string = |header: Option<&http::request_response::header::Raw>| header
|
||||
.and_then(|raw| raw.one())
|
||||
.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,
|
||||
{
|
||||
fn read_metadata(&self, req: &minihttp::Req) -> M {
|
||||
let origin = req.header("origin")
|
||||
.unwrap_or_else(|| "unknown")
|
||||
.to_owned();
|
||||
let origin = req.header("origin").map(|h| h.to_owned());
|
||||
let user_agent = req.header("user-agent").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)
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,18 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Ethcore rpc.
|
||||
#![warn(missing_docs)]
|
||||
#![cfg_attr(feature="nightly", feature(plugin))]
|
||||
#![cfg_attr(feature="nightly", plugin(clippy))]
|
||||
//! Parity RPC.
|
||||
|
||||
#![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 multihash;
|
||||
extern crate order_stat;
|
||||
extern crate rand;
|
||||
extern crate rustc_serialize;
|
||||
extern crate semver;
|
||||
extern crate serde;
|
||||
@@ -28,10 +33,6 @@ extern crate serde_json;
|
||||
extern crate time;
|
||||
extern crate tokio_timer;
|
||||
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_http_server as http;
|
||||
@@ -41,6 +42,7 @@ extern crate jsonrpc_pubsub;
|
||||
|
||||
extern crate ethash;
|
||||
extern crate ethcore;
|
||||
extern crate ethcore_devtools as devtools;
|
||||
extern crate ethcore_io as io;
|
||||
extern crate ethcore_ipc;
|
||||
extern crate ethcore_light as light;
|
||||
@@ -66,8 +68,6 @@ extern crate serde_derive;
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate ethjson;
|
||||
#[cfg(test)]
|
||||
extern crate ethcore_devtools as devtools;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
@@ -75,9 +75,12 @@ extern crate pretty_assertions;
|
||||
|
||||
pub extern crate jsonrpc_ws_server as ws;
|
||||
|
||||
mod metadata;
|
||||
mod authcodes;
|
||||
mod http_common;
|
||||
pub mod v1;
|
||||
|
||||
pub mod tests;
|
||||
|
||||
pub use jsonrpc_pubsub::Session as PubSubSession;
|
||||
pub use ipc::{Server as IpcServer, MetaExtractor as IpcMetaExtractor, RequestContext as IpcRequestContext};
|
||||
pub use http::{
|
||||
@@ -86,8 +89,11 @@ pub use http::{
|
||||
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::extractors::{RpcExtractor, WsExtractor, WsStats, WsDispatcher};
|
||||
pub use authcodes::{AuthCodes, TimeProvider};
|
||||
pub use http_common::HttpMetaExtractor;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use http::tokio_core;
|
||||
@@ -100,6 +106,16 @@ pub enum HttpServer {
|
||||
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
|
||||
#[derive(Debug)]
|
||||
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.
|
||||
pub enum HttpSettings<R: RequestMiddleware> {
|
||||
/// 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) => {
|
||||
let mut builder = http::ServerBuilder::new(handler)
|
||||
.event_loop_remote(remote)
|
||||
.meta_extractor(metadata::HyperMetaExtractor::new(extractor))
|
||||
.meta_extractor(http_common::HyperMetaExtractor::new(extractor))
|
||||
.cors(cors_domains.into())
|
||||
.allowed_hosts(allowed_hosts.into());
|
||||
|
||||
@@ -177,7 +185,7 @@ pub fn start_http<M, S, H, T, R>(
|
||||
HttpSettings::Threads(threads) => {
|
||||
minihttp::ServerBuilder::new(handler)
|
||||
.threads(threads)
|
||||
.meta_extractor(metadata::MiniMetaExtractor::new(extractor))
|
||||
.meta_extractor(http_common::MiniMetaExtractor::new(extractor))
|
||||
.cors(cors_domains.into())
|
||||
.allowed_hosts(allowed_hosts.into())
|
||||
.start_http(addr)
|
||||
@@ -205,13 +213,14 @@ pub fn start_ipc<M, S, H, T>(
|
||||
}
|
||||
|
||||
/// 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,
|
||||
handler: H,
|
||||
remote: tokio_core::reactor::Remote,
|
||||
allowed_origins: ws::DomainsValidation<ws::Origin>,
|
||||
allowed_hosts: ws::DomainsValidation<ws::Host>,
|
||||
extractor: T,
|
||||
middleware: V,
|
||||
stats: U,
|
||||
) -> Result<ws::Server, ws::Error> where
|
||||
M: jsonrpc_core::Metadata,
|
||||
@@ -219,9 +228,11 @@ pub fn start_ws<M, S, H, T, U>(
|
||||
H: Into<jsonrpc_core::MetaIoHandler<M, S>>,
|
||||
T: ws::MetaExtractor<M>,
|
||||
U: ws::SessionStats,
|
||||
V: ws::RequestMiddleware,
|
||||
{
|
||||
ws::ServerBuilder::new(handler)
|
||||
.event_loop_remote(remote)
|
||||
.request_middleware(middleware)
|
||||
.allowed_origins(allowed_origins)
|
||||
.allowed_hosts(allowed_hosts)
|
||||
.session_meta_extractor(extractor)
|
||||
|
||||
84
rpc/src/tests/helpers.rs
Normal file
84
rpc/src/tests/helpers.rs
Normal 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
|
||||
}
|
||||
}
|
||||
21
rpc/src/tests/mod.rs
Normal file
21
rpc/src/tests/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
// 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/>.
|
||||
|
||||
//! RPC integration tests.
|
||||
|
||||
mod helpers;
|
||||
#[cfg(test)] mod rpc;
|
||||
pub mod ws;
|
||||
172
rpc/src/tests/rpc.rs
Normal file
172
rpc/src/tests/rpc.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
185
rpc/src/tests/ws.rs
Normal file
185
rpc/src/tests/ws.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
// 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 tests.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use devtools::http_client;
|
||||
use jsonrpc_core::MetaIoHandler;
|
||||
use rand;
|
||||
use ws;
|
||||
|
||||
use v1::{extractors, informant};
|
||||
use tests::helpers::{GuardedAuthCodes, Server};
|
||||
|
||||
/// Setup a mock signer for tests
|
||||
pub fn serve() -> (Server<ws::Server>, usize, GuardedAuthCodes) {
|
||||
let port = 35000 + rand::random::<usize>() % 10000;
|
||||
let address = format!("127.0.0.1:{}", port).parse().unwrap();
|
||||
let io = MetaIoHandler::default();
|
||||
let authcodes = GuardedAuthCodes::new();
|
||||
let stats = Arc::new(informant::RpcStats::default());
|
||||
|
||||
let res = Server::new(|remote| ::start_ws(
|
||||
&address,
|
||||
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
|
||||
pub fn request(server: Server<ws::Server>, request: &str) -> http_client::Response {
|
||||
http_client::request(server.server.addr(), request)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod testing {
|
||||
use std::time;
|
||||
use util::Hashable;
|
||||
use devtools::http_client;
|
||||
use super::{serve, request};
|
||||
|
||||
#[test]
|
||||
fn should_not_redirect_to_parity_host() {
|
||||
// given
|
||||
let (server, port, _) = serve();
|
||||
|
||||
// when
|
||||
let response = request(server,
|
||||
&format!("\
|
||||
GET / HTTP/1.1\r\n\
|
||||
Host: 127.0.0.1:{}\r\n\
|
||||
Connection: close\r\n\
|
||||
\r\n\
|
||||
{{}}
|
||||
", port)
|
||||
);
|
||||
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 200 Ok".to_owned());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_block_if_authorization_is_incorrect() {
|
||||
// given
|
||||
let (server, port, _) = serve();
|
||||
|
||||
// when
|
||||
let response = request(server,
|
||||
&format!("\
|
||||
GET / HTTP/1.1\r\n\
|
||||
Host: 127.0.0.1:{}\r\n\
|
||||
Connection: Upgrade\r\n\
|
||||
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n\
|
||||
Sec-WebSocket-Protocol: wrong\r\n\
|
||||
Sec-WebSocket-Version: 13\r\n\
|
||||
\r\n\
|
||||
{{}}
|
||||
", port)
|
||||
);
|
||||
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned());
|
||||
http_client::assert_security_headers_present(&response.headers, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_allow_if_authorization_is_correct() {
|
||||
// given
|
||||
let (server, port, mut authcodes) = serve();
|
||||
let code = authcodes.generate_new().unwrap().replace("-", "");
|
||||
authcodes.to_file(&authcodes.path).unwrap();
|
||||
let timestamp = time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
|
||||
// when
|
||||
let response = request(server,
|
||||
&format!("\
|
||||
GET / HTTP/1.1\r\n\
|
||||
Host: 127.0.0.1:{}\r\n\
|
||||
Connection: Close\r\n\
|
||||
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n\
|
||||
Sec-WebSocket-Protocol: {:?}_{}\r\n\
|
||||
Sec-WebSocket-Version: 13\r\n\
|
||||
\r\n\
|
||||
{{}}
|
||||
",
|
||||
port,
|
||||
format!("{}:{}", code, timestamp).sha3(),
|
||||
timestamp,
|
||||
)
|
||||
);
|
||||
|
||||
// then
|
||||
assert_eq!(response.status, "HTTP/1.1 101 Switching Protocols".to_owned());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_allow_initial_connection_but_only_once() {
|
||||
// given
|
||||
let (server, port, authcodes) = serve();
|
||||
let code = "initial";
|
||||
let timestamp = time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
assert!(authcodes.is_empty());
|
||||
|
||||
// when
|
||||
let response1 = http_client::request(server.addr(),
|
||||
&format!("\
|
||||
GET / HTTP/1.1\r\n\
|
||||
Host: 127.0.0.1:{}\r\n\
|
||||
Connection: Close\r\n\
|
||||
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n\
|
||||
Sec-WebSocket-Protocol:{:?}_{}\r\n\
|
||||
Sec-WebSocket-Version: 13\r\n\
|
||||
\r\n\
|
||||
{{}}
|
||||
",
|
||||
port,
|
||||
format!("{}:{}", code, timestamp).sha3(),
|
||||
timestamp,
|
||||
)
|
||||
);
|
||||
let response2 = http_client::request(server.addr(),
|
||||
&format!("\
|
||||
GET / HTTP/1.1\r\n\
|
||||
Host: 127.0.0.1:{}\r\n\
|
||||
Connection: Close\r\n\
|
||||
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n\
|
||||
Sec-WebSocket-Protocol:{:?}_{}\r\n\
|
||||
Sec-WebSocket-Version: 13\r\n\
|
||||
\r\n\
|
||||
{{}}
|
||||
",
|
||||
port,
|
||||
format!("{}:{}", code, timestamp).sha3(),
|
||||
timestamp,
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// then
|
||||
assert_eq!(response1.status, "HTTP/1.1 101 Switching Protocols".to_owned());
|
||||
assert_eq!(response2.status, "HTTP/1.1 403 Forbidden".to_owned());
|
||||
http_client::assert_security_headers_present(&response2.headers, None);
|
||||
}
|
||||
}
|
||||
263
rpc/src/v1/extractors.rs
Normal file
263
rpc/src/v1/extractors.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
33
rpc/src/v1/helpers/dapps.rs
Normal file
33
rpc/src/v1/helpers/dapps.rs
Normal 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)()
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
Error {
|
||||
code: ErrorCode::ServerError(codes::UNSUPPORTED_REQUEST),
|
||||
|
||||
@@ -19,10 +19,10 @@ pub mod errors;
|
||||
|
||||
pub mod accounts;
|
||||
pub mod block_import;
|
||||
pub mod dapps;
|
||||
pub mod dispatch;
|
||||
pub mod fake_sign;
|
||||
pub mod light_fetch;
|
||||
pub mod informant;
|
||||
pub mod oneshot;
|
||||
pub mod ipfs;
|
||||
pub mod secretstore;
|
||||
@@ -50,3 +50,7 @@ pub use self::signing_queue::{
|
||||
pub use self::signer::SignerService;
|
||||
pub use self::subscribers::Subscribers;
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -27,21 +27,21 @@ const TOKEN_LIFETIME_SECS: u32 = 3600;
|
||||
|
||||
/// Manages communication with Signer crate
|
||||
pub struct SignerService {
|
||||
is_enabled: bool,
|
||||
queue: Arc<ConfirmationsQueue>,
|
||||
web_proxy_tokens: Mutex<TransientHashMap<String, ()>>,
|
||||
generate_new_token: Box<Fn() -> Result<String, String> + Send + Sync + 'static>,
|
||||
address: Option<(String, u16)>,
|
||||
}
|
||||
|
||||
impl SignerService {
|
||||
/// 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 {
|
||||
SignerService {
|
||||
queue: Arc::new(ConfirmationsQueue::default()),
|
||||
web_proxy_tokens: Mutex::new(TransientHashMap::new(TOKEN_LIFETIME_SECS)),
|
||||
generate_new_token: Box::new(new_token),
|
||||
address: address,
|
||||
is_enabled: is_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,20 +69,15 @@ impl SignerService {
|
||||
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.
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.address.is_some()
|
||||
self.is_enabled
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Creates new Signer Service for tests.
|
||||
pub fn new_test(address: Option<(String, u16)>) -> Self {
|
||||
SignerService::new(|| Ok("new_token".into()), address)
|
||||
pub fn new_test(is_enabled: bool) -> Self {
|
||||
SignerService::new(|| Ok("new_token".into()), is_enabled)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ use light::client::LightChainClient;
|
||||
|
||||
use jsonrpc_core::Error;
|
||||
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::light_fetch::LightFetch;
|
||||
use v1::metadata::Metadata;
|
||||
@@ -54,8 +54,8 @@ pub struct ParityClient {
|
||||
logger: Arc<RotatingLogger>,
|
||||
settings: Arc<NetworkSettings>,
|
||||
signer: Option<Arc<SignerService>>,
|
||||
dapps_interface: Option<String>,
|
||||
dapps_port: Option<u16>,
|
||||
dapps_address: Option<(String, u16)>,
|
||||
ws_address: Option<(String, u16)>,
|
||||
eip86_transition: u64,
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ impl ParityClient {
|
||||
logger: Arc<RotatingLogger>,
|
||||
settings: Arc<NetworkSettings>,
|
||||
signer: Option<Arc<SignerService>>,
|
||||
dapps_interface: Option<String>,
|
||||
dapps_port: Option<u16>,
|
||||
dapps_address: Option<(String, u16)>,
|
||||
ws_address: Option<(String, u16)>,
|
||||
) -> Self {
|
||||
ParityClient {
|
||||
light_dispatch: light_dispatch,
|
||||
@@ -77,8 +77,8 @@ impl ParityClient {
|
||||
logger: logger,
|
||||
settings: settings,
|
||||
signer: signer,
|
||||
dapps_interface: dapps_interface,
|
||||
dapps_port: dapps_port,
|
||||
dapps_address: dapps_address,
|
||||
ws_address: ws_address,
|
||||
eip86_transition: client.eip86_transition(),
|
||||
}
|
||||
}
|
||||
@@ -294,22 +294,14 @@ impl Parity for ParityClient {
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn signer_port(&self) -> Result<u16, Error> {
|
||||
self.signer
|
||||
.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
|
||||
fn dapps_url(&self) -> Result<String, Error> {
|
||||
helpers::to_url(&self.dapps_address)
|
||||
.ok_or_else(|| errors::dapps_disabled())
|
||||
}
|
||||
|
||||
fn dapps_interface(&self) -> Result<String, Error> {
|
||||
self.dapps_interface.clone()
|
||||
.ok_or_else(|| errors::dapps_disabled())
|
||||
fn ws_url(&self) -> Result<String, Error> {
|
||||
helpers::to_url(&self.ws_address)
|
||||
.ok_or_else(|| errors::ws_disabled())
|
||||
}
|
||||
|
||||
fn next_nonce(&self, address: H160) -> BoxFuture<U256, Error> {
|
||||
|
||||
@@ -26,21 +26,24 @@ use futures::{BoxFuture, Future};
|
||||
use util::sha3;
|
||||
|
||||
use jsonrpc_core::Error;
|
||||
use v1::helpers::dapps::DappsService;
|
||||
use v1::helpers::errors;
|
||||
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.
|
||||
pub struct ParitySetClient<F> {
|
||||
net: Arc<ManageNetwork>,
|
||||
dapps: Option<Arc<DappsService>>,
|
||||
fetch: F,
|
||||
}
|
||||
|
||||
impl<F: Fetch> ParitySetClient<F> {
|
||||
/// 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 {
|
||||
net: net,
|
||||
dapps: dapps,
|
||||
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> {
|
||||
Err(errors::light_unimplemented(None))
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ use crypto::DEFAULT_MAC;
|
||||
|
||||
use jsonrpc_core::Error;
|
||||
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::metadata::Metadata;
|
||||
use v1::traits::Parity;
|
||||
@@ -67,8 +67,8 @@ pub struct ParityClient<C, M, S: ?Sized, U> where
|
||||
logger: Arc<RotatingLogger>,
|
||||
settings: Arc<NetworkSettings>,
|
||||
signer: Option<Arc<SignerService>>,
|
||||
dapps_interface: Option<String>,
|
||||
dapps_port: Option<u16>,
|
||||
dapps_address: Option<(String, u16)>,
|
||||
ws_address: Option<(String, u16)>,
|
||||
eip86_transition: u64,
|
||||
}
|
||||
|
||||
@@ -89,8 +89,8 @@ impl<C, M, S: ?Sized, U> ParityClient<C, M, S, U> where
|
||||
logger: Arc<RotatingLogger>,
|
||||
settings: Arc<NetworkSettings>,
|
||||
signer: Option<Arc<SignerService>>,
|
||||
dapps_interface: Option<String>,
|
||||
dapps_port: Option<u16>,
|
||||
dapps_address: Option<(String, u16)>,
|
||||
ws_address: Option<(String, u16)>,
|
||||
) -> Self {
|
||||
ParityClient {
|
||||
client: Arc::downgrade(client),
|
||||
@@ -102,8 +102,8 @@ impl<C, M, S: ?Sized, U> ParityClient<C, M, S, U> where
|
||||
logger: logger,
|
||||
settings: settings,
|
||||
signer: signer,
|
||||
dapps_interface: dapps_interface,
|
||||
dapps_port: dapps_port,
|
||||
dapps_address: dapps_address,
|
||||
ws_address: ws_address,
|
||||
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> {
|
||||
self.signer
|
||||
.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
|
||||
fn dapps_url(&self) -> Result<String, Error> {
|
||||
helpers::to_url(&self.dapps_address)
|
||||
.ok_or_else(|| errors::dapps_disabled())
|
||||
}
|
||||
|
||||
fn dapps_interface(&self) -> Result<String, Error> {
|
||||
self.dapps_interface.clone()
|
||||
.ok_or_else(|| errors::dapps_disabled())
|
||||
fn ws_url(&self) -> Result<String, Error> {
|
||||
helpers::to_url(&self.ws_address)
|
||||
.ok_or_else(|| errors::ws_disabled())
|
||||
}
|
||||
|
||||
fn next_nonce(&self, address: H160) -> BoxFuture<U256, Error> {
|
||||
|
||||
@@ -28,9 +28,10 @@ use util::sha3;
|
||||
use updater::{Service as UpdateService};
|
||||
|
||||
use jsonrpc_core::Error;
|
||||
use v1::helpers::dapps::DappsService;
|
||||
use v1::helpers::errors;
|
||||
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.
|
||||
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>,
|
||||
updater: Weak<U>,
|
||||
net: Weak<ManageNetwork>,
|
||||
dapps: Option<Arc<DappsService>>,
|
||||
fetch: F,
|
||||
eip86_transition: u64,
|
||||
}
|
||||
@@ -46,12 +48,20 @@ impl<C, M, U, F> ParitySetClient<C, M, U, F>
|
||||
where C: MiningBlockChainClient + 'static,
|
||||
{
|
||||
/// 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 {
|
||||
client: Arc::downgrade(client),
|
||||
miner: Arc::downgrade(miner),
|
||||
updater: Arc::downgrade(updater),
|
||||
net: Arc::downgrade(net),
|
||||
dapps: dapps,
|
||||
fetch: fetch,
|
||||
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> {
|
||||
let updater = take_weak!(self.updater);
|
||||
Ok(updater.upgrade_ready().map(Into::into))
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Parity RPC requests Metadata.
|
||||
use std::sync::Arc;
|
||||
|
||||
use jsonrpc_core;
|
||||
@@ -35,7 +36,9 @@ impl Metadata {
|
||||
pub fn dapp_id(&self) -> DappId {
|
||||
// TODO [ToDr] Extract dapp info from Ws connections.
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,14 +52,30 @@ macro_rules! try_bf {
|
||||
#[macro_use]
|
||||
mod helpers;
|
||||
mod impls;
|
||||
mod metadata;
|
||||
mod types;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub mod extractors;
|
||||
pub mod informant;
|
||||
pub mod metadata;
|
||||
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::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::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;
|
||||
}
|
||||
|
||||
37
rpc/src/v1/tests/helpers/dapps.rs
Normal file
37
rpc/src/v1/tests/helpers/dapps.rs
Normal 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(),
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,16 @@
|
||||
|
||||
//! Test rpc services.
|
||||
|
||||
mod sync_provider;
|
||||
mod miner_service;
|
||||
mod dapps;
|
||||
mod fetch;
|
||||
mod miner_service;
|
||||
mod snapshot_service;
|
||||
mod sync_provider;
|
||||
mod update_service;
|
||||
|
||||
pub use self::sync_provider::{Config, TestSyncProvider};
|
||||
pub use self::miner_service::TestMinerService;
|
||||
pub use self::dapps::TestDappsService;
|
||||
pub use self::fetch::TestFetch;
|
||||
pub use self::miner_service::TestMinerService;
|
||||
pub use self::snapshot_service::TestSnapshotService;
|
||||
pub use self::update_service::TestUpdater;
|
||||
pub use self::sync_provider::{Config, TestSyncProvider};
|
||||
pub use self::update_service::TestUpdater;
|
||||
|
||||
@@ -41,8 +41,8 @@ pub struct Dependencies {
|
||||
pub settings: Arc<NetworkSettings>,
|
||||
pub network: Arc<ManageNetwork>,
|
||||
pub accounts: Arc<AccountProvider>,
|
||||
pub dapps_interface: Option<String>,
|
||||
pub dapps_port: Option<u16>,
|
||||
pub dapps_address: Option<(String, u16)>,
|
||||
pub ws_address: Option<(String, u16)>,
|
||||
}
|
||||
|
||||
impl Dependencies {
|
||||
@@ -66,8 +66,8 @@ impl Dependencies {
|
||||
}),
|
||||
network: Arc::new(TestManageNetwork),
|
||||
accounts: Arc::new(AccountProvider::transient_provider()),
|
||||
dapps_interface: Some("127.0.0.1".into()),
|
||||
dapps_port: Some(18080),
|
||||
dapps_address: Some(("127.0.0.1".into(), 18080)),
|
||||
ws_address: Some(("127.0.0.1".into(), 18546)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +84,8 @@ impl Dependencies {
|
||||
self.logger.clone(),
|
||||
self.settings.clone(),
|
||||
signer,
|
||||
self.dapps_interface.clone(),
|
||||
self.dapps_port,
|
||||
self.dapps_address.clone(),
|
||||
self.ws_address.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ fn rpc_parity_node_name() {
|
||||
#[test]
|
||||
fn rpc_parity_unsigned_transactions_count() {
|
||||
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 response = r#"{"jsonrpc":"2.0","result":0,"id":1}"#;
|
||||
@@ -386,16 +386,17 @@ fn rpc_parity_encrypt() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_parity_signer_port() {
|
||||
fn rpc_parity_ws_address() {
|
||||
// given
|
||||
let deps = Dependencies::new();
|
||||
let io1 = deps.with_signer(SignerService::new_test(Some(("127.0.0.1".into(), 18180))));
|
||||
let mut deps = Dependencies::new();
|
||||
let io1 = deps.default_client();
|
||||
deps.ws_address = None;
|
||||
let io2 = deps.default_client();
|
||||
|
||||
// when
|
||||
let request = r#"{"jsonrpc": "2.0", "method": "parity_signerPort", "params": [], "id": 1}"#;
|
||||
let response1 = r#"{"jsonrpc":"2.0","result":18180,"id":1}"#;
|
||||
let response2 = r#"{"jsonrpc":"2.0","error":{"code":-32000,"message":"Trusted Signer is disabled. This API is not available."},"id":1}"#;
|
||||
let request = r#"{"jsonrpc": "2.0", "method": "parity_wsUrl", "params": [], "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":"WebSockets Server is disabled. This API is not available."},"id":1}"#;
|
||||
|
||||
// then
|
||||
assert_eq!(io1.handle_request_sync(request), Some(response1.to_owned()));
|
||||
@@ -403,34 +404,16 @@ fn rpc_parity_signer_port() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rpc_parity_dapps_port() {
|
||||
fn rpc_parity_dapps_address() {
|
||||
// given
|
||||
let mut deps = Dependencies::new();
|
||||
let io1 = deps.default_client();
|
||||
deps.dapps_port = None;
|
||||
deps.dapps_address = None;
|
||||
let io2 = deps.default_client();
|
||||
|
||||
// when
|
||||
let request = r#"{"jsonrpc": "2.0", "method": "parity_dappsPort", "params": [], "id": 1}"#;
|
||||
let response1 = r#"{"jsonrpc":"2.0","result":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 request = r#"{"jsonrpc": "2.0", "method": "parity_dappsUrl", "params": [], "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
|
||||
|
||||
@@ -25,7 +25,7 @@ use ethsync::ManageNetwork;
|
||||
|
||||
use jsonrpc_core::IoHandler;
|
||||
use v1::{ParitySet, ParitySetClient};
|
||||
use v1::tests::helpers::{TestMinerService, TestFetch, TestUpdater};
|
||||
use v1::tests::helpers::{TestMinerService, TestFetch, TestUpdater, TestDappsService};
|
||||
use super::manage_network::TestManageNetwork;
|
||||
|
||||
fn miner_service() -> Arc<TestMinerService> {
|
||||
@@ -46,8 +46,14 @@ fn updater_service() -> Arc<TestUpdater> {
|
||||
|
||||
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 {
|
||||
ParitySetClient::new(client, miner, updater, &(net.clone() as Arc<ManageNetwork>), TestFetch::default())
|
||||
fn parity_set_client(
|
||||
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]
|
||||
@@ -232,3 +238,18 @@ fn rpc_parity_remove_transaction() {
|
||||
miner.pending_transactions.lock().insert(hash, signed);
|
||||
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()));
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ fn miner_service() -> Arc<TestMinerService> {
|
||||
}
|
||||
|
||||
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 opt_accounts = Some(accounts.clone());
|
||||
let client = blockchain_client();
|
||||
|
||||
@@ -47,7 +47,7 @@ struct SigningTester {
|
||||
|
||||
impl Default for SigningTester {
|
||||
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 miner = Arc::new(TestMinerService::default());
|
||||
let accounts = Arc::new(AccountProvider::transient_provider());
|
||||
|
||||
@@ -151,17 +151,13 @@ build_rpc_trait! {
|
||||
#[rpc(name = "parity_localTransactions")]
|
||||
fn local_transactions(&self) -> Result<BTreeMap<H256, LocalTransactionStatus>, Error>;
|
||||
|
||||
/// Returns current Trusted Signer port or an error if signer is disabled.
|
||||
#[rpc(name = "parity_signerPort")]
|
||||
fn signer_port(&self) -> Result<u16, Error>;
|
||||
/// Returns current Dapps Server interface and port or an error if dapps server is disabled.
|
||||
#[rpc(name = "parity_dappsUrl")]
|
||||
fn dapps_url(&self) -> Result<String, Error>;
|
||||
|
||||
/// Returns current Dapps Server port or an error if dapps server is disabled.
|
||||
#[rpc(name = "parity_dappsPort")]
|
||||
fn dapps_port(&self) -> Result<u16, 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 current WS Server interface and port or an error if ws server is disabled.
|
||||
#[rpc(name = "parity_wsUrl")]
|
||||
fn ws_url(&self) -> Result<String, Error>;
|
||||
|
||||
/// Returns next nonce for particular sender. Should include all transactions in the queue.
|
||||
#[rpc(async, name = "parity_nextNonce")]
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
use jsonrpc_core::Error;
|
||||
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! {
|
||||
/// Parity-specific rpc interface for operations altering the settings.
|
||||
@@ -96,6 +96,10 @@ build_rpc_trait! {
|
||||
#[rpc(async, name = "parity_hashContent")]
|
||||
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?
|
||||
#[rpc(name = "parity_upgradeReady")]
|
||||
fn upgrade_ready(&self) -> Result<Option<ReleaseInfo>, Error>;
|
||||
|
||||
@@ -283,12 +283,15 @@ mod tests {
|
||||
nonce: Some(1.into()),
|
||||
condition: None,
|
||||
}),
|
||||
origin: Origin::Signer(5.into()),
|
||||
origin: Origin::Signer {
|
||||
dapp: "http://parity.io".into(),
|
||||
session: 5.into(),
|
||||
}
|
||||
};
|
||||
|
||||
// when
|
||||
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
|
||||
assert_eq!(res.unwrap(), expected.to_owned());
|
||||
|
||||
57
rpc/src/v1/types/dapps.rs
Normal file
57
rpc/src/v1/types/dapps.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ mod bytes;
|
||||
mod call_request;
|
||||
mod confirmations;
|
||||
mod consensus_status;
|
||||
mod dapps;
|
||||
mod derivation;
|
||||
mod filter;
|
||||
mod hash;
|
||||
@@ -55,6 +56,7 @@ pub use self::confirmations::{
|
||||
TransactionModification, SignRequest, DecryptRequest, Either
|
||||
};
|
||||
pub use self::consensus_status::*;
|
||||
pub use self::dapps::LocalDapp;
|
||||
pub use self::derivation::{DeriveHash, DeriveHierarchical, Derive};
|
||||
pub use self::filter::{Filter, FilterChanges};
|
||||
pub use self::hash::{H64, H160, H256, H512, H520, H2048};
|
||||
|
||||
@@ -33,12 +33,22 @@ pub enum Origin {
|
||||
/// IPC server (includes session hash)
|
||||
#[serde(rename="ipc")]
|
||||
Ipc(H256),
|
||||
/// WS server (includes session hash)
|
||||
/// WS server
|
||||
#[serde(rename="ws")]
|
||||
Ws(H256),
|
||||
/// Signer (includes session hash)
|
||||
Ws {
|
||||
/// Dapp id
|
||||
dapp: DappId,
|
||||
/// Session id
|
||||
session: H256,
|
||||
},
|
||||
/// Signer (authorized WS server)
|
||||
#[serde(rename="signer")]
|
||||
Signer(H256),
|
||||
Signer {
|
||||
/// Dapp id
|
||||
dapp: DappId,
|
||||
/// Session id
|
||||
session: H256
|
||||
},
|
||||
/// Unknown
|
||||
#[serde(rename="unknown")]
|
||||
Unknown,
|
||||
@@ -53,11 +63,11 @@ impl Default for Origin {
|
||||
impl fmt::Display for Origin {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
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::Ipc(ref session) => write!(f, "IPC (session: {})", session),
|
||||
Origin::Ws(ref session) => write!(f, "WebSocket (session: {})", session),
|
||||
Origin::Signer(ref session) => write!(f, "UI (session: {})", session),
|
||||
Origin::Ws { ref session, ref dapp } => write!(f, "{} via WebSocket (session: {})", dapp, session),
|
||||
Origin::Signer { ref session, ref dapp } => write!(f, "{} via UI (session: {})", dapp, session),
|
||||
Origin::Unknown => write!(f, "unknown origin"),
|
||||
}
|
||||
}
|
||||
@@ -114,9 +124,15 @@ mod tests {
|
||||
let o1 = Origin::Rpc("test service".into());
|
||||
let o2 = Origin::Dapps("http://parity.io".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 o6 = Origin::Ws(5.into());
|
||||
let o6 = Origin::Ws {
|
||||
dapp: "http://parity.io".into(),
|
||||
session: 5.into(),
|
||||
};
|
||||
|
||||
// when
|
||||
let res1 = serde_json::to_string(&o1).unwrap();
|
||||
@@ -130,9 +146,9 @@ mod tests {
|
||||
assert_eq!(res1, r#"{"rpc":"test service"}"#);
|
||||
assert_eq!(res2, r#"{"dapp":"http://parity.io"}"#);
|
||||
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!(res6, r#"{"ws":"0x0000000000000000000000000000000000000000000000000000000000000005"}"#);
|
||||
assert_eq!(res6, r#"{"ws":{"dapp":"http://parity.io","session":"0x0000000000000000000000000000000000000000000000000000000000000005"}}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user