Signer provenance (#4477)

* Signer - Tracking Request Provenance

* Basic UI

* Changing messages

* VecDeque::from

* Fix dapps tests

* Addressing UI grumbles
This commit is contained in:
Tomasz Drwięga 2017-02-14 22:45:43 +01:00 committed by Gav Wood
parent d925cc05da
commit 5369a129ae
46 changed files with 745 additions and 269 deletions

3
Cargo.lock generated
View File

@ -25,10 +25,9 @@ dependencies = [
"ethcore-util 1.6.0",
"ethsync 1.6.0",
"fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)",
"isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"jsonrpc-core 6.0.0 (git+https://github.com/ethcore/jsonrpc.git)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"number_prefix 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -16,7 +16,6 @@ number_prefix = "0.2"
rpassword = "0.2.1"
semver = "0.5"
ansi_term = "0.7"
lazy_static = "0.2"
regex = "0.1"
isatty = "0.1"
toml = "0.2"
@ -24,7 +23,7 @@ serde = "0.9"
serde_json = "0.9"
app_dirs = "1.1.1"
fdlimit = "0.1"
hyper = { version = "0.9", default-features = false }
hyper = { default-features = false, git = "https://github.com/ethcore/hyper" }
ctrlc = { git = "https://github.com/ethcore/rust-ctrlc.git" }
jsonrpc-core = { git = "https://github.com/ethcore/jsonrpc.git" }
ethsync = { path = "sync" }

View File

@ -77,8 +77,7 @@ impl HttpMetaExtractor<Metadata> for MetadataExtractor {
})
});
Metadata {
dapp_id: dapp_id,
origin: Origin::Dapps,
origin: Origin::Dapps(dapp_id.map(Into::into).unwrap_or_default()),
}
}
}

View File

