diff --git a/Cargo.lock b/Cargo.lock index b94a67cb7..83b7e78dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,9 +34,11 @@ dependencies = [ "number_prefix 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", "parity-hash-fetch 1.5.0", "parity-updater 1.5.0", + "parity-rpc-client 1.4.0", "regex 0.1.68 (registry+https://github.com/rust-lang/crates.io-index)", "rlp 0.1.0", "rpassword 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rpc-cli 1.4.0", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -733,6 +735,14 @@ dependencies = [ "miniz-sys 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "futures" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "gcc" version = "0.3.35" @@ -874,6 +884,18 @@ name = "itoa" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "jsonrpc-core" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_codegen 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "jsonrpc-core" version = "4.0.0" @@ -1307,6 +1329,7 @@ dependencies = [ ] [[package]] +<<<<<<< HEAD name = "parity-hash-fetch" version = "1.5.0" dependencies = [ @@ -1316,6 +1339,25 @@ dependencies = [ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)", "rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)", +======= +name = "parity-rpc-client" +version = "1.4.0" +dependencies = [ + "ethcore-rpc 1.5.0", + "ethcore-signer 1.5.0", + "ethcore-util 1.5.0", + "futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "jsonrpc-core 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "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)", + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "ws 0.5.3 (git+https://github.com/ethcore/ws-rs.git?branch=mio-upstream-stable)", +>>>>>>> origin/master ] [[package]] @@ -1337,7 +1379,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#762c6d10f8640a6c4b875d776490282680bfe3e2" +source = "git+https://github.com/ethcore/js-precompiled.git#ad6617a73dbb17c53dddc0fc567e70ea5b8e882f" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1600,6 +1642,28 @@ dependencies = [ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rpassword" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", + "termios 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rpc-cli" +version = "1.4.0" +dependencies = [ + "ethcore-bigint 0.1.2", + "ethcore-rpc 1.5.0", + "ethcore-util 1.5.0", + "futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "parity-rpc-client 1.4.0", + "rpassword 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rust-crypto" version = "0.2.36" @@ -1856,6 +1920,14 @@ name = "target_info" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "tempdir" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "term" version = "0.2.14" @@ -2131,6 +2203,7 @@ dependencies = [ "checksum ethabi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f7b0c53453517f620847be51943db329276ae52f2e210cfc659e81182864be2f" "checksum fdlimit 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b1ee15a7050e5580b3712877157068ea713b245b080ff302ae2ca973cfcd9baa" "checksum flate2 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "3eeb481e957304178d2e782f2da1257f1434dfecbae883bafb61ada2a9fea3bb" +"checksum futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0bad0a2ac64b227fdc10c254051ae5af542cf19c9328704fd4092f7914196897" "checksum gcc 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)" = "91ecd03771effb0c968fd6950b37e89476a578aaf1c70297d8e92b6516ec3312" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" "checksum hamming 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65043da274378d68241eb9a8f8f8aa54e349136f7b8e12f63e3ef44043cc30e1" @@ -2144,6 +2217,7 @@ dependencies = [ "checksum isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7408a548dc0e406b7912d9f84c261cc533c1866e047644a811c133c56041ac0c" "checksum itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)" = "086e1fa5fe48840b1cfdef3a20c7e3115599f8d5c4c87ef32a794a7cdd184d76" "checksum itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3088ea4baeceb0284ee9eea42f591226e6beaecf65373e41b38d95a1b8e7a1" +"checksum jsonrpc-core 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3c5094610b07f28f3edaf3947b732dadb31dbba4941d4d0c1c7a8350208f4414" "checksum jsonrpc-core 4.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "" "checksum jsonrpc-http-server 6.1.1 (git+https://github.com/ethcore/jsonrpc.git)" = "" "checksum jsonrpc-ipc-server 0.2.4 (git+https://github.com/ethcore/jsonrpc.git)" = "" @@ -2218,6 +2292,7 @@ dependencies = [ "checksum rocksdb-sys 0.3.0 (git+https://github.com/ethcore/rust-rocksdb)" = "" "checksum rotor 0.6.3 (git+https://github.com/ethcore/rotor)" = "" "checksum rpassword 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5d3a99497c5c544e629cc8b359ae5ede321eba5fa8e5a8078f3ced727a976c3f" +"checksum rpassword 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ab6e42be826e215f30ff830904f8f4a0933c6e2ae890e1af8b408f5bae60081e" "checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" "checksum rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)" = "6159e4e6e559c81bd706afe9c8fd68f547d3e851ce12e76b1de7914bab61691b" "checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" @@ -2249,6 +2324,7 @@ dependencies = [ "checksum syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44bded3cabafc65c90b663b1071bd2d198a9ab7515e6ce729e4570aaf53c407e" "checksum syntex_syntax 0.42.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7628a0506e8f9666fdabb5f265d0059b059edac9a3f810bda077abb5d826bd8d" "checksum target_info 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c63f48baada5c52e65a29eef93ab4f8982681b67f9e8d29c7b05abcfec2b9ffe" +"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" "checksum term 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "f2077e54d38055cf1ca0fd7933a2e00cd3ec8f6fed352b2a377f06dcdaaf3281" "checksum term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3deff8a2b3b6607d6d7cc32ac25c0b33709453ca9cceac006caac51e963cf94a" "checksum termios 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" diff --git a/Cargo.toml b/Cargo.toml index aea39c8ff..8c621b387 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,9 @@ ethcore-ipc-hypervisor = { path = "ipc/hypervisor" } ethcore-logger = { path = "logger" } ethcore-stratum = { path = "stratum" } ethcore-dapps = { path = "dapps", optional = true } +clippy = { version = "0.0.103", optional = true} +rpc-cli = { path = "rpc_cli" } +parity-rpc-client = { path = "rpc_client" } ethcore-light = { path = "ethcore/light" } parity-hash-fetch = { path = "hash-fetch" } parity-updater = { path = "updater" } diff --git a/ethcore/src/account_provider/mod.rs b/ethcore/src/account_provider/mod.rs index d91849fd5..dab19dbc0 100644 --- a/ethcore/src/account_provider/mod.rs +++ b/ethcore/src/account_provider/mod.rs @@ -18,7 +18,7 @@ mod stores; -use self::stores::{AddressBook, DappsSettingsStore}; +use self::stores::{AddressBook, DappsSettingsStore, NewDappsPolicy}; use std::fmt; use std::collections::HashMap; @@ -156,10 +156,49 @@ impl AccountProvider { Ok(accounts) } + /// Sets a whitelist of accounts exposed for unknown dapps. + /// `None` means that all accounts will be visible. + pub fn set_new_dapps_whitelist(&self, accounts: Option>) -> Result<(), Error> { + self.dapps_settings.write().set_policy(match accounts { + None => NewDappsPolicy::AllAccounts, + Some(accounts) => NewDappsPolicy::Whitelist(accounts), + }); + Ok(()) + } + + /// Gets a whitelist of accounts exposed for unknown dapps. + /// `None` means that all accounts will be visible. + pub fn new_dapps_whitelist(&self) -> Result>, Error> { + Ok(match self.dapps_settings.read().policy() { + NewDappsPolicy::AllAccounts => None, + NewDappsPolicy::Whitelist(accounts) => Some(accounts), + }) + } + + /// Gets a list of dapps recently requesting accounts. + pub fn recent_dapps(&self) -> Result, Error> { + Ok(self.dapps_settings.read().recent_dapps()) + } + + /// Marks dapp as recently used. + pub fn note_dapp_used(&self, dapp: DappId) -> Result<(), Error> { + let mut dapps = self.dapps_settings.write(); + dapps.mark_dapp_used(dapp.clone()); + Ok(()) + } + /// Gets addresses visile for dapp. pub fn dapps_addresses(&self, dapp: DappId) -> Result, Error> { - let accounts = self.dapps_settings.read().get(); - Ok(accounts.get(&dapp).map(|settings| settings.accounts.clone()).unwrap_or_else(Vec::new)) + let dapps = self.dapps_settings.read(); + + let accounts = dapps.settings().get(&dapp).map(|settings| settings.accounts.clone()); + match accounts { + Some(accounts) => Ok(accounts), + None => match dapps.policy() { + NewDappsPolicy::AllAccounts => self.accounts(), + NewDappsPolicy::Whitelist(accounts) => Ok(accounts), + } + } } /// Sets addresses visile for dapp. @@ -423,6 +462,8 @@ mod tests { // given let ap = AccountProvider::transient_provider(); let app = "app1".to_owned(); + // set `AllAccounts` policy + ap.set_new_dapps_whitelist(None).unwrap(); // when ap.set_dapps_addresses(app.clone(), vec![1.into(), 2.into()]).unwrap(); @@ -430,4 +471,23 @@ mod tests { // then assert_eq!(ap.dapps_addresses(app.clone()).unwrap(), vec![1.into(), 2.into()]); } + + #[test] + fn should_set_dapps_policy() { + // given + let ap = AccountProvider::transient_provider(); + let address = ap.new_account("test").unwrap(); + + // When returning nothing + ap.set_new_dapps_whitelist(Some(vec![])).unwrap(); + assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![]); + + // change to all + ap.set_new_dapps_whitelist(None).unwrap(); + assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![address]); + + // change to a whitelist + ap.set_new_dapps_whitelist(Some(vec![1.into()])).unwrap(); + assert_eq!(ap.dapps_addresses("app1".into()).unwrap(), vec![1.into()]); + } } diff --git a/ethcore/src/account_provider/stores.rs b/ethcore/src/account_provider/stores.rs index d7e96243c..d4f2093ee 100644 --- a/ethcore/src/account_provider/stores.rs +++ b/ethcore/src/account_provider/stores.rs @@ -17,11 +17,11 @@ //! Address Book and Dapps Settings Store use std::{fs, fmt, hash, ops}; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::path::PathBuf; use ethstore::ethkey::Address; -use ethjson::misc::{AccountMeta, DappsSettings as JsonSettings}; +use ethjson::misc::{AccountMeta, DappsSettings as JsonSettings, NewDappsPolicy as JsonNewDappsPolicy}; use account_provider::DappId; /// Disk-backed map from Address to String. Uses JSON. @@ -105,43 +105,106 @@ impl From for JsonSettings { } } +/// Dapps user settings +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum NewDappsPolicy { + AllAccounts, + Whitelist(Vec
), +} + +impl From for NewDappsPolicy { + fn from(s: JsonNewDappsPolicy) -> Self { + match s { + JsonNewDappsPolicy::AllAccounts => NewDappsPolicy::AllAccounts, + JsonNewDappsPolicy::Whitelist(accounts) => NewDappsPolicy::Whitelist( + accounts.into_iter().map(Into::into).collect() + ), + } + } +} + +impl From for JsonNewDappsPolicy { + fn from(s: NewDappsPolicy) -> Self { + match s { + NewDappsPolicy::AllAccounts => JsonNewDappsPolicy::AllAccounts, + NewDappsPolicy::Whitelist(accounts) => JsonNewDappsPolicy::Whitelist( + accounts.into_iter().map(Into::into).collect() + ), + } + } +} + +const MAX_RECENT_DAPPS: usize = 10; + /// Disk-backed map from DappId to Settings. Uses JSON. pub struct DappsSettingsStore { - cache: DiskMap, + /// Dapps Settings + settings: DiskMap, + /// New Dapps Policy + policy: DiskMap, + /// Recently Accessed Dapps (transient) + recent: VecDeque, } impl DappsSettingsStore { /// Creates new store at given directory path. pub fn new(path: String) -> Self { let mut r = DappsSettingsStore { - cache: DiskMap::new(path, "dapps_accounts.json".into()) + settings: DiskMap::new(path.clone(), "dapps_accounts.json".into()), + policy: DiskMap::new(path.clone(), "dapps_policy.json".into()), + recent: VecDeque::with_capacity(MAX_RECENT_DAPPS), }; - r.cache.revert(JsonSettings::read_dapps_settings); + r.settings.revert(JsonSettings::read_dapps_settings); + r.policy.revert(JsonNewDappsPolicy::read_new_dapps_policy); r } /// Creates transient store (no changes are saved to disk). pub fn transient() -> Self { DappsSettingsStore { - cache: DiskMap::transient() + settings: DiskMap::transient(), + policy: DiskMap::transient(), + recent: VecDeque::with_capacity(MAX_RECENT_DAPPS), } } /// Get copy of the dapps settings - pub fn get(&self) -> HashMap { - self.cache.clone() + pub fn settings(&self) -> HashMap { + self.settings.clone() } - fn save(&self) { - self.cache.save(JsonSettings::write_dapps_settings) + /// Returns current new dapps policy + pub fn policy(&self) -> NewDappsPolicy { + self.policy.get("default").cloned().unwrap_or(NewDappsPolicy::AllAccounts) } + /// Returns recent dapps (in order of last request) + pub fn recent_dapps(&self) -> Vec { + self.recent.iter().cloned().collect() + } + + /// Marks recent dapp as used + pub fn mark_dapp_used(&mut self, dapp: DappId) { + self.recent.retain(|id| id != &dapp); + self.recent.push_front(dapp); + while self.recent.len() > MAX_RECENT_DAPPS { + self.recent.pop_back(); + } + } + + /// Sets current new dapps policy + pub fn set_policy(&mut self, policy: NewDappsPolicy) { + self.policy.insert("default".into(), policy); + self.policy.save(JsonNewDappsPolicy::write_new_dapps_policy); + } + + /// Sets accounts for specific dapp. pub fn set_accounts(&mut self, id: DappId, accounts: Vec
) { { - let mut settings = self.cache.entry(id).or_insert_with(DappsSettings::default); + let mut settings = self.settings.entry(id).or_insert_with(DappsSettings::default); settings.accounts = accounts; } - self.save(); + self.settings.save(JsonSettings::write_dapps_settings); } } @@ -216,7 +279,7 @@ impl DiskMap { #[cfg(test)] mod tests { - use super::{AddressBook, DappsSettingsStore, DappsSettings}; + use super::{AddressBook, DappsSettingsStore, DappsSettings, NewDappsPolicy}; use std::collections::HashMap; use ethjson::misc::AccountMeta; use devtools::RandomTempPath; @@ -232,25 +295,6 @@ mod tests { assert_eq!(b.get(), hash_map![1.into() => AccountMeta{name: "One".to_owned(), meta: "{1:1}".to_owned(), uuid: None}]); } - #[test] - fn should_save_and_reload_dapps_settings() { - // given - let temp = RandomTempPath::create_dir(); - let path = temp.as_str().to_owned(); - let mut b = DappsSettingsStore::new(path.clone()); - - // when - b.set_accounts("dappOne".into(), vec![1.into(), 2.into()]); - - // then - let b = DappsSettingsStore::new(path); - assert_eq!(b.get(), hash_map![ - "dappOne".into() => DappsSettings { - accounts: vec![1.into(), 2.into()], - } - ]); - } - #[test] fn should_remove_address() { let temp = RandomTempPath::create_dir(); @@ -268,4 +312,58 @@ mod tests { 3.into() => AccountMeta{name: "Three".to_owned(), meta: "{}".to_owned(), uuid: None} ]); } + + #[test] + fn should_save_and_reload_dapps_settings() { + // given + let temp = RandomTempPath::create_dir(); + let path = temp.as_str().to_owned(); + let mut b = DappsSettingsStore::new(path.clone()); + + // when + b.set_accounts("dappOne".into(), vec![1.into(), 2.into()]); + + // then + let b = DappsSettingsStore::new(path); + assert_eq!(b.settings(), hash_map![ + "dappOne".into() => DappsSettings { + accounts: vec![1.into(), 2.into()], + } + ]); + } + + #[test] + fn should_maintain_a_list_of_recent_dapps() { + let mut store = DappsSettingsStore::transient(); + assert!(store.recent_dapps().is_empty(), "Initially recent dapps should be empty."); + + store.mark_dapp_used("dapp1".into()); + assert_eq!(store.recent_dapps(), vec!["dapp1".to_owned()]); + + store.mark_dapp_used("dapp2".into()); + assert_eq!(store.recent_dapps(), vec!["dapp2".to_owned(), "dapp1".to_owned()]); + + store.mark_dapp_used("dapp1".into()); + assert_eq!(store.recent_dapps(), vec!["dapp1".to_owned(), "dapp2".to_owned()]); + } + + #[test] + fn should_store_dapps_policy() { + // given + let temp = RandomTempPath::create_dir(); + let path = temp.as_str().to_owned(); + let mut store = DappsSettingsStore::new(path.clone()); + + // Test default policy + assert_eq!(store.policy(), NewDappsPolicy::AllAccounts); + + // when + store.set_policy(NewDappsPolicy::Whitelist(vec![1.into(), 2.into()])); + + // then + let store = DappsSettingsStore::new(path); + assert_eq!(store.policy.clone(), hash_map![ + "default".into() => NewDappsPolicy::Whitelist(vec![1.into(), 2.into()]) + ]); + } } diff --git a/js/package.json b/js/package.json index e3b397025..009795973 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.124", + "version": "0.2.125", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", diff --git a/js/src/api/contract/contract.js b/js/src/api/contract/contract.js index 95dcf2e72..68c0371a1 100644 --- a/js/src/api/contract/contract.js +++ b/js/src/api/contract/contract.js @@ -342,7 +342,8 @@ export default class Contract { options: _options, autoRemove, callback, - filterId + filterId, + id: subscriptionId }; if (skipInitFetch) { @@ -452,13 +453,13 @@ export default class Contract { }) ) .then((logsArray) => { - logsArray.forEach((logs, subscriptionId) => { + logsArray.forEach((logs, index) => { if (!logs || !logs.length) { return; } try { - this.sendData(subscriptionId, null, this.parseEventLogs(logs)); + this._sendData(subscriptions[index].id, null, this.parseEventLogs(logs)); } catch (error) { console.error('_sendSubscriptionChanges', error); } diff --git a/json/src/misc/dapps_settings.rs b/json/src/misc/dapps_settings.rs index 74a12a331..fdbb671eb 100644 --- a/json/src/misc/dapps_settings.rs +++ b/json/src/misc/dapps_settings.rs @@ -49,3 +49,32 @@ impl DappsSettings { serde_json::to_writer(writer, &m.iter().map(|(a, m)| (a.clone().into(), m.clone().into())).collect::>()) } } + +/// Accounts policy for new dapps. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum NewDappsPolicy { + /// All accounts are exposed by default. + AllAccounts, + /// Only accounts listed here are exposed by default for new dapps. + Whitelist(Vec), +} + +impl NewDappsPolicy { + /// Read a hash map of `String -> NewDappsPolicy` + pub fn read_new_dapps_policy(reader: R) -> Result, serde_json::Error> where + R: io::Read, + S: From + Clone, + { + serde_json::from_reader(reader).map(|ok: HashMap| + ok.into_iter().map(|(a, m)| (a.into(), m.into())).collect() + ) + } + + /// Write a hash map of `String -> NewDappsPolicy` + pub fn write_new_dapps_policy(m: &HashMap, writer: &mut W) -> Result<(), serde_json::Error> where + W: io::Write, + S: Into + Clone, + { + serde_json::to_writer(writer, &m.iter().map(|(a, m)| (a.clone().into(), m.clone().into())).collect::>()) + } +} diff --git a/json/src/misc/mod.rs b/json/src/misc/mod.rs index b64afcc31..0a1e93fa9 100644 --- a/json/src/misc/mod.rs +++ b/json/src/misc/mod.rs @@ -19,5 +19,5 @@ mod account_meta; mod dapps_settings; -pub use self::dapps_settings::DappsSettings; +pub use self::dapps_settings::{DappsSettings, NewDappsPolicy}; pub use self::account_meta::AccountMeta; diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index 790a35399..91f407e1e 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -32,6 +32,8 @@ usage! { cmd_import: bool, cmd_signer: bool, cmd_new_token: bool, + cmd_sign: bool, + cmd_reject: bool, cmd_snapshot: bool, cmd_restore: bool, cmd_ui: bool, @@ -44,6 +46,7 @@ usage! { arg_pid_file: String, arg_file: Option, arg_path: Vec, + arg_id: Option, // Flags // -- Legacy Options @@ -515,6 +518,8 @@ mod tests { cmd_blocks: false, cmd_import: false, cmd_signer: false, + cmd_sign: false, + cmd_reject: false, cmd_new_token: false, cmd_snapshot: false, cmd_restore: false, @@ -527,6 +532,7 @@ mod tests { // Arguments arg_pid_file: "".into(), arg_file: None, + arg_id: None, arg_path: vec![], // -- Operating Options diff --git a/parity/cli/usage.txt b/parity/cli/usage.txt index 231e4778d..e05d94751 100644 --- a/parity/cli/usage.txt +++ b/parity/cli/usage.txt @@ -12,6 +12,9 @@ Usage: parity import [ ] [options] parity export (blocks | state) [ ] [options] parity signer new-token [options] + parity signer list [options] + parity signer sign [ ] [ --password FILE ] [options] + parity signer reject [options] parity snapshot [options] parity restore [ ] [options] parity tools hash diff --git a/parity/configuration.rs b/parity/configuration.rs index 0fee3799d..4019c4ec3 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -44,6 +44,8 @@ use presale::ImportWallet; use account::{AccountCmd, NewAccount, ListAccounts, ImportAccounts, ImportFromGethAccounts}; use snapshot::{self, SnapshotCommand}; +const AUTHCODE_FILENAME: &'static str = "authcodes"; + #[derive(Debug, PartialEq)] pub enum Cmd { Run(RunCmd), @@ -52,6 +54,21 @@ pub enum Cmd { ImportPresaleWallet(ImportWallet), Blockchain(BlockchainCmd), SignerToken(SignerConfiguration), + SignerSign { + id: Option, + pwfile: Option, + port: u16, + authfile: PathBuf, + }, + SignerList { + port: u16, + authfile: PathBuf + }, + SignerReject { + id: Option, + port: u16, + authfile: PathBuf + }, Snapshot(SnapshotCommand), Hash(Option), } @@ -105,8 +122,36 @@ impl Configuration { let cmd = if self.args.flag_version { Cmd::Version - } else if self.args.cmd_signer && self.args.cmd_new_token { - Cmd::SignerToken(signer_conf) + } else if self.args.cmd_signer { + let mut authfile = PathBuf::from(signer_conf.signer_path.clone()); + authfile.push(AUTHCODE_FILENAME); + + if self.args.cmd_new_token { + Cmd::SignerToken(signer_conf) + } else if self.args.cmd_sign { + let pwfile = self.args.flag_password.get(0).map(|pwfile| { + PathBuf::from(pwfile) + }); + Cmd::SignerSign { + id: self.args.arg_id, + pwfile: pwfile, + port: signer_conf.port, + authfile: authfile, + } + } else if self.args.cmd_reject { + Cmd::SignerReject { + id: self.args.arg_id, + port: signer_conf.port, + authfile: authfile, + } + } else if self.args.cmd_list { + Cmd::SignerList { + port: signer_conf.port, + authfile: authfile, + } + } else { + unreachable!(); + } } else if self.args.cmd_tools && self.args.cmd_hash { Cmd::Hash(self.args.arg_file) } else if self.args.cmd_db && self.args.cmd_kill { @@ -1176,4 +1221,3 @@ mod tests { assert!(conf.init_reserved_nodes().is_ok()); } } - diff --git a/parity/main.rs b/parity/main.rs index 64e3508cc..cd6dc8a17 100644 --- a/parity/main.rs +++ b/parity/main.rs @@ -72,6 +72,8 @@ extern crate ethcore_stratum; #[cfg(feature = "dapps")] extern crate ethcore_dapps; +extern crate rpc_cli; + macro_rules! dependency { ($dep_ty:ident, $url:expr) => { { @@ -155,6 +157,9 @@ fn execute(command: Execute, can_restart: bool) -> Result presale::execute(presale_cmd).map(|s| PostExecutionAction::Print(s)), Cmd::Blockchain(blockchain_cmd) => blockchain::execute(blockchain_cmd).map(|s| PostExecutionAction::Print(s)), Cmd::SignerToken(signer_cmd) => signer::execute(signer_cmd).map(|s| PostExecutionAction::Print(s)), + Cmd::SignerSign { id, pwfile, port, authfile } => rpc_cli::signer_sign(id, pwfile, port, authfile).map(|s| PostExecutionAction::Print(s)), + Cmd::SignerList { port, authfile } => rpc_cli::signer_list(port, authfile).map(|s| PostExecutionAction::Print(s)), + Cmd::SignerReject { id, port, authfile } => rpc_cli::signer_reject(id, port, authfile).map(|s| PostExecutionAction::Print(s)), Cmd::Snapshot(snapshot_cmd) => snapshot::execute(snapshot_cmd).map(|s| PostExecutionAction::Print(s)), } } diff --git a/rpc/src/v1/impls/eth.rs b/rpc/src/v1/impls/eth.rs index 1364af033..055e57475 100644 --- a/rpc/src/v1/impls/eth.rs +++ b/rpc/src/v1/impls/eth.rs @@ -340,7 +340,11 @@ impl Eth for EthClient where let dapp = id.0; let store = take_weak!(self.accounts); - let accounts = try!(store.dapps_addresses(dapp.into()).map_err(|e| errors::internal("Could not fetch accounts.", e))); + let accounts = try!(store + .note_dapp_used(dapp.clone().into()) + .and_then(|_| store.dapps_addresses(dapp.into())) + .map_err(|e| errors::internal("Could not fetch accounts.", e)) + ); Ok(accounts.into_iter().map(Into::into).collect()) } diff --git a/rpc/src/v1/impls/parity_accounts.rs b/rpc/src/v1/impls/parity_accounts.rs index bf53c7273..5fb21ccc7 100644 --- a/rpc/src/v1/impls/parity_accounts.rs +++ b/rpc/src/v1/impls/parity_accounts.rs @@ -164,19 +164,51 @@ impl ParityAccounts for ParityAccountsClient where C: MiningBlock fn set_dapps_addresses(&self, dapp: DappId, addresses: Vec) -> Result { let store = take_weak!(self.accounts); - let addresses = addresses.into_iter().map(Into::into).collect(); - store.set_dapps_addresses(dapp.into(), addresses) + store.set_dapps_addresses(dapp.into(), into_vec(addresses)) .map_err(|e| errors::account("Couldn't set dapps addresses.", e)) .map(|_| true) } + fn dapps_addresses(&self, dapp: DappId) -> Result, Error> { + let store = take_weak!(self.accounts); + + store.dapps_addresses(dapp.into()) + .map_err(|e| errors::account("Couldn't get dapps addresses.", e)) + .map(into_vec) + } + + fn set_new_dapps_whitelist(&self, whitelist: Option>) -> Result { + let store = take_weak!(self.accounts); + + store + .set_new_dapps_whitelist(whitelist.map(into_vec)) + .map_err(|e| errors::account("Couldn't set dapps whitelist.", e)) + .map(|_| true) + } + + fn new_dapps_whitelist(&self) -> Result>, Error> { + let store = take_weak!(self.accounts); + + store.new_dapps_whitelist() + .map_err(|e| errors::account("Couldn't get dapps whitelist.", e)) + .map(|accounts| accounts.map(into_vec)) + } + + fn recent_dapps(&self) -> Result, Error> { + let store = take_weak!(self.accounts); + + store.recent_dapps() + .map_err(|e| errors::account("Couldn't get recent dapps.", e)) + .map(into_vec) + } + fn import_geth_accounts(&self, addresses: Vec) -> Result, Error> { let store = take_weak!(self.accounts); store - .import_geth_accounts(addresses.into_iter().map(Into::into).collect(), false) - .map(|imported| imported.into_iter().map(Into::into).collect()) + .import_geth_accounts(into_vec(addresses), false) + .map(into_vec) .map_err(|e| errors::account("Couldn't import Geth accounts", e)) } @@ -184,10 +216,12 @@ impl ParityAccounts for ParityAccountsClient where C: MiningBlock try!(self.active()); let store = take_weak!(self.accounts); - Ok(store.list_geth_accounts(false) - .into_iter() - .map(Into::into) - .collect() - ) + Ok(into_vec(store.list_geth_accounts(false))) } } + +fn into_vec(a: Vec) -> Vec where + A: Into +{ + a.into_iter().map(Into::into).collect() +} diff --git a/rpc/src/v1/tests/mocked/eth.rs b/rpc/src/v1/tests/mocked/eth.rs index ea8409ef2..f428bbcbd 100644 --- a/rpc/src/v1/tests/mocked/eth.rs +++ b/rpc/src/v1/tests/mocked/eth.rs @@ -354,11 +354,18 @@ fn rpc_eth_gas_price() { #[test] fn rpc_eth_accounts() { let tester = EthTester::default(); - let _address = tester.accounts_provider.new_account("").unwrap(); + let address = tester.accounts_provider.new_account("").unwrap(); + tester.accounts_provider.set_new_dapps_whitelist(None).unwrap(); + // with current policy it should return the account + let request = r#"{"jsonrpc": "2.0", "method": "eth_accounts", "params": [], "id": 1}"#; + let response = r#"{"jsonrpc":"2.0","result":[""#.to_owned() + &format!("0x{:?}", address) + r#""],"id":1}"#; + assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned())); + + tester.accounts_provider.set_new_dapps_whitelist(Some(vec![1.into()])).unwrap(); // even with some account it should return empty list (no dapp detected) let request = r#"{"jsonrpc": "2.0", "method": "eth_accounts", "params": [], "id": 1}"#; - let response = r#"{"jsonrpc":"2.0","result":[],"id":1}"#; + let response = r#"{"jsonrpc":"2.0","result":["0x0000000000000000000000000000000000000001"],"id":1}"#; assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned())); // when we add visible address it should return that. diff --git a/rpc/src/v1/tests/mocked/parity_accounts.rs b/rpc/src/v1/tests/mocked/parity_accounts.rs index a30b6c43c..851af9ebd 100644 --- a/rpc/src/v1/tests/mocked/parity_accounts.rs +++ b/rpc/src/v1/tests/mocked/parity_accounts.rs @@ -117,7 +117,7 @@ fn should_be_able_to_set_meta() { } #[test] -fn rpc_parity_set_dapps_accounts() { +fn rpc_parity_set_and_get_dapps_accounts() { // given let tester = setup(); assert_eq!(tester.accounts.dapps_addresses("app1".into()).unwrap(), vec![]); @@ -129,6 +129,52 @@ fn rpc_parity_set_dapps_accounts() { // then assert_eq!(tester.accounts.dapps_addresses("app1".into()).unwrap(), vec![10.into()]); + let request = r#"{"jsonrpc": "2.0", "method": "parity_getDappsAddresses","params":["app1"], "id": 1}"#; + let response = r#"{"jsonrpc":"2.0","result":["0x000000000000000000000000000000000000000a"],"id":1}"#; + assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned())); +} + +#[test] +fn rpc_parity_set_and_get_new_dapps_whitelist() { + // given + let tester = setup(); + + // when set to whitelist + let request = r#"{"jsonrpc": "2.0", "method": "parity_setNewDappsWhitelist","params":[["0x000000000000000000000000000000000000000a"]], "id": 1}"#; + let response = r#"{"jsonrpc":"2.0","result":true,"id":1}"#; + assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned())); + + // then + assert_eq!(tester.accounts.new_dapps_whitelist().unwrap(), Some(vec![10.into()])); + let request = r#"{"jsonrpc": "2.0", "method": "parity_getNewDappsWhitelist","params":[], "id": 1}"#; + let response = r#"{"jsonrpc":"2.0","result":["0x000000000000000000000000000000000000000a"],"id":1}"#; + assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned())); + + // when set to empty + let request = r#"{"jsonrpc": "2.0", "method": "parity_setNewDappsWhitelist","params":[null], "id": 1}"#; + let response = r#"{"jsonrpc":"2.0","result":true,"id":1}"#; + assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned())); + + // then + assert_eq!(tester.accounts.new_dapps_whitelist().unwrap(), None); + let request = r#"{"jsonrpc": "2.0", "method": "parity_getNewDappsWhitelist","params":[], "id": 1}"#; + let response = r#"{"jsonrpc":"2.0","result":null,"id":1}"#; + assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned())); +} + +#[test] +fn rpc_parity_recent_dapps() { + // given + let tester = setup(); + + // when + // trigger dapp usage + tester.accounts.note_dapp_used("dapp1".into()).unwrap(); + + // then + let request = r#"{"jsonrpc": "2.0", "method": "parity_listRecentDapps","params":[], "id": 1}"#; + let response = r#"{"jsonrpc":"2.0","result":["dapp1"],"id":1}"#; + assert_eq!(tester.io.handle_request_sync(request), Some(response.to_owned())); } #[test] diff --git a/rpc/src/v1/traits/parity_accounts.rs b/rpc/src/v1/traits/parity_accounts.rs index d21a8459e..bf360c3c2 100644 --- a/rpc/src/v1/traits/parity_accounts.rs +++ b/rpc/src/v1/traits/parity_accounts.rs @@ -78,6 +78,24 @@ build_rpc_trait! { #[rpc(name = "parity_setDappsAddresses")] fn set_dapps_addresses(&self, DappId, Vec) -> Result; + /// Gets accounts exposed for particular dapp. + #[rpc(name = "parity_getDappsAddresses")] + fn dapps_addresses(&self, DappId) -> Result, Error>; + + /// Sets accounts exposed for new dapps. + /// `None` means that all accounts will be exposed. + #[rpc(name = "parity_setNewDappsWhitelist")] + fn set_new_dapps_whitelist(&self, Option>) -> Result; + + /// Gets accounts exposed for new dapps. + /// `None` means that all accounts will be exposed. + #[rpc(name = "parity_getNewDappsWhitelist")] + fn new_dapps_whitelist(&self) -> Result>, Error>; + + /// Sets accounts exposed for particular dapp. + #[rpc(name = "parity_listRecentDapps")] + fn recent_dapps(&self) -> Result, Error>; + /// Imports a number of Geth accounts, with the list provided as the argument. #[rpc(name = "parity_importGethAccounts")] fn import_geth_accounts(&self, Vec) -> Result, Error>; diff --git a/rpc/src/v1/types/confirmations.rs b/rpc/src/v1/types/confirmations.rs index db9f541d2..d7dc3f210 100644 --- a/rpc/src/v1/types/confirmations.rs +++ b/rpc/src/v1/types/confirmations.rs @@ -18,11 +18,13 @@ use std::fmt; use serde::{Serialize, Serializer}; +use util::log::Colour; + use v1::types::{U256, TransactionRequest, RichRawTransaction, H160, H256, H520, Bytes}; use v1::helpers; /// Confirmation waiting in a queue -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct ConfirmationRequest { /// Id of this confirmation pub id: U256, @@ -39,8 +41,25 @@ impl From for ConfirmationRequest { } } +impl fmt::Display for ConfirmationRequest { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "#{}: {}", self.id, self.payload) + } +} + +impl fmt::Display for ConfirmationPayload { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ConfirmationPayload::SendTransaction(ref transaction) => write!(f, "{}", transaction), + ConfirmationPayload::SignTransaction(ref transaction) => write!(f, "(Sign only) {}", transaction), + ConfirmationPayload::Signature(ref sign) => write!(f, "{}", sign), + ConfirmationPayload::Decrypt(ref decrypt) => write!(f, "{}", decrypt), + } + } +} + /// Sign request -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct SignRequest { /// Address pub address: H160, @@ -57,8 +76,19 @@ impl From<(H160, H256)> for SignRequest { } } +impl fmt::Display for SignRequest { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "sign 0x{:?} with {}", + self.hash, + Colour::White.bold().paint(format!("0x{:?}", self.address)), + ) + } +} + /// Decrypt request -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct DecryptRequest { /// Address pub address: H160, @@ -75,6 +105,16 @@ impl From<(H160, Bytes)> for DecryptRequest { } } +impl fmt::Display for DecryptRequest { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "decrypt data with {}", + Colour::White.bold().paint(format!("0x{:?}", self.address)), + ) + } +} + /// Confirmation response for particular payload #[derive(Debug, Clone, PartialEq)] pub enum ConfirmationResponse { @@ -111,7 +151,7 @@ pub struct ConfirmationResponseWithToken { } /// Confirmation payload, i.e. the thing to be confirmed -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum ConfirmationPayload { /// Send Transaction #[serde(rename="sendTransaction")] @@ -145,7 +185,7 @@ impl From for ConfirmationPayload { } /// Possible modifications to the confirmed transaction sent by `Trusted Signer` -#[derive(Debug, PartialEq, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TransactionModification { /// Modified gas price @@ -325,4 +365,3 @@ mod tests { assert_eq!(res.unwrap(), expected.to_owned()); } } - diff --git a/rpc/src/v1/types/dapp_id.rs b/rpc/src/v1/types/dapp_id.rs index fbd016e8a..fd98d7c8c 100644 --- a/rpc/src/v1/types/dapp_id.rs +++ b/rpc/src/v1/types/dapp_id.rs @@ -26,6 +26,12 @@ impl Into for DappId { } } +impl From for DappId { + fn from(s: String) -> Self { + DappId(s) + } +} + #[cfg(test)] mod tests { diff --git a/rpc/src/v1/types/transaction_request.rs b/rpc/src/v1/types/transaction_request.rs index 258346d56..c5613c5b2 100644 --- a/rpc/src/v1/types/transaction_request.rs +++ b/rpc/src/v1/types/transaction_request.rs @@ -18,6 +18,9 @@ use v1::types::{Bytes, H160, U256}; use v1::helpers; +use util::log::Colour; + +use std::fmt; /// Transaction request coming from RPC #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Serialize, Deserialize)] @@ -40,6 +43,43 @@ pub struct TransactionRequest { pub nonce: Option, } +pub fn format_ether(i: U256) -> String { + let mut string = format!("{}", i); + let idx = string.len() as isize - 18; + if idx <= 0 { + let mut prefix = String::from("0."); + for _ in 0..idx.abs() { + prefix.push('0'); + } + string = prefix + &string; + } else { + string.insert(idx as usize, '.'); + } + String::from(string.trim_right_matches('0') + .trim_right_matches('.')) +} + +impl fmt::Display for TransactionRequest { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let eth = self.value.unwrap_or(U256::from(0)); + match self.to { + Some(ref to) => write!( + f, + "{} ETH from {} to 0x{:?}", + Colour::White.bold().paint(format_ether(eth)), + Colour::White.bold().paint(format!("0x{:?}", self.from)), + to + ), + None => write!( + f, + "{} ETH from {} for contract creation", + Colour::White.bold().paint(format_ether(eth)), + Colour::White.bold().paint(format!("0x{:?}", self.from)), + ), + } + } +} + impl From for TransactionRequest { fn from(r: helpers::TransactionRequest) -> Self { TransactionRequest { @@ -191,5 +231,15 @@ mod tests { assert!(deserialized.is_err(), "Should be error because to is empty"); } -} + #[test] + fn test_format_ether() { + assert_eq!(&format_ether(U256::from(1000000000000000000u64)), "1"); + assert_eq!(&format_ether(U256::from(500000000000000000u64)), "0.5"); + assert_eq!(&format_ether(U256::from(50000000000000000u64)), "0.05"); + assert_eq!(&format_ether(U256::from(5000000000000000u64)), "0.005"); + assert_eq!(&format_ether(U256::from(2000000000000000000u64)), "2"); + assert_eq!(&format_ether(U256::from(2500000000000000000u64)), "2.5"); + assert_eq!(&format_ether(U256::from(10000000000000000000u64)), "10"); + } +} diff --git a/rpc/src/v1/types/uint.rs b/rpc/src/v1/types/uint.rs index 2dc9093c1..afa5c31dd 100644 --- a/rpc/src/v1/types/uint.rs +++ b/rpc/src/v1/types/uint.rs @@ -15,6 +15,7 @@ // along with Parity. If not, see . use std::str::FromStr; +use std::fmt; use serde; use util::{U256 as EthU256, U128 as EthU128, Uint}; @@ -46,6 +47,18 @@ macro_rules! impl_uint { } } + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } + } + + impl fmt::LowerHex for $name { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:#x}", self.0) + } + } + impl serde::Serialize for $name { fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: serde::Serializer { serializer.serialize_str(&format!("0x{}", self.0.to_hex())) diff --git a/rpc_cli/Cargo.toml b/rpc_cli/Cargo.toml new file mode 100644 index 000000000..8169d3b71 --- /dev/null +++ b/rpc_cli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +authors = ["Ethcore "] +description = "Parity Cli Tool" +homepage = "http://ethcore.io" +license = "GPL-3.0" +name = "rpc-cli" +version = "1.4.0" + +[dependencies] +futures = "0.1" +rpassword = "0.3.0" +ethcore-bigint = { path = "../util/bigint" } +ethcore-rpc = { path = "../rpc" } +parity-rpc-client = { path = "../rpc_client" } +ethcore-util = { path = "../util" } diff --git a/rpc_cli/src/lib.rs b/rpc_cli/src/lib.rs new file mode 100644 index 000000000..ce4933071 --- /dev/null +++ b/rpc_cli/src/lib.rs @@ -0,0 +1,182 @@ +extern crate futures; + +extern crate ethcore_util as util; +extern crate ethcore_rpc as rpc; +extern crate ethcore_bigint as bigint; +extern crate rpassword; + +extern crate parity_rpc_client as client; + +use rpc::v1::types::{U256, ConfirmationRequest}; +use client::signer_client::SignerRpc; +use std::io::{Write, BufRead, BufReader, stdout, stdin}; +use std::path::PathBuf; +use std::fs::File; + +use futures::Future; + +fn sign_interactive( + signer: &mut SignerRpc, + password: &str, + request: ConfirmationRequest +) { + print!("\n{}\nSign this transaction? (y)es/(N)o/(r)eject: ", request); + let _ = stdout().flush(); + match BufReader::new(stdin()).lines().next() { + Some(Ok(line)) => { + match line.to_lowercase().chars().nth(0) { + Some('y') => { + match sign_transaction(signer, request.id, password) { + Ok(s) | Err(s) => println!("{}", s), + } + } + Some('r') => { + match reject_transaction(signer, request.id) { + Ok(s) | Err(s) => println!("{}", s), + } + } + _ => () + } + } + _ => println!("Could not read from stdin") + } +} + +fn sign_transactions( + signer: &mut SignerRpc, + password: String +) -> Result { + try!(signer.requests_to_confirm().map(|reqs| { + match reqs { + Ok(ref reqs) if reqs.is_empty() => { + Ok("No transactions in signing queue".to_owned()) + } + Ok(reqs) => { + for r in reqs { + sign_interactive(signer, &password, r) + } + Ok("".to_owned()) + } + Err(err) => { + Err(format!("error: {:?}", err)) + } + } + }).map_err(|err| { + format!("{:?}", err) + }).wait()) +} + +fn list_transactions(signer: &mut SignerRpc) -> Result { + try!(signer.requests_to_confirm().map(|reqs| { + match reqs { + Ok(ref reqs) if reqs.is_empty() => { + Ok("No transactions in signing queue".to_owned()) + } + Ok(ref reqs) => { + Ok(format!("Transaction queue:\n{}", reqs + .iter() + .map(|r| format!("{}", r)) + .collect::>() + .join("\n"))) + } + Err(err) => { + Err(format!("error: {:?}", err)) + } + } + }).map_err(|err| { + format!("{:?}", err) + }).wait()) +} + +fn sign_transaction( + signer: &mut SignerRpc, id: U256, password: &str +) -> Result { + try!(signer.confirm_request(id, None, None, password).map(|res| { + match res { + Ok(u) => Ok(format!("Signed transaction id: {:#x}", u)), + Err(e) => Err(format!("{:?}", e)), + } + }).map_err(|err| { + format!("{:?}", err) + }).wait()) +} + +fn reject_transaction( + signer: &mut SignerRpc, id: U256) -> Result +{ + try!(signer.reject_request(id).map(|res| { + match res { + Ok(true) => Ok(format!("Rejected transaction id {:#x}", id)), + Ok(false) => Err(format!("No such request")), + Err(e) => Err(format!("{:?}", e)), + } + }).map_err(|err| { + format!("{:?}", err) + }).wait()) +} + +// cmds + +pub fn signer_list( + signerport: u16, authfile: PathBuf +) -> Result { + let addr = &format!("ws://127.0.0.1:{}", signerport); + let mut signer = try!(SignerRpc::new(addr, &authfile).map_err(|err| { + format!("{:?}", err) + })); + list_transactions(&mut signer) +} + +pub fn signer_reject( + id: Option, signerport: u16, authfile: PathBuf +) -> Result { + let id = try!(id.ok_or(format!("id required for signer reject"))); + let addr = &format!("ws://127.0.0.1:{}", signerport); + let mut signer = try!(SignerRpc::new(addr, &authfile).map_err(|err| { + format!("{:?}", err) + })); + reject_transaction(&mut signer, U256::from(id)) +} + +pub fn signer_sign( + id: Option, + pwfile: Option, + signerport: u16, + authfile: PathBuf +) -> Result { + let password; + match pwfile { + Some(pwfile) => { + match File::open(pwfile) { + Ok(fd) => { + match BufReader::new(fd).lines().next() { + Some(Ok(line)) => password = line, + _ => return Err(format!("No password in file")) + } + }, + Err(e) => + return Err(format!("Could not open password file: {}", e)) + } + } + None => { + password = match rpassword::prompt_password_stdout("Password: ") { + Ok(p) => p, + Err(e) => return Err(format!("{}", e)), + } + } + } + + let addr = &format!("ws://127.0.0.1:{}", signerport); + let mut signer = try!(SignerRpc::new(addr, &authfile).map_err(|err| { + format!("{:?}", err) + })); + + match id { + Some(id) => { + sign_transaction(&mut signer, U256::from(id), &password) + }, + None => { + sign_transactions(&mut signer, password) + } + } +} diff --git a/rpc_client/Cargo.toml b/rpc_client/Cargo.toml new file mode 100644 index 000000000..9b93a1a5b --- /dev/null +++ b/rpc_client/Cargo.toml @@ -0,0 +1,23 @@ +[package] +authors = ["Ethcore "] +description = "Parity Rpc Client" +homepage = "http://ethcore.io" +license = "GPL-3.0" +name = "parity-rpc-client" +version = "1.4.0" + +[dependencies] +futures = "0.1" +jsonrpc-core = "3.0.2" +lazy_static = "0.2.1" +log = "0.3.6" +matches = "0.1.2" +rand = "0.3.14" +serde = "0.8" +serde_json = "0.8" +tempdir = "0.3.5" +url = "1.2.0" +ws = { git = "https://github.com/ethcore/ws-rs.git", branch = "mio-upstream-stable" } +ethcore-rpc = { path = "../rpc" } +ethcore-signer = { path = "../signer" } +ethcore-util = { path = "../util" } diff --git a/rpc_client/src/client.rs b/rpc_client/src/client.rs new file mode 100644 index 000000000..9ee9f5c9d --- /dev/null +++ b/rpc_client/src/client.rs @@ -0,0 +1,322 @@ +extern crate jsonrpc_core; + +use std::fmt::{Debug, Formatter, Error as FmtError}; +use std::io::{BufReader, BufRead}; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::collections::BTreeMap; +use std::thread; +use std::time; + +use std::path::PathBuf; +use util::{Hashable, Mutex}; +use url::Url; +use std::fs::File; + +use ws::{ + self, + Request, + Handler, + Sender, + Handshake, + Error as WsError, + ErrorKind as WsErrorKind, + Message, + Result as WsResult, +}; + +use serde::Deserialize; +use serde_json::{ + self as json, + Value as JsonValue, + Error as JsonError, +}; + +use futures::{BoxFuture, Canceled, Complete, Future, oneshot, done}; + +use jsonrpc_core::{Id, Version, Params, Error as JsonRpcError}; +use jsonrpc_core::request::MethodCall; +use jsonrpc_core::response::{SyncOutput, Success, Failure}; + +/// The actual websocket connection handler, passed into the +/// event loop of ws-rs +struct RpcHandler { + pending: Pending, + // Option is used here as temporary storage until connection + // is setup and the values are moved into the new `Rpc` + complete: Option>>, + auth_code: String, + out: Option, +} + +impl RpcHandler { + fn new( + out: Sender, + auth_code: String, + complete: Complete> + ) -> Self { + RpcHandler { + out: Some(out), + auth_code: auth_code, + pending: Pending::new(), + complete: Some(complete), + } + } +} + +impl Handler for RpcHandler { + fn build_request(&mut self, url: &Url) -> WsResult { + match Request::from_url(url) { + Ok(mut r) => { + let timestamp = try!(time::UNIX_EPOCH.elapsed().map_err(|err| { + WsError::new(WsErrorKind::Internal, format!("{}", err)) + })); + let secs = timestamp.as_secs(); + let hashed = format!("{}:{}", self.auth_code, secs).sha3(); + let proto = format!("{:?}_{}", hashed, secs); + r.add_protocol(&proto); + Ok(r) + }, + Err(e) => + Err(WsError::new(WsErrorKind::Internal, format!("{}", e))), + } + } + fn on_error(&mut self, err: WsError) { + match self.complete.take() { + Some(c) => c.complete(Err(RpcError::WsError(err))), + None => println!("unexpected error: {}", err), + } + } + fn on_open(&mut self, _: Handshake) -> WsResult<()> { + match (self.complete.take(), self.out.take()) { + (Some(c), Some(out)) => { + c.complete(Ok(Rpc { + out: out, + counter: AtomicUsize::new(0), + pending: self.pending.clone(), + })); + Ok(()) + }, + _ => { + let msg = format!("on_open called twice"); + Err(WsError::new(WsErrorKind::Internal, msg)) + } + } + } + fn on_message(&mut self, msg: Message) -> WsResult<()> { + let ret: Result; + let response_id; + let string = &msg.to_string(); + match json::from_str::(&string) { + Ok(SyncOutput::Success(Success { result, id: Id::Num(id), .. })) => + { + ret = Ok(result); + response_id = id as usize; + } + Ok(SyncOutput::Failure(Failure { error, id: Id::Num(id), .. })) => { + ret = Err(error); + response_id = id as usize; + } + Err(e) => { + warn!( + target: "rpc-client", + "recieved invalid message: {}\n {:?}", + string, + e + ); + return Ok(()) + }, + _ => { + warn!( + target: "rpc-client", + "recieved invalid message: {}", + string + ); + return Ok(()) + } + } + + match self.pending.remove(response_id) { + Some(c) => c.complete(ret.map_err(|err| { + RpcError::JsonRpc(err) + })), + None => warn!( + target: "rpc-client", + "warning: unexpected id: {}", + response_id + ), + } + Ok(()) + } +} + +/// Keeping track of issued requests to be matched up with responses +#[derive(Clone)] +struct Pending( + Arc>>>> +); + +impl Pending { + fn new() -> Self { + Pending(Arc::new(Mutex::new(BTreeMap::new()))) + } + fn insert(&mut self, k: usize, v: Complete>) { + self.0.lock().insert(k, v); + } + fn remove( + &mut self, + k: usize + ) -> Option>> { + self.0.lock().remove(&k) + } +} + +fn get_authcode(path: &PathBuf) -> Result { + if let Ok(fd) = File::open(path) { + if let Some(Ok(line)) = BufReader::new(fd).lines().next() { + let mut parts = line.split(';'); + let token = parts.next(); + + if let Some(code) = token { + return Ok(code.into()); + } + } + } + Err(RpcError::NoAuthCode) +} + +/// The handle to the connection +pub struct Rpc { + out: Sender, + counter: AtomicUsize, + pending: Pending, +} + +impl Rpc { + /// Blocking, returns a new initialized connection or RpcError + pub fn new(url: &str, authpath: &PathBuf) -> Result { + let rpc = try!(Self::connect(url, authpath).map(|rpc| rpc).wait()); + rpc + } + /// Non-blocking, returns a future + pub fn connect( + url: &str, authpath: &PathBuf + ) -> BoxFuture, Canceled> { + let (c, p) = oneshot::>(); + match get_authcode(authpath) { + Err(e) => return done(Ok(Err(e))).boxed(), + Ok(code) => { + let url = String::from(url); + // The ws::connect takes a FnMut closure, which means c cannot + // be moved into it, since it's consumed on complete. + // Therefore we wrap it in an option and pick it out once. + let mut once = Some(c); + thread::spawn(move || { + let conn = ws::connect(url, |out| { + // this will panic if the closure is called twice, + // which it should never be. + let c = once.take() + .expect("connection closure called only once"); + RpcHandler::new(out, code.clone(), c) + }); + match conn { + Err(err) => { + // since ws::connect is only called once, it cannot + // both fail and succeed. + let c = once.take() + .expect("connection closure called only once"); + c.complete(Err(RpcError::WsError(err))); + }, + // c will complete on the `on_open` event in the Handler + _ => () + } + }); + p.boxed() + } + } + } + /// Non-blocking, returns a future of the request response + pub fn request( + &mut self, method: &'static str, params: Vec + ) -> BoxFuture, Canceled> + where T: Deserialize + Send + Sized { + + let (c, p) = oneshot::>(); + + let id = self.counter.fetch_add(1, Ordering::Relaxed); + self.pending.insert(id, c); + + let request = MethodCall { + jsonrpc: Version::V2, + method: method.to_owned(), + params: Some(Params::Array(params)), + id: Id::Num(id as u64), + }; + + let serialized = json::to_string(&request) + .expect("request is serializable"); + let _ = self.out.send(serialized); + + p.map(|result| { + match result { + Ok(json) => { + let t: T = try!(json::from_value(json)); + Ok(t) + }, + Err(err) => Err(err) + } + }).boxed() + } +} + +pub enum RpcError { + WrongVersion(String), + ParseError(JsonError), + MalformedResponse(String), + JsonRpc(JsonRpcError), + WsError(WsError), + Canceled(Canceled), + UnexpectedId, + NoAuthCode, +} + +impl Debug for RpcError { + fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> { + match *self { + RpcError::WrongVersion(ref s) + => write!(f, "Expected version 2.0, got {}", s), + RpcError::ParseError(ref err) + => write!(f, "ParseError: {}", err), + RpcError::MalformedResponse(ref s) + => write!(f, "Malformed response: {}", s), + RpcError::JsonRpc(ref json) + => write!(f, "JsonRpc error: {:?}", json), + RpcError::WsError(ref s) + => write!(f, "Websocket error: {}", s), + RpcError::Canceled(ref s) + => write!(f, "Futures error: {:?}", s), + RpcError::UnexpectedId + => write!(f, "Unexpected response id"), + RpcError::NoAuthCode + => write!(f, "No authcodes available"), + } + } +} + +impl From for RpcError { + fn from(err: JsonError) -> RpcError { + RpcError::ParseError(err) + } +} + +impl From for RpcError { + fn from(err: WsError) -> RpcError { + RpcError::WsError(err) + } +} + +impl From for RpcError { + fn from(err: Canceled) -> RpcError { + RpcError::Canceled(err) + } +} diff --git a/rpc_client/src/lib.rs b/rpc_client/src/lib.rs new file mode 100644 index 000000000..3164480e5 --- /dev/null +++ b/rpc_client/src/lib.rs @@ -0,0 +1,73 @@ +pub mod client; +pub mod signer_client; + +extern crate ws; +extern crate ethcore_signer; +extern crate url; +extern crate futures; +extern crate ethcore_util as util; +extern crate ethcore_rpc as rpc; +extern crate serde; +extern crate serde_json; +extern crate rand; +extern crate tempdir; +extern crate jsonrpc_core; + +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate matches; + +#[macro_use] +extern crate log; + +#[cfg(test)] +mod tests { + use futures::Future; + use std::path::PathBuf; + use client::{Rpc, RpcError}; + use ethcore_signer; + + #[test] + fn test_connection_refused() { + let (_srv, port, mut authcodes) = ethcore_signer::tests::serve(); + + let _ = authcodes.generate_new(); + authcodes.to_file(&authcodes.path).unwrap(); + + let connect = Rpc::connect(&format!("ws://127.0.0.1:{}", port - 1), + authcodes.path.as_path()); + + let _ = connect.map(|conn| { + assert!(matches!(&conn, &Err(RpcError::WsError(_)))); + }).wait(); + } + + #[test] + fn test_authcode_fail() { + let (_srv, port, _) = ethcore_signer::tests::serve(); + let path = PathBuf::from("nonexist"); + + let connect = Rpc::connect(&format!("ws://127.0.0.1:{}", port), &path); + + let _ = connect.map(|conn| { + assert!(matches!(&conn, &Err(RpcError::NoAuthCode))); + }).wait(); + } + + #[test] + fn test_authcode_correct() { + let (_srv, port, mut authcodes) = ethcore_signer::tests::serve(); + + let _ = authcodes.generate_new(); + authcodes.to_file(&authcodes.path).unwrap(); + + let connect = Rpc::connect(&format!("ws://127.0.0.1:{}", port), + authcodes.path.as_path()); + + let _ = connect.map(|conn| { + assert!(conn.is_ok()) + }).wait(); + } + +} diff --git a/rpc_client/src/signer_client.rs b/rpc_client/src/signer_client.rs new file mode 100644 index 000000000..8a2eccd5d --- /dev/null +++ b/rpc_client/src/signer_client.rs @@ -0,0 +1,43 @@ +use client::{Rpc, RpcError}; +use rpc::v1::types::{ConfirmationRequest, + TransactionModification, + U256}; +use serde_json::{Value as JsonValue, to_value}; +use std::path::PathBuf; +use futures::{BoxFuture, Canceled}; + +pub struct SignerRpc { + rpc: Rpc, +} + +impl SignerRpc { + pub fn new(url: &str, authfile: &PathBuf) -> Result { + Ok(SignerRpc { rpc: try!(Rpc::new(&url, authfile)) }) + } + pub fn requests_to_confirm(&mut self) -> + BoxFuture, RpcError>, Canceled> + { + self.rpc.request("signer_requestsToConfirm", vec![]) + } + pub fn confirm_request( + &mut self, + id: U256, + new_gas: Option, + new_gas_price: Option, + pwd: &str + ) -> BoxFuture, Canceled> + { + self.rpc.request("signer_confirmRequest", vec![ + to_value(&format!("{:#x}", id)), + to_value(&TransactionModification { gas_price: new_gas_price, gas: new_gas }), + to_value(&pwd), + ]) + } + pub fn reject_request(&mut self, id: U256) -> + BoxFuture, Canceled> + { + self.rpc.request("signer_rejectRequest", vec![ + JsonValue::String(format!("{:#x}", id)) + ]) + } +} diff --git a/signer/src/lib.rs b/signer/src/lib.rs index 3d73b0668..196fb4fae 100644 --- a/signer/src/lib.rs +++ b/signer/src/lib.rs @@ -52,13 +52,13 @@ extern crate ethcore_io as io; extern crate ethcore_rpc as rpc; extern crate jsonrpc_core; extern crate ws; -#[cfg(test)] + extern crate ethcore_devtools as devtools; mod authcode_store; mod ws_server; -#[cfg(test)] -mod tests; +/// Exported tests for use in signer RPC client testing +pub mod tests; pub use authcode_store::*; pub use ws_server::*; diff --git a/signer/src/tests/mod.rs b/signer/src/tests/mod.rs index 043b0f693..ab46a0e6f 100644 --- a/signer/src/tests/mod.rs +++ b/signer/src/tests/mod.rs @@ -15,20 +15,23 @@ // along with Parity. If not, see . use std::ops::{Deref, DerefMut}; -use std::time; use std::sync::Arc; -use devtools::{http_client, RandomTempPath}; + +use devtools::http_client; +use devtools::RandomTempPath; + use rpc::ConfirmationsQueue; -use util::Hashable; use rand; use ServerBuilder; use Server; use AuthCodes; +/// Struct representing authcodes pub struct GuardedAuthCodes { authcodes: AuthCodes, - path: RandomTempPath, + /// The path to the mock authcodes + pub path: RandomTempPath, } impl Deref for GuardedAuthCodes { type Target = AuthCodes; @@ -42,6 +45,7 @@ impl DerefMut for GuardedAuthCodes { } } +/// Setup a mock signer for testsp pub fn serve() -> (Server, usize, GuardedAuthCodes) { let mut path = RandomTempPath::new(); path.panic_on_drop_failure = false; @@ -56,194 +60,202 @@ pub fn serve() -> (Server, usize, GuardedAuthCodes) { }) } +/// Test a single request to running server pub fn request(server: Server, request: &str) -> http_client::Response { http_client::request(server.addr(), request) } -#[test] -fn should_reject_invalid_host() { - // given - let server = serve().0; +#[cfg(test)] +mod testing { + use std::time; + use util::Hashable; + use devtools::http_client; + use super::{serve, request}; - // when - let response = request(server, - "\ - GET / HTTP/1.1\r\n\ - Host: test:8180\r\n\ - Connection: close\r\n\ - \r\n\ - {} - " - ); + #[test] + fn should_reject_invalid_host() { + // given + let server = serve().0; - // then - assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned()); - assert!(response.body.contains("URL Blocked")); - http_client::assert_security_headers_present(&response.headers, None); + // when + let response = request(server, + "\ + GET / HTTP/1.1\r\n\ + Host: test:8180\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 403 FORBIDDEN".to_owned()); + assert!(response.body.contains("URL Blocked")); + http_client::assert_security_headers_present(&response.headers, None); + } + + #[test] + fn should_allow_home_parity_host() { + // given + let server = serve().0; + + // when + let response = request(server, + "\ + GET http://home.parity/ HTTP/1.1\r\n\ + Host: home.parity\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + http_client::assert_security_headers_present(&response.headers, None); + } + + #[test] + fn should_serve_styles_even_on_disallowed_domain() { + // given + let server = serve().0; + + // when + let response = request(server, + "\ + GET /styles.css HTTP/1.1\r\n\ + Host: test:8180\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + http_client::assert_security_headers_present(&response.headers, None); + } + + #[test] + fn should_return_200_ok_for_connect_requests() { + // given + let server = serve().0; + + // when + let response = request(server, + "\ + CONNECT home.parity:8080 HTTP/1.1\r\n\ + Host: home.parity\r\n\ + Connection: close\r\n\ + \r\n\ + {} + " + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + } + + #[test] + 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); + } } - -#[test] -fn should_allow_home_parity_host() { - // given - let server = serve().0; - - // when - let response = request(server, - "\ - GET http://home.parity/ HTTP/1.1\r\n\ - Host: home.parity\r\n\ - Connection: close\r\n\ - \r\n\ - {} - " - ); - - // then - assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); - http_client::assert_security_headers_present(&response.headers, None); -} - -#[test] -fn should_serve_styles_even_on_disallowed_domain() { - // given - let server = serve().0; - - // when - let response = request(server, - "\ - GET /styles.css HTTP/1.1\r\n\ - Host: test:8180\r\n\ - Connection: close\r\n\ - \r\n\ - {} - " - ); - - // then - assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); - http_client::assert_security_headers_present(&response.headers, None); -} - -#[test] -fn should_return_200_ok_for_connect_requests() { - // given - let server = serve().0; - - // when - let response = request(server, - "\ - CONNECT home.parity:8080 HTTP/1.1\r\n\ - Host: home.parity\r\n\ - Connection: close\r\n\ - \r\n\ - {} - " - ); - - // then - assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); -} - -#[test] -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); -} -