@ -55,8 +55,8 @@ fn should_extract_metadata() {
// given
let mut io = MetaIoHandler::default();
io.add_method_with_meta("rpc_test", |_params, meta: Metadata| {
assert_eq!(meta.dapp_id, Some("https://parity.io/".to_owned()));
assert_eq!(meta.origin, Origin::Dapps);
assert_eq!(meta.origin, Origin::Dapps("https://parity.io/".into()));
assert_eq!(meta.dapp_id(), "https://parity.io/".into());
future::ok(Value::String("Hello World!".into())).boxed()
});
let server = serve_with_rpc(io);
@ -89,8 +89,8 @@ fn should_extract_metadata_from_custom_header() {
// given
let mut io = MetaIoHandler::default();
io.add_method_with_meta("rpc_test", |_params, meta: Metadata| {
assert_eq!(meta.dapp_id, Some("https://parity.io/".to_owned()));
assert_eq!(meta.origin, Origin::Dapps);
assert_eq!(meta.origin, Origin::Dapps("https://parity.io/".into()));
assert_eq!(meta.dapp_id(), "https://parity.io/".into());
future::ok(Value::String("Hello World!".into())).boxed()
});
let server = serve_with_rpc(io);

View File

@ -649,8 +649,8 @@ mod tests {
use account_provider::AccountProvider;
use spec::Spec;
use engines::{Engine, EngineError, Seal};
use super::*;
use super::message::*;
use super::{Step, View, Height, message_info_rlp, message_full_rlp};
use super::message::VoteStep;
/// Accounts inserted with "0" and "1" are validators. First proposer is "0".
fn setup() -> (Spec, Arc<AccountProvider>) {

View File

@ -305,7 +305,7 @@ impl<T> TraceDatabase for TraceDB<T> where T: DatabaseExtras {
}
fn trace(&self, block_number: BlockNumber, tx_position: usize, trace_position: Vec<usize>) -> Option<LocalizedTrace> {
let trace_position_deq = trace_position.into_iter().collect::<VecDeque<usize>>();
let trace_position_deq = VecDeque::from(trace_position);
self.extras.block_hash(block_number)
.and_then(|block_hash| self.transactions_traces(&block_hash)
.and_then(|traces| traces.into_iter().nth(tx_position))

View File

@ -203,6 +203,13 @@ export function outSignerRequest (request) {
request[key].signTransaction = outTransaction(request[key].signTransaction);
request[key].sendTransaction = outTransaction(request[key].sendTransaction);
break;
case 'origin':
const type = Object.keys(request[key])[0];
const details = request[key][type];
request[key] = { type, details };
break;
}
});
}

View File

@ -28,6 +28,7 @@ export default class Ws extends JsonRpcBase {
this._url = url;
this._token = token;
this._messages = {};
this._sessionHash = null;
this._connecting = false;
this._connected = false;
@ -78,12 +79,14 @@ export default class Ws extends JsonRpcBase {
this._ws.onmessage = null;
this._ws.close();
this._ws = null;
this._sessionHash = null;
}
this._connecting = true;
this._connected = false;
this._lastError = null;
this._sessionHash = sha3;
this._ws = new WebSocket(this._url, hash);
this._ws.onerror = this._onError;
this._ws.onopen = this._onOpen;
@ -255,6 +258,10 @@ export default class Ws extends JsonRpcBase {
return this._token;
}
get sessionHash () {
return this._sessionHash;
}
get isAutoConnect () {
return this._autoConnect;
}

View File

@ -0,0 +1,17 @@
// 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/>.
export default from './requestOrigin';

View File

@ -0,0 +1,43 @@
/* 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/>.
*/
.container {
text-align: left;
margin: 3em .5em;
opacity: 0.6;
font-size: 0.8em;
.unknown {
color: #e44;
}
.url {
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.hash {
margin-left: .2em;
}
.hash, .url {
margin-bottom: -.2em;
display: inline-block;
}
}

View File

@ -0,0 +1,116 @@
// 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/>.
import React, { Component, PropTypes } from 'react';
import IdentityIcon from '~/ui/IdentityIcon';
import styles from './requestOrigin.css';
export default class RequestOrigin extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
origin: PropTypes.shape({
type: PropTypes.oneOf(['unknown', 'dapp', 'rpc', 'ipc', 'signer']),
details: PropTypes.string.isRequired
}).isRequired
};
render () {
const { origin } = this.props;
return (
<div className={ styles.container }>
Requested { this.renderOrigin(origin) }
</div>
);
}
renderOrigin (origin) {
if (origin.type === 'unknown') {
return (
<span className={ styles.unknown }>via unknown interface</span>
);
}
if (origin.type === 'dapp') {
return (
<span>
by a dapp at <span className={ styles.url }>
{ origin.details || 'unknown URL' }
</span>
</span>
);
}
if (origin.type === 'rpc') {
return (
<span>
via RPC <span className={ styles.url }>
({ origin.details || 'unidentified' })
</span>
</span>
);
}
if (origin.type === 'ipc') {
return (
<span>
via IPC session
<span
className={ styles.hash }
title={ origin.details }
>
<IdentityIcon
address={ origin.details }
tiny
/>
</span>
</span>
);
}
if (origin.type === 'signer') {
return this.renderSigner(origin.details);
}
}
renderSigner (session) {
if (session.substr(2) === this.context.api.transport.sessionHash) {
return (
<span title={ session }>via current tab</span>
);
}
return (
<span>
via UI session
<span
className={ styles.hash }
title={ `UI Session id: ${session}` }
>
<IdentityIcon
address={ session }
tiny
/>
</span>
</span>
);
}
}

View File

@ -0,0 +1,72 @@
// 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/>.
import { shallow } from 'enzyme';
import React from 'react';
import RequestOrigin from './';
const context = {
context: {
api: {
transport: {
sessionHash: '1234'
}
}
}
};
describe('views/Signer/components/RequestOrigin', () => {
it('renders unknown', () => {
expect(shallow(
<RequestOrigin origin={ { type: 'unknown', details: '' } } />,
context
).text()).to.equal('Requested via unknown interface');
});
it('renders dapps', () => {
expect(shallow(
<RequestOrigin origin={ { type: 'dapp', details: 'http://parity.io' } } />,
context
).text()).to.equal('Requested by a dapp at http://parity.io');
});
it('renders rpc', () => {
expect(shallow(
<RequestOrigin origin={ { type: 'rpc', details: '' } } />,
context
).text()).to.equal('Requested via RPC (unidentified)');
});
it('renders ipc', () => {
expect(shallow(
<RequestOrigin origin={ { type: 'ipc', details: '0x1234' } } />,
context
).text()).to.equal('Requested via IPC session<Connect(IdentityIcon) />');
});
it('renders signer', () => {
expect(shallow(
<RequestOrigin origin={ { type: 'signer', details: '0x12345' } } />,
context
).text()).to.equal('Requested via UI session<Connect(IdentityIcon) />');
expect(shallow(
<RequestOrigin origin={ { type: 'signer', details: '0x1234' } } />,
context
).text()).to.equal('Requested via current tab');
});
});

View File

@ -30,6 +30,7 @@ export default class RequestPending extends Component {
isTest: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired,
origin: PropTypes.object.isRequired,
payload: PropTypes.oneOfType([
PropTypes.shape({ sendTransaction: PropTypes.object.isRequired }),
PropTypes.shape({ sign: PropTypes.object.isRequired }),
@ -51,7 +52,7 @@ export default class RequestPending extends Component {
};
render () {
const { className, date, focus, gasLimit, id, isSending, isTest, onReject, payload, store } = this.props;
const { className, date, focus, gasLimit, id, isSending, isTest, onReject, payload, store, origin } = this.props;
if (payload.sign) {
const { sign } = payload;
@ -68,6 +69,7 @@ export default class RequestPending extends Component {
isTest={ isTest }
onConfirm={ this.onConfirm }
onReject={ onReject }
origin={ origin }
store={ store }
/>
);
@ -87,6 +89,7 @@ export default class RequestPending extends Component {
isTest={ isTest }
onConfirm={ this.onConfirm }
onReject={ onReject }
origin={ origin }
store={ store }
transaction={ transaction }
/>

View File

@ -19,6 +19,7 @@ import { observer } from 'mobx-react';
import Account from '../Account';
import TransactionPendingForm from '../TransactionPendingForm';
import RequestOrigin from '../RequestOrigin';
import styles from './signRequest.css';
@ -40,9 +41,9 @@ export default class SignRequest extends Component {
};
static propTypes = {
id: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
data: PropTypes.string.isRequired,
id: PropTypes.object.isRequired,
isFinished: PropTypes.bool.isRequired,
isTest: PropTypes.bool.isRequired,
store: PropTypes.object.isRequired,
@ -52,11 +53,16 @@ export default class SignRequest extends Component {
isSending: PropTypes.bool,
onConfirm: PropTypes.func,
onReject: PropTypes.func,
origin: PropTypes.any,
status: PropTypes.string
};
static defaultProps = {
focus: false
focus: false,
origin: {
type: 'unknown',
details: ''
}
};
componentWillMount () {
@ -92,7 +98,7 @@ export default class SignRequest extends Component {
renderDetails () {
const { api } = this.context;
const { address, isTest, store, data } = this.props;
const { address, isTest, store, data, origin } = this.props;
const { balances, externalLink } = store;
const balance = balances[address];
@ -110,6 +116,7 @@ export default class SignRequest extends Component {
externalLink={ externalLink }
isTest={ isTest }
/>
<RequestOrigin origin={ origin } />
</div>
<div className={ styles.info } title={ api.util.sha3(data) }>
<p>A request to sign data using your account:</p>

View File

@ -39,15 +39,17 @@
width: 40%;
vertical-align: top;
img {
display: inline-block;
width: 50px;
height: 50px;
margin: 5px;
}
.account {
img {
display: inline-block;
width: 50px;
height: 50px;
margin: 5px;
}
span {
display: block;
span {
display: block;
}
}
}

View File

@ -22,6 +22,8 @@ import { Button, MethodDecoding } from '~/ui';
import * as tUtil from '../util/transaction';
import Account from '../Account';
import RequestOrigin from '../RequestOrigin';
import styles from './transactionMainDetails.css';
export default class TransactionMainDetails extends Component {
@ -33,11 +35,19 @@ export default class TransactionMainDetails extends Component {
gasStore: PropTypes.object,
id: PropTypes.object.isRequired,
isTest: PropTypes.bool.isRequired,
origin: PropTypes.any,
totalValue: PropTypes.object.isRequired,
transaction: PropTypes.object.isRequired,
value: PropTypes.object.isRequired
};
static defaultProps = {
origin: {
type: 'unknown',
details: ''
}
};
componentWillMount () {
const { totalValue, value } = this.props;
@ -51,7 +61,7 @@ export default class TransactionMainDetails extends Component {
}
render () {
const { children, externalLink, from, fromBalance, gasStore, isTest, transaction } = this.props;
const { children, externalLink, from, fromBalance, gasStore, isTest, transaction, origin } = this.props;
return (
<div className={ styles.transaction }>
@ -64,6 +74,7 @@ export default class TransactionMainDetails extends Component {
isTest={ isTest }
/>
</div>
<RequestOrigin origin={ origin } />
</div>
<div className={ styles.method }>
<MethodDecoding

View File

@ -43,6 +43,7 @@ export default class TransactionPending extends Component {
nonce: PropTypes.number,
onConfirm: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired,
origin: PropTypes.any,
store: PropTypes.object.isRequired,
transaction: PropTypes.shape({
condition: PropTypes.object,
@ -56,7 +57,11 @@ export default class TransactionPending extends Component {
};
static defaultProps = {
focus: false
focus: false,
origin: {
type: 'unknown',
details: ''
}
};
gasStore = new GasPriceEditor.Store(this.context.api, {
@ -87,7 +92,7 @@ export default class TransactionPending extends Component {
}
renderTransaction () {
const { className, focus, id, isSending, isTest, store, transaction } = this.props;
const { className, focus, id, isSending, isTest, store, transaction, origin } = this.props;
const { totalValue } = this.state;
const { balances, externalLink } = store;
const { from, value } = transaction;
@ -104,6 +109,7 @@ export default class TransactionPending extends Component {
gasStore={ this.gasStore }
id={ id }
isTest={ isTest }
origin={ origin }
totalValue={ totalValue }
transaction={ transaction }
value={ value }

View File

@ -81,7 +81,7 @@ class Embedded extends Component {
renderPending = (data, index) => {
const { actions, gasLimit, isTest } = this.props;
const { date, id, isSending, payload } = data;
const { date, id, isSending, payload, origin } = data;
return (
<RequestPending
@ -95,6 +95,7 @@ class Embedded extends Component {
key={ id }
onConfirm={ actions.startConfirmRequest }
onReject={ actions.startRejectRequest }
origin={ origin }
payload={ payload }
store={ this.store }
/>

View File

@ -107,7 +107,7 @@ class RequestsPage extends Component {
renderPending = (data, index) => {
const { actions, gasLimit, isTest } = this.props;
const { date, id, isSending, payload } = data;
const { date, id, isSending, payload, origin } = data;
return (
<RequestPending
@ -121,6 +121,7 @@ class RequestsPage extends Component {
key={ id }
onConfirm={ actions.startConfirmRequest }
onReject={ actions.startRejectRequest }
origin={ origin }
payload={ payload }
store={ this.store }
/>

View File

@ -28,7 +28,7 @@ extern crate ctrlc;
extern crate docopt;
extern crate env_logger;
extern crate fdlimit;
extern crate hyper; // for price_info.rs
extern crate hyper;
extern crate isatty;
extern crate jsonrpc_core;
extern crate num_cpus;
@ -60,15 +60,14 @@ extern crate parity_reactor;
extern crate parity_updater as updater;
extern crate rpc_cli;
#[macro_use]
extern crate log as rlog;
#[cfg(feature="stratum")]
extern crate ethcore_stratum;
#[cfg(feature = "dapps")]
extern crate ethcore_dapps;
#[macro_use]
extern crate log as rlog;
macro_rules! dependency {
($dep_ty:ident, $url:expr) => {
{

View File

@ -21,9 +21,10 @@ use std::io;
use io::PanicHandler;
use dir::default_data_path;
use ethcore_rpc::{self as rpc, RpcServerError, IpcServerError, Metadata};
use ethcore_rpc::{self as rpc, RpcServerError, IpcServerError, Metadata, Origin};
use ethcore_rpc::informant::{RpcStats, Middleware};
use helpers::parity_ipc_path;
use hyper;
use jsonrpc_core::MetaIoHandler;
use jsonrpc_core::reactor::{RpcHandler, Remote};
use rpc_apis;
@ -89,6 +90,18 @@ pub struct Dependencies {
pub stats: Arc<RpcStats>,
}
pub struct RpcExtractor;
impl rpc::HttpMetaExtractor<Metadata> for RpcExtractor {
fn read_metadata(&self, req: &hyper::server::Request<hyper::net::HttpStream>) -> Metadata {
let origin = req.headers().get::<hyper::header::Origin>()
.map(|origin| format!("{}://{}", origin.scheme, origin.host))
.unwrap_or_else(|| "unknown".into());
let mut metadata = Metadata::default();
metadata.origin = Origin::Rpc(origin);
metadata
}
}
pub fn new_http(conf: HttpConfiguration, deps: &Dependencies) -> Result<Option<HttpServer>, String> {
if !conf.enabled {
return Ok(None);
@ -113,7 +126,7 @@ pub fn setup_http_rpc_server(
let apis = setup_apis(apis, dependencies);
let handler = RpcHandler::new(Arc::new(apis), dependencies.remote.clone());
let ph = dependencies.panic_handler.clone();
let start_result = rpc::start_http(url, cors_domains, allowed_hosts, ph, handler);
let start_result = rpc::start_http(url, cors_domains, allowed_hosts, ph, handler, RpcExtractor);
match start_result {
Err(RpcServerError::IoError(err)) => match err.kind() {
io::ErrorKind::AddrInUse => Err(format!("RPC 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)),

View File

@ -23,12 +23,14 @@ pub use ethcore_signer::Server as SignerServer;
use ansi_term::Colour;
use dir::default_data_path;
use ethcore_rpc::informant::RpcStats;
use ethcore_rpc;
use ethcore_signer as signer;
use helpers::replace_home;
use io::{ForwardPanic, PanicHandler};
use jsonrpc_core::reactor::{RpcHandler, Remote};
use rpc_apis;
use util::path::restrict_permissions_owner;
use util::H256;
const CODES_FILENAME: &'static str = "authcodes";
@ -67,6 +69,16 @@ pub struct NewToken {
pub message: String,
}
#[derive(Debug, Default, Clone)]
pub struct StandardExtractor;
impl signer::MetaExtractor<ethcore_rpc::Metadata> for StandardExtractor {
fn extract_metadata(&self, session: &H256) -> ethcore_rpc::Metadata {
let mut metadata = ethcore_rpc::Metadata::default();
metadata.origin = ethcore_rpc::Origin::Signer((*session).into());
metadata
}
}
pub fn start(conf: Configuration, deps: Dependencies) -> Result<Option<SignerServer>, String> {
if !conf.enabled {
Ok(None)
@ -133,7 +145,7 @@ fn do_start(conf: Configuration, deps: Dependencies) -> Result<SignerServer, Str
let server = server.stats(deps.rpc_stats.clone());
let apis = rpc_apis::setup_rpc(deps.rpc_stats, deps.apis, rpc_apis::ApiSet::SafeContext);
let handler = RpcHandler::new(Arc::new(apis), deps.remote);
server.start(addr, handler)
server.start_with_extractor(addr, handler, StandardExtractor)
};
match start_result {

View File

@ -65,19 +65,24 @@ use io::PanicHandler;
use jsonrpc_core::reactor::RpcHandler;
pub use ipc::{Server as IpcServer, Error as IpcServerError};
pub use jsonrpc_http_server::{ServerBuilder, Server, RpcServerError};
pub use jsonrpc_http_server::{ServerBuilder, Server, RpcServerError, HttpMetaExtractor};
pub mod v1;
pub use v1::{SigningQueue, SignerService, ConfirmationsQueue, NetworkSettings, Metadata, Origin, informant, dispatch};
pub use v1::block_import::is_major_importing;
/// Start http server asynchronously and returns result with `Server` handle on success or an error.
pub fn start_http<M: jsonrpc_core::Metadata, S: jsonrpc_core::Middleware<M>>(
pub fn start_http<M, T, S>(
addr: &SocketAddr,
cors_domains: Option<Vec<String>>,
allowed_hosts: Option<Vec<String>>,
panic_handler: Arc<PanicHandler>,
handler: RpcHandler<M, S>,
) -> Result<Server, RpcServerError> {
extractor: T,
) -> Result<Server, RpcServerError> where
M: jsonrpc_core::Metadata,
S: jsonrpc_core::Middleware<M>,
T: HttpMetaExtractor<M>,
{
let cors_domains = cors_domains.map(|domains| {
domains.into_iter()
@ -90,6 +95,7 @@ pub fn start_http<M: jsonrpc_core::Metadata, S: jsonrpc_core::Middleware<M>>(
});
ServerBuilder::with_rpc_handler(handler)
.meta_extractor(Arc::new(extractor))
.cors(cors_domains.into())
.allowed_hosts(allowed_hosts.into())
.panic_handler(move || {

View File

@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
use util::{Address, U256, Bytes};
use v1::types::TransactionCondition;
use v1::types::{Origin, TransactionCondition};
/// Transaction request coming from RPC
#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)]
@ -102,6 +102,8 @@ pub struct ConfirmationRequest {
pub id: U256,
/// Payload to confirm
pub payload: ConfirmationPayload,
/// Request origin
pub origin: Origin,
}
/// Payload to confirm in Trusted Signer

View File

@ -22,8 +22,7 @@ use jsonrpc_core;
use util::{Mutex, RwLock, U256, Address};
use ethcore::account_provider::DappId;
use v1::helpers::{ConfirmationRequest, ConfirmationPayload};
use v1::metadata::Metadata;
use v1::types::{ConfirmationResponse, H160 as RpcH160};
use v1::types::{ConfirmationResponse, H160 as RpcH160, Origin, DappId as RpcDappId};
/// Result that can be returned from JSON RPC.
pub type RpcResult = Result<ConfirmationResponse, jsonrpc_core::Error>;
@ -37,9 +36,9 @@ pub enum DefaultAccount {
ForDapp(DappId),
}
impl From<Metadata> for DefaultAccount {
fn from(meta: Metadata) -> Self {
DefaultAccount::ForDapp(meta.dapp_id.unwrap_or_default().into())
impl From<RpcDappId> for DefaultAccount {
fn from(dapp_id: RpcDappId) -> Self {
DefaultAccount::ForDapp(dapp_id.into())
}
}
@ -84,7 +83,7 @@ const QUEUE_LIMIT: usize = 50;
pub trait SigningQueue: Send + Sync {
/// Add new request to the queue.
/// Returns a `ConfirmationPromise` that can be used to await for resolution of given request.
fn add_request(&self, request: ConfirmationPayload) -> Result<ConfirmationPromise, QueueAddError>;
fn add_request(&self, request: ConfirmationPayload, origin: Origin) -> Result<ConfirmationPromise, QueueAddError>;
/// Removes a request from the queue.
/// Notifies possible token holders that request was rejected.
@ -267,7 +266,7 @@ impl Drop for ConfirmationsQueue {
}
impl SigningQueue for ConfirmationsQueue {
fn add_request(&self, request: ConfirmationPayload) -> Result<ConfirmationPromise, QueueAddError> {
fn add_request(&self, request: ConfirmationPayload, origin: Origin) -> Result<ConfirmationPromise, QueueAddError> {
if self.len() > QUEUE_LIMIT {
return Err(QueueAddError::LimitReached);
}
@ -290,6 +289,7 @@ impl SigningQueue for ConfirmationsQueue {
request: ConfirmationRequest {
id: id,
payload: request,
origin: origin,
},
});
queue.get(&id).map(|token| token.as_promise()).expect("Token was just inserted.")
@ -362,7 +362,7 @@ mod test {
// when
let q = queue.clone();
let handle = thread::spawn(move || {
let v = q.add_request(request).unwrap();
let v = q.add_request(request, Default::default()).unwrap();
let (tx, rx) = mpsc::channel();
v.wait_for_result(move |res| {
tx.send(res).unwrap();
@ -397,7 +397,7 @@ mod test {
*v = Some(notification);
}).expect("Should be closed nicely.")
});
queue.add_request(request).unwrap();
queue.add_request(request, Default::default()).unwrap();
queue.finish();
// then
@ -413,7 +413,7 @@ mod test {
let request = request();
// when
queue.add_request(request.clone()).unwrap();
queue.add_request(request.clone(), Default::default()).unwrap();
let all = queue.requests();
// then

View File

@ -311,7 +311,7 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
}
fn author(&self, meta: Metadata) -> BoxFuture<RpcH160, Error> {
let dapp = meta.dapp_id.unwrap_or_default();
let dapp = meta.dapp_id();
let author = move || {
let mut miner = take_weak!(self.miner).author();
@ -342,7 +342,7 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where
}
fn accounts(&self, meta: Metadata) -> BoxFuture<Vec<RpcH160>, Error> {
let dapp = meta.dapp_id.unwrap_or_default();
let dapp = meta.dapp_id();
let accounts = move || {
let accounts = self.dapp_accounts(dapp.into())?;

View File

@ -178,7 +178,7 @@ impl Eth for EthClient {
}
fn accounts(&self, meta: Metadata) -> BoxFuture<Vec<RpcH160>, Error> {
let dapp: DappId = meta.dapp_id.unwrap_or_default().into();
let dapp: DappId = meta.dapp_id().into();
let accounts = self.accounts
.note_dapp_used(dapp.clone())

View File

@ -145,7 +145,7 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
}
fn default_account(&self, meta: Self::Metadata) -> BoxFuture<H160, Error> {
let dapp_id = meta.dapp_id.unwrap_or_default();
let dapp_id = meta.dapp_id();
let default_account = move || {
Ok(take_weak!(self.accounts)
.dapps_addresses(dapp_id.into())

View File

@ -101,7 +101,7 @@ impl<D: Dispatcher + 'static> Personal for PersonalClient<D> {
let default = match request.from.as_ref() {
Some(account) => Ok(account.clone().into()),
None => accounts
.default_address(meta.dapp_id.unwrap_or_default().into())
.default_address(meta.dapp_id().into())
.map_err(|e| errors::account("Cannot find default account.", e)),
};

View File

@ -38,7 +38,8 @@ use v1::types::{
RichRawTransaction as RpcRichRawTransaction,
TransactionRequest as RpcTransactionRequest,
ConfirmationPayload as RpcConfirmationPayload,
ConfirmationResponse as RpcConfirmationResponse
ConfirmationResponse as RpcConfirmationResponse,
Origin,
};
const MAX_PENDING_DURATION: u64 = 60 * 60;
@ -81,7 +82,7 @@ impl<D: Dispatcher + 'static> SigningQueueClient<D> {
}
}
fn dispatch(&self, payload: RpcConfirmationPayload, default_account: DefaultAccount) -> BoxFuture<DispatchResult, Error> {
fn dispatch(&self, payload: RpcConfirmationPayload, default_account: DefaultAccount, origin: Origin) -> BoxFuture<DispatchResult, Error> {
let accounts = take_weakf!(self.accounts);
let default_account = match default_account {
DefaultAccount::Provided(acc) => acc,
@ -100,7 +101,7 @@ impl<D: Dispatcher + 'static> SigningQueueClient<D> {
.boxed()
} else {
future::done(
signer.add_request(payload)
signer.add_request(payload, origin)
.map(DispatchResult::Promise)
.map_err(|_| errors::request_rejected_limit())
).boxed()
@ -113,23 +114,26 @@ impl<D: Dispatcher + 'static> SigningQueueClient<D> {
impl<D: Dispatcher + 'static> ParitySigning for SigningQueueClient<D> {
type Metadata = Metadata;
fn post_sign(&self, address: RpcH160, data: RpcBytes) -> BoxFuture<RpcEither<RpcU256, RpcConfirmationResponse>, Error> {
fn post_sign(&self, meta: Metadata, address: RpcH160, data: RpcBytes) -> BoxFuture<RpcEither<RpcU256, RpcConfirmationResponse>, Error> {
let pending = self.pending.clone();
self.dispatch(RpcConfirmationPayload::Signature((address.clone(), data).into()), DefaultAccount::Provided(address.into()))
.map(move |result| match result {
DispatchResult::Value(v) => RpcEither::Or(v),
DispatchResult::Promise(promise) => {
let id = promise.id();
pending.lock().insert(id, promise);
RpcEither::Either(id.into())
},
})
.boxed()
self.dispatch(
RpcConfirmationPayload::Signature((address.clone(), data).into()),
DefaultAccount::Provided(address.into()),
meta.origin
).map(move |result| match result {
DispatchResult::Value(v) => RpcEither::Or(v),
DispatchResult::Promise(promise) => {
let id = promise.id();
pending.lock().insert(id, promise);
RpcEither::Either(id.into())
},
})
.boxed()
}
fn post_transaction(&self, meta: Metadata, request: RpcTransactionRequest) -> BoxFuture<RpcEither<RpcU256, RpcConfirmationResponse>, Error> {
let pending = self.pending.clone();
self.dispatch(RpcConfirmationPayload::SendTransaction(request), meta.into())
self.dispatch(RpcConfirmationPayload::SendTransaction(request), meta.dapp_id().into(), meta.origin)
.map(move |result| match result {
DispatchResult::Value(v) => RpcEither::Or(v),
DispatchResult::Promise(promise) => {
@ -156,8 +160,12 @@ impl<D: Dispatcher + 'static> ParitySigning for SigningQueueClient<D> {
res
}
fn decrypt_message(&self, address: RpcH160, data: RpcBytes) -> BoxFuture<RpcBytes, Error> {
let res = self.dispatch(RpcConfirmationPayload::Decrypt((address.clone(), data).into()), address.into());
fn decrypt_message(&self, meta: Metadata, address: RpcH160, data: RpcBytes) -> BoxFuture<RpcBytes, Error> {
let res = self.dispatch(
RpcConfirmationPayload::Decrypt((address.clone(), data).into()),
address.into(),
meta.origin,
);
let (ready, p) = futures::oneshot();
@ -181,8 +189,12 @@ impl<D: Dispatcher + 'static> ParitySigning for SigningQueueClient<D> {
impl<D: Dispatcher + 'static> EthSigning for SigningQueueClient<D> {
type Metadata = Metadata;
fn sign(&self, address: RpcH160, data: RpcBytes) -> BoxFuture<RpcH520, Error> {
let res = self.dispatch(RpcConfirmationPayload::Signature((address.clone(), data).into()), address.into());
fn sign(&self, meta: Metadata, address: RpcH160, data: RpcBytes) -> BoxFuture<RpcH520, Error> {
let res = self.dispatch(
RpcConfirmationPayload::Signature((address.clone(), data).into()),
address.into(),
meta.origin,
);
let (ready, p) = futures::oneshot();
@ -200,7 +212,11 @@ impl<D: Dispatcher + 'static> EthSigning for SigningQueueClient<D> {
}
fn send_transaction(&self, meta: Metadata, request: RpcTransactionRequest) -> BoxFuture<RpcH256, Error> {
let res = self.dispatch(RpcConfirmationPayload::SendTransaction(request), meta.into());
let res = self.dispatch(
RpcConfirmationPayload::SendTransaction(request),
meta.dapp_id().into(),
meta.origin,
);
let (ready, p) = futures::oneshot();
@ -218,7 +234,11 @@ impl<D: Dispatcher + 'static> EthSigning for SigningQueueClient<D> {
}
fn sign_transaction(&self, meta: Metadata, request: RpcTransactionRequest) -> BoxFuture<RpcRichRawTransaction, Error> {
let res = self.dispatch(RpcConfirmationPayload::SignTransaction(request), meta.into());
let res = self.dispatch(
RpcConfirmationPayload::SignTransaction(request),
meta.dapp_id().into(),
meta.origin,
);
let (ready, p) = futures::oneshot();

View File

@ -72,7 +72,7 @@ impl<D: Dispatcher + 'static> EthSigning for SigningUnsafeClient<D>
{
type Metadata = Metadata;
fn sign(&self, address: RpcH160, data: RpcBytes) -> BoxFuture<RpcH520, Error> {
fn sign(&self, _: Metadata, address: RpcH160, data: RpcBytes) -> BoxFuture<RpcH520, Error> {
self.handle(RpcConfirmationPayload::Signature((address.clone(), data).into()), address.into())
.then(|res| match res {
Ok(RpcConfirmationResponse::Signature(signature)) => Ok(signature),
@ -83,7 +83,7 @@ impl<D: Dispatcher + 'static> EthSigning for SigningUnsafeClient<D>
}
fn send_transaction(&self, meta: Metadata, request: RpcTransactionRequest) -> BoxFuture<RpcH256, Error> {
self.handle(RpcConfirmationPayload::SendTransaction(request), meta.into())
self.handle(RpcConfirmationPayload::SendTransaction(request), meta.dapp_id().into())
.then(|res| match res {
Ok(RpcConfirmationResponse::SendTransaction(hash)) => Ok(hash),
Err(e) => Err(e),
@ -93,7 +93,7 @@ impl<D: Dispatcher + 'static> EthSigning for SigningUnsafeClient<D>
}
fn sign_transaction(&self, meta: Metadata, request: RpcTransactionRequest) -> BoxFuture<RpcRichRawTransaction, Error> {
self.handle(RpcConfirmationPayload::SignTransaction(request), meta.into())
self.handle(RpcConfirmationPayload::SignTransaction(request), meta.dapp_id().into())
.then(|res| match res {
Ok(RpcConfirmationResponse::SignTransaction(tx)) => Ok(tx),
Err(e) => Err(e),
@ -106,7 +106,7 @@ impl<D: Dispatcher + 'static> EthSigning for SigningUnsafeClient<D>
impl<D: Dispatcher + 'static> ParitySigning for SigningUnsafeClient<D> {
type Metadata = Metadata;
fn decrypt_message(&self, address: RpcH160, data: RpcBytes) -> BoxFuture<RpcBytes, Error> {
fn decrypt_message(&self, _: Metadata, address: RpcH160, data: RpcBytes) -> BoxFuture<RpcBytes, Error> {
self.handle(RpcConfirmationPayload::Decrypt((address.clone(), data).into()), address.into())
.then(|res| match res {
Ok(RpcConfirmationResponse::Decrypt(data)) => Ok(data),
@ -116,7 +116,7 @@ impl<D: Dispatcher + 'static> ParitySigning for SigningUnsafeClient<D> {
.boxed()
}
fn post_sign(&self, _: RpcH160, _: RpcBytes) -> BoxFuture<RpcEither<RpcU256, RpcConfirmationResponse>, Error> {
fn post_sign(&self, _: Metadata, _: RpcH160, _: RpcBytes) -> BoxFuture<RpcEither<RpcU256, RpcConfirmationResponse>, Error> {
// We don't support this in non-signer mode.
future::err(errors::signer_disabled()).boxed()
}

View File

@ -16,33 +16,22 @@
use jsonrpc_core;
use v1::types::{DappId, Origin};
/// RPC methods metadata.
#[derive(Clone, Default, Debug, PartialEq)]
pub struct Metadata {
/// Current dapplication identifier
pub dapp_id: Option<String>,
/// Request origin
pub origin: Origin,
}
/// RPC request origin
#[derive(Clone, Debug, PartialEq)]
pub enum Origin {
/// RPC server
Rpc,
/// Dapps server
Dapps,
/// IPC server
Ipc,
/// Signer
Signer,
/// Unknown
Unknown,
}
impl Default for Origin {
fn default() -> Self {
Origin::Unknown
impl Metadata {
/// Get
pub fn dapp_id(&self) -> DappId {
match self.origin {
Origin::Dapps(ref dapp_id) => dapp_id.clone(),
_ => DappId::default(),
}
}
}

View File

@ -61,4 +61,5 @@ pub mod types;
pub use self::traits::{Web3, Eth, EthFilter, EthSigning, Net, Parity, ParityAccounts, ParitySet, ParitySigning, Signer, Personal, Traces, Rpc};
pub use self::impls::*;
pub use self::helpers::{SigningQueue, SignerService, ConfirmationsQueue, NetworkSettings, block_import, informant, dispatch};
pub use self::metadata::{Metadata, Origin};
pub use self::metadata::Metadata;
pub use self::types::Origin;

View File

@ -37,6 +37,7 @@ use v1::{Eth, EthClient, EthClientOptions, EthFilter, EthFilterClient, EthSignin
use v1::helpers::dispatch::FullDispatcher;
use v1::tests::helpers::{TestSyncProvider, Config, TestMinerService, TestSnapshotService};
use v1::metadata::Metadata;
use v1::types::Origin;
fn blockchain_client() -> Arc<TestBlockChainClient> {
let client = TestBlockChainClient::new();
@ -387,7 +388,7 @@ fn rpc_eth_accounts() {
let request = r#"{"jsonrpc": "2.0", "method": "eth_accounts", "params": [], "id": 1}"#;
let response = r#"{"jsonrpc":"2.0","result":["0x000000000000000000000000000000000000000a"],"id":1}"#;
let mut meta = Metadata::default();
meta.dapp_id = Some("app1".into());
meta.origin = Origin::Dapps("app1".into());
assert_eq!((*tester.io).handle_request_sync(request, meta), Some(response.to_owned()));
}

View File

@ -25,7 +25,7 @@ use rlp::encode;
use serde_json;
use jsonrpc_core::IoHandler;
use v1::{SignerClient, Signer};
use v1::{SignerClient, Signer, Origin};
use v1::metadata::Metadata;
use v1::tests::helpers::TestMinerService;
use v1::helpers::{SigningQueue, SignerService, FilledTransactionRequest, ConfirmationPayload};
@ -88,15 +88,15 @@ fn should_return_list_of_items_to_confirm() {
data: vec![],
nonce: None,
condition: None,
})).unwrap();
tester.signer.add_request(ConfirmationPayload::Signature(1.into(), vec![5].into())).unwrap();
}), Origin::Dapps("http://parity.io".into())).unwrap();
tester.signer.add_request(ConfirmationPayload::Signature(1.into(), vec![5].into()), Origin::Unknown).unwrap();
// when
let request = r#"{"jsonrpc":"2.0","method":"signer_requestsToConfirm","params":[],"id":1}"#;
let response = concat!(
r#"{"jsonrpc":"2.0","result":["#,
r#"{"id":"0x1","payload":{"sendTransaction":{"condition":null,"data":"0x","from":"0x0000000000000000000000000000000000000001","gas":"0x989680","gasPrice":"0x2710","nonce":null,"to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","value":"0x1"}}},"#,
r#"{"id":"0x2","payload":{"sign":{"address":"0x0000000000000000000000000000000000000001","data":"0x05"}}}"#,
r#"{"id":"0x1","origin":{"dapp":"http://parity.io"},"payload":{"sendTransaction":{"condition":null,"data":"0x","from":"0x0000000000000000000000000000000000000001","gas":"0x989680","gasPrice":"0x2710","nonce":null,"to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567","value":"0x1"}}},"#,
r#"{"id":"0x2","origin":"unknown","payload":{"sign":{"address":"0x0000000000000000000000000000000000000001","data":"0x05"}}}"#,
r#"],"id":1}"#
);
@ -119,7 +119,7 @@ fn should_reject_transaction_from_queue_without_dispatching() {
data: vec![],
nonce: None,
condition: None,
})).unwrap();
}), Origin::Unknown).unwrap();
assert_eq!(tester.signer.requests().len(), 1);
// when
@ -146,7 +146,7 @@ fn should_not_remove_transaction_if_password_is_invalid() {
data: vec![],
nonce: None,
condition: None,
})).unwrap();
}), Origin::Unknown).unwrap();
assert_eq!(tester.signer.requests().len(), 1);
// when
@ -162,7 +162,7 @@ fn should_not_remove_transaction_if_password_is_invalid() {
fn should_not_remove_sign_if_password_is_invalid() {
// given
let tester = signer_tester();
tester.signer.add_request(ConfirmationPayload::Signature(0.into(), vec![5].into())).unwrap();
tester.signer.add_request(ConfirmationPayload::Signature(0.into(), vec![5].into()), Origin::Unknown).unwrap();
assert_eq!(tester.signer.requests().len(), 1);
// when
@ -190,7 +190,7 @@ fn should_confirm_transaction_and_dispatch() {
data: vec![],
nonce: None,
condition: None,
})).unwrap();
}), Origin::Unknown).unwrap();
let t = Transaction {
nonce: U256::zero(),
@ -236,7 +236,7 @@ fn should_alter_the_sender_and_nonce() {
data: vec![],
nonce: Some(10.into()),
condition: None,
})).unwrap();
}), Origin::Unknown).unwrap();
let t = Transaction {
nonce: U256::zero(),
@ -286,7 +286,7 @@ fn should_confirm_transaction_with_token() {
data: vec![],
nonce: None,
condition: None,
})).unwrap();
}), Origin::Unknown).unwrap();
let t = Transaction {
nonce: U256::zero(),
@ -335,7 +335,7 @@ fn should_confirm_transaction_with_rlp() {
data: vec![],
nonce: None,
condition: None,
})).unwrap();
}), Origin::Unknown).unwrap();
let t = Transaction {
nonce: U256::zero(),
@ -383,7 +383,7 @@ fn should_return_error_when_sender_does_not_match() {
data: vec![],
nonce: None,
condition: None,
})).unwrap();
}), Origin::Unknown).unwrap();
let t = Transaction {
nonce: U256::zero(),

View File

@ -27,8 +27,8 @@ build_rpc_trait! {
type Metadata;
/// Signs the hash of data with given address signature.
#[rpc(async, name = "eth_sign")]
fn sign(&self, H160, Bytes) -> BoxFuture<H520, Error>;
#[rpc(meta, name = "eth_sign")]
fn sign(&self, Self::Metadata, H160, Bytes) -> BoxFuture<H520, Error>;
/// Sends transaction; will block waiting for signer to return the
/// transaction hash.

View File

@ -27,8 +27,8 @@ build_rpc_trait! {
/// Posts sign request asynchronously.
/// Will return a confirmation ID for later use with check_transaction.
#[rpc(async, name = "parity_postSign")]
fn post_sign(&self, H160, Bytes) -> BoxFuture<Either<U256, ConfirmationResponse>, Error>;
#[rpc(meta, name = "parity_postSign")]
fn post_sign(&self, Self::Metadata, H160, Bytes) -> BoxFuture<Either<U256, ConfirmationResponse>, Error>;
/// Posts transaction asynchronously.
/// Will return a transaction ID for later use with check_transaction.
@ -42,7 +42,7 @@ build_rpc_trait! {
/// Decrypt some ECIES-encrypted message.
/// First parameter is the address with which it is encrypted, second is the ciphertext.
#[rpc(async, name = "parity_decryptMessage")]
fn decrypt_message(&self, H160, Bytes) -> BoxFuture<Bytes, Error>;
#[rpc(meta, name = "parity_decryptMessage")]
fn decrypt_message(&self, Self::Metadata, H160, Bytes) -> BoxFuture<Bytes, Error>;
}
}

View File

@ -21,16 +21,19 @@ use serde::{Serialize, Serializer};
use util::log::Colour;
use util::bytes::ToPretty;
use v1::types::{U256, TransactionRequest, RichRawTransaction, H160, H256, H520, Bytes, TransactionCondition};
use v1::types::{U256, TransactionRequest, RichRawTransaction, H160, H256, H520, Bytes, TransactionCondition, Origin};
use v1::helpers;
/// Confirmation waiting in a queue
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConfirmationRequest {
/// Id of this confirmation
pub id: U256,
/// Payload
pub payload: ConfirmationPayload,
/// Request origin
pub origin: Origin,
}
impl From<helpers::ConfirmationRequest> for ConfirmationRequest {
@ -38,13 +41,14 @@ impl From<helpers::ConfirmationRequest> for ConfirmationRequest {
ConfirmationRequest {
id: c.id.into(),
payload: c.payload.into(),
origin: c.origin,
}
}
}
impl fmt::Display for ConfirmationRequest {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "#{}: {}", self.id, self.payload)
write!(f, "#{}: {} coming from {}", self.id, self.payload, self.origin)
}
}
@ -61,6 +65,7 @@ impl fmt::Display for ConfirmationPayload {
/// Sign request
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SignRequest {
/// Address
pub address: H160,
@ -90,6 +95,7 @@ impl fmt::Display for SignRequest {
/// Decrypt request
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DecryptRequest {
/// Address
pub address: H160,
@ -153,6 +159,7 @@ pub struct ConfirmationResponseWithToken {
/// Confirmation payload, i.e. the thing to be confirmed
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum ConfirmationPayload {
/// Send Transaction
#[serde(rename="sendTransaction")]
@ -249,11 +256,12 @@ mod tests {
let request = helpers::ConfirmationRequest {
id: 15.into(),
payload: helpers::ConfirmationPayload::Signature(1.into(), vec![5].into()),
origin: Origin::Rpc("test service".into()),
};
// when
let res = serde_json::to_string(&ConfirmationRequest::from(request));
let expected = r#"{"id":"0xf","payload":{"sign":{"address":"0x0000000000000000000000000000000000000001","data":"0x05"}}}"#;
let expected = r#"{"id":"0xf","payload":{"sign":{"address":"0x0000000000000000000000000000000000000001","data":"0x05"}},"origin":{"rpc":"test service"}}"#;
// then
assert_eq!(res.unwrap(), expected.to_owned());
@ -275,11 +283,12 @@ mod tests {
nonce: Some(1.into()),
condition: None,
}),
origin: Origin::Signer(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}}}"#;
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"}}"#;
// then
assert_eq!(res.unwrap(), expected.to_owned());
@ -301,11 +310,12 @@ mod tests {
nonce: Some(1.into()),
condition: None,
}),
origin: Origin::Dapps("http://parity.io".into()),
};
// when
let res = serde_json::to_string(&ConfirmationRequest::from(request));
let expected = r#"{"id":"0xf","payload":{"signTransaction":{"from":"0x0000000000000000000000000000000000000000","to":null,"gasPrice":"0x2710","gas":"0x3a98","value":"0x186a0","data":"0x010203","nonce":"0x1","condition":null}}}"#;
let expected = r#"{"id":"0xf","payload":{"signTransaction":{"from":"0x0000000000000000000000000000000000000000","to":null,"gasPrice":"0x2710","gas":"0x3a98","value":"0x186a0","data":"0x010203","nonce":"0x1","condition":null}},"origin":{"dapp":"http://parity.io"}}"#;
// then
assert_eq!(res.unwrap(), expected.to_owned());
@ -319,11 +329,12 @@ mod tests {
payload: helpers::ConfirmationPayload::Decrypt(
10.into(), vec![1, 2, 3].into(),
),
origin: Default::default(),
};
// when
let res = serde_json::to_string(&ConfirmationRequest::from(request));
let expected = r#"{"id":"0xf","payload":{"decrypt":{"address":"0x000000000000000000000000000000000000000a","msg":"0x010203"}}}"#;
let expected = r#"{"id":"0xf","payload":{"decrypt":{"address":"0x000000000000000000000000000000000000000a","msg":"0x010203"}},"origin":"unknown"}"#;
// then
assert_eq!(res.unwrap(), expected.to_owned());

View File

@ -1,80 +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/>.
//! Dapp Id type
use ethcore::account_provider::DappId as EthDappId;
/// Dapplication Internal Id
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
pub struct DappId(pub String);
impl Into<String> for DappId {
fn into(self) -> String {
self.0
}
}
impl From<String> for DappId {
fn from(s: String) -> Self {
DappId(s)
}
}
impl From<EthDappId> for DappId {
fn from(id: EthDappId) -> Self {
DappId(id.into())
}
}
impl Into<EthDappId> for DappId {
fn into(self) -> EthDappId {
Into::<String>::into(self).into()
}
}
#[cfg(test)]
mod tests {
use serde_json;
use super::DappId;
#[test]
fn should_serialize_dapp_id() {
// given
let id = DappId("testapp".into());
// when
let res = serde_json::to_string(&id).unwrap();
// then
assert_eq!(res, r#""testapp""#);
}
#[test]
fn should_deserialize_dapp_id() {
// given
let id = r#""testapp""#;
// when
let res: DappId = serde_json::from_str(id).unwrap();
// then
assert_eq!(res, DappId("testapp".into()));
}
}

View File

@ -36,11 +36,18 @@ macro_rules! impl_hash {
}
impl fmt::Debug for $name {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0.to_hex())
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let hex = self.0.to_hex();
write!(f, "{}..{}", &hex[0..2], &hex[$size-2..$size])
}
}
impl<T> From<T> for $name where $other: From<T> {
fn from(o: T) -> Self {
$name($other::from(o).0)

View File

@ -18,29 +18,30 @@
//! RPC types
mod account_info;
mod bytes;
mod block;
mod block_number;
mod bytes;
mod call_request;
mod confirmations;
mod dapp_id;
mod consensus_status;
mod filter;
mod hash;
mod histogram;
mod index;
mod log;
mod provenance;
mod receipt;
mod rpc_settings;
mod sync;
mod trace;
mod trace_filter;
mod transaction;
mod transaction_request;
mod transaction_condition;
mod receipt;
mod rpc_settings;
mod trace;
mod trace_filter;
mod uint;
mod work;
mod histogram;
mod consensus_status;
pub use self::account_info::{AccountInfo, HwAccountInfo};
pub use self::bytes::Bytes;
pub use self::block::{RichBlock, Block, BlockTransactions};
pub use self::block_number::BlockNumber;
@ -49,24 +50,24 @@ pub use self::confirmations::{
ConfirmationPayload, ConfirmationRequest, ConfirmationResponse, ConfirmationResponseWithToken,
TransactionModification, SignRequest, DecryptRequest, Either
};
pub use self::dapp_id::DappId;
pub use self::consensus_status::*;
pub use self::filter::{Filter, FilterChanges};
pub use self::hash::{H64, H160, H256, H512, H520, H2048};
pub use self::histogram::Histogram;
pub use self::index::Index;
pub use self::log::Log;
pub use self::provenance::{Origin, DappId};
pub use self::receipt::Receipt;
pub use self::rpc_settings::RpcSettings;
pub use self::sync::{
SyncStatus, SyncInfo, Peers, PeerInfo, PeerNetworkInfo, PeerProtocolsInfo,
TransactionStats, ChainStatus, EthProtocolInfo, LesProtocolInfo,
};
pub use self::trace::{LocalizedTrace, TraceResults};
pub use self::trace_filter::TraceFilter;
pub use self::transaction::{Transaction, RichRawTransaction, LocalTransactionStatus};
pub use self::transaction_request::TransactionRequest;
pub use self::transaction_condition::TransactionCondition;
pub use self::receipt::Receipt;
pub use self::rpc_settings::RpcSettings;
pub use self::trace::{LocalizedTrace, TraceResults};
pub use self::trace_filter::TraceFilter;
pub use self::uint::{U128, U256};
pub use self::work::Work;
pub use self::histogram::Histogram;
pub use self::consensus_status::*;
pub use self::account_info::{AccountInfo, HwAccountInfo};

View File

@ -0,0 +1,154 @@
// 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/>.
//! Request Provenance
use std::fmt;
use ethcore::account_provider::DappId as EthDappId;
use v1::types::H256;
/// RPC request origin
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Origin {
/// RPC server (includes request origin)
#[serde(rename="rpc")]
Rpc(String),
/// Dapps server (includes DappId)
#[serde(rename="dapp")]
Dapps(DappId),
/// IPC server (includes session hash)
#[serde(rename="ipc")]
Ipc(H256),
/// Signer (includes session hash)
#[serde(rename="signer")]
Signer(H256),
/// Unknown
#[serde(rename="unknown")]
Unknown,
}
impl Default for Origin {
fn default() -> Self {
Origin::Unknown
}
}
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::Dapps(ref origin) => write!(f, "Dapp {}", origin),
Origin::Ipc(ref session) => write!(f, "IPC (session: {})", session),
Origin::Signer(ref session) => write!(f, "UI (session: {})", session),
Origin::Unknown => write!(f, "unknown origin"),
}
}
}
/// Dapplication Internal Id
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
pub struct DappId(pub String);
impl fmt::Display for DappId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Into<String> for DappId {
fn into(self) -> String {
self.0
}
}
impl From<String> for DappId {
fn from(s: String) -> Self {
DappId(s)
}
}
impl<'a> From<&'a str> for DappId {
fn from(s: &'a str) -> Self {
DappId(s.to_owned())
}
}
impl From<EthDappId> for DappId {
fn from(id: EthDappId) -> Self {
DappId(id.into())
}
}
impl Into<EthDappId> for DappId {
fn into(self) -> EthDappId {
Into::<String>::into(self).into()
}
}
#[cfg(test)]
mod tests {
use serde_json;
use super::{DappId, Origin};
#[test]
fn should_serialize_origin() {
// given
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 o5 = Origin::Unknown;
// when
let res1 = serde_json::to_string(&o1).unwrap();
let res2 = serde_json::to_string(&o2).unwrap();
let res3 = serde_json::to_string(&o3).unwrap();
let res4 = serde_json::to_string(&o4).unwrap();
let res5 = serde_json::to_string(&o5).unwrap();
// then
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!(res5, r#""unknown""#);
}
#[test]
fn should_serialize_dapp_id() {
// given
let id = DappId("testapp".into());
// when
let res = serde_json::to_string(&id).unwrap();
// then
assert_eq!(res, r#""testapp""#);
}
#[test]
fn should_deserialize_dapp_id() {
// given
let id = r#""testapp""#;
// when
let res: DappId = serde_json::from_str(id).unwrap();
// then
assert_eq!(res, DappId("testapp".into()));
}
}

View File

@ -18,6 +18,7 @@ use ethcore;
/// Represents condition on minimum block number or block timestamp.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum TransactionCondition {
/// Valid at this minimum block number.
#[serde(rename="block")]

View File

@ -18,6 +18,9 @@ extern crate log;
#[cfg(test)]
mod tests {
#[macro_use]
extern crate matches;
use futures::Future;
use std::path::PathBuf;
use client::{Rpc, RpcError};

View File

@ -33,6 +33,8 @@ use rpc::informant::RpcStats;
mod session;
pub use self::session::MetaExtractor;
/// Signer startup error
#[derive(Debug)]
pub enum ServerError {
@ -51,6 +53,11 @@ impl From<ws::Error> for ServerError {
}
}
/// 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>,
@ -86,6 +93,17 @@ impl ServerBuilder {
/// 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>>(self, addr: SocketAddr, handler: RpcHandler<M, S>) -> Result<Server, ServerError> {
self.start_with_extractor(addr, handler, 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>, T: session::MetaExtractor<M>>(
self,
addr: SocketAddr,
handler: RpcHandler<M, S>,
meta_extractor: T,
) -> Result<Server, ServerError> {
Server::start(
addr,
handler,
@ -93,8 +111,10 @@ impl ServerBuilder {
self.authcodes_path,
self.skip_origin_validation,
self.stats,
meta_extractor,
)
}
}
/// `WebSockets` server implementation.
@ -114,13 +134,14 @@ impl Server {
/// 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>>(
fn start<M: Metadata, S: Middleware<M>, T: session::MetaExtractor<M>>(
addr: SocketAddr,
handler: RpcHandler<M, S>,
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();
@ -135,7 +156,7 @@ impl Server {
let origin = format!("{}", addr);
let port = addr.port();
let ws = ws::Builder::new().with_settings(config).build(
session::Factory::new(handler, origin, port, authcodes_path, skip_origin_validation, stats)
session::Factory::new(handler, origin, port, authcodes_path, skip_origin_validation, stats, meta_extractor)
)?;
let panic_handler = PanicHandler::new_in_arc();

View File

@ -16,15 +16,16 @@
//! Session handlers factory.
use ws;
use authcode_store::AuthCodes;
use std::path::{PathBuf, Path};
use std::sync::Arc;
use std::str::FromStr;
use authcode_store::AuthCodes;
use jsonrpc_core::{Metadata, Middleware};
use jsonrpc_core::reactor::RpcHandler;
use rpc::informant::RpcStats;
use util::{H256, version};
use ws;
#[cfg(feature = "parity-ui")]
mod ui {
@ -78,35 +79,39 @@ fn origin_is_allowed(self_origin: &str, header: Option<&[u8]>) -> bool {
}
}
fn auth_is_valid(codes_path: &Path, protocols: ws::Result<Vec<&str>>) -> bool {
fn auth_token_hash(codes_path: &Path, protocols: ws::Result<Vec<&str>>) -> Option<H256> {
match protocols {
Ok(ref protocols) if protocols.len() == 1 => {
protocols.iter().any(|protocol| {
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());
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)
.map(|mut codes| {
// remove old tokens
codes.clear_garbage();
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.");
}
res
})
.unwrap_or(false)
} else {
false
}
})
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
}
},
_ => false
_ => None,
}
}
@ -125,7 +130,16 @@ fn add_headers(mut response: ws::Response, mime: &str) -> ws::Response {
response
}
pub struct Session<M: Metadata, S: Middleware<M>> {
/// 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,
@ -134,16 +148,16 @@ pub struct Session<M: Metadata, S: Middleware<M>> {
handler: RpcHandler<M, S>,
file_handler: Arc<ui::Handler>,
stats: Option<Arc<RpcStats>>,
meta_extractor: T,
}
impl<M: Metadata, S: Middleware<M>> Drop for Session<M, S> {
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>> ws::Handler for Session<M, S> {
#[cfg_attr(feature="dev", allow(collapsible_if))]
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);
@ -186,9 +200,15 @@ impl<M: Metadata, S: Middleware<M>> ws::Handler for Session<M, S> {
// (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
if !auth_is_valid(&self.authcodes_path, req.protocols()) {
info!(target: "signer", "Unauthorized connection to Signer API blocked.");
return Ok(error(ErrorType::Forbidden, "Not Authorized", "Request to this API was not authorized.", None));
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.");
@ -214,8 +234,8 @@ impl<M: Metadata, S: Middleware<M>> ws::Handler for Session<M, S> {
fn on_message(&mut self, msg: ws::Message) -> ws::Result<()> {
let req = msg.as_text()?;
let out = self.out.clone();
// TODO [ToDr] Extract metadata for PubSub/Session
let metadata = Default::default();
// TODO [ToDr] Move to on_connect
let metadata = self.meta_extractor.extract_metadata(&self.session_id);
self.handler.handle_request(req, metadata, move |response| {
if let Some(result) = response {
@ -229,24 +249,26 @@ impl<M: Metadata, S: Middleware<M>> ws::Handler for Session<M, S> {
}
}
pub struct Factory<M: Metadata, S: Middleware<M>> {
pub struct Factory<M: Metadata, S: Middleware<M>, T> {
handler: RpcHandler<M, S>,
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>> Factory<M, S> {
impl<M: Metadata, S: Middleware<M>, T> Factory<M, S, T> {
pub fn new(
handler: RpcHandler<M, S>,
self_origin: String,
self_port: u16,
self_port: u16,
authcodes_path: PathBuf,
skip_origin_validation: bool,
stats: Option<Arc<RpcStats>>,
meta_extractor: T,
) -> Self {
Factory {
handler: handler,
@ -254,25 +276,28 @@ impl<M: Metadata, S: Middleware<M>> Factory<M, S> {
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>> ws::Factory for Factory<M, S> {
type Handler = Session<M, S>;
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(),
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(),
}