From cd4d489b57ff48d53995d79cd8fde22a57d7724e Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 3 Feb 2017 20:01:09 +0100 Subject: [PATCH 01/10] Add block & timestamp conditions to Signer (#4411) * WIP * WIP (with lint) * Update ui/RadioButtons * transaction.condition * Date & Time selection in-place * Swap to condition-only * Fix tests, align naming * Pick error properly from validation * Update tests * condition: time sent withough ms * Format numbers as base-10 * override popup styles (zIndex) * Pass condition to signer * Update expectation (failing test typo) * Adjust min/max height for expanded bar * Fix address display * Fix name display * Number inputs for gas/gasPrice/blockNumber * Default blockNumber to 1 (align with min setting) * Update tests with min value * Add Block Number * Fix failing tests (after blockNumber intro) --- js/src/api/format/input.js | 16 ++ js/src/api/format/output.js | 20 +- .../AdvancedStep/advancedStep.js | 33 +-- .../modals/ExecuteContract/executeContract.js | 34 +-- js/src/modals/Transfer/Extras/extras.js | 25 +- js/src/modals/Transfer/store.js | 90 +++----- js/src/modals/Transfer/transfer.js | 4 +- js/src/redux/providers/signerMiddleware.js | 4 +- js/src/ui/Form/InputAddress/inputAddress.js | 5 +- js/src/ui/Form/InputDate/index.js | 17 ++ js/src/ui/Form/InputDate/inputDate.css | 22 ++ js/src/ui/Form/InputDate/inputDate.js | 53 +++++ js/src/ui/Form/InputTime/index.js | 17 ++ js/src/ui/Form/InputTime/inputTime.css | 22 ++ js/src/ui/Form/InputTime/inputTime.js | 54 +++++ js/src/ui/Form/Label/index.js | 17 ++ js/src/ui/Form/Label/label.css | 24 ++ js/src/ui/Form/Label/label.js | 40 ++++ js/src/ui/Form/RadioButtons/radioButtons.css | 27 ++- js/src/ui/Form/RadioButtons/radioButtons.js | 36 +-- js/src/ui/Form/index.js | 14 +- js/src/ui/GasPriceEditor/gasPriceEditor.css | 40 ++++ js/src/ui/GasPriceEditor/gasPriceEditor.js | 217 +++++++++++++++--- .../ui/GasPriceEditor/gasPriceEditor.spec.js | 72 ++++-- js/src/ui/GasPriceEditor/store.js | 91 +++++++- js/src/ui/GasPriceEditor/store.spec.js | 93 +++++++- js/src/ui/MethodDecoding/methodDecoding.js | 23 +- js/src/ui/index.js | 5 +- js/src/views/ParityBar/parityBar.css | 10 +- .../transactionMainDetails.js | 6 +- .../TransactionPending/transactionPending.js | 18 +- 31 files changed, 894 insertions(+), 255 deletions(-) create mode 100644 js/src/ui/Form/InputDate/index.js create mode 100644 js/src/ui/Form/InputDate/inputDate.css create mode 100644 js/src/ui/Form/InputDate/inputDate.js create mode 100644 js/src/ui/Form/InputTime/index.js create mode 100644 js/src/ui/Form/InputTime/inputTime.css create mode 100644 js/src/ui/Form/InputTime/inputTime.js create mode 100644 js/src/ui/Form/Label/index.js create mode 100644 js/src/ui/Form/Label/label.css create mode 100644 js/src/ui/Form/Label/label.js diff --git a/js/src/api/format/input.js b/js/src/api/format/input.js index 05593fc5e..74ddd4e56 100644 --- a/js/src/api/format/input.js +++ b/js/src/api/format/input.js @@ -127,6 +127,18 @@ export function inNumber16 (number) { return inHex(bn.toString(16)); } +export function inOptionsCondition (condition) { + if (condition) { + if (condition.block) { + condition.block = condition.block ? inNumber10(condition.block) : null; + } else if (condition.time) { + condition.time = inNumber10(Math.floor(condition.time.getTime() / 1000)); + } + } + + return condition; +} + export function inOptions (options) { if (options) { Object.keys(options).forEach((key) => { @@ -136,6 +148,10 @@ export function inOptions (options) { options[key] = inAddress(options[key]); break; + case 'condition': + options[key] = inOptionsCondition(options[key]); + break; + case 'gas': case 'gasPrice': options[key] = inNumber16((new BigNumber(options[key])).round()); diff --git a/js/src/api/format/output.js b/js/src/api/format/output.js index 35ec67ebf..dd61c82b1 100644 --- a/js/src/api/format/output.js +++ b/js/src/api/format/output.js @@ -221,6 +221,18 @@ export function outSyncing (syncing) { return syncing; } +export function outTransactionCondition (condition) { + if (condition) { + if (condition.block) { + condition.block = outNumber(condition.block); + } else if (condition.time) { + condition.time = outDate(condition.time); + } + } + + return condition; +} + export function outTransaction (tx) { if (tx) { Object.keys(tx).forEach((key) => { @@ -234,8 +246,14 @@ export function outTransaction (tx) { tx[key] = outNumber(tx[key]); break; + case 'condition': + tx[key] = outTransactionCondition(tx[key]); + break; + case 'minBlock': - tx[key] = tx[key] ? outNumber(tx[key]) : null; + tx[key] = tx[key] + ? outNumber(tx[key]) + : null; break; case 'creates': diff --git a/js/src/modals/ExecuteContract/AdvancedStep/advancedStep.js b/js/src/modals/ExecuteContract/AdvancedStep/advancedStep.js index cf592c5cb..268ca1f70 100644 --- a/js/src/modals/ExecuteContract/AdvancedStep/advancedStep.js +++ b/js/src/modals/ExecuteContract/AdvancedStep/advancedStep.js @@ -15,45 +15,22 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Input, GasPriceEditor } from '~/ui'; +import { GasPriceEditor } from '~/ui'; import styles from '../executeContract.css'; export default class AdvancedStep extends Component { static propTypes = { - gasStore: PropTypes.object.isRequired, - minBlock: PropTypes.string, - minBlockError: PropTypes.string, - onMinBlockChange: PropTypes.func + gasStore: PropTypes.object.isRequired }; render () { - const { gasStore, minBlock, minBlockError, onMinBlockChange } = this.props; + const { gasStore } = this.props; return ( -
- - } - label={ - - } - value={ minBlock } - onSubmit={ onMinBlockChange } - /> -
- -
+
+
); } diff --git a/js/src/modals/ExecuteContract/executeContract.js b/js/src/modals/ExecuteContract/executeContract.js index 6f750a8dc..0828a5f3e 100644 --- a/js/src/modals/ExecuteContract/executeContract.js +++ b/js/src/modals/ExecuteContract/executeContract.js @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import BigNumber from 'bignumber.js'; import { pick } from 'lodash'; import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; @@ -100,8 +99,6 @@ class ExecuteContract extends Component { fromAddressError: null, func: null, funcError: null, - minBlock: '0', - minBlockError: null, rejected: false, sending: false, step: STEP_DETAILS, @@ -167,8 +164,8 @@ class ExecuteContract extends Component { renderDialogActions () { const { onClose, fromAddress } = this.props; - const { advancedOptions, sending, step, fromAddressError, minBlockError, valuesError } = this.state; - const hasError = fromAddressError || minBlockError || valuesError.find((error) => error); + const { advancedOptions, sending, step, fromAddressError, valuesError } = this.state; + const hasError = fromAddressError || valuesError.find((error) => error); const cancelBtn = (
diff --git a/js/src/views/Signer/components/TransactionPending/transactionPending.js b/js/src/views/Signer/components/TransactionPending/transactionPending.js index 24d8589c7..a49f5c2e0 100644 --- a/js/src/views/Signer/components/TransactionPending/transactionPending.js +++ b/js/src/views/Signer/components/TransactionPending/transactionPending.js @@ -45,6 +45,7 @@ export default class TransactionPending extends Component { onReject: PropTypes.func.isRequired, store: PropTypes.object.isRequired, transaction: PropTypes.shape({ + condition: PropTypes.object, data: PropTypes.string, from: PropTypes.string.isRequired, gas: PropTypes.object.isRequired, @@ -59,6 +60,7 @@ export default class TransactionPending extends Component { }; gasStore = new GasPriceEditor.Store(this.context.api, { + condition: this.props.transaction.condition, gas: this.props.transaction.gas.toFixed(), gasLimit: this.props.gasLimit, gasPrice: this.props.transaction.gasPrice.toFixed() @@ -80,7 +82,7 @@ export default class TransactionPending extends Component { render () { return this.gasStore.isEditing - ? this.renderGasEditor() + ? this.renderTxEditor() : this.renderTransaction(); } @@ -115,7 +117,7 @@ export default class TransactionPending extends Component { ); } - renderGasEditor () { + renderTxEditor () { const { className } = this.props; return ( @@ -133,15 +135,21 @@ export default class TransactionPending extends Component { onConfirm = (data) => { const { id, transaction } = this.props; const { password, wallet } = data; - const { gas, gasPrice } = this.gasStore.overrideTransaction(transaction); + const { condition, gas, gasPrice } = this.gasStore.overrideTransaction(transaction); - this.props.onConfirm({ + const options = { gas, gasPrice, id, password, wallet - }); + }; + + if (condition && (condition.block || condition.time)) { + options.condition = condition; + } + + this.props.onConfirm(options); } onReject = () => { From f48d8b0ef67a89217b439a76f9e7e3ea0881d657 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 3 Feb 2017 20:01:49 +0100 Subject: [PATCH 02/10] Extension installation overlay (#4423) * Extension installation overlay * Pr gumbles * Spelling * Update Chrome URL --- js/package.json | 1 + .../views/Application/Extension/extension.css | 52 +++++++++++ .../views/Application/Extension/extension.js | 74 +++++++++++++++ js/src/views/Application/Extension/index.js | 17 ++++ js/src/views/Application/Extension/store.js | 89 +++++++++++++++++++ js/src/views/Application/application.js | 2 + 6 files changed, 235 insertions(+) create mode 100644 js/src/views/Application/Extension/extension.css create mode 100644 js/src/views/Application/Extension/extension.js create mode 100644 js/src/views/Application/Extension/index.js create mode 100644 js/src/views/Application/Extension/store.js diff --git a/js/package.json b/js/package.json index 9eb0cd804..ff3320f50 100644 --- a/js/package.json +++ b/js/package.json @@ -193,6 +193,7 @@ "scryptsy": "2.0.0", "solc": "ngotchac/solc-js", "store": "1.3.20", + "useragent.js": "0.5.6", "utf8": "2.1.2", "valid-url": "1.0.9", "validator": "6.2.0", diff --git a/js/src/views/Application/Extension/extension.css b/js/src/views/Application/Extension/extension.css new file mode 100644 index 000000000..98d094a9f --- /dev/null +++ b/js/src/views/Application/Extension/extension.css @@ -0,0 +1,52 @@ +/* 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 . +*/ + +.body { + background: #f80; + color: white; + opacity: 1; + max-width: 500px; + padding: 1em 4em 1em 2em; + position: fixed; + right: 1.5em; + top: 1.5em; + z-index: 1000; + + .button { + background: rgba(0, 0, 0, 0.5); + color: white !important; + + svg { + fill: white !important; + } + } + + .buttonrow { + text-align: right; + } + + p { + color: white; + } + + .close { + cursor: pointer; + position: absolute; + right: 1em; + top: 1em; + } +} diff --git a/js/src/views/Application/Extension/extension.js b/js/src/views/Application/Extension/extension.js new file mode 100644 index 000000000..aff332f9a --- /dev/null +++ b/js/src/views/Application/Extension/extension.js @@ -0,0 +1,74 @@ +// 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 . + +import { observer } from 'mobx-react'; +import React, { Component } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { Button } from '~/ui'; +import { CloseIcon, CheckIcon } from '~/ui/Icons'; + +import Store from './store'; +import styles from './extension.css'; + +@observer +export default class Extension extends Component { + store = new Store(); + + render () { + const { showWarning } = this.store; + + if (!showWarning) { + return null; + } + + return ( +
+ +

+ +

+

+

+ ); + } + + onClose = () => { + this.store.snoozeWarning(); + } + + onInstallClick = () => { + this.store.installExtension(); + } +} diff --git a/js/src/views/Application/Extension/index.js b/js/src/views/Application/Extension/index.js new file mode 100644 index 000000000..ac1cfa015 --- /dev/null +++ b/js/src/views/Application/Extension/index.js @@ -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 . + +export default from './extension'; diff --git a/js/src/views/Application/Extension/store.js b/js/src/views/Application/Extension/store.js new file mode 100644 index 000000000..40a3f09e7 --- /dev/null +++ b/js/src/views/Application/Extension/store.js @@ -0,0 +1,89 @@ +// 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 . + +/* global chrome */ + +import { action, computed, observable } from 'mobx'; + +import store from 'store'; +import browser from 'useragent.js/lib/browser'; + +const A_DAY = 24 * 60 * 60 * 1000; +const NEXT_DISPLAY = '_parity::extensionWarning::nextDisplay'; + +// 'https://chrome.google.com/webstore/detail/parity-ethereum-integrati/himekenlppkgeaoeddcliojfddemadig'; +const EXTENSION_PAGE = 'https://chrome.google.com/webstore/detail/himekenlppkgeaoeddcliojfddemadig'; + +export default class Store { + @observable isInstalling = false; + @observable nextDisplay = 0; + @observable shouldInstall = false; + + constructor () { + this.nextDisplay = store.get(NEXT_DISPLAY) || 0; + this.testInstall(); + } + + @computed get showWarning () { + return !this.isInstalling && this.shouldInstall && (Date.now() > this.nextDisplay); + } + + @action setInstalling = (isInstalling) => { + this.isInstalling = isInstalling; + } + + @action snoozeWarning = (sleep = A_DAY) => { + this.nextDisplay = Date.now() + sleep; + store.set(NEXT_DISPLAY, this.nextDisplay); + } + + @action testInstall = () => { + this.shouldInstall = this.readStatus(); + } + + readStatus = () => { + const hasExtension = Symbol.for('parity.extension') in window; + const ua = browser.analyze(navigator.userAgent || ''); + + if (hasExtension) { + return false; + } + + return (ua || {}).name.toLowerCase() === 'chrome'; + } + + installExtension = () => { + this.setInstalling(true); + + return new Promise((resolve, reject) => { + const link = document.createElement('link'); + + link.setAttribute('rel', 'chrome-webstore-item'); + link.setAttribute('href', EXTENSION_PAGE); + document.querySelector('head').appendChild(link); + + if (chrome && chrome.webstore && chrome.webstore.install) { + chrome.webstore.install(EXTENSION_PAGE, resolve, reject); + } else { + reject(new Error('Direct installation failed.')); + } + }) + .catch((error) => { + console.warn('Unable to perform direct install', error); + window.open(EXTENSION_PAGE, '_blank'); + }); + } +} diff --git a/js/src/views/Application/application.js b/js/src/views/Application/application.js index 4b905b3d4..377dcecbc 100644 --- a/js/src/views/Application/application.js +++ b/js/src/views/Application/application.js @@ -26,6 +26,7 @@ import ParityBar from '../ParityBar'; import Snackbar from './Snackbar'; import Container from './Container'; import DappContainer from './DappContainer'; +import Extension from './Extension'; import FrameError from './FrameError'; import Status from './Status'; import Store from './store'; @@ -106,6 +107,7 @@ class Application extends Component { ? : null } + ); From a39324632d9e0305f039fb39af1828cdb4c50828 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Fri, 3 Feb 2017 19:12:35 +0000 Subject: [PATCH 03/10] [ci skip] js-precompiled 20170203-190642 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2c365632..708407403 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1553,7 +1553,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#416d00db677b8219f7548bb4dfa2f25c4b19f36e" +source = "git+https://github.com/ethcore/js-precompiled.git#d7b092baca362229cb2f1d75e7c5922503feb3f9" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index ff3320f50..3596aaa93 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.62", + "version": "0.3.63", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", From a68ca7444ece7306dc94c6174a64832e54c967a1 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot Date: Fri, 3 Feb 2017 19:22:26 +0000 Subject: [PATCH 04/10] [ci skip] js-precompiled 20170203-191721 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 708407403..d17b75c43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1553,7 +1553,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#d7b092baca362229cb2f1d75e7c5922503feb3f9" +source = "git+https://github.com/ethcore/js-precompiled.git#6d27795a53494f201905f512e3c41d497ce03c54" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index 3596aaa93..a9aa63386 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.63", + "version": "0.3.64", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", From c7f5ee481da237d18a32bb486de2f28c2b57ee53 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Fri, 3 Feb 2017 22:44:43 +0100 Subject: [PATCH 05/10] Extend Portal component with title, buttons & steps (as per Modal) (#4392) * Allow Portal to take title & buttons props * Fix tests * Portal consistent in screen center * Allow hiding of Close (e.g. FirstRun usage) * Set overflow style on body based on open * Don't lock scroll for child popups (overlaps) * Override buttons to be white * Expose ~/ui/Modal/Title as re-usable component * Use ~/ui/Title to render the Title * Update tests * Added a portal example with buttons and steps * Address PR comments * Fix AddressSelect with new container withing container * Move legend to "buttons" * AddressSelect extra padding --- js/src/modals/AddDapps/addDapps.css | 1 - js/src/modals/AddDapps/addDapps.js | 16 ++- .../DappPermissions/dappPermissions.css | 6 -- .../modals/DappPermissions/dappPermissions.js | 39 ++++---- js/src/ui/Container/Title/title.css | 28 +++--- js/src/ui/Container/Title/title.js | 54 ++++++---- .../ui/Form/AddressSelect/addressSelect.css | 11 ++- js/src/ui/Form/AddressSelect/addressSelect.js | 58 ++++++----- js/src/ui/Modal/modal.css | 14 --- js/src/ui/Modal/modal.js | 7 +- js/src/ui/Portal/portal.css | 61 +++++++++--- js/src/ui/Portal/portal.example.js | 24 +++++ js/src/ui/Portal/portal.js | 98 ++++++++++++++++--- js/src/ui/Portal/portal.spec.js | 17 ++++ js/src/ui/{Modal => }/Title/index.js | 0 js/src/ui/Title/title.css | 26 +++++ js/src/ui/{Modal => }/Title/title.js | 42 +++++--- js/src/ui/index.js | 2 + 18 files changed, 345 insertions(+), 159 deletions(-) rename js/src/ui/{Modal => }/Title/index.js (100%) create mode 100644 js/src/ui/Title/title.css rename js/src/ui/{Modal => }/Title/title.js (65%) diff --git a/js/src/modals/AddDapps/addDapps.css b/js/src/modals/AddDapps/addDapps.css index 8de8f7f80..b26ffa623 100644 --- a/js/src/modals/AddDapps/addDapps.css +++ b/js/src/modals/AddDapps/addDapps.css @@ -20,7 +20,6 @@ } .container { - margin-top: 1.5em; overflow-y: auto; } diff --git a/js/src/modals/AddDapps/addDapps.js b/js/src/modals/AddDapps/addDapps.js index 0a4d19af9..9b1fcc760 100644 --- a/js/src/modals/AddDapps/addDapps.js +++ b/js/src/modals/AddDapps/addDapps.js @@ -18,7 +18,7 @@ import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; -import { ContainerTitle, DappCard, Portal, SectionList } from '~/ui'; +import { DappCard, Portal, SectionList } from '~/ui'; import { CheckIcon } from '~/ui/Icons'; import styles from './addDapps.css'; @@ -41,15 +41,13 @@ export default class AddDapps extends Component { className={ styles.modal } onClose={ store.closeModal } open + title={ + + } > - - } - />
{ diff --git a/js/src/modals/DappPermissions/dappPermissions.css b/js/src/modals/DappPermissions/dappPermissions.css index 8b7c03a6a..22df5d24b 100644 --- a/js/src/modals/DappPermissions/dappPermissions.css +++ b/js/src/modals/DappPermissions/dappPermissions.css @@ -15,12 +15,7 @@ /* along with Parity. If not, see . */ -.modal { - flex-direction: column; -} - .container { - margin-top: 1.5em; overflow-y: auto; } @@ -65,7 +60,6 @@ .legend { opacity: 0.75; - margin-top: 1em; span { line-height: 24px; diff --git a/js/src/modals/DappPermissions/dappPermissions.js b/js/src/modals/DappPermissions/dappPermissions.js index dd6318368..4cd7cc837 100644 --- a/js/src/modals/DappPermissions/dappPermissions.js +++ b/js/src/modals/DappPermissions/dappPermissions.js @@ -18,7 +18,7 @@ import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; -import { AccountCard, ContainerTitle, Portal, SectionList } from '~/ui'; +import { AccountCard, Portal, SectionList } from '~/ui'; import { CheckIcon, StarIcon, StarOutlineIcon } from '~/ui/Icons'; import styles from './dappPermissions.css'; @@ -38,18 +38,27 @@ export default class DappPermissions extends Component { return ( + , + defaultIcon: + } } + /> +
+ } onClose={ store.closeModal } open + title={ + + } > - - } - />
-
- , - defaultIcon: - } } - /> -
); } diff --git a/js/src/ui/Container/Title/title.css b/js/src/ui/Container/Title/title.css index 0dec03905..b597aea3e 100644 --- a/js/src/ui/Container/Title/title.css +++ b/js/src/ui/Container/Title/title.css @@ -14,30 +14,36 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ -.byline, .description { + +$bylineColor: #aaa; +$bylineLineHeight: 1.2rem; +$bylineMaxHeight: 2.4rem; +$titleLineHeight: 2rem; +$smallFontSize: 0.75rem; + +.byline, +.description { + color: $bylineColor; + display: -webkit-box; + line-height: $bylineLineHeight; + max-height: $bylineMaxHeight; overflow: hidden; position: relative; - line-height: 1.2em; - max-height: 2.4em; - - display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; - color: #aaa; - * { - color: #aaa !important; + color: $bylineColor !important; } } .description { - font-size: 0.75em; + font-size: $smallFontSize; margin: 0.5em 0 0; } .title { - text-transform: uppercase; + line-height: $titleLineHeight; margin: 0; - line-height: 34px; + text-transform: uppercase; } diff --git a/js/src/ui/Container/Title/title.js b/js/src/ui/Container/Title/title.js index 1eb264201..217ad13bf 100644 --- a/js/src/ui/Container/Title/title.js +++ b/js/src/ui/Container/Title/title.js @@ -29,29 +29,41 @@ export default class Title extends Component { } render () { - const { byline, className, title } = this.props; - - const byLine = typeof byline === 'string' - ? ( - - { byline } - - ) - : byline; + const { className, title } = this.props; return (

{ title }

-
- { byLine } -
+ { this.renderByline() } { this.renderDescription() }
); } + renderByline () { + const { byline } = this.props; + + if (!byline) { + return null; + } + + return ( +
+ { + typeof byline === 'string' + ? ( + + { byline } + + ) + : byline + } +
+ ); + } + renderDescription () { const { description } = this.props; @@ -59,17 +71,17 @@ export default class Title extends Component { return null; } - const desc = typeof description === 'string' - ? ( - - { description } - - ) - : description; - return (
- { desc } + { + typeof description === 'string' + ? ( + + { description } + + ) + : description + }
); } diff --git a/js/src/ui/Form/AddressSelect/addressSelect.css b/js/src/ui/Form/AddressSelect/addressSelect.css index 0502a271f..43d6a7075 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.css +++ b/js/src/ui/Form/AddressSelect/addressSelect.css @@ -73,6 +73,12 @@ } } +.title { + display: flex; + flex-direction: column; + position: relative; +} + .label { margin: 1rem 0.5rem 0.25em; color: rgba(255, 255, 255, 0.498039); @@ -102,14 +108,11 @@ } .categories { - flex: 1; - display: flex; + flex: 1; flex-direction: row; justify-content: flex-start; - margin: 2rem 0 0; - > * { flex: 1; } diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index ce3aad2d3..d3c7462c3 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -180,34 +180,38 @@ class AddressSelect extends Component { onClose={ this.handleClose } onKeyDown={ this.handleKeyDown } open={ expanded } + title={ +
+ +
+ + { this.renderLoader() } +
+ +
+ +
+ + { this.renderCurrentInput() } + { this.renderRegistryValues() } +
+ } > - -
- - { this.renderLoader() } -
- -
- -
- - { this.renderCurrentInput() } - { this.renderRegistryValues() } { this.renderAccounts() } ); diff --git a/js/src/ui/Modal/modal.css b/js/src/ui/Modal/modal.css index 2b8172f2f..5eb419a76 100644 --- a/js/src/ui/Modal/modal.css +++ b/js/src/ui/Modal/modal.css @@ -47,20 +47,6 @@ .title { background: rgba(0, 0, 0, 0.25) !important; padding: 1em; - margin-bottom: 0; - - h3 { - margin: 0; - text-transform: uppercase; - } - - .steps { - margin-bottom: -1em; - } -} - -.waiting { - margin: 1em -1em -1em -1em; } .overlay { diff --git a/js/src/ui/Modal/modal.js b/js/src/ui/Modal/modal.js index 7dde32f45..7c7e19883 100644 --- a/js/src/ui/Modal/modal.js +++ b/js/src/ui/Modal/modal.js @@ -22,7 +22,7 @@ import { connect } from 'react-redux'; import { nodeOrStringProptype } from '~/util/proptypes'; import Container from '../Container'; -import Title from './Title'; +import Title from '../Title'; const ACTIONS_STYLE = { borderStyle: 'none' }; const TITLE_STYLE = { borderStyle: 'none' }; @@ -63,11 +63,12 @@ class Modal extends Component { const contentStyle = muiTheme.parity.getBackgroundStyle(null, settings.backgroundSeed); const header = ( ); const classes = `${styles.dialog} ${className}`; diff --git a/js/src/ui/Portal/portal.css b/js/src/ui/Portal/portal.css index daa646ba9..d88150bfe 100644 --- a/js/src/ui/Portal/portal.css +++ b/js/src/ui/Portal/portal.css @@ -16,13 +16,14 @@ */ $modalMargin: 1.5em; +$modalPadding: 1.5em; $modalBackZ: 2500; /* This should be the default case, the Portal used as a stand-alone modal */ -$modalBottom: 15vh; +$modalBottom: $modalMargin; $modalLeft: $modalMargin; $modalRight: $modalMargin; -$modalTop: 0; +$modalTop: $modalMargin; $modalZ: 3500; /* This is the case where popped-up over another modal, Portal or otherwise */ @@ -55,7 +56,9 @@ $popoverZ: 3600; background-color: rgba(0, 0, 0, 1); box-sizing: border-box; display: flex; - padding: 1.5em; + flex-direction: column; + justify-content: space-between; + padding: $modalPadding; position: fixed; * { @@ -77,22 +80,48 @@ $popoverZ: 3600; width: calc(100vw - $popoverLeft - $popoverRight); z-index: $popoverZ; } -} -.closeIcon { - font-size: 4em; - position: absolute; - right: 1rem; - top: 0.5rem; - z-index: 100; + .buttonRow { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-end; + padding: $modalPadding 0 0 0; - &, * { - height: 48px !important; - width: 48px !important; + button:not([disabled]) { + color: white !important; + + svg { + fill: white !important; + } + } } - &:hover { - cursor: pointer; - opacity: 0.5; + .childContainer { + flex: 1; + overflow-x: hidden; + overflow-y: auto; + } + + .closeIcon { + font-size: 4em; + position: absolute; + right: 1rem; + top: 0.5rem; + z-index: 100; + + &, * { + height: 48px !important; + width: 48px !important; + } + + &:hover { + cursor: pointer; + opacity: 0.5; + } + } + + .titleRow { + margin-bottom: $modalPadding; } } diff --git a/js/src/ui/Portal/portal.example.js b/js/src/ui/Portal/portal.example.js index e8c51008e..7305a27b0 100644 --- a/js/src/ui/Portal/portal.example.js +++ b/js/src/ui/Portal/portal.example.js @@ -16,6 +16,7 @@ import React, { Component } from 'react'; +import { Button } from '~/ui'; import PlaygroundExample from '~/playground/playgroundExample'; import Modal from '../Modal'; @@ -77,6 +78,29 @@ export default class PortalExample extends Component { </Portal> </div> </PlaygroundExample> + + <PlaygroundExample name='Portal with Buttons'> + <div> + <button onClick={ this.handleOpen(4) }>Open</button> + <Portal + activeStep={ 0 } + buttons={ [ + <Button + key='close' + label='close' + onClick={ this.handleClose } + /> + ] } + isChildModal + open={ open[4] || false } + onClose={ this.handleClose } + steps={ [ 'step 1', 'step 2' ] } + title='Portal with button' + > + <p>This is the fourth portal</p> + </Portal> + </div> + </PlaygroundExample> </div> ); } diff --git a/js/src/ui/Portal/portal.js b/js/src/ui/Portal/portal.js index f334405ad..03b8b1f18 100644 --- a/js/src/ui/Portal/portal.js +++ b/js/src/ui/Portal/portal.js @@ -20,8 +20,10 @@ import ReactDOM from 'react-dom'; import ReactPortal from 'react-portal'; import keycode from 'keycode'; +import { nodeOrStringProptype } from '~/util/proptypes'; import { CloseIcon } from '~/ui/Icons'; import ParityBackground from '~/ui/ParityBackground'; +import Title from '~/ui/Title'; import styles from './portal.css'; @@ -29,14 +31,35 @@ export default class Portal extends Component { static propTypes = { onClose: PropTypes.func.isRequired, open: PropTypes.bool.isRequired, + activeStep: PropTypes.number, + busy: PropTypes.bool, + busySteps: PropTypes.array, + buttons: PropTypes.array, children: PropTypes.node, className: PropTypes.string, + hideClose: PropTypes.bool, isChildModal: PropTypes.bool, - onKeyDown: PropTypes.func + onKeyDown: PropTypes.func, + steps: PropTypes.array, + title: nodeOrStringProptype() }; + componentDidMount () { + this.setBodyOverflow(this.props.open); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.open !== this.props.open) { + this.setBodyOverflow(nextProps.open); + } + } + + componentWillUnmount () { + this.setBodyOverflow(false); + } + render () { - const { children, className, isChildModal, open } = this.props; + const { activeStep, busy, busySteps, children, className, isChildModal, open, steps, title } = this.props; if (!open) { return null; @@ -69,32 +92,72 @@ export default class Portal extends Component { onKeyUp={ this.handleKeyUp } /> <ParityBackground className={ styles.parityBackground } /> - <div - className={ styles.closeIcon } - onClick={ this.handleClose } - > - <CloseIcon /> + { this.renderClose() } + <Title + activeStep={ activeStep } + busy={ busy } + busySteps={ busySteps } + className={ styles.titleRow } + steps={ steps } + title={ title } + /> + <div className={ styles.childContainer }> + { children } </div> - { children } + { this.renderButtons() } </div> </div> </ReactPortal> ); } + renderButtons () { + const { buttons } = this.props; + + if (!buttons) { + return null; + } + + return ( + <div className={ styles.buttonRow }> + { buttons } + </div> + ); + } + + renderClose () { + const { hideClose } = this.props; + + if (hideClose) { + return null; + } + + return ( + <CloseIcon + className={ styles.closeIcon } + onClick={ this.handleClose } + /> + ); + } + stopEvent = (event) => { event.preventDefault(); event.stopPropagation(); } handleClose = () => { - this.props.onClose(); + const { hideClose, onClose } = this.props; + + if (!hideClose) { + onClose(); + } } handleKeyDown = (event) => { const { onKeyDown } = this.props; event.persist(); + return onKeyDown ? onKeyDown(event) : false; @@ -111,10 +174,11 @@ export default class Portal extends Component { } handleDOMAction = (ref, method) => { - const refItem = typeof ref === 'string' - ? this.refs[ref] - : ref; - const element = ReactDOM.findDOMNode(refItem); + const element = ReactDOM.findDOMNode( + typeof ref === 'string' + ? this.refs[ref] + : ref + ); if (!element || typeof element[method] !== 'function') { console.warn('could not find', ref, 'or method', method); @@ -123,4 +187,12 @@ export default class Portal extends Component { return element[method](); } + + setBodyOverflow (open) { + if (!this.props.isChildModal) { + document.body.style.overflow = open + ? 'hidden' + : null; + } + } } diff --git a/js/src/ui/Portal/portal.spec.js b/js/src/ui/Portal/portal.spec.js index 6d2f5d5f3..fdc1ab4a7 100644 --- a/js/src/ui/Portal/portal.spec.js +++ b/js/src/ui/Portal/portal.spec.js @@ -44,4 +44,21 @@ describe('ui/Portal', () => { it('renders defaults', () => { expect(component).to.be.ok; }); + + describe('title rendering', () => { + const TITLE = 'some test title'; + let title; + + beforeEach(() => { + title = render({ title: TITLE }).find('Title'); + }); + + it('renders the specified title', () => { + expect(title).to.have.length(1); + }); + + it('renders the passed title', () => { + expect(title.props().title).to.equal(TITLE); + }); + }); }); diff --git a/js/src/ui/Modal/Title/index.js b/js/src/ui/Title/index.js similarity index 100% rename from js/src/ui/Modal/Title/index.js rename to js/src/ui/Title/index.js diff --git a/js/src/ui/Title/title.css b/js/src/ui/Title/title.css new file mode 100644 index 000000000..c211b0586 --- /dev/null +++ b/js/src/ui/Title/title.css @@ -0,0 +1,26 @@ +/* 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/>. +*/ + +.title { + .steps { + margin: -0.5em 0 -1em 0; + } + + .waiting { + margin: 1em -1em -1em -1em; + } +} diff --git a/js/src/ui/Modal/Title/title.js b/js/src/ui/Title/title.js similarity index 65% rename from js/src/ui/Modal/Title/title.js rename to js/src/ui/Title/title.js index d39bbb772..6f1319bd0 100644 --- a/js/src/ui/Modal/Title/title.js +++ b/js/src/ui/Title/title.js @@ -14,35 +14,49 @@ // 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 { LinearProgress } from 'material-ui'; import { Step, Stepper, StepLabel } from 'material-ui/Stepper'; +import React, { Component, PropTypes } from 'react'; +// TODO: It would make sense (going forward) to replace all uses of +// ContainerTitle with this component. In that case the styles for the +// h3 (title) can be pulled from there. (As it stands the duplication +// between the 2 has been removed, but as a short-term DRY only) +import { Title as ContainerTitle } from '~/ui/Container'; import { nodeOrStringProptype } from '~/util/proptypes'; -import styles from '../modal.css'; +import styles from './title.css'; export default class Title extends Component { static propTypes = { + activeStep: PropTypes.number, busy: PropTypes.bool, - current: PropTypes.number, + busySteps: PropTypes.array, + className: PropTypes.string, steps: PropTypes.array, - waiting: PropTypes.array, title: nodeOrStringProptype() } render () { - const { current, steps, title } = this.props; + const { activeStep, className, steps, title } = this.props; + + if (!title && !steps) { + return null; + } return ( - <div className={ styles.title }> - <h3> - { + <div + className={ + [styles.title, className].join(' ') + } + > + <ContainerTitle + title={ steps - ? steps[current] + ? steps[activeStep || 0] : title } - </h3> + /> { this.renderSteps() } { this.renderWaiting() } </div> @@ -50,7 +64,7 @@ export default class Title extends Component { } renderSteps () { - const { current, steps } = this.props; + const { activeStep, steps } = this.props; if (!steps) { return; @@ -58,7 +72,7 @@ export default class Title extends Component { return ( <div className={ styles.steps }> - <Stepper activeStep={ current }> + <Stepper activeStep={ activeStep }> { this.renderTimeline() } </Stepper> </div> @@ -80,8 +94,8 @@ export default class Title extends Component { } renderWaiting () { - const { current, busy, waiting } = this.props; - const isWaiting = busy || (waiting || []).includes(current); + const { activeStep, busy, busySteps } = this.props; + const isWaiting = busy || (busySteps || []).includes(activeStep); if (!isWaiting) { return null; diff --git a/js/src/ui/index.js b/js/src/ui/index.js index eed56d369..967465165 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -55,6 +55,7 @@ import SectionList from './SectionList'; import ShortenedHash from './ShortenedHash'; import SignerIcon from './SignerIcon'; import Tags from './Tags'; +import Title from './Title'; import Tooltips, { Tooltip } from './Tooltips'; import TxHash from './TxHash'; import TxList from './TxList'; @@ -119,6 +120,7 @@ export { SectionList, SignerIcon, Tags, + Title, Tooltip, Tooltips, TxHash, From c9d38cac6e3469c17700654b402da113d2c85ba1 Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Fri, 3 Feb 2017 22:18:13 +0000 Subject: [PATCH 06/10] [ci skip] js-precompiled 20170203-221154 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d17b75c43..cbb85572f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1553,7 +1553,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#6d27795a53494f201905f512e3c41d497ce03c54" +source = "git+https://github.com/ethcore/js-precompiled.git#f79c8ca30126b57c951560fefe29764fd7cad0ee" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index a9aa63386..a702f2795 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.64", + "version": "0.3.65", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team <admin@parity.io>", From b4c24d5ab3a56393250e86b845c87882e7aedaa4 Mon Sep 17 00:00:00 2001 From: Jaco Greeff <jacogr@gmail.com> Date: Sat, 4 Feb 2017 09:52:14 +0100 Subject: [PATCH 07/10] Web view with web3.site support (#4313) * Web-domain based routing * Support base32-encoded urls * Proper support for non-domain based routing * Handling long domain names * Switching to web3.site * Encoding for *.web3.site urls * Add DappUrlInput component * Update Web views with store * Update spec description * Update spec description * edited url does not allow in-place store edits * Fixing dapps access on 127.0.0.1:8180 * Use /web/<hash> urls for iframe * Redirecting to parity.web3.site * Disabling the redirection --- Cargo.lock | 7 + dapps/Cargo.toml | 1 + dapps/src/api/api.rs | 1 + dapps/src/apps/mod.rs | 4 +- dapps/src/endpoint.rs | 1 + dapps/src/lib.rs | 7 +- dapps/src/page/handler.rs | 1 + dapps/src/router/mod.rs | 49 +++-- dapps/src/tests/api.rs | 6 +- dapps/src/tests/fetch.rs | 160 ++++++++++---- dapps/src/tests/redirection.rs | 4 +- dapps/src/tests/validation.rs | 2 +- dapps/src/web.rs | 79 +++---- devtools/src/http_client.rs | 2 +- js/package.json | 1 + js/src/ui/Form/DappUrlInput/dappUrlInput.js | 61 ++++++ .../ui/Form/DappUrlInput/dappUrlInput.spec.js | 70 ++++++ js/src/ui/Form/DappUrlInput/index.js | 17 ++ js/src/ui/Form/index.js | 2 + js/src/ui/index.js | 5 +- js/src/util/dapplink.js | 60 ++++++ js/src/util/dapplink.spec.js | 83 +++++++ js/src/views/Web/AddressBar/addressBar.js | 93 +++----- .../views/Web/AddressBar/addressBar.spec.js | 48 +++++ js/src/views/Web/store.js | 158 ++++++++++++++ js/src/views/Web/store.spec.js | 202 ++++++++++++++++++ js/src/views/Web/web.js | 109 +++------- js/src/views/Web/web.spec.js | 56 +++++ signer/src/tests/mod.rs | 28 ++- signer/src/ws_server/mod.rs | 3 +- signer/src/ws_server/session.rs | 31 ++- 31 files changed, 1071 insertions(+), 280 deletions(-) create mode 100644 js/src/ui/Form/DappUrlInput/dappUrlInput.js create mode 100644 js/src/ui/Form/DappUrlInput/dappUrlInput.spec.js create mode 100644 js/src/ui/Form/DappUrlInput/index.js create mode 100644 js/src/util/dapplink.js create mode 100644 js/src/util/dapplink.spec.js create mode 100644 js/src/views/Web/AddressBar/addressBar.spec.js create mode 100644 js/src/views/Web/store.js create mode 100644 js/src/views/Web/store.spec.js create mode 100644 js/src/views/Web/web.spec.js diff --git a/Cargo.lock b/Cargo.lock index cbb85572f..bda84ce62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,6 +106,11 @@ dependencies = [ "syntex_syntax 0.33.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "base32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bigint" version = "1.0.0" @@ -409,6 +414,7 @@ dependencies = [ name = "ethcore-dapps" version = "1.6.0" dependencies = [ + "base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "clippy 0.0.103 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", "ethcore-devtools 1.6.0", @@ -2463,6 +2469,7 @@ dependencies = [ "checksum app_dirs 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b7d1c0d48a81bbb13043847f957971f4d87c81542d80ece5e84ba3cba4058fd4" "checksum arrayvec 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "16e3bdb2f54b3ace0285975d59a97cf8ed3855294b2b6bc651fcf22a9c352975" "checksum aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07d344974f0a155f091948aa389fb1b912d3a58414fbdb9c8d446d193ee3496a" +"checksum base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1b9605ba46d61df0410d8ac686b0007add8172eba90e8e909c347856fe794d8c" "checksum bigint 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2311bcd71b281e142a095311c22509f0d6bcd87b3000d7dbaa810929b9d6f6ae" "checksum bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9bf6104718e80d7b26a68fdbacff3481cfc05df670821affc7e9cbc1884400c" "checksum bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5b97c2c8e8bbb4251754f559df8af22fb264853c7d009084a576cdf12565089d" diff --git a/dapps/Cargo.toml b/dapps/Cargo.toml index 09c72cb47..459a5ea70 100644 --- a/dapps/Cargo.toml +++ b/dapps/Cargo.toml @@ -23,6 +23,7 @@ serde = "0.8" serde_json = "0.8" linked-hash-map = "0.3" parity-dapps-glue = "1.4" +base32 = "0.3" mime = "0.2" mime_guess = "1.6.1" time = "0.1.35" diff --git a/dapps/src/api/api.rs b/dapps/src/api/api.rs index 305d9bc83..b521c0ba1 100644 --- a/dapps/src/api/api.rs +++ b/dapps/src/api/api.rs @@ -123,6 +123,7 @@ impl server::Handler<net::HttpStream> for RestApiRouter { return Next::write(); } + // TODO [ToDr] Consider using `path.app_params` instead let url = extract_url(&request); if url.is_none() { // Just return 404 if we can't parse URL diff --git a/dapps/src/apps/mod.rs b/dapps/src/apps/mod.rs index 1959940e7..b85f0dde9 100644 --- a/dapps/src/apps/mod.rs +++ b/dapps/src/apps/mod.rs @@ -32,8 +32,8 @@ pub mod manifest; extern crate parity_ui; -pub const HOME_PAGE: &'static str = "home"; -pub const DAPPS_DOMAIN: &'static str = ".parity"; +pub const HOME_PAGE: &'static str = "parity"; +pub const DAPPS_DOMAIN: &'static str = ".web3.site"; pub const RPC_PATH: &'static str = "rpc"; pub const API_PATH: &'static str = "api"; pub const UTILS_PATH: &'static str = "parity-utils"; diff --git a/dapps/src/endpoint.rs b/dapps/src/endpoint.rs index 5bfa1c3d5..648d82ff8 100644 --- a/dapps/src/endpoint.rs +++ b/dapps/src/endpoint.rs @@ -22,6 +22,7 @@ use std::collections::BTreeMap; #[derive(Debug, PartialEq, Default, Clone)] pub struct EndpointPath { pub app_id: String, + pub app_params: Vec<String>, pub host: String, pub port: u16, pub using_dapps_domains: bool, diff --git a/dapps/src/lib.rs b/dapps/src/lib.rs index 4cf9c333f..9ae163f88 100644 --- a/dapps/src/lib.rs +++ b/dapps/src/lib.rs @@ -19,6 +19,7 @@ #![warn(missing_docs)] #![cfg_attr(feature="nightly", plugin(clippy))] +extern crate base32; extern crate hyper; extern crate time; extern crate url as url_lib; @@ -91,11 +92,11 @@ impl<F> SyncStatus for F where F: Fn() -> bool + Send + Sync { /// Validates Web Proxy tokens pub trait WebProxyTokens: Send + Sync { /// Should return true if token is a valid web proxy access token. - fn is_web_proxy_token_valid(&self, token: &String) -> bool; + fn is_web_proxy_token_valid(&self, token: &str) -> bool; } impl<F> WebProxyTokens for F where F: Fn(String) -> bool + Send + Sync { - fn is_web_proxy_token_valid(&self, token: &String) -> bool { self(token.to_owned()) } + fn is_web_proxy_token_valid(&self, token: &str) -> bool { self(token.to_owned()) } } /// Webapps HTTP+RPC server build. @@ -409,6 +410,6 @@ mod util_tests { // then assert_eq!(none, Vec::<String>::new()); - assert_eq!(some, vec!["http://home.parity".to_owned(), "http://127.0.0.1:18180".into()]); + assert_eq!(some, vec!["http://parity.web3.site".to_owned(), "http://127.0.0.1:18180".into()]); } } diff --git a/dapps/src/page/handler.rs b/dapps/src/page/handler.rs index 31e5da072..c2b68f750 100644 --- a/dapps/src/page/handler.rs +++ b/dapps/src/page/handler.rs @@ -252,6 +252,7 @@ fn should_extract_path_with_appid() { prefix: None, path: EndpointPath { app_id: "app".to_owned(), + app_params: vec![], host: "".to_owned(), port: 8080, using_dapps_domains: true, diff --git a/dapps/src/router/mod.rs b/dapps/src/router/mod.rs index 116ac7d8e..dbaf4dbb0 100644 --- a/dapps/src/router/mod.rs +++ b/dapps/src/router/mod.rs @@ -97,9 +97,7 @@ impl<A: Authorization + 'static> server::Handler<HttpStream> for Router<A> { => { trace!(target: "dapps", "Redirecting to correct web request: {:?}", referer_url); - // TODO [ToDr] Some nice util for this! - let using_domain = if referer.using_dapps_domains { 0 } else { 1 }; - let len = cmp::min(referer_url.path.len(), using_domain + 3); // token + protocol + hostname + let len = cmp::min(referer_url.path.len(), 2); // /web/<encoded>/ let base = referer_url.path[..len].join("/"); let requested = url.map(|u| u.path.join("/")).unwrap_or_default(); Redirection::boxed(&format!("/{}/{}", base, requested)) @@ -262,20 +260,27 @@ fn extract_endpoint(url: &Option<Url>) -> (Option<EndpointPath>, SpecialEndpoint match *url { Some(ref url) => match url.host { Host::Domain(ref domain) if domain.ends_with(DAPPS_DOMAIN) => { - let len = domain.len() - DAPPS_DOMAIN.len(); - let id = domain[0..len].to_owned(); + let id = &domain[0..(domain.len() - DAPPS_DOMAIN.len())]; + let (id, params) = if let Some(split) = id.rfind('.') { + let (params, id) = id.split_at(split); + (id[1..].to_owned(), [params.to_owned()].into_iter().chain(&url.path).cloned().collect()) + } else { + (id.to_owned(), url.path.clone()) + }; (Some(EndpointPath { app_id: id, + app_params: params, host: domain.clone(), port: url.port, using_dapps_domains: true, }), special_endpoint(url)) }, _ if url.path.len() > 1 => { - let id = url.path[0].clone(); + let id = url.path[0].to_owned(); (Some(EndpointPath { - app_id: id.clone(), + app_id: id, + app_params: url.path[1..].to_vec(), host: format!("{}", url.host), port: url.port, using_dapps_domains: false, @@ -296,6 +301,7 @@ fn should_extract_endpoint() { extract_endpoint(&Url::parse("http://localhost:8080/status/index.html").ok()), (Some(EndpointPath { app_id: "status".to_owned(), + app_params: vec!["index.html".to_owned()], host: "localhost".to_owned(), port: 8080, using_dapps_domains: false, @@ -307,6 +313,7 @@ fn should_extract_endpoint() { extract_endpoint(&Url::parse("http://localhost:8080/rpc/").ok()), (Some(EndpointPath { app_id: "rpc".to_owned(), + app_params: vec!["".to_owned()], host: "localhost".to_owned(), port: 8080, using_dapps_domains: false, @@ -314,10 +321,11 @@ fn should_extract_endpoint() { ); assert_eq!( - extract_endpoint(&Url::parse("http://my.status.parity/parity-utils/inject.js").ok()), + extract_endpoint(&Url::parse("http://my.status.web3.site/parity-utils/inject.js").ok()), (Some(EndpointPath { - app_id: "my.status".to_owned(), - host: "my.status.parity".to_owned(), + app_id: "status".to_owned(), + app_params: vec!["my".to_owned(), "parity-utils".into(), "inject.js".into()], + host: "my.status.web3.site".to_owned(), port: 80, using_dapps_domains: true, }), SpecialEndpoint::Utils) @@ -325,10 +333,11 @@ fn should_extract_endpoint() { // By Subdomain assert_eq!( - extract_endpoint(&Url::parse("http://my.status.parity/test.html").ok()), + extract_endpoint(&Url::parse("http://status.web3.site/test.html").ok()), (Some(EndpointPath { - app_id: "my.status".to_owned(), - host: "my.status.parity".to_owned(), + app_id: "status".to_owned(), + app_params: vec!["test.html".to_owned()], + host: "status.web3.site".to_owned(), port: 80, using_dapps_domains: true, }), SpecialEndpoint::None) @@ -336,10 +345,11 @@ fn should_extract_endpoint() { // RPC by subdomain assert_eq!( - extract_endpoint(&Url::parse("http://my.status.parity/rpc/").ok()), + extract_endpoint(&Url::parse("http://my.status.web3.site/rpc/").ok()), (Some(EndpointPath { - app_id: "my.status".to_owned(), - host: "my.status.parity".to_owned(), + app_id: "status".to_owned(), + app_params: vec!["my".to_owned(), "rpc".into(), "".into()], + host: "my.status.web3.site".to_owned(), port: 80, using_dapps_domains: true, }), SpecialEndpoint::Rpc) @@ -347,10 +357,11 @@ fn should_extract_endpoint() { // API by subdomain assert_eq!( - extract_endpoint(&Url::parse("http://my.status.parity/api/").ok()), + extract_endpoint(&Url::parse("http://my.status.web3.site/api/").ok()), (Some(EndpointPath { - app_id: "my.status".to_owned(), - host: "my.status.parity".to_owned(), + app_id: "status".to_owned(), + app_params: vec!["my".to_owned(), "api".into(), "".into()], + host: "my.status.web3.site".to_owned(), port: 80, using_dapps_domains: true, }), SpecialEndpoint::Api) diff --git a/dapps/src/tests/api.rs b/dapps/src/tests/api.rs index 3c0ae528a..05e285264 100644 --- a/dapps/src/tests/api.rs +++ b/dapps/src/tests/api.rs @@ -143,7 +143,7 @@ fn should_return_signer_port_cors_headers_for_home_parity() { "\ POST /api/ping HTTP/1.1\r\n\ Host: localhost:8080\r\n\ - Origin: http://home.parity\r\n\ + Origin: http://parity.web3.site\r\n\ Connection: close\r\n\ \r\n\ {} @@ -153,8 +153,8 @@ fn should_return_signer_port_cors_headers_for_home_parity() { // then assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); assert!( - response.headers_raw.contains("Access-Control-Allow-Origin: http://home.parity"), - "CORS header for home.parity missing: {:?}", + response.headers_raw.contains("Access-Control-Allow-Origin: http://parity.web3.site"), + "CORS header for parity.web3.site missing: {:?}", response.headers ); } diff --git a/dapps/src/tests/fetch.rs b/dapps/src/tests/fetch.rs index 8f2e22e5c..4f343b3a3 100644 --- a/dapps/src/tests/fetch.rs +++ b/dapps/src/tests/fetch.rs @@ -31,7 +31,7 @@ fn should_resolve_dapp() { let response = request(server, "\ GET / HTTP/1.1\r\n\ - Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.parity\r\n\ + Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.web3.site\r\n\ Connection: close\r\n\ \r\n\ " @@ -52,7 +52,7 @@ fn should_return_503_when_syncing_but_should_make_the_calls() { let response = request(server, "\ GET / HTTP/1.1\r\n\ - Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.parity\r\n\ + Host: 1472a9e190620cdf6b31f383373e45efcfe869a820c91f9ccd7eb9fb45e4985d.web3.site\r\n\ Connection: close\r\n\ \r\n\ " @@ -81,7 +81,7 @@ fn should_return_502_on_hash_mismatch() { let response = request(server, "\ GET / HTTP/1.1\r\n\ - Host: 94f093625c06887d94d9fee0d5f9cc4aaa46f33d24d1c7e4b5237e7c37d547dd.parity\r\n\ + Host: 94f093625c06887d94d9fee0d5f9cc4aaa46f33d24d1c7e4b5237e7c37d547dd.web3.site\r\n\ Connection: close\r\n\ \r\n\ " @@ -112,7 +112,7 @@ fn should_return_error_for_invalid_dapp_zip() { let response = request(server, "\ GET / HTTP/1.1\r\n\ - Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\ + Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\ Connection: close\r\n\ \r\n\ " @@ -144,7 +144,7 @@ fn should_return_fetched_dapp_content() { let response1 = http_client::request(server.addr(), "\ GET /index.html HTTP/1.1\r\n\ - Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.parity\r\n\ + Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.web3.site\r\n\ Connection: close\r\n\ \r\n\ " @@ -152,7 +152,7 @@ fn should_return_fetched_dapp_content() { let response2 = http_client::request(server.addr(), "\ GET /manifest.json HTTP/1.1\r\n\ - Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.parity\r\n\ + Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.web3.site\r\n\ Connection: close\r\n\ \r\n\ " @@ -207,7 +207,7 @@ fn should_return_fetched_content() { let response = request(server, "\ GET / HTTP/1.1\r\n\ - Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\ + Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\ Connection: close\r\n\ \r\n\ " @@ -234,7 +234,7 @@ fn should_cache_content() { ); let request_str = "\ GET / HTTP/1.1\r\n\ - Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\ + Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\ Connection: close\r\n\ \r\n\ "; @@ -265,7 +265,7 @@ fn should_not_request_content_twice() { ); let request_str = "\ GET / HTTP/1.1\r\n\ - Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\ + Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.web3.site\r\n\ Connection: close\r\n\ \r\n\ "; @@ -298,6 +298,17 @@ fn should_not_request_content_twice() { response2.assert_status("HTTP/1.1 200 OK"); } +#[test] +fn should_encode_and_decode_base32() { + use base32; + + let encoded = base32::encode(base32::Alphabet::Crockford, "token+https://parity.io".as_bytes()); + assert_eq!("EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY", &encoded); + + let data = base32::decode(base32::Alphabet::Crockford, "EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY").unwrap(); + assert_eq!("token+https://parity.io", &String::from_utf8(data).unwrap()); +} + #[test] fn should_stream_web_content() { // given @@ -306,8 +317,8 @@ fn should_stream_web_content() { // when let response = request(server, "\ - GET /web/token/https/parity.io/ HTTP/1.1\r\n\ - Host: localhost:8080\r\n\ + GET / HTTP/1.1\r\n\ + Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\ Connection: close\r\n\ \r\n\ " @@ -322,20 +333,90 @@ fn should_stream_web_content() { } #[test] -fn should_return_error_on_invalid_token() { +fn should_support_base32_encoded_web_urls() { // given let (server, fetch) = serve_with_fetch("token"); // when let response = request(server, "\ - GET /web/invalidtoken/https/parity.io/ HTTP/1.1\r\n\ + GET /styles.css?test=123 HTTP/1.1\r\n\ + Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\ + Connection: close\r\n\ + \r\n\ + " + ); + + // then + response.assert_status("HTTP/1.1 200 OK"); + assert_security_headers_for_embed(&response.headers); + + fetch.assert_requested("https://parity.io/styles.css?test=123"); + fetch.assert_no_more_requests(); +} + +#[test] +fn should_correctly_handle_long_label_when_splitted() { + // given + let (server, fetch) = serve_with_fetch("xolrg9fePeQyKLnL"); + + // when + let response = request(server, + "\ + GET /styles.css?test=123 HTTP/1.1\r\n\ + Host: f1qprwk775k6am35a5wmpk3e9gnpgx3me1sk.mbsfcdqpwx3jd5h7ax39dxq2wvb5dhqpww3fe9t2wrvfdm.web.web3.site\r\n\ + Connection: close\r\n\ + \r\n\ + " + ); + + // then + response.assert_status("HTTP/1.1 200 OK"); + assert_security_headers_for_embed(&response.headers); + + fetch.assert_requested("https://contribution.melonport.com/styles.css?test=123"); + fetch.assert_no_more_requests(); +} + + +#[test] +fn should_support_base32_encoded_web_urls_as_path() { + // given + let (server, fetch) = serve_with_fetch("token"); + + // when + let response = request(server, + "\ + GET /web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/styles.css?test=123 HTTP/1.1\r\n\ Host: localhost:8080\r\n\ Connection: close\r\n\ \r\n\ " ); + // then + response.assert_status("HTTP/1.1 200 OK"); + assert_security_headers_for_embed(&response.headers); + + fetch.assert_requested("https://parity.io/styles.css?test=123"); + fetch.assert_no_more_requests(); +} + +#[test] +fn should_return_error_on_invalid_token() { + // given + let (server, fetch) = serve_with_fetch("test"); + + // when + let response = request(server, + "\ + GET / HTTP/1.1\r\n\ + Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\ + Connection: close\r\n\ + \r\n\ + " + ); + // then response.assert_status("HTTP/1.1 400 Bad Request"); assert_security_headers_for_embed(&response.headers); @@ -365,28 +446,6 @@ fn should_return_error_on_invalid_protocol() { fetch.assert_no_more_requests(); } -#[test] -fn should_redirect_if_trailing_slash_is_missing() { - // given - let (server, fetch) = serve_with_fetch("token"); - - // when - let response = request(server, - "\ - GET /web/token/https/parity.io HTTP/1.1\r\n\ - Host: localhost:8080\r\n\ - Connection: close\r\n\ - \r\n\ - " - ); - - // then - response.assert_status("HTTP/1.1 302 Found"); - response.assert_header("Location", "/web/token/https/parity.io/"); - - fetch.assert_no_more_requests(); -} - #[test] fn should_disallow_non_get_requests() { // given @@ -395,8 +454,8 @@ fn should_disallow_non_get_requests() { // when let response = request(server, "\ - POST /token/https/parity.io/ HTTP/1.1\r\n\ - Host: web.parity\r\n\ + POST / HTTP/1.1\r\n\ + Host: EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY.web.web3.site\r\n\ Content-Type: application/json\r\n\ Connection: close\r\n\ \r\n\ @@ -423,14 +482,37 @@ fn should_fix_absolute_requests_based_on_referer() { GET /styles.css HTTP/1.1\r\n\ Host: localhost:8080\r\n\ Connection: close\r\n\ - Referer: http://localhost:8080/web/token/https/parity.io/\r\n\ + Referer: http://localhost:8080/web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/\r\n\ \r\n\ " ); // then response.assert_status("HTTP/1.1 302 Found"); - response.assert_header("Location", "/web/token/https/parity.io/styles.css"); + response.assert_header("Location", "/web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/styles.css"); + + fetch.assert_no_more_requests(); +} + +#[test] +fn should_fix_absolute_requests_based_on_referer_in_url() { + // given + let (server, fetch) = serve_with_fetch("token"); + + // when + let response = request(server, + "\ + GET /styles.css HTTP/1.1\r\n\ + Host: localhost:8080\r\n\ + Connection: close\r\n\ + Referer: http://localhost:8080/?__referer=web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/\r\n\ + \r\n\ + " + ); + + // then + response.assert_status("HTTP/1.1 302 Found"); + response.assert_header("Location", "/web/EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY/styles.css"); fetch.assert_no_more_requests(); } diff --git a/dapps/src/tests/redirection.rs b/dapps/src/tests/redirection.rs index a19888620..8b529a851 100644 --- a/dapps/src/tests/redirection.rs +++ b/dapps/src/tests/redirection.rs @@ -105,7 +105,7 @@ fn should_display_404_on_invalid_dapp_with_domain() { let response = request(server, "\ GET / HTTP/1.1\r\n\ - Host: invaliddapp.parity\r\n\ + Host: invaliddapp.web3.site\r\n\ Connection: close\r\n\ \r\n\ " @@ -179,7 +179,7 @@ fn should_serve_proxy_pac() { // then assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); - assert_eq!(response.body, "D5\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"home.parity\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:18180\";\n\t}\n\n\tif (shExpMatch(host, \"*.parity\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:8080\";\n\t}\n\n\treturn \"DIRECT\";\n}\n\n0\n\n".to_owned()); + assert_eq!(response.body, "DD\n\nfunction FindProxyForURL(url, host) {\n\tif (shExpMatch(host, \"parity.web3.site\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:18180\";\n\t}\n\n\tif (shExpMatch(host, \"*.web3.site\"))\n\t{\n\t\treturn \"PROXY 127.0.0.1:8080\";\n\t}\n\n\treturn \"DIRECT\";\n}\n\n0\n\n".to_owned()); assert_security_headers(&response.headers); } diff --git a/dapps/src/tests/validation.rs b/dapps/src/tests/validation.rs index e43621313..afeb7b5ef 100644 --- a/dapps/src/tests/validation.rs +++ b/dapps/src/tests/validation.rs @@ -66,7 +66,7 @@ fn should_serve_dapps_domains() { let response = request(server, "\ GET / HTTP/1.1\r\n\ - Host: ui.parity\r\n\ + Host: ui.web3.site\r\n\ Connection: close\r\n\ \r\n\ {} diff --git a/dapps/src/web.rs b/dapps/src/web.rs index 8287a8585..0e872daf2 100644 --- a/dapps/src/web.rs +++ b/dapps/src/web.rs @@ -20,6 +20,7 @@ use std::sync::Arc; use fetch::{self, Fetch}; use parity_reactor::Remote; +use base32; use hyper::{self, server, net, Next, Encoder, Decoder}; use hyper::status::StatusCode; @@ -27,7 +28,7 @@ use apps; use endpoint::{Endpoint, Handler, EndpointPath}; use handlers::{ ContentFetcherHandler, ContentHandler, ContentValidator, ValidatorResponse, - StreamingHandler, Redirection, extract_url, + StreamingHandler, extract_url, }; use url::Url; use WebProxyTokens; @@ -86,9 +87,10 @@ impl ContentValidator for WebInstaller { ); if is_html { handler.set_initial_content(&format!( - r#"<script src="/{}/inject.js"></script><script>history.replaceState({{}}, "", "/?{}{}")</script>"#, + r#"<script src="/{}/inject.js"></script><script>history.replaceState({{}}, "", "/?{}{}/{}")</script>"#, apps::UTILS_PATH, apps::URL_REFERER, + apps::WEB_PATH, &self.referer, )); } @@ -99,7 +101,6 @@ impl ContentValidator for WebInstaller { enum State<F: Fetch> { Initial, Error(ContentHandler), - Redirecting(Redirection), Fetching(ContentFetcherHandler<WebInstaller, F>), } @@ -114,25 +115,26 @@ struct WebHandler<F: Fetch> { } impl<F: Fetch> WebHandler<F> { - fn extract_target_url(&self, url: Option<Url>) -> Result<(String, String), State<F>> { - let (path, query) = match url { - Some(url) => (url.path, url.query), - None => { - return Err(State::Error(ContentHandler::error( - StatusCode::BadRequest, "Invalid URL", "Couldn't parse URL", None, self.embeddable_on.clone() - ))); - } - }; + fn extract_target_url(&self, url: Option<Url>) -> Result<String, State<F>> { + let token_and_url = self.path.app_params.get(0) + .map(|encoded| encoded.replace('.', "")) + .and_then(|encoded| base32::decode(base32::Alphabet::Crockford, &encoded.to_uppercase())) + .and_then(|data| String::from_utf8(data).ok()) + .ok_or_else(|| State::Error(ContentHandler::error( + StatusCode::BadRequest, + "Invalid parameter", + "Couldn't parse given parameter:", + self.path.app_params.get(0).map(String::as_str), + self.embeddable_on.clone() + )))?; - // Support domain based routing. - let idx = match path.get(0).map(|m| m.as_ref()) { - Some(apps::WEB_PATH) => 1, - _ => 0, - }; + let mut token_it = token_and_url.split('+'); + let token = token_it.next(); + let target_url = token_it.next(); // Check if token supplied in URL is correct. - match path.get(idx) { - Some(ref token) if self.web_proxy_tokens.is_web_proxy_token_valid(token) => {}, + match token { + Some(token) if self.web_proxy_tokens.is_web_proxy_token_valid(token) => {}, _ => { return Err(State::Error(ContentHandler::error( StatusCode::BadRequest, "Invalid Access Token", "Invalid or old web proxy access token supplied.", Some("Try refreshing the page."), self.embeddable_on.clone() @@ -141,9 +143,8 @@ impl<F: Fetch> WebHandler<F> { } // Validate protocol - let protocol = match path.get(idx + 1).map(|a| a.as_str()) { - Some("http") => "http", - Some("https") => "https", + let mut target_url = match target_url { + Some(url) if url.starts_with("http://") || url.starts_with("https://") => url.to_owned(), _ => { return Err(State::Error(ContentHandler::error( StatusCode::BadRequest, "Invalid Protocol", "Invalid protocol used.", None, self.embeddable_on.clone() @@ -151,28 +152,35 @@ impl<F: Fetch> WebHandler<F> { } }; - // Redirect if address to main page does not end with / - if let None = path.get(idx + 3) { - return Err(State::Redirecting( - Redirection::new(&format!("/{}/", path.join("/"))) - )); + if !target_url.ends_with("/") { + target_url = format!("{}/", target_url); } - let query = match query { - Some(query) => format!("?{}", query), + // TODO [ToDr] Should just use `path.app_params` + let (path, query) = match (&url, self.path.using_dapps_domains) { + (&Some(ref url), true) => (&url.path[..], &url.query), + (&Some(ref url), false) => (&url.path[2..], &url.query), + _ => { + return Err(State::Error(ContentHandler::error( + StatusCode::BadRequest, "Invalid URL", "Couldn't parse URL", None, self.embeddable_on.clone() + ))); + } + }; + + let query = match *query { + Some(ref query) => format!("?{}", query), None => "".into(), }; - Ok((format!("{}://{}{}", protocol, path[idx + 2..].join("/"), query), path[0..].join("/"))) + Ok(format!("{}{}{}", target_url, path.join("/"), query)) } } impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> { fn on_request(&mut self, request: server::Request<net::HttpStream>) -> Next { let url = extract_url(&request); - // First extract the URL (reject invalid URLs) - let (target_url, referer) = match self.extract_target_url(url) { + let target_url = match self.extract_target_url(url) { Ok(url) => url, Err(error) => { self.state = error; @@ -186,7 +194,9 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> { self.control.clone(), WebInstaller { embeddable_on: self.embeddable_on.clone(), - referer: referer, + referer: self.path.app_params.get(0) + .expect("`target_url` is valid; app_params is not empty;qed") + .to_owned(), }, self.embeddable_on.clone(), self.remote.clone(), @@ -202,7 +212,6 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> { match self.state { State::Initial => Next::end(), State::Error(ref mut handler) => handler.on_request_readable(decoder), - State::Redirecting(ref mut handler) => handler.on_request_readable(decoder), State::Fetching(ref mut handler) => handler.on_request_readable(decoder), } } @@ -211,7 +220,6 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> { match self.state { State::Initial => Next::end(), State::Error(ref mut handler) => handler.on_response(res), - State::Redirecting(ref mut handler) => handler.on_response(res), State::Fetching(ref mut handler) => handler.on_response(res), } } @@ -220,7 +228,6 @@ impl<F: Fetch> server::Handler<net::HttpStream> for WebHandler<F> { match self.state { State::Initial => Next::end(), State::Error(ref mut handler) => handler.on_response_writable(encoder), - State::Redirecting(ref mut handler) => handler.on_response_writable(encoder), State::Fetching(ref mut handler) => handler.on_response_writable(encoder), } } diff --git a/devtools/src/http_client.rs b/devtools/src/http_client.rs index f2b8c7931..de59a7a71 100644 --- a/devtools/src/http_client.rs +++ b/devtools/src/http_client.rs @@ -87,7 +87,7 @@ pub fn request(address: &SocketAddr, request: &str) -> Response { let _ = req.read_to_string(&mut response); let mut lines = response.lines(); - let status = lines.next().unwrap().to_owned(); + let status = lines.next().expect("Expected a response").to_owned(); let headers_raw = read_block(&mut lines, false); let headers = headers_raw.split('\n').map(|v| v.to_owned()).collect(); let body = read_block(&mut lines, true); diff --git a/js/package.json b/js/package.json index a702f2795..b10c180a0 100644 --- a/js/package.json +++ b/js/package.json @@ -140,6 +140,7 @@ "yargs": "6.6.0" }, "dependencies": { + "base32.js": "0.1.0", "bignumber.js": "3.0.1", "blockies": "0.0.2", "brace": "0.9.0", diff --git a/js/src/ui/Form/DappUrlInput/dappUrlInput.js b/js/src/ui/Form/DappUrlInput/dappUrlInput.js new file mode 100644 index 000000000..a513059df --- /dev/null +++ b/js/src/ui/Form/DappUrlInput/dappUrlInput.js @@ -0,0 +1,61 @@ +// 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 keycode from 'keycode'; +import React, { Component, PropTypes } from 'react'; + +export default class DappUrlInput extends Component { + static propTypes = { + className: PropTypes.string, + onChange: PropTypes.func.isRequired, + onGoto: PropTypes.func.isRequired, + onRestore: PropTypes.func.isRequired, + url: PropTypes.string.isRequired + } + + render () { + const { className, url } = this.props; + + return ( + <input + className={ className } + onChange={ this.onChange } + onKeyDown={ this.onKeyDown } + type='text' + value={ url } + /> + ); + } + + onChange = (event) => { + this.props.onChange(event.target.value); + }; + + onKeyDown = (event) => { + switch (keycode(event)) { + case 'esc': + this.props.onRestore(); + break; + + case 'enter': + this.props.onGoto(); + break; + + default: + break; + } + }; +} diff --git a/js/src/ui/Form/DappUrlInput/dappUrlInput.spec.js b/js/src/ui/Form/DappUrlInput/dappUrlInput.spec.js new file mode 100644 index 000000000..23a352ae6 --- /dev/null +++ b/js/src/ui/Form/DappUrlInput/dappUrlInput.spec.js @@ -0,0 +1,70 @@ +// 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 sinon from 'sinon'; + +import DappUrlInput from './'; + +let component; +let onChange; +let onGoto; +let onRestore; + +function render (props = { url: 'http://some.url' }) { + onChange = sinon.stub(); + onGoto = sinon.stub(); + onRestore = sinon.stub(); + + component = shallow( + <DappUrlInput + onChange={ onChange } + onGoto={ onGoto } + onRestore={ onRestore } + { ...props } + /> + ); + + return component; +} + +describe('ui/Form/DappUrlInput', () => { + it('renders defaults', () => { + expect(render()).to.be.ok; + }); + + describe('events', () => { + describe('onChange', () => { + it('calls the onChange callback as provided', () => { + component.simulate('change', { target: { value: 'testing' } }); + expect(onChange).to.have.been.calledWith('testing'); + }); + }); + + describe('onKeyDown', () => { + it('calls the onGoto callback on enter', () => { + component.simulate('keyDown', { keyCode: 13 }); + expect(onGoto).to.have.been.called; + }); + + it('calls the onRestor callback on esc', () => { + component.simulate('keyDown', { keyCode: 27 }); + expect(onRestore).to.have.been.called; + }); + }); + }); +}); diff --git a/js/src/ui/Form/DappUrlInput/index.js b/js/src/ui/Form/DappUrlInput/index.js new file mode 100644 index 000000000..a30c4dbe5 --- /dev/null +++ b/js/src/ui/Form/DappUrlInput/index.js @@ -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 './dappUrlInput'; diff --git a/js/src/ui/Form/index.js b/js/src/ui/Form/index.js index 9e0823934..21fd2986c 100644 --- a/js/src/ui/Form/index.js +++ b/js/src/ui/Form/index.js @@ -15,6 +15,7 @@ // along with Parity. If not, see <http://www.gnu.org/licenses/>. import AddressSelect from './AddressSelect'; +import DappUrlInput from './DappUrlInput'; import FormWrap from './FormWrap'; import Input from './Input'; import InputAddress from './InputAddress'; @@ -31,6 +32,7 @@ import TypedInput from './TypedInput'; export default from './form'; export { AddressSelect, + DappUrlInput, FormWrap, Input, InputAddress, diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 967465165..41903adf1 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -35,7 +35,7 @@ import DappIcon from './DappIcon'; import Editor from './Editor'; import Errors from './Errors'; import Features, { FEATURES, FeaturesStore } from './Features'; -import Form, { AddressSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form'; +import Form, { AddressSelect, DappUrlInput, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form'; import GasPriceEditor from './GasPriceEditor'; import GasPriceSelector from './GasPriceSelector'; import Icons from './Icons'; @@ -80,8 +80,9 @@ export { ContextProvider, CopyToClipboard, CurrencySymbol, - DappIcon, DappCard, + DappIcon, + DappUrlInput, Editor, Errors, FEATURES, diff --git a/js/src/util/dapplink.js b/js/src/util/dapplink.js new file mode 100644 index 000000000..1a73d184f --- /dev/null +++ b/js/src/util/dapplink.js @@ -0,0 +1,60 @@ +// 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 base32 from 'base32.js'; + +const BASE_URL = '.web.web3.site'; +const ENCODER_OPTS = { type: 'crockford' }; + +export function encodePath (token, url) { + const encoder = new base32.Encoder(ENCODER_OPTS); + const chars = `${token}+${url}` + .split('') + .map((char) => char.charCodeAt(0)); + + return encoder + .write(chars) // add the characters to encode + .finalize(); // create the encoded string +} + +export function encodeUrl (token, url) { + const encoded = encodePath(token, url) + .match(/.{1,63}/g) // split into 63-character chunks, max length is 64 for URLs parts + .join('.'); // add '.' between URL parts + + return `${encoded}${BASE_URL}`; +} + +// TODO: This export is really more a helper along the way of verifying the actual +// encoding (being able to decode test values from the node layer), than meant to +// be used as-is. Should the need arrise to decode URLs as well (instead of just +// producing), it would make sense to further split the output into the token/URL +export function decode (encoded) { + const decoder = new base32.Decoder(ENCODER_OPTS); + const sanitized = encoded + .replace(BASE_URL, '') // remove the BASE URL + .split('.') // split the string on the '.' (63-char boundaries) + .join(''); // combine without the '.' + + return decoder + .write(sanitized) // add the string to decode + .finalize() // create the decoded buffer + .toString(); // create string from buffer +} + +export { + BASE_URL +}; diff --git a/js/src/util/dapplink.spec.js b/js/src/util/dapplink.spec.js new file mode 100644 index 000000000..49a3aa197 --- /dev/null +++ b/js/src/util/dapplink.spec.js @@ -0,0 +1,83 @@ +// 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 { BASE_URL, decode, encodePath, encodeUrl } from './dapplink'; + +const TEST_TOKEN = 'token'; +const TEST_URL = 'https://parity.io'; +const TEST_URL_LONG = 'http://some.very.very.very.long.long.long.domain.example.com'; +const TEST_PREFIX = 'EHQPPSBE5DM78X3GECX2YBVGC5S6JX3S5SMPY'; +const TEST_PREFIX_LONG = [ + 'EHQPPSBE5DM78X3G78QJYWVFDNJJWXK5E9WJWXK5E9WJWXK5E9WJWV3FDSKJWV3', 'FDSKJWV3FDSKJWS3FDNGPJVHECNW62VBGDHJJWRVFDM' +].join('.'); +const TEST_RESULT = `${TEST_PREFIX}${BASE_URL}`; +const TEST_ENCODED = `${TEST_TOKEN}+${TEST_URL}`; + +describe('util/ethlink', () => { + describe('decode', () => { + it('decodes into encoded url', () => { + expect(decode(TEST_PREFIX)).to.equal(TEST_ENCODED); + }); + + it('decodes full into encoded url', () => { + expect(decode(TEST_RESULT)).to.equal(TEST_ENCODED); + }); + }); + + describe('encodePath', () => { + it('encodes a url/token combination', () => { + expect(encodePath(TEST_TOKEN, TEST_URL)).to.equal(TEST_PREFIX); + }); + + it('changes when token changes', () => { + expect(encodePath('test-token-2', TEST_URL)).not.to.equal(TEST_PREFIX); + }); + + it('changes when url changes', () => { + expect(encodePath(TEST_TOKEN, 'http://other.example.com')).not.to.equal(TEST_PREFIX); + }); + }); + + describe('encodeUrl', () => { + it('encodes a url/token combination', () => { + expect(encodeUrl(TEST_TOKEN, TEST_URL)).to.equal(TEST_RESULT); + }); + + it('changes when token changes', () => { + expect(encodeUrl('test-token-2', TEST_URL)).not.to.equal(TEST_RESULT); + }); + + it('changes when url changes', () => { + expect(encodeUrl(TEST_TOKEN, 'http://other.example.com')).not.to.equal(TEST_RESULT); + }); + + describe('splitting', () => { + let encoded; + + beforeEach(() => { + encoded = encodeUrl(TEST_TOKEN, TEST_URL_LONG); + }); + + it('splits long values into boundary parts', () => { + expect(encoded).to.equal(`${TEST_PREFIX_LONG}${BASE_URL}`); + }); + + it('first part 63 characters', () => { + expect(encoded.split('.')[0].length).to.equal(63); + }); + }); + }); +}); diff --git a/js/src/views/Web/AddressBar/addressBar.js b/js/src/views/Web/AddressBar/addressBar.js index 54dc6c85c..af02f3272 100644 --- a/js/src/views/Web/AddressBar/addressBar.js +++ b/js/src/views/Web/AddressBar/addressBar.js @@ -14,101 +14,62 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. +import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; -import Refresh from 'material-ui/svg-icons/navigation/refresh'; -import Close from 'material-ui/svg-icons/navigation/close'; import Subdirectory from 'material-ui/svg-icons/navigation/subdirectory-arrow-left'; -import { Button } from '~/ui'; - -const KEY_ESC = 27; -const KEY_ENTER = 13; +import { Button, DappUrlInput } from '~/ui'; +import { CloseIcon, RefreshIcon } from '~/ui/Icons'; +@observer export default class AddressBar extends Component { static propTypes = { className: PropTypes.string, - isLoading: PropTypes.bool.isRequired, - onChange: PropTypes.func.isRequired, - onRefresh: PropTypes.func.isRequired, - url: PropTypes.string.isRequired + store: PropTypes.object.isRequired }; - state = { - currentUrl: this.props.url - }; - - componentWillReceiveProps (nextProps) { - if (this.props.url === nextProps.url) { - return; - } - - this.setState({ - currentUrl: nextProps.url - }); - } - - isPristine () { - return this.state.currentUrl === this.props.url; - } - render () { - const { isLoading } = this.props; - const { currentUrl } = this.state; - const isPristine = this.isPristine(); + const { isLoading, isPristine, nextUrl } = this.props.store; return ( <div className={ this.props.className }> <Button disabled={ isLoading } + onClick={ this.onRefreshUrl } icon={ isLoading - ? <Close /> - : <Refresh /> + ? <CloseIcon /> + : <RefreshIcon /> } - onClick={ this.onGo } /> - <input - onChange={ this.onUpdateUrl } - onKeyDown={ this.onKey } - type='text' - value={ currentUrl } + <DappUrlInput + onChange={ this.onChangeUrl } + onGoto={ this.onGotoUrl } + onRestore={ this.onRestoreUrl } + url={ nextUrl } /> <Button disabled={ isPristine } + onClick={ this.onGotoUrl } icon={ <Subdirectory /> } - onClick={ this.onGo } /> </div> ); } - onUpdateUrl = (ev) => { - this.setState({ - currentUrl: ev.target.value - }); - }; + onRefreshUrl = () => { + this.props.store.reload(); + } - onKey = (ev) => { - const key = ev.which; + onChangeUrl = (url) => { + this.props.store.setNextUrl(url); + } - if (key === KEY_ESC) { - this.setState({ - currentUrl: this.props.url - }); - return; - } + onGotoUrl = () => { + this.props.store.gotoUrl(); + } - if (key === KEY_ENTER) { - this.onGo(); - return; - } - }; - - onGo = () => { - if (this.isPristine()) { - this.props.onRefresh(); - } else { - this.props.onChange(this.state.currentUrl); - } - }; + onRestoreUrl = () => { + this.props.store.restoreUrl(); + } } diff --git a/js/src/views/Web/AddressBar/addressBar.spec.js b/js/src/views/Web/AddressBar/addressBar.spec.js new file mode 100644 index 000000000..7208d0f7b --- /dev/null +++ b/js/src/views/Web/AddressBar/addressBar.spec.js @@ -0,0 +1,48 @@ +// 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 AddressBar from './'; + +let component; +let store; + +function createStore () { + store = { + nextUrl: 'https://parity.io' + }; + + return store; +} + +function render (props = {}) { + component = shallow( + <AddressBar + className='testClass' + store={ createStore() } + /> + ); + + return component; +} + +describe('views/Web/AddressBar', () => { + it('renders defaults', () => { + expect(render()).to.be.ok; + }); +}); diff --git a/js/src/views/Web/store.js b/js/src/views/Web/store.js new file mode 100644 index 000000000..542b47a7b --- /dev/null +++ b/js/src/views/Web/store.js @@ -0,0 +1,158 @@ +// 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 { action, computed, observable, transaction } from 'mobx'; +import localStore from 'store'; +import { parse as parseUrl } from 'url'; + +import { encodePath, encodeUrl } from '~/util/dapplink'; + +const DEFAULT_URL = 'https://mkr.market'; +const LS_LAST_ADDRESS = '_parity::webLastAddress'; + +const hasProtocol = /^https?:\/\//; + +let instance = null; + +export default class Store { + @observable counter = Date.now(); + @observable currentUrl = null; + @observable history = []; + @observable isLoading = false; + @observable parsedUrl = null; + @observable nextUrl = null; + @observable token = null; + + constructor (api) { + this._api = api; + + this.nextUrl = this.currentUrl = this.loadLastUrl(); + } + + @computed get encodedPath () { + return `${this._api.dappsUrl}/web/${encodePath(this.token, this.currentUrl)}?t=${this.counter}`; + } + + @computed get encodedUrl () { + return `http://${encodeUrl(this.token, this.currentUrl)}:${this._api.dappsPort}?t=${this.counter}`; + } + + @computed get frameId () { + return `_web_iframe_${this.counter}`; + } + + @computed get isPristine () { + return this.currentUrl === this.nextUrl; + } + + @action gotoUrl = (_url) => { + transaction(() => { + let url = (_url || this.nextUrl).trim().replace(/\/+$/, ''); + + if (!hasProtocol.test(url)) { + url = `https://${url}`; + } + + this.setNextUrl(url); + this.setCurrentUrl(this.nextUrl); + }); + } + + @action reload = () => { + transaction(() => { + this.setLoading(true); + this.counter = Date.now(); + }); + } + + @action restoreUrl = () => { + this.setNextUrl(this.currentUrl); + } + + @action setHistory = (history) => { + this.history = history; + } + + @action setLoading = (isLoading) => { + this.isLoading = isLoading; + } + + @action setToken = (token) => { + this.token = token; + } + + @action setCurrentUrl = (_url) => { + const url = _url || this.currentUrl; + + transaction(() => { + this.currentUrl = url; + this.parsedUrl = parseUrl(url); + + this.saveLastUrl(); + + this.reload(); + }); + } + + @action setNextUrl = (url) => { + this.nextUrl = url; + } + + generateToken = () => { + this.setToken(null); + + return this._api.signer + .generateWebProxyAccessToken() + .then((token) => { + this.setToken(token); + }) + .catch((error) => { + console.warn('generateToken', error); + }); + } + + loadHistory = () => { + return this._api.parity + .listRecentDapps() + .then((apps) => { + this.setHistory(apps); + }) + .catch((error) => { + console.warn('loadHistory', error); + }); + } + + loadLastUrl = () => { + return localStore.get(LS_LAST_ADDRESS) || DEFAULT_URL; + } + + saveLastUrl = () => { + return localStore.set(LS_LAST_ADDRESS, this.currentUrl); + } + + static get (api) { + if (!instance) { + instance = new Store(api); + } + + return instance; + } +} + +export { + DEFAULT_URL, + LS_LAST_ADDRESS +}; diff --git a/js/src/views/Web/store.spec.js b/js/src/views/Web/store.spec.js new file mode 100644 index 000000000..8a8dd268c --- /dev/null +++ b/js/src/views/Web/store.spec.js @@ -0,0 +1,202 @@ +// 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 sinon from 'sinon'; + +import Store from './store'; + +const TEST_HISTORY = ['somethingA', 'somethingB']; +const TEST_TOKEN = 'testing-123'; +const TEST_URL1 = 'http://some.test.domain.com'; +const TEST_URL2 = 'http://something.different.com'; + +let api; +let store; + +function createApi () { + api = { + dappsPort: 8080, + dappsUrl: 'http://home.web3.site:8080', + parity: { + listRecentDapps: sinon.stub().resolves(TEST_HISTORY) + }, + signer: { + generateWebProxyAccessToken: sinon.stub().resolves(TEST_TOKEN) + } + }; + + return api; +} + +function create () { + store = new Store(createApi()); + + return store; +} + +describe('views/Web/Store', () => { + beforeEach(() => { + create(); + }); + + describe('@action', () => { + describe('gotoUrl', () => { + it('uses the nextUrl when none specified', () => { + store.setNextUrl('https://parity.io'); + store.gotoUrl(); + + expect(store.currentUrl).to.equal('https://parity.io'); + }); + + it('adds https when no protocol', () => { + store.gotoUrl('google.com'); + + expect(store.currentUrl).to.equal('https://google.com'); + }); + }); + + describe('restoreUrl', () => { + it('sets the nextUrl to the currentUrl', () => { + store.setCurrentUrl(TEST_URL1); + store.setNextUrl(TEST_URL2); + store.restoreUrl(); + + expect(store.nextUrl).to.equal(TEST_URL1); + }); + }); + + describe('setCurrentUrl', () => { + beforeEach(() => { + store.setCurrentUrl(TEST_URL1); + }); + + it('sets the url', () => { + expect(store.currentUrl).to.equal(TEST_URL1); + }); + }); + + describe('setHistory', () => { + it('sets the history', () => { + store.setHistory(TEST_HISTORY); + expect(store.history.peek()).to.deep.equal(TEST_HISTORY); + }); + }); + + describe('setLoading', () => { + beforeEach(() => { + store.setLoading(true); + }); + + it('sets the loading state (true)', () => { + expect(store.isLoading).to.be.true; + }); + + it('sets the loading state (false)', () => { + store.setLoading(false); + + expect(store.isLoading).to.be.false; + }); + }); + + describe('setNextUrl', () => { + it('sets the url', () => { + store.setNextUrl(TEST_URL1); + + expect(store.nextUrl).to.equal(TEST_URL1); + }); + }); + + describe('setToken', () => { + it('sets the token', () => { + store.setToken(TEST_TOKEN); + + expect(store.token).to.equal(TEST_TOKEN); + }); + }); + }); + + describe('@computed', () => { + describe('encodedUrl', () => { + describe('encodedPath', () => { + it('encodes current', () => { + store.setCurrentUrl(TEST_URL1); + expect(store.encodedPath).to.match( + /http:\/\/home\.web3\.site:8080\/web\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\?t=[0-9]*$/ + ); + }); + }); + + it('encodes current', () => { + store.setCurrentUrl(TEST_URL1); + expect(store.encodedUrl).to.match( + /^http:\/\/DSTPRV1BD1T78W1T5WQQ6VVDCMQ78SBKEGQ68VVDC5MPWBK3DXPG\.web\.web3\.site:8080\?t=[0-9]*$/ + ); + }); + }); + + describe('frameId', () => { + it('creates an id', () => { + expect(store.frameId).to.be.ok; + }); + }); + + describe('isPristine', () => { + it('is true when current === next', () => { + store.setCurrentUrl(TEST_URL1); + store.setNextUrl(TEST_URL1); + + expect(store.isPristine).to.be.true; + }); + + it('is false when current !== next', () => { + store.setCurrentUrl(TEST_URL1); + store.setNextUrl(TEST_URL2); + + expect(store.isPristine).to.be.false; + }); + }); + }); + + describe('operations', () => { + describe('generateToken', () => { + beforeEach(() => { + return store.generateToken(); + }); + + it('calls signer_generateWebProxyAccessToken', () => { + expect(api.signer.generateWebProxyAccessToken).to.have.been.calledOnce; + }); + + it('sets the token as retrieved', () => { + expect(store.token).to.equal(TEST_TOKEN); + }); + }); + + describe('loadHistory', () => { + beforeEach(() => { + return store.loadHistory(); + }); + + it('calls parity_listRecentDapps', () => { + expect(api.parity.listRecentDapps).to.have.been.calledOnce; + }); + + it('sets the history as retrieved', () => { + expect(store.history.peek()).to.deep.equal(TEST_HISTORY); + }); + }); + }); +}); diff --git a/js/src/views/Web/web.js b/js/src/views/Web/web.js index 124fd8bb5..2d94b1b16 100644 --- a/js/src/views/Web/web.js +++ b/js/src/views/Web/web.js @@ -14,19 +14,16 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. +import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; -import store from 'store'; -import { parse as parseUrl, format as formatUrl } from 'url'; -import { parse as parseQuery } from 'querystring'; +import { FormattedMessage } from 'react-intl'; import AddressBar from './AddressBar'; +import Store from './store'; import styles from './web.css'; -const LS_LAST_ADDRESS = '_parity::webLastAddress'; - -const hasProtocol = /^https?:\/\//; - +@observer export default class Web extends Component { static contextTypes = { api: PropTypes.object.isRequired @@ -36,120 +33,62 @@ export default class Web extends Component { params: PropTypes.object.isRequired } - state = { - displayedUrl: null, - isLoading: true, - token: null, - url: null - }; + store = Store.get(this.context.api); componentDidMount () { - const { api } = this.context; - const { params } = this.props; - - api - .signer - .generateWebProxyAccessToken() - .then((token) => { - this.setState({ token }); - }); - - this.setUrl(params.url); + this.store.gotoUrl(this.props.params.url); + return this.store.generateToken(); } componentWillReceiveProps (props) { - this.setUrl(props.params.url); + this.store.gotoUrl(props.params.url); } - setUrl = (url) => { - url = url || store.get(LS_LAST_ADDRESS) || 'https://mkr.market'; - if (!hasProtocol.test(url)) { - url = `https://${url}`; - } - - this.setState({ url, displayedUrl: url }); - }; - render () { - const { displayedUrl, isLoading, token } = this.state; + const { currentUrl, token } = this.store; if (!token) { return ( <div className={ styles.wrapper }> <h1 className={ styles.loading }> - Requesting access token... + <FormattedMessage + id='web.requestToken' + defaultMessage='Requesting access token...' + /> </h1> </div> ); } - const { dappsUrl } = this.context.api; - const { url } = this.state; + return currentUrl + ? this.renderFrame() + : null; + } - if (!url || !token) { - return null; - } - - const parsed = parseUrl(url); - const { protocol, host, path } = parsed; - const address = `${dappsUrl}/web/${token}/${protocol.slice(0, -1)}/${host}${path}`; + renderFrame () { + const { encodedPath, frameId } = this.store; return ( <div className={ styles.wrapper }> <AddressBar className={ styles.url } - isLoading={ isLoading } - onChange={ this.onUrlChange } - onRefresh={ this.onRefresh } - url={ displayedUrl } + store={ this.store } /> <iframe className={ styles.frame } frameBorder={ 0 } - name={ name } + id={ frameId } + name={ frameId } onLoad={ this.iframeOnLoad } sandbox='allow-forms allow-same-origin allow-scripts' scrolling='auto' - src={ address } + src={ encodedPath } /> </div> ); } - onUrlChange = (url) => { - if (!hasProtocol.test(url)) { - url = `https://${url}`; - } - - store.set(LS_LAST_ADDRESS, url); - - this.setState({ - isLoading: true, - displayedUrl: url, - url: url - }); - }; - - onRefresh = () => { - const { displayedUrl } = this.state; - - // Insert timestamp - // This is a hack to prevent caching. - const parsed = parseUrl(displayedUrl); - - parsed.query = parseQuery(parsed.query); - parsed.query.t = Date.now().toString(); - delete parsed.search; - - this.setState({ - isLoading: true, - url: formatUrl(parsed) - }); - }; - iframeOnLoad = () => { - this.setState({ - isLoading: false - }); + this.store.setLoading(false); }; } diff --git a/js/src/views/Web/web.spec.js b/js/src/views/Web/web.spec.js new file mode 100644 index 000000000..94a125afa --- /dev/null +++ b/js/src/views/Web/web.spec.js @@ -0,0 +1,56 @@ +// 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 Web from './'; + +const TEST_URL = 'https://mkr.market'; + +let api; +let component; + +function createApi () { + api = {}; + + return api; +} + +function render (url = TEST_URL) { + component = shallow( + <Web params={ { url } } />, + { + context: { api: createApi() } + } + ); + + return component; +} + +describe('views/Web', () => { + beforeEach(() => { + render(); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); + + it('renders loading with no token', () => { + expect(component.find('FormattedMessage').props().id).to.equal('web.requestToken'); + }); +}); diff --git a/signer/src/tests/mod.rs b/signer/src/tests/mod.rs index 429269841..7de3a167a 100644 --- a/signer/src/tests/mod.rs +++ b/signer/src/tests/mod.rs @@ -126,8 +126,8 @@ mod testing { // when let response = request(server, "\ - GET http://home.parity/ HTTP/1.1\r\n\ - Host: home.parity\r\n\ + GET http://parity.web3.site/ HTTP/1.1\r\n\ + Host: parity.web3.site\r\n\ Connection: close\r\n\ \r\n\ {} @@ -139,6 +139,26 @@ mod testing { http_client::assert_security_headers_present(&response.headers, None); } + #[test] + fn should_not_redirect_to_parity_host() { + // given + let (server, port, _) = serve(); + + // when + let response = request(server, + &format!("\ + GET / HTTP/1.1\r\n\ + Host: 127.0.0.1:{}\r\n\ + Connection: close\r\n\ + \r\n\ + {{}} + ", port) + ); + + // then + assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned()); + } + #[test] fn should_serve_styles_even_on_disallowed_domain() { // given @@ -168,8 +188,8 @@ mod testing { // when let response = request(server, "\ - CONNECT home.parity:8080 HTTP/1.1\r\n\ - Host: home.parity\r\n\ + CONNECT parity.web3.site:8080 HTTP/1.1\r\n\ + Host: parity.web3.site\r\n\ Connection: close\r\n\ \r\n\ {} diff --git a/signer/src/ws_server/mod.rs b/signer/src/ws_server/mod.rs index acc85e684..6983ef2fe 100644 --- a/signer/src/ws_server/mod.rs +++ b/signer/src/ws_server/mod.rs @@ -109,8 +109,9 @@ impl Server { // Create WebSocket let origin = format!("{}", addr); + let port = addr.port(); let ws = ws::Builder::new().with_settings(config).build( - session::Factory::new(handler, origin, authcodes_path, skip_origin_validation) + session::Factory::new(handler, origin, port, authcodes_path, skip_origin_validation) )?; let panic_handler = PanicHandler::new_in_arc(); diff --git a/signer/src/ws_server/session.rs b/signer/src/ws_server/session.rs index e91e384cf..9fa80de93 100644 --- a/signer/src/ws_server/session.rs +++ b/signer/src/ws_server/session.rs @@ -64,22 +64,16 @@ mod ui { } } -const HOME_DOMAIN: &'static str = "home.parity"; +const HOME_DOMAIN: &'static str = "parity.web3.site"; fn origin_is_allowed(self_origin: &str, header: Option<&[u8]>) -> bool { - match header { - None => false, - Some(h) => { - let v = String::from_utf8(h.to_owned()).ok(); - match v { - Some(ref origin) if origin.starts_with("chrome-extension://") => true, - Some(ref origin) if origin.starts_with(self_origin) => true, - Some(ref origin) if origin.starts_with(&format!("http://{}", self_origin)) => true, - Some(ref origin) if origin.starts_with(HOME_DOMAIN) => true, - Some(ref origin) if origin.starts_with(&format!("http://{}", HOME_DOMAIN)) => true, - _ => false - } - } + match header.map(|h| String::from_utf8_lossy(h).into_owned()) { + Some(ref origin) if origin.starts_with("chrome-extension://") => true, + Some(ref origin) if origin.starts_with(self_origin) => true, + Some(ref origin) if origin.starts_with(&format!("http://{}", self_origin)) => true, + Some(ref origin) if origin.starts_with(HOME_DOMAIN) => true, + Some(ref origin) if origin.starts_with(&format!("http://{}", HOME_DOMAIN)) => true, + _ => false, } } @@ -134,6 +128,7 @@ pub struct Session<M: Metadata> { out: ws::Sender, skip_origin_validation: bool, self_origin: String, + self_port: u16, authcodes_path: PathBuf, handler: RpcHandler<M>, file_handler: Arc<ui::Handler>, @@ -146,7 +141,8 @@ impl<M: Metadata> ws::Handler for Session<M> { // TODO [ToDr] ws server is not handling proxied requests correctly: // Trim domain name from resource part: - let resource = req.resource().trim_left_matches(&format!("http://{}", HOME_DOMAIN)); + let resource = req.resource().trim_left_matches(&format!("http://{}:{}", HOME_DOMAIN, self.self_port)); + let resource = resource.trim_left_matches(&format!("http://{}", HOME_DOMAIN)); // Styles file is allowed for error pages to display nicely. let is_styles_file = resource == "/styles.css"; @@ -229,16 +225,18 @@ pub struct Factory<M: Metadata> { handler: RpcHandler<M>, skip_origin_validation: bool, self_origin: String, + self_port: u16, authcodes_path: PathBuf, file_handler: Arc<ui::Handler>, } impl<M: Metadata> Factory<M> { - pub fn new(handler: RpcHandler<M>, self_origin: String, authcodes_path: PathBuf, skip_origin_validation: bool) -> Self { + pub fn new(handler: RpcHandler<M>, self_origin: String, self_port: u16, authcodes_path: PathBuf, skip_origin_validation: bool) -> Self { Factory { handler: handler, skip_origin_validation: skip_origin_validation, self_origin: self_origin, + self_port: self_port, authcodes_path: authcodes_path, file_handler: Arc::new(ui::Handler::default()), } @@ -254,6 +252,7 @@ impl<M: Metadata> ws::Factory for Factory<M> { 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(), file_handler: self.file_handler.clone(), } From 4553f517ce8e1758e57da5d79eb9ead913fc1ec5 Mon Sep 17 00:00:00 2001 From: Gav Wood <gavin@parity.io> Date: Sat, 4 Feb 2017 16:00:28 +0100 Subject: [PATCH 08/10] Fix eth_sign/parity_postSign (#4432) * Fix dispatch for signing. * Remove console log * Fix signing & tests. --- js/src/redux/providers/signerMiddleware.js | 2 -- rpc/src/v1/helpers/dispatch.rs | 10 +++++++++- rpc/src/v1/tests/mocked/eth.rs | 12 ++++++------ rpc/src/v1/tests/mocked/signing.rs | 9 ++++----- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/js/src/redux/providers/signerMiddleware.js b/js/src/redux/providers/signerMiddleware.js index a58c9d8e8..34aa569a5 100644 --- a/js/src/redux/providers/signerMiddleware.js +++ b/js/src/redux/providers/signerMiddleware.js @@ -57,8 +57,6 @@ export default class SignerMiddleware { const handlePromise = (promise) => { promise .then((txHash) => { - console.log('confirmRequest', id, txHash); - if (!txHash) { store.dispatch(actions.errorConfirmRequest({ id, err: 'Unable to confirm.' })); return; diff --git a/rpc/src/v1/helpers/dispatch.rs b/rpc/src/v1/helpers/dispatch.rs index c97f9d938..126b26bc2 100644 --- a/rpc/src/v1/helpers/dispatch.rs +++ b/rpc/src/v1/helpers/dispatch.rs @@ -17,7 +17,7 @@ use std::fmt::Debug; use std::ops::Deref; use rlp; -use util::{Address, H256, U256, Uint, Bytes}; +use util::{Address, H520, H256, U256, Uint, Bytes}; use util::bytes::ToPretty; use util::sha3::Hashable; @@ -112,6 +112,14 @@ pub fn execute<C, M>(client: &C, miner: &M, accounts: &AccountProvider, payload: ConfirmationPayload::Signature(address, data) => { signature(accounts, address, data.sha3(), pass) .map(|result| result + .map(|rsv| { + let mut vrs = [0u8; 65]; + let rsv = rsv.as_ref(); + vrs[0] = rsv[64] + 27; + vrs[1..33].copy_from_slice(&rsv[0..32]); + vrs[33..65].copy_from_slice(&rsv[32..64]); + H520(vrs) + }) .map(RpcH520::from) .map(ConfirmationResponse::Signature) ) diff --git a/rpc/src/v1/tests/mocked/eth.rs b/rpc/src/v1/tests/mocked/eth.rs index dc20ed9d8..c6476d16b 100644 --- a/rpc/src/v1/tests/mocked/eth.rs +++ b/rpc/src/v1/tests/mocked/eth.rs @@ -22,7 +22,8 @@ use rustc_serialize::hex::{FromHex, ToHex}; use time::get_time; use rlp; -use util::{Uint, U256, Address, H256, FixedHash, Mutex, Hashable}; +use util::{Uint, U256, Address, H256, FixedHash, Mutex}; +use ethkey::Secret; use ethcore::account_provider::AccountProvider; use ethcore::client::{TestBlockChainClient, EachBlockWith, Executed, TransactionId}; use ethcore::log_entry::{LocalizedLogEntry, LogEntry}; @@ -296,10 +297,9 @@ fn rpc_eth_submit_hashrate() { fn rpc_eth_sign() { let tester = EthTester::default(); - let account = tester.accounts_provider.new_account("abcd").unwrap(); + let account = tester.accounts_provider.insert_account(Secret::from_slice(&[69u8; 32]).unwrap(), "abcd").unwrap(); tester.accounts_provider.unlock_account_permanently(account, "abcd".into()).unwrap(); - let message = "0cc175b9c0f1b6a831c399e26977266192eb5ffee6ae2fec3ad71c777531578f".from_hex().unwrap(); - let signed = tester.accounts_provider.sign(account, None, message.sha3()).unwrap(); + let _message = "0cc175b9c0f1b6a831c399e26977266192eb5ffee6ae2fec3ad71c777531578f".from_hex().unwrap(); let req = r#"{ "jsonrpc": "2.0", @@ -310,9 +310,9 @@ fn rpc_eth_sign() { ], "id": 1 }"#; - let res = r#"{"jsonrpc":"2.0","result":""#.to_owned() + &format!("0x{}", signed) + r#"","id":1}"#; + let res = r#"{"jsonrpc":"2.0","result":"0x1b5100b2be0aafd86271c8f49891262920bfbfeaeccb2ef1d0b2053aefc3ddb399483eb3c902ecf4add3156461a61f59e924a65eb5e6cdbab0a158d45db5f87cdf","id":1}"#; - assert_eq!(tester.io.handle_request_sync(&req), Some(res)); + assert_eq!(tester.io.handle_request_sync(&req), Some(res.into())); } #[test] diff --git a/rpc/src/v1/tests/mocked/signing.rs b/rpc/src/v1/tests/mocked/signing.rs index 0f77325dd..6e971511d 100644 --- a/rpc/src/v1/tests/mocked/signing.rs +++ b/rpc/src/v1/tests/mocked/signing.rs @@ -27,7 +27,8 @@ use v1::types::ConfirmationResponse; use v1::tests::helpers::TestMinerService; use v1::tests::mocked::parity; -use util::{Address, FixedHash, Uint, U256, ToPretty, Hashable}; +use util::{Address, FixedHash, Uint, U256, ToPretty}; +use ethkey::Secret; use ethcore::account_provider::AccountProvider; use ethcore::client::TestBlockChainClient; use ethcore::transaction::{Transaction, Action, SignedTransaction}; @@ -186,11 +187,9 @@ fn should_sign_if_account_is_unlocked() { // given let tester = eth_signing(); let data = vec![5u8]; - let acc = tester.accounts.new_account("test").unwrap(); + let acc = tester.accounts.insert_account(Secret::from_slice(&[69u8; 32]).unwrap(), "test").unwrap(); tester.accounts.unlock_account_permanently(acc, "test".into()).unwrap(); - let signature = tester.accounts.sign(acc, None, data.sha3()).unwrap(); - // when let request = r#"{ "jsonrpc": "2.0", @@ -201,7 +200,7 @@ fn should_sign_if_account_is_unlocked() { ], "id": 1 }"#; - let response = r#"{"jsonrpc":"2.0","result":""#.to_owned() + format!("0x{}", signature).as_ref() + r#"","id":1}"#; + let response = r#"{"jsonrpc":"2.0","result":"0x1bb3062482b0687e9c97c7609ea60c1649959dbb334f71b3d5cacd496e0848ba8137bc765756627722389c6c39bc77700ccdc8916916a0eb03bcf5191d4f74dc65","id":1}"#; assert_eq!(tester.io.handle_request_sync(&request), Some(response.to_owned())); assert_eq!(tester.signer.requests().len(), 0); } From d7b937fe88564091a1e03fb8fb8e38a27ebe37cb Mon Sep 17 00:00:00 2001 From: GitLab Build Bot <jaco+gitlab@ethcore.io> Date: Sat, 4 Feb 2017 15:32:05 +0000 Subject: [PATCH 09/10] [ci skip] js-precompiled 20170204-152807 --- Cargo.lock | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bda84ce62..5ca4ce1e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1559,7 +1559,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#f79c8ca30126b57c951560fefe29764fd7cad0ee" +source = "git+https://github.com/ethcore/js-precompiled.git#4110b5bc85a15ae3f0b5c02b1c3caf8423f51b50" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/js/package.json b/js/package.json index b10c180a0..705dd51df 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.3.65", + "version": "0.3.66", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team <admin@parity.io>", From 248cd5e036a1c0b2cf9b20e880d9c3a7035bee8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Drwi=C4=99ga?= <tomusdrw@users.noreply.github.com> Date: Sat, 4 Feb 2017 22:18:19 +0100 Subject: [PATCH 10/10] RPC middleware: Informant & Client.keep_alive (#4384) * Adding RPC informant structs * RPC informant * Middleware counting RPC requests * Moving client keep_alive to middleware --- Cargo.lock | 44 +-- dapps/src/lib.rs | 11 +- dapps/src/rpc.rs | 12 +- dapps/src/tests/rpc.rs | 6 +- ethcore/src/client/client.rs | 23 +- ethcore/src/client/traits.rs | 4 - parity/blockchain.rs | 2 +- parity/dapps.rs | 4 +- parity/informant.rs | 32 ++- parity/rpc.rs | 10 +- parity/rpc_apis.rs | 31 ++- parity/run.rs | 9 +- parity/signer.rs | 25 +- rpc/Cargo.toml | 1 + rpc/src/lib.rs | 12 +- rpc/src/v1/helpers/informant.rs | 301 +++++++++++++++++++++ rpc/src/v1/helpers/mod.rs | 7 +- rpc/src/v1/impls/eth.rs | 75 ----- rpc/src/v1/impls/eth_filter.rs | 16 -- rpc/src/v1/impls/parity.rs | 70 ----- rpc/src/v1/impls/parity_accounts.rs | 28 +- rpc/src/v1/impls/parity_set.rs | 37 +-- rpc/src/v1/impls/personal.rs | 11 - rpc/src/v1/impls/signer.rs | 14 - rpc/src/v1/impls/signing.rs | 20 +- rpc/src/v1/impls/signing_unsafe.rs | 7 - rpc/src/v1/impls/traces.rs | 14 - rpc/src/v1/mod.rs | 2 +- rpc/src/v1/tests/mocked/parity_accounts.rs | 13 +- signer/src/ws_server/mod.rs | 46 +++- signer/src/ws_server/session.rs | 40 ++- 31 files changed, 520 insertions(+), 407 deletions(-) create mode 100644 rpc/src/v1/helpers/informant.rs diff --git a/Cargo.lock b/Cargo.lock index 5ca4ce1e3..48b95149b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,7 +28,7 @@ dependencies = [ "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)", "isatty 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)", + "jsonrpc-core 5.1.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)", @@ -423,7 +423,7 @@ dependencies = [ "fetch 0.1.0", "futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)", - "jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)", + "jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-http-server 7.0.0 (git+https://github.com/ethcore/jsonrpc.git)", "linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -594,11 +594,12 @@ dependencies = [ "ethsync 1.6.0", "fetch 0.1.0", "futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)", + "jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-http-server 7.0.0 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-ipc-server 1.0.0 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-macros 0.2.0 (git+https://github.com/ethcore/jsonrpc.git)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "order-stat 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "parity-reactor 0.1.0", "parity-updater 1.6.0", "rlp 0.1.0", @@ -621,7 +622,7 @@ dependencies = [ "ethcore-io 1.6.0", "ethcore-rpc 1.6.0", "ethcore-util 1.6.0", - "jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)", + "jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "parity-ui 1.6.0", @@ -641,7 +642,7 @@ dependencies = [ "ethcore-ipc-nano 1.6.0", "ethcore-util 1.6.0", "futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)", + "jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-macros 0.2.0 (git+https://github.com/ethcore/jsonrpc.git)", "jsonrpc-tcp-server 1.0.0 (git+https://github.com/ethcore/jsonrpc.git)", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1001,8 +1002,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "jsonrpc-core" -version = "5.0.0" -source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c" +version = "5.1.0" +source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156" dependencies = [ "futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1015,11 +1016,10 @@ dependencies = [ [[package]] name = "jsonrpc-http-server" version = "7.0.0" -source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c" +source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156" dependencies = [ - "futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)", - "jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)", + "jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-core 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1028,11 +1028,11 @@ dependencies = [ [[package]] name = "jsonrpc-ipc-server" version = "1.0.0" -source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c" +source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156" dependencies = [ "bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)", + "jsonrpc-core 5.1.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)", "mio 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1044,21 +1044,19 @@ dependencies = [ [[package]] name = "jsonrpc-macros" version = "0.2.0" -source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c" +source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156" dependencies = [ - "futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)", + "jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)", "serde 0.8.19 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "jsonrpc-tcp-server" version = "1.0.0" -source = "git+https://github.com/ethcore/jsonrpc.git#5eeee0980e4d2682a831c633fa03a8af99e0d68c" +source = "git+https://github.com/ethcore/jsonrpc.git#d179ce34d8da8ea1cd67e93a5b4cb1e30f48c156" dependencies = [ "env_logger 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)", + "jsonrpc-core 5.1.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)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1477,6 +1475,11 @@ dependencies = [ "user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "order-stat" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "owning_ref" version = "0.2.2" @@ -1528,7 +1531,7 @@ dependencies = [ "ethcore-signer 1.6.0", "ethcore-util 1.6.0", "futures 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)", + "jsonrpc-core 5.1.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)", "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2521,7 +2524,7 @@ dependencies = [ "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 itoa 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "91fd9dc2c587067de817fec4ad355e3818c3d893a78cab32a0a474c7a15bb8d5" -"checksum jsonrpc-core 5.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>" +"checksum jsonrpc-core 5.1.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>" "checksum jsonrpc-http-server 7.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>" "checksum jsonrpc-ipc-server 1.0.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>" "checksum jsonrpc-macros 0.2.0 (git+https://github.com/ethcore/jsonrpc.git)" = "<none>" @@ -2570,6 +2573,7 @@ dependencies = [ "checksum ole32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2c49021782e5233cd243168edfa8037574afed4eba4bbaf538b3d8d1789d8c" "checksum openssl 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "12be61c7eaa23228316ff02c39807e4c1b1af84ba81420f19fd58dade304b25c" "checksum openssl-sys 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d2845e841700e7b04282ceaa115407ea84e0db918ae689ad9ceb6f06fa6046bd" +"checksum order-stat 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "efa535d5117d3661134dbf1719b6f0ffe06f2375843b13935db186cd094105eb" "checksum owning_ref 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8d91377085359426407a287ab16884a0111ba473aa6844ff01d4ec20ce3d75e7" "checksum parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "98378dec0a185da2b7180308752f0bad73aaa949c3e0a3b0528d0e067945f7ab" "checksum parity-ui-precompiled 1.4.0 (git+https://github.com/ethcore/js-precompiled.git)" = "<none>" diff --git a/dapps/src/lib.rs b/dapps/src/lib.rs index 9ae163f88..50dcb39b1 100644 --- a/dapps/src/lib.rs +++ b/dapps/src/lib.rs @@ -70,9 +70,10 @@ use std::sync::{Arc, Mutex}; use std::net::SocketAddr; use std::collections::HashMap; -use ethcore_rpc::Metadata; +use ethcore_rpc::{Metadata}; use fetch::{Fetch, Client as FetchClient}; use hash_fetch::urlhint::ContractClient; +use jsonrpc_core::Middleware; use jsonrpc_core::reactor::RpcHandler; use router::auth::{Authorization, NoAuth, HttpBasicAuth}; use parity_reactor::Remote; @@ -179,7 +180,7 @@ impl<T: Fetch> ServerBuilder<T> { /// Asynchronously start server with no authentication, /// returns result with `Server` handle on success or an error. - pub fn start_unsecured_http(self, addr: &SocketAddr, handler: RpcHandler<Metadata>) -> Result<Server, ServerError> { + pub fn start_unsecured_http<S: Middleware<Metadata>>(self, addr: &SocketAddr, handler: RpcHandler<Metadata, S>) -> Result<Server, ServerError> { let fetch = self.fetch_client()?; Server::start_http( addr, @@ -199,7 +200,7 @@ impl<T: Fetch> ServerBuilder<T> { /// Asynchronously start server with `HTTP Basic Authentication`, /// return result with `Server` handle on success or an error. - pub fn start_basic_auth_http(self, addr: &SocketAddr, username: &str, password: &str, handler: RpcHandler<Metadata>) -> Result<Server, ServerError> { + pub fn start_basic_auth_http<S: Middleware<Metadata>>(self, addr: &SocketAddr, username: &str, password: &str, handler: RpcHandler<Metadata, S>) -> Result<Server, ServerError> { let fetch = self.fetch_client()?; Server::start_http( addr, @@ -258,11 +259,11 @@ impl Server { } } - fn start_http<A: Authorization + 'static, F: Fetch>( + fn start_http<A: Authorization + 'static, F: Fetch, T: Middleware<Metadata>>( addr: &SocketAddr, hosts: Option<Vec<String>>, authorization: A, - handler: RpcHandler<Metadata>, + handler: RpcHandler<Metadata, T>, dapps_path: PathBuf, extra_dapps: Vec<PathBuf>, signer_address: Option<(String, u16)>, diff --git a/dapps/src/rpc.rs b/dapps/src/rpc.rs index 1fffec6ee..bf1b1dc93 100644 --- a/dapps/src/rpc.rs +++ b/dapps/src/rpc.rs @@ -18,11 +18,15 @@ use std::sync::{Arc, Mutex}; use hyper; use ethcore_rpc::{Metadata, Origin}; +use jsonrpc_core::Middleware; use jsonrpc_core::reactor::RpcHandler; use jsonrpc_http_server::{Rpc, ServerHandler, PanicHandler, AccessControlAllowOrigin, HttpMetaExtractor}; use endpoint::{Endpoint, EndpointPath, Handler}; -pub fn rpc(handler: RpcHandler<Metadata>, panic_handler: Arc<Mutex<Option<Box<Fn() -> () + Send>>>>) -> Box<Endpoint> { +pub fn rpc<T: Middleware<Metadata>>( + handler: RpcHandler<Metadata, T>, + panic_handler: Arc<Mutex<Option<Box<Fn() -> () + Send>>>>, +) -> Box<Endpoint> { Box::new(RpcEndpoint { handler: handler, meta_extractor: Arc::new(MetadataExtractor), @@ -33,15 +37,15 @@ pub fn rpc(handler: RpcHandler<Metadata>, panic_handler: Arc<Mutex<Option<Box<Fn }) } -struct RpcEndpoint { - handler: RpcHandler<Metadata>, +struct RpcEndpoint<T: Middleware<Metadata>> { + handler: RpcHandler<Metadata, T>, meta_extractor: Arc<HttpMetaExtractor<Metadata>>, panic_handler: Arc<Mutex<Option<Box<Fn() -> () + Send>>>>, cors_domain: Option<Vec<AccessControlAllowOrigin>>, allowed_hosts: Option<Vec<String>>, } -impl Endpoint for RpcEndpoint { +impl<T: Middleware<Metadata>> Endpoint for RpcEndpoint<T> { fn to_async_handler(&self, _path: EndpointPath, control: hyper::Control) -> Box<Handler> { let panic_handler = PanicHandler { handler: self.panic_handler.clone() }; Box::new(ServerHandler::new( diff --git a/dapps/src/tests/rpc.rs b/dapps/src/tests/rpc.rs index 54834e264..7c2486099 100644 --- a/dapps/src/tests/rpc.rs +++ b/dapps/src/tests/rpc.rs @@ -23,7 +23,7 @@ use tests::helpers::{serve_with_rpc, request}; #[test] fn should_serve_rpc() { // given - let mut io = MetaIoHandler::new(); + let mut io = MetaIoHandler::default(); io.add_method("rpc_test", |_| { Ok(Value::String("Hello World!".into())) }); @@ -53,7 +53,7 @@ fn should_serve_rpc() { #[test] fn should_extract_metadata() { // given - let mut io = MetaIoHandler::new(); + 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); @@ -87,7 +87,7 @@ fn should_extract_metadata() { #[test] fn should_extract_metadata_from_custom_header() { // given - let mut io = MetaIoHandler::new(); + 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); diff --git a/ethcore/src/client/client.rs b/ethcore/src/client/client.rs index 77189c3fd..d85685f3f 100644 --- a/ethcore/src/client/client.rs +++ b/ethcore/src/client/client.rs @@ -262,6 +262,18 @@ impl Client { Ok(client) } + /// Wakes up client if it's a sleep. + pub fn keep_alive(&self) { + let should_wake = match *self.mode.lock() { + Mode::Dark(..) | Mode::Passive(..) => true, + _ => false, + }; + if should_wake { + self.wake_up(); + (*self.sleep_state.lock()).last_activity = Some(Instant::now()); + } + } + /// Adds an actor to be notified on certain events pub fn add_notify(&self, target: Arc<ChainNotify>) { self.notify.write().push(Arc::downgrade(&target)); @@ -1011,17 +1023,6 @@ impl BlockChainClient for Client { Ok(ret) } - fn keep_alive(&self) { - let should_wake = match *self.mode.lock() { - Mode::Dark(..) | Mode::Passive(..) => true, - _ => false, - }; - if should_wake { - self.wake_up(); - (*self.sleep_state.lock()).last_activity = Some(Instant::now()); - } - } - fn mode(&self) -> IpcMode { let r = self.mode.lock().clone().into(); trace!(target: "mode", "Asked for mode = {:?}. returning {:?}", &*self.mode.lock(), r); diff --git a/ethcore/src/client/traits.rs b/ethcore/src/client/traits.rs index 519c9185a..dfb251296 100644 --- a/ethcore/src/client/traits.rs +++ b/ethcore/src/client/traits.rs @@ -46,10 +46,6 @@ use encoded; /// Blockchain database client. Owns and manages a blockchain and a block queue. pub trait BlockChainClient : Sync + Send { - /// Should be called by any external-facing interface when actively using the client. - /// To minimise chatter, there's no need to call more than once every 30s. - fn keep_alive(&self) {} - /// Get raw block header data by block id. fn block_header(&self, id: BlockId) -> Option<encoded::Header>; diff --git a/parity/blockchain.rs b/parity/blockchain.rs index c7e9e780f..59cfd0a59 100644 --- a/parity/blockchain.rs +++ b/parity/blockchain.rs @@ -242,7 +242,7 @@ fn execute_import(cmd: ImportBlockchain) -> Result<(), String> { } }; - let informant = Arc::new(Informant::new(client.clone(), None, None, None, cmd.with_color)); + let informant = Arc::new(Informant::new(client.clone(), None, None, None, None, cmd.with_color)); service.register_io_handler(informant).map_err(|_| "Unable to register informant handler".to_owned())?; let do_import = |bytes| { diff --git a/parity/dapps.rs b/parity/dapps.rs index 9f674eadb..572d32b48 100644 --- a/parity/dapps.rs +++ b/parity/dapps.rs @@ -19,6 +19,7 @@ use std::sync::Arc; use dir::default_data_path; use ethcore::client::Client; +use ethcore_rpc::informant::RpcStats; use ethsync::SyncProvider; use hash_fetch::fetch::Client as FetchClient; use helpers::replace_home; @@ -64,6 +65,7 @@ pub struct Dependencies { pub remote: Remote, pub fetch: FetchClient, pub signer: Arc<SignerService>, + pub stats: Arc<RpcStats>, } pub fn new(configuration: Configuration, deps: Dependencies) -> Result<Option<WebappServer>, String> { @@ -174,7 +176,7 @@ mod server { } else { rpc_apis::ApiSet::UnsafeContext }; - let apis = rpc_apis::setup_rpc(Default::default(), deps.apis.clone(), api_set); + let apis = rpc_apis::setup_rpc(deps.stats, deps.apis.clone(), api_set); let handler = RpcHandler::new(Arc::new(apis), deps.remote); let start_result = match auth { None => { diff --git a/parity/informant.rs b/parity/informant.rs index c82c9b2ae..2fa95db9a 100644 --- a/parity/informant.rs +++ b/parity/informant.rs @@ -30,7 +30,8 @@ use ethcore::service::ClientIoMessage; use ethcore::snapshot::service::Service as SnapshotService; use ethcore::snapshot::{RestorationStatus, SnapshotService as SS}; use number_prefix::{binary_prefix, Standalone, Prefixed}; -use ethcore_rpc::is_major_importing; +use ethcore_rpc::{is_major_importing}; +use ethcore_rpc::informant::RpcStats; use rlp::View; pub struct Informant { @@ -41,6 +42,7 @@ pub struct Informant { snapshot: Option<Arc<SnapshotService>>, sync: Option<Arc<SyncProvider>>, net: Option<Arc<ManageNetwork>>, + rpc_stats: Option<Arc<RpcStats>>, last_import: Mutex<Instant>, skipped: AtomicUsize, skipped_txs: AtomicUsize, @@ -63,13 +65,20 @@ pub trait MillisecondDuration { impl MillisecondDuration for Duration { fn as_milliseconds(&self) -> u64 { - self.as_secs() * 1000 + self.subsec_nanos() as u64 / 1000000 + self.as_secs() * 1000 + self.subsec_nanos() as u64 / 1_000_000 } } impl Informant { /// Make a new instance potentially `with_color` output. - pub fn new(client: Arc<Client>, sync: Option<Arc<SyncProvider>>, net: Option<Arc<ManageNetwork>>, snapshot: Option<Arc<SnapshotService>>, with_color: bool) -> Self { + pub fn new( + client: Arc<Client>, + sync: Option<Arc<SyncProvider>>, + net: Option<Arc<ManageNetwork>>, + snapshot: Option<Arc<SnapshotService>>, + rpc_stats: Option<Arc<RpcStats>>, + with_color: bool, + ) -> Self { Informant { report: RwLock::new(None), last_tick: RwLock::new(Instant::now()), @@ -78,6 +87,7 @@ impl Informant { snapshot: snapshot, sync: sync, net: net, + rpc_stats: rpc_stats, last_import: Mutex::new(Instant::now()), skipped: AtomicUsize::new(0), skipped_txs: AtomicUsize::new(0), @@ -102,6 +112,7 @@ impl Informant { let cache_info = self.client.blockchain_cache_info(); let network_config = self.net.as_ref().map(|n| n.network_config()); let sync_status = self.sync.as_ref().map(|s| s.status()); + let rpc_stats = self.rpc_stats.as_ref(); let importing = is_major_importing(sync_status.map(|s| s.state), self.client.queue_info()); let (snapshot_sync, snapshot_current, snapshot_total) = self.snapshot.as_ref().map_or((false, 0, 0), |s| @@ -126,10 +137,10 @@ impl Informant { false => t, }; - info!(target: "import", "{} {} {}", + info!(target: "import", "{} {} {} {}", match importing { true => match snapshot_sync { - false => format!("Syncing {} {} {} {}+{} Qed", + false => format!("Syncing {} {} {} {}+{} Qed", paint(White.bold(), format!("{:>8}", format!("#{}", chain_info.best_block_number))), paint(White.bold(), format!("{}", chain_info.best_block_hash)), { @@ -170,7 +181,16 @@ impl Informant { Some(ref sync_info) => format!(" {} sync", paint(Blue.bold(), format!("{:>8}", format_bytes(sync_info.mem_used)))), _ => String::new(), } - ) + ), + match rpc_stats { + Some(ref rpc_stats) => format!( + "RPC: {} conn, {} req/s, {} µs", + paint(Blue.bold(), format!("{:2}", rpc_stats.sessions())), + paint(Blue.bold(), format!("{:2}", rpc_stats.requests_rate())), + paint(Blue.bold(), format!("{:3}", rpc_stats.approximated_roundtrip())), + ), + _ => String::new(), + }, ); *write_report = Some(report); diff --git a/parity/rpc.rs b/parity/rpc.rs index 53d4a4293..fd077f1ff 100644 --- a/parity/rpc.rs +++ b/parity/rpc.rs @@ -22,6 +22,7 @@ use io::PanicHandler; use dir::default_data_path; use ethcore_rpc::{self as rpc, RpcServerError, IpcServerError, Metadata}; +use ethcore_rpc::informant::{RpcStats, Middleware}; use helpers::parity_ipc_path; use jsonrpc_core::MetaIoHandler; use jsonrpc_core::reactor::{RpcHandler, Remote}; @@ -85,6 +86,7 @@ pub struct Dependencies { pub panic_handler: Arc<PanicHandler>, pub apis: Arc<rpc_apis::Dependencies>, pub remote: Remote, + pub stats: Arc<RpcStats>, } pub fn new_http(conf: HttpConfiguration, deps: &Dependencies) -> Result<Option<HttpServer>, String> { @@ -97,8 +99,8 @@ pub fn new_http(conf: HttpConfiguration, deps: &Dependencies) -> Result<Option<H Ok(Some(setup_http_rpc_server(deps, &addr, conf.cors, conf.hosts, conf.apis)?)) } -fn setup_apis(apis: ApiSet, deps: &Dependencies) -> MetaIoHandler<Metadata> { - rpc_apis::setup_rpc(MetaIoHandler::default(), deps.apis.clone(), apis) +fn setup_apis(apis: ApiSet, deps: &Dependencies) -> MetaIoHandler<Metadata, Middleware> { + rpc_apis::setup_rpc(deps.stats.clone(), deps.apis.clone(), apis) } pub fn setup_http_rpc_server( @@ -122,12 +124,12 @@ pub fn setup_http_rpc_server( } } -pub fn new_ipc(conf: IpcConfiguration, deps: &Dependencies) -> Result<Option<IpcServer<Metadata>>, String> { +pub fn new_ipc(conf: IpcConfiguration, deps: &Dependencies) -> Result<Option<IpcServer<Metadata, Middleware>>, String> { if !conf.enabled { return Ok(None); } Ok(Some(setup_ipc_rpc_server(deps, &conf.socket_addr, conf.apis)?)) } -pub fn setup_ipc_rpc_server(dependencies: &Dependencies, addr: &str, apis: ApiSet) -> Result<IpcServer<Metadata>, String> { +pub fn setup_ipc_rpc_server(dependencies: &Dependencies, addr: &str, apis: ApiSet) -> Result<IpcServer<Metadata, Middleware>, String> { let apis = setup_apis(apis, dependencies); let handler = RpcHandler::new(Arc::new(apis), dependencies.remote.clone()); match rpc::start_ipc(addr, handler) { diff --git a/parity/rpc_apis.rs b/parity/rpc_apis.rs index b77bb3899..4c5e88406 100644 --- a/parity/rpc_apis.rs +++ b/parity/rpc_apis.rs @@ -14,22 +14,25 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. +use std::cmp::PartialEq; use std::collections::BTreeMap; use std::collections::HashSet; -use std::cmp::PartialEq; use std::str::FromStr; use std::sync::Arc; -use util::RotatingLogger; -use jsonrpc_core::{MetaIoHandler}; -use ethcore::miner::{Miner, ExternalMiner}; -use ethcore::client::Client; -use ethcore::account_provider::AccountProvider; -use ethcore::snapshot::SnapshotService; -use ethsync::{ManageNetwork, SyncProvider}; -use ethcore_rpc::{Metadata, NetworkSettings}; + pub use ethcore_rpc::SignerService; -use updater::Updater; + +use ethcore::account_provider::AccountProvider; +use ethcore::client::Client; +use ethcore::miner::{Miner, ExternalMiner}; +use ethcore::snapshot::SnapshotService; +use ethcore_rpc::{Metadata, NetworkSettings}; +use ethcore_rpc::informant::{Middleware, RpcStats, ClientNotifier}; +use ethsync::{ManageNetwork, SyncProvider}; use hash_fetch::fetch::Client as FetchClient; +use jsonrpc_core::{MetaIoHandler}; +use updater::Updater; +use util::RotatingLogger; #[derive(Debug, PartialEq, Clone, Eq, Hash)] pub enum Api { @@ -182,9 +185,13 @@ macro_rules! add_signing_methods { } } -pub fn setup_rpc(mut handler: MetaIoHandler<Metadata>, deps: Arc<Dependencies>, apis: ApiSet) -> MetaIoHandler<Metadata> { +pub fn setup_rpc(stats: Arc<RpcStats>, deps: Arc<Dependencies>, apis: ApiSet) -> MetaIoHandler<Metadata, Middleware> { use ethcore_rpc::v1::*; + let mut handler = MetaIoHandler::with_middleware(Middleware::new(stats, ClientNotifier { + client: deps.client.clone(), + })); + // it's turned into vector, cause ont of the cases requires &[] let apis = apis.list_apis().into_iter().collect::<Vec<_>>(); for api in &apis { @@ -244,7 +251,7 @@ pub fn setup_rpc(mut handler: MetaIoHandler<Metadata>, deps: Arc<Dependencies>, add_signing_methods!(ParitySigning, handler, deps); }, Api::ParityAccounts => { - handler.extend_with(ParityAccountsClient::new(&deps.secret_store, &deps.client).to_delegate()); + handler.extend_with(ParityAccountsClient::new(&deps.secret_store).to_delegate()); }, Api::ParitySet => { handler.extend_with(ParitySetClient::new( diff --git a/parity/run.rs b/parity/run.rs index d16430a5e..9247b254a 100644 --- a/parity/run.rs +++ b/parity/run.rs @@ -18,7 +18,7 @@ use std::sync::Arc; use std::net::{TcpListener}; use ctrlc::CtrlC; use fdlimit::raise_fd_limit; -use ethcore_rpc::{NetworkSettings, is_major_importing}; +use ethcore_rpc::{NetworkSettings, informant, is_major_importing}; use ethsync::NetworkConfiguration; use util::{Colour, version, RotatingLogger, Mutex, Condvar}; use io::{MayPanic, ForwardPanic, PanicHandler}; @@ -358,6 +358,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R service.add_notify(updater.clone()); // set up dependencies for rpc servers + let rpc_stats = Arc::new(informant::RpcStats::default()); let signer_path = cmd.signer_conf.signer_path.clone(); let deps_for_rpc_apis = Arc::new(rpc_apis::Dependencies { signer_service: Arc::new(rpc_apis::SignerService::new(move || { @@ -390,6 +391,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R panic_handler: panic_handler.clone(), apis: deps_for_rpc_apis.clone(), remote: event_loop.raw_remote(), + stats: rpc_stats.clone(), }; // start rpc servers @@ -405,6 +407,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R remote: event_loop.raw_remote(), fetch: fetch.clone(), signer: deps_for_rpc_apis.signer_service.clone(), + stats: rpc_stats.clone(), }; let dapps_server = dapps::new(cmd.dapps_conf.clone(), dapps_deps)?; @@ -413,6 +416,7 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R panic_handler: panic_handler.clone(), apis: deps_for_rpc_apis.clone(), remote: event_loop.raw_remote(), + rpc_stats: rpc_stats.clone(), }; let signer_server = signer::start(cmd.signer_conf.clone(), signer_deps)?; @@ -422,7 +426,8 @@ pub fn execute(cmd: RunCmd, can_restart: bool, logger: Arc<RotatingLogger>) -> R Some(sync_provider.clone()), Some(manage_network.clone()), Some(snapshot_service.clone()), - cmd.logger_config.color + Some(rpc_stats.clone()), + cmd.logger_config.color, )); service.add_notify(informant.clone()); service.register_io_handler(informant.clone()).map_err(|_| "Unable to register informant handler".to_owned())?; diff --git a/parity/signer.rs b/parity/signer.rs index ded4e68b6..9b22968dc 100644 --- a/parity/signer.rs +++ b/parity/signer.rs @@ -15,18 +15,21 @@ // along with Parity. If not, see <http://www.gnu.org/licenses/>. use std::io; -use std::sync::Arc; use std::path::PathBuf; -use ansi_term::Colour; -use io::{ForwardPanic, PanicHandler}; -use util::path::restrict_permissions_owner; -use rpc_apis; -use ethcore_signer as signer; -use dir::default_data_path; -use helpers::replace_home; -use jsonrpc_core::reactor::{RpcHandler, Remote}; +use std::sync::Arc; + pub use ethcore_signer::Server as SignerServer; +use ansi_term::Colour; +use dir::default_data_path; +use ethcore_rpc::informant::RpcStats; +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; + const CODES_FILENAME: &'static str = "authcodes"; #[derive(Debug, PartialEq, Clone)] @@ -55,6 +58,7 @@ pub struct Dependencies { pub panic_handler: Arc<PanicHandler>, pub apis: Arc<rpc_apis::Dependencies>, pub remote: Remote, + pub rpc_stats: Arc<RpcStats>, } pub struct NewToken { @@ -126,7 +130,8 @@ fn do_start(conf: Configuration, deps: Dependencies) -> Result<SignerServer, Str info!("If you do not intend this, exit now."); } let server = server.skip_origin_validation(conf.skip_origin_validation); - let apis = rpc_apis::setup_rpc(Default::default(), deps.apis, rpc_apis::ApiSet::SafeContext); + 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) }; diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index a11112032..63eabb8a7 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -17,6 +17,7 @@ serde_json = "0.8" rustc-serialize = "0.3" time = "0.1" transient-hashmap = "0.1" +order-stat = "0.1" jsonrpc-core = { git = "https://github.com/ethcore/jsonrpc.git" } jsonrpc-http-server = { git = "https://github.com/ethcore/jsonrpc.git" } jsonrpc-ipc-server = { git = "https://github.com/ethcore/jsonrpc.git" } diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index d50ee7cfd..4f1b151b6 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -40,6 +40,7 @@ extern crate time; extern crate rlp; extern crate fetch; extern crate futures; +extern crate order_stat; extern crate parity_updater as updater; extern crate parity_reactor; @@ -63,16 +64,16 @@ use jsonrpc_core::reactor::RpcHandler; pub use ipc::{Server as IpcServer, Error as IpcServerError}; pub use jsonrpc_http_server::{ServerBuilder, Server, RpcServerError}; pub mod v1; -pub use v1::{SigningQueue, SignerService, ConfirmationsQueue, NetworkSettings, Metadata, Origin}; +pub use v1::{SigningQueue, SignerService, ConfirmationsQueue, NetworkSettings, Metadata, Origin, informant}; 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>( +pub fn start_http<M: jsonrpc_core::Metadata, S: jsonrpc_core::Middleware<M>>( addr: &SocketAddr, cors_domains: Option<Vec<String>>, allowed_hosts: Option<Vec<String>>, panic_handler: Arc<PanicHandler>, - handler: RpcHandler<M>, + handler: RpcHandler<M, S>, ) -> Result<Server, RpcServerError> { let cors_domains = cors_domains.map(|domains| { @@ -95,7 +96,10 @@ pub fn start_http<M: jsonrpc_core::Metadata>( } /// Start ipc server asynchronously and returns result with `Server` handle on success or an error. -pub fn start_ipc<M: jsonrpc_core::Metadata>(addr: &str, handler: RpcHandler<M>) -> Result<ipc::Server<M>, ipc::Error> { +pub fn start_ipc<M: jsonrpc_core::Metadata, S: jsonrpc_core::Middleware<M>>( + addr: &str, + handler: RpcHandler<M, S>, +) -> Result<ipc::Server<M, S>, ipc::Error> { let server = ipc::Server::with_rpc_handler(addr, handler)?; server.run_async()?; Ok(server) diff --git a/rpc/src/v1/helpers/informant.rs b/rpc/src/v1/helpers/informant.rs new file mode 100644 index 000000000..c1bbbe6c2 --- /dev/null +++ b/rpc/src/v1/helpers/informant.rs @@ -0,0 +1,301 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +//! RPC Requests Statistics + +use std::fmt; +use std::sync::Arc; +use std::sync::atomic::{self, AtomicUsize}; +use std::time; +use futures::Future; +use jsonrpc_core as rpc; +use order_stat; +use util::RwLock; + +const RATE_SECONDS: usize = 10; +const STATS_SAMPLES: usize = 60; + +struct RateCalculator { + era: time::Instant, + samples: [u16; RATE_SECONDS], +} + +impl fmt::Debug for RateCalculator { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{} req/s", self.rate()) + } +} + +impl Default for RateCalculator { + fn default() -> Self { + RateCalculator { + era: time::Instant::now(), + samples: [0; RATE_SECONDS], + } + } +} + +impl RateCalculator { + fn elapsed(&self) -> u64 { + self.era.elapsed().as_secs() + } + + pub fn tick(&mut self) -> u16 { + if self.elapsed() >= RATE_SECONDS as u64 { + self.era = time::Instant::now(); + self.samples[0] = 0; + } + + let pos = self.elapsed() as usize % RATE_SECONDS; + let next = (pos + 1) % RATE_SECONDS; + self.samples[next] = 0; + self.samples[pos] = self.samples[pos].saturating_add(1); + self.samples[pos] + } + + fn current_rate(&self) -> usize { + let now = match self.elapsed() { + i if i >= RATE_SECONDS as u64 => RATE_SECONDS, + i => i as usize + 1, + }; + let sum: usize = self.samples[0..now].iter().map(|x| *x as usize).sum(); + sum / now + } + + pub fn rate(&self) -> usize { + if self.elapsed() > RATE_SECONDS as u64 { + 0 + } else { + self.current_rate() + } + } +} + +struct StatsCalculator<T = u32> { + filled: bool, + idx: usize, + samples: [T; STATS_SAMPLES], +} + +impl<T: Default + Copy> Default for StatsCalculator<T> { + fn default() -> Self { + StatsCalculator { + filled: false, + idx: 0, + samples: [T::default(); STATS_SAMPLES], + } + } +} + +impl<T: fmt::Display + Default + Copy + Ord> fmt::Debug for StatsCalculator<T> { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "median: {} ms", self.approximated_median()) + } +} + +impl<T: Default + Copy + Ord> StatsCalculator<T> { + pub fn add(&mut self, sample: T) { + self.idx += 1; + if self.idx >= STATS_SAMPLES { + self.filled = true; + self.idx = 0; + } + + self.samples[self.idx] = sample; + } + + /// Returns aproximate of media + pub fn approximated_median(&self) -> T { + let mut copy = [T::default(); STATS_SAMPLES]; + copy.copy_from_slice(&self.samples); + let bound = if self.filled { STATS_SAMPLES } else { self.idx + 1 }; + + let (_, &mut median) = order_stat::median_of_medians(&mut copy[0..bound]); + median + } +} + +/// RPC Statistics +#[derive(Default, Debug)] +pub struct RpcStats { + requests: RwLock<RateCalculator>, + roundtrips: RwLock<StatsCalculator<u32>>, + active_sessions: AtomicUsize, +} + +impl RpcStats { + /// Count session opened + pub fn open_session(&self) { + self.active_sessions.fetch_add(1, atomic::Ordering::SeqCst); + } + + /// Count session closed. + /// Silently overflows if closing unopened session. + pub fn close_session(&self) { + self.active_sessions.fetch_sub(1, atomic::Ordering::SeqCst); + } + + /// Count request. Returns number of requests in current second. + pub fn count_request(&self) -> u16 { + self.requests.write().tick() + } + + /// Add roundtrip time (microseconds) + pub fn add_roundtrip(&self, microseconds: u32) { + self.roundtrips.write().add(microseconds) + } + + /// Returns number of open sessions + pub fn sessions(&self) -> usize { + self.active_sessions.load(atomic::Ordering::Relaxed) + } + + /// Returns requests rate + pub fn requests_rate(&self) -> usize { + self.requests.read().rate() + } + + /// Returns approximated roundtrip in microseconds + pub fn approximated_roundtrip(&self) -> u32 { + self.roundtrips.read().approximated_median() + } +} + +/// Notifies about RPC activity. +pub trait ActivityNotifier: Send + Sync + 'static { + /// Activity on RPC interface + fn active(&self); +} + +/// Stats-counting RPC middleware +pub struct Middleware<T: ActivityNotifier = ClientNotifier> { + stats: Arc<RpcStats>, + notifier: T, +} + +impl<T: ActivityNotifier> Middleware<T> { + /// Create new Middleware with stats counter and activity notifier. + pub fn new(stats: Arc<RpcStats>, notifier: T) -> Self { + Middleware { + stats: stats, + notifier: notifier, + } + } + + fn as_micro(dur: time::Duration) -> u32 { + (dur.as_secs() * 1_000_000) as u32 + dur.subsec_nanos() / 1_000 + } +} + +impl<M: rpc::Metadata, T: ActivityNotifier> rpc::Middleware<M> for Middleware<T> { + fn on_request<F>(&self, request: rpc::Request, meta: M, process: F) -> rpc::FutureResponse where + F: FnOnce(rpc::Request, M) -> rpc::FutureResponse, + { + let start = time::Instant::now(); + let response = process(request, meta); + + self.notifier.active(); + let stats = self.stats.clone(); + stats.count_request(); + response.map(move |res| { + stats.add_roundtrip(Self::as_micro(start.elapsed())); + res + }).boxed() + } +} + +/// Client Notifier +pub struct ClientNotifier { + /// Client + pub client: Arc<::ethcore::client::Client>, +} + +impl ActivityNotifier for ClientNotifier { + fn active(&self) { + self.client.keep_alive() + } +} + +#[cfg(test)] +mod tests { + + use super::{RateCalculator, StatsCalculator, RpcStats}; + + #[test] + fn should_calculate_rate() { + // given + let mut avg = RateCalculator::default(); + + // when + avg.tick(); + avg.tick(); + avg.tick(); + let rate = avg.rate(); + + // then + assert_eq!(rate, 3usize); + } + + #[test] + fn should_approximate_median() { + // given + let mut stats = StatsCalculator::default(); + stats.add(5); + stats.add(100); + stats.add(3); + stats.add(15); + stats.add(20); + stats.add(6); + + // when + let median = stats.approximated_median(); + + // then + assert_eq!(median, 5); + } + + #[test] + fn should_count_rpc_stats() { + // given + let stats = RpcStats::default(); + assert_eq!(stats.sessions(), 0); + assert_eq!(stats.requests_rate(), 0); + assert_eq!(stats.approximated_roundtrip(), 0); + + // when + stats.open_session(); + stats.close_session(); + stats.open_session(); + stats.count_request(); + stats.count_request(); + stats.add_roundtrip(125); + + // then + assert_eq!(stats.sessions(), 1); + assert_eq!(stats.requests_rate(), 2); + assert_eq!(stats.approximated_roundtrip(), 125); + } + + #[test] + fn should_be_sync_and_send() { + let stats = RpcStats::default(); + is_sync(stats); + } + + fn is_sync<F: Send + Sync>(x: F) { + drop(x) + } +} diff --git a/rpc/src/v1/helpers/mod.rs b/rpc/src/v1/helpers/mod.rs index da24f0a5b..04953ae31 100644 --- a/rpc/src/v1/helpers/mod.rs +++ b/rpc/src/v1/helpers/mod.rs @@ -17,16 +17,18 @@ #[macro_use] pub mod errors; -pub mod dispatch; pub mod block_import; +pub mod dispatch; +pub mod informant; +mod network_settings; mod poll_manager; mod poll_filter; mod requests; mod signer; mod signing_queue; -mod network_settings; +pub use self::network_settings::NetworkSettings; pub use self::poll_manager::PollManager; pub use self::poll_filter::{PollFilter, limit_logs}; pub use self::requests::{ @@ -36,4 +38,3 @@ pub use self::signing_queue::{ ConfirmationsQueue, ConfirmationPromise, ConfirmationResult, SigningQueue, QueueEvent, DefaultAccount, }; pub use self::signer::SignerService; -pub use self::network_settings::NetworkSettings; diff --git a/rpc/src/v1/impls/eth.rs b/rpc/src/v1/impls/eth.rs index 5270e417d..1784cda18 100644 --- a/rpc/src/v1/impls/eth.rs +++ b/rpc/src/v1/impls/eth.rs @@ -258,20 +258,6 @@ fn check_known<C>(client: &C, number: BlockNumber) -> Result<(), Error> where C: const MAX_QUEUE_SIZE_TO_MINE_ON: usize = 4; // because uncles go back 6. -impl<C, SN: ?Sized, S: ?Sized, M, EM> EthClient<C, SN, S, M, EM> where - C: MiningBlockChainClient + 'static, - SN: SnapshotService + 'static, - S: SyncProvider + 'static, - M: MinerService + 'static, - EM: ExternalMinerService + 'static { - - fn active(&self) -> Result<(), Error> { - // TODO: only call every 30s at most. - take_weak!(self.client).keep_alive(); - Ok(()) - } -} - #[cfg(windows)] static SOLC: &'static str = "solc.exe"; @@ -288,8 +274,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where type Metadata = Metadata; fn protocol_version(&self) -> Result<String, Error> { - self.active()?; - let version = take_weak!(self.sync).status().protocol_version.to_owned(); Ok(format!("{}", version)) } @@ -297,7 +281,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where fn syncing(&self) -> Result<SyncStatus, Error> { use ethcore::snapshot::RestorationStatus; - self.active()?; let status = take_weak!(self.sync).status(); let client = take_weak!(self.client); let snapshot_status = take_weak!(self.snapshot).status(); @@ -331,8 +314,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where let dapp = meta.dapp_id.unwrap_or_default(); let author = move || { - self.active()?; - let mut miner = take_weak!(self.miner).author(); if miner == 0.into() { let accounts = self.dapp_accounts(dapp.into())?; @@ -348,20 +329,14 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn is_mining(&self) -> Result<bool, Error> { - self.active()?; - Ok(take_weak!(self.miner).is_sealing()) } fn hashrate(&self) -> Result<RpcU256, Error> { - self.active()?; - Ok(RpcU256::from(self.external_miner.hashrate())) } fn gas_price(&self) -> Result<RpcU256, Error> { - self.active()?; - let (client, miner) = (take_weak!(self.client), take_weak!(self.miner)); Ok(RpcU256::from(default_gas_price(&*client, &*miner))) } @@ -370,8 +345,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where let dapp = meta.dapp_id.unwrap_or_default(); let accounts = move || { - self.active()?; - let accounts = self.dapp_accounts(dapp.into())?; Ok(accounts.into_iter().map(Into::into).collect()) }; @@ -380,14 +353,10 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn block_number(&self) -> Result<RpcU256, Error> { - self.active()?; - Ok(RpcU256::from(take_weak!(self.client).chain_info().best_block_number)) } fn balance(&self, address: RpcH160, num: Trailing<BlockNumber>) -> Result<RpcU256, Error> { - self.active()?; - let address = address.into(); match num.0 { BlockNumber::Pending => Ok(take_weak!(self.miner).balance(&*take_weak!(self.client), &address).into()), @@ -404,7 +373,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn storage_at(&self, address: RpcH160, pos: RpcU256, num: Trailing<BlockNumber>) -> Result<RpcH256, Error> { - self.active()?; let address: Address = RpcH160::into(address); let position: U256 = RpcU256::into(pos); match num.0 { @@ -422,8 +390,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn transaction_count(&self, address: RpcH160, num: Trailing<BlockNumber>) -> Result<RpcU256, Error> { - self.active()?; - let address: Address = RpcH160::into(address); match num.0 { BlockNumber::Pending => Ok(take_weak!(self.miner).nonce(&*take_weak!(self.client), &address).into()), @@ -440,7 +406,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn block_transaction_count_by_hash(&self, hash: RpcH256) -> Result<Option<RpcU256>, Error> { - self.active()?; Ok( take_weak!(self.client).block(BlockId::Hash(hash.into())) .map(|block| block.transactions_count().into()) @@ -448,8 +413,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn block_transaction_count_by_number(&self, num: BlockNumber) -> Result<Option<RpcU256>, Error> { - self.active()?; - match num { BlockNumber::Pending => Ok(Some( take_weak!(self.miner).status().transactions_in_pending_block.into() @@ -462,8 +425,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn block_uncles_count_by_hash(&self, hash: RpcH256) -> Result<Option<RpcU256>, Error> { - self.active()?; - Ok( take_weak!(self.client).block(BlockId::Hash(hash.into())) .map(|block| block.uncles_count().into()) @@ -471,8 +432,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn block_uncles_count_by_number(&self, num: BlockNumber) -> Result<Option<RpcU256>, Error> { - self.active()?; - match num { BlockNumber::Pending => Ok(Some(0.into())), _ => Ok( @@ -483,8 +442,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn code_at(&self, address: RpcH160, num: Trailing<BlockNumber>) -> Result<Bytes, Error> { - self.active()?; - let address: Address = RpcH160::into(address); match num.0 { BlockNumber::Pending => Ok(take_weak!(self.miner).code(&*take_weak!(self.client), &address).map_or_else(Bytes::default, Bytes::new)), @@ -501,19 +458,14 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn block_by_hash(&self, hash: RpcH256, include_txs: bool) -> Result<Option<RichBlock>, Error> { - self.active()?; - self.block(BlockId::Hash(hash.into()), include_txs) } fn block_by_number(&self, num: BlockNumber, include_txs: bool) -> Result<Option<RichBlock>, Error> { - self.active()?; - self.block(num.into(), include_txs) } fn transaction_by_hash(&self, hash: RpcH256) -> Result<Option<Transaction>, Error> { - self.active()?; let hash: H256 = hash.into(); let miner = take_weak!(self.miner); let client = take_weak!(self.client); @@ -521,20 +473,14 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn transaction_by_block_hash_and_index(&self, hash: RpcH256, index: Index) -> Result<Option<Transaction>, Error> { - self.active()?; - self.transaction(TransactionId::Location(BlockId::Hash(hash.into()), index.value())) } fn transaction_by_block_number_and_index(&self, num: BlockNumber, index: Index) -> Result<Option<Transaction>, Error> { - self.active()?; - self.transaction(TransactionId::Location(num.into(), index.value())) } fn transaction_receipt(&self, hash: RpcH256) -> Result<Option<Receipt>, Error> { - self.active()?; - let miner = take_weak!(self.miner); let best_block = take_weak!(self.client).chain_info().best_block_number; let hash: H256 = hash.into(); @@ -549,20 +495,14 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn uncle_by_block_hash_and_index(&self, hash: RpcH256, index: Index) -> Result<Option<RichBlock>, Error> { - self.active()?; - self.uncle(UncleId { block: BlockId::Hash(hash.into()), position: index.value() }) } fn uncle_by_block_number_and_index(&self, num: BlockNumber, index: Index) -> Result<Option<RichBlock>, Error> { - self.active()?; - self.uncle(UncleId { block: num.into(), position: index.value() }) } fn compilers(&self) -> Result<Vec<String>, Error> { - self.active()?; - let mut compilers = vec![]; if Command::new(SOLC).output().is_ok() { compilers.push("solidity".to_owned()) @@ -591,7 +531,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn work(&self, no_new_work_timeout: Trailing<u64>) -> Result<Work, Error> { - self.active()?; let no_new_work_timeout = no_new_work_timeout.0; let client = take_weak!(self.client); @@ -643,8 +582,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn submit_work(&self, nonce: RpcH64, pow_hash: RpcH256, mix_hash: RpcH256) -> Result<bool, Error> { - self.active()?; - let nonce: H64 = nonce.into(); let pow_hash: H256 = pow_hash.into(); let mix_hash: H256 = mix_hash.into(); @@ -657,14 +594,11 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn submit_hashrate(&self, rate: RpcU256, id: RpcH256) -> Result<bool, Error> { - self.active()?; self.external_miner.submit_hashrate(rate.into(), id.into()); Ok(true) } fn send_raw_transaction(&self, raw: Bytes) -> Result<RpcH256, Error> { - self.active()?; - UntrustedRlp::new(&raw.into_vec()).as_val() .map_err(errors::from_rlp_error) .and_then(|tx| SignedTransaction::new(tx).map_err(errors::from_transaction_error)) @@ -679,8 +613,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn call(&self, request: CallRequest, num: Trailing<BlockNumber>) -> Result<Bytes, Error> { - self.active()?; - let request = CallRequest::into(request); let signed = self.sign_call(request)?; @@ -695,8 +627,6 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn estimate_gas(&self, request: CallRequest, num: Trailing<BlockNumber>) -> Result<RpcU256, Error> { - self.active()?; - let request = CallRequest::into(request); let signed = self.sign_call(request)?; take_weak!(self.client).estimate_gas(&signed, num.0.into()) @@ -705,19 +635,14 @@ impl<C, SN: ?Sized, S: ?Sized, M, EM> Eth for EthClient<C, SN, S, M, EM> where } fn compile_lll(&self, _: String) -> Result<Bytes, Error> { - self.active()?; - rpc_unimplemented!() } fn compile_serpent(&self, _: String) -> Result<Bytes, Error> { - self.active()?; - rpc_unimplemented!() } fn compile_solidity(&self, code: String) -> Result<Bytes, Error> { - self.active()?; let maybe_child = Command::new(SOLC) .arg("--bin") .arg("--optimize") diff --git a/rpc/src/v1/impls/eth_filter.rs b/rpc/src/v1/impls/eth_filter.rs index 9ca9692b1..cf3398498 100644 --- a/rpc/src/v1/impls/eth_filter.rs +++ b/rpc/src/v1/impls/eth_filter.rs @@ -50,19 +50,12 @@ impl<C, M> EthFilterClient<C, M> where polls: Mutex::new(PollManager::new()), } } - - fn active(&self) -> Result<(), Error> { - // TODO: only call every 30s at most. - take_weak!(self.client).keep_alive(); - Ok(()) - } } impl<C, M> EthFilter for EthFilterClient<C, M> where C: BlockChainClient + 'static, M: MinerService + 'static { fn new_filter(&self, filter: Filter) -> Result<RpcU256, Error> { - self.active()?; let mut polls = self.polls.lock(); let block_number = take_weak!(self.client).chain_info().best_block_number; let id = polls.create_poll(PollFilter::Logs(block_number, Default::default(), filter)); @@ -70,16 +63,12 @@ impl<C, M> EthFilter for EthFilterClient<C, M> } fn new_block_filter(&self) -> Result<RpcU256, Error> { - self.active()?; - let mut polls = self.polls.lock(); let id = polls.create_poll(PollFilter::Block(take_weak!(self.client).chain_info().best_block_number)); Ok(id.into()) } fn new_pending_transaction_filter(&self) -> Result<RpcU256, Error> { - self.active()?; - let mut polls = self.polls.lock(); let best_block = take_weak!(self.client).chain_info().best_block_number; let pending_transactions = take_weak!(self.miner).pending_transactions_hashes(best_block); @@ -88,7 +77,6 @@ impl<C, M> EthFilter for EthFilterClient<C, M> } fn filter_changes(&self, index: Index) -> Result<FilterChanges, Error> { - self.active()?; let client = take_weak!(self.client); let mut polls = self.polls.lock(); match polls.poll_mut(&index.value()) { @@ -180,8 +168,6 @@ impl<C, M> EthFilter for EthFilterClient<C, M> } fn filter_logs(&self, index: Index) -> Result<Vec<Log>, Error> { - self.active()?; - let mut polls = self.polls.lock(); match polls.poll(&index.value()) { Some(&PollFilter::Logs(ref _block_number, ref _previous_log, ref filter)) => { @@ -207,8 +193,6 @@ impl<C, M> EthFilter for EthFilterClient<C, M> } fn uninstall_filter(&self, index: Index) -> Result<bool, Error> { - self.active()?; - self.polls.lock().remove_poll(&index.value()); Ok(true) } diff --git a/rpc/src/v1/impls/parity.rs b/rpc/src/v1/impls/parity.rs index 055ccebeb..e872b8dc3 100644 --- a/rpc/src/v1/impls/parity.rs +++ b/rpc/src/v1/impls/parity.rs @@ -101,12 +101,6 @@ impl<C, M, S: ?Sized, U> ParityClient<C, M, S, U> where dapps_port: dapps_port, } } - - fn active(&self) -> Result<(), Error> { - // TODO: only call every 30s at most. - take_weak!(self.client).keep_alive(); - Ok(()) - } } impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where @@ -118,8 +112,6 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where type Metadata = Metadata; fn accounts_info(&self, dapp: Trailing<DappId>) -> Result<BTreeMap<String, BTreeMap<String, String>>, Error> { - self.active()?; - let dapp = dapp.0; let store = take_weak!(self.accounts); @@ -149,8 +141,6 @@ 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 default_account = move || { - self.active()?; - Ok(take_weak!(self.accounts) .dapps_addresses(dapp_id.into()) .ok() @@ -163,57 +153,39 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where } fn transactions_limit(&self) -> Result<usize, Error> { - self.active()?; - Ok(take_weak!(self.miner).transactions_limit()) } fn min_gas_price(&self) -> Result<U256, Error> { - self.active()?; - Ok(U256::from(take_weak!(self.miner).minimal_gas_price())) } fn extra_data(&self) -> Result<Bytes, Error> { - self.active()?; - Ok(Bytes::new(take_weak!(self.miner).extra_data())) } fn gas_floor_target(&self) -> Result<U256, Error> { - self.active()?; - Ok(U256::from(take_weak!(self.miner).gas_floor_target())) } fn gas_ceil_target(&self) -> Result<U256, Error> { - self.active()?; - Ok(U256::from(take_weak!(self.miner).gas_ceil_target())) } fn dev_logs(&self) -> Result<Vec<String>, Error> { - self.active()?; - let logs = self.logger.logs(); Ok(logs.as_slice().to_owned()) } fn dev_logs_levels(&self) -> Result<String, Error> { - self.active()?; - Ok(self.logger.levels().to_owned()) } fn net_chain(&self) -> Result<String, Error> { - self.active()?; - Ok(self.settings.chain.clone()) } fn net_peers(&self) -> Result<Peers, Error> { - self.active()?; - let sync = take_weak!(self.sync); let sync_status = sync.status(); let net_config = take_weak!(self.net).network_config(); @@ -228,20 +200,14 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where } fn net_port(&self) -> Result<u16, Error> { - self.active()?; - Ok(self.settings.network_port) } fn node_name(&self) -> Result<String, Error> { - self.active()?; - Ok(self.settings.name.clone()) } fn registry_address(&self) -> Result<Option<H160>, Error> { - self.active()?; - Ok( take_weak!(self.client) .additional_params() @@ -252,7 +218,6 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where } fn rpc_settings(&self) -> Result<RpcSettings, Error> { - self.active()?; Ok(RpcSettings { enabled: self.settings.rpc_enabled, interface: self.settings.rpc_interface.clone(), @@ -261,19 +226,14 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where } fn default_extra_data(&self) -> Result<Bytes, Error> { - self.active()?; - Ok(Bytes::new(version_data())) } fn gas_price_histogram(&self) -> Result<Histogram, Error> { - self.active()?; take_weak!(self.client).gas_price_histogram(100, 10).ok_or_else(errors::not_enough_data).map(Into::into) } fn unsigned_transactions_count(&self) -> Result<usize, Error> { - self.active()?; - match self.signer { None => Err(errors::signer_disabled()), Some(ref signer) => Ok(signer.len()), @@ -281,56 +241,40 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where } fn generate_secret_phrase(&self) -> Result<String, Error> { - self.active()?; - Ok(random_phrase(12)) } fn phrase_to_address(&self, phrase: String) -> Result<H160, Error> { - self.active()?; - Ok(Brain::new(phrase).generate().unwrap().address().into()) } fn list_accounts(&self, count: u64, after: Option<H160>, block_number: Trailing<BlockNumber>) -> Result<Option<Vec<H160>>, Error> { - self.active()?; - Ok(take_weak!(self.client) .list_accounts(block_number.0.into(), after.map(Into::into).as_ref(), count) .map(|a| a.into_iter().map(Into::into).collect())) } fn list_storage_keys(&self, address: H160, count: u64, after: Option<H256>, block_number: Trailing<BlockNumber>) -> Result<Option<Vec<H256>>, Error> { - self.active()?; - Ok(take_weak!(self.client) .list_storage(block_number.0.into(), &address.into(), after.map(Into::into).as_ref(), count) .map(|a| a.into_iter().map(Into::into).collect())) } fn encrypt_message(&self, key: H512, phrase: Bytes) -> Result<Bytes, Error> { - self.active()?; - ecies::encrypt(&key.into(), &DEFAULT_MAC, &phrase.0) .map_err(errors::encryption_error) .map(Into::into) } fn pending_transactions(&self) -> Result<Vec<Transaction>, Error> { - self.active()?; - Ok(take_weak!(self.miner).pending_transactions().into_iter().map(Into::into).collect::<Vec<_>>()) } fn future_transactions(&self) -> Result<Vec<Transaction>, Error> { - self.active()?; - Ok(take_weak!(self.miner).future_transactions().into_iter().map(Into::into).collect::<Vec<_>>()) } fn pending_transactions_stats(&self) -> Result<BTreeMap<H256, TransactionStats>, Error> { - self.active()?; - let stats = take_weak!(self.sync).transactions_stats(); Ok(stats.into_iter() .map(|(hash, stats)| (hash.into(), stats.into())) @@ -339,8 +283,6 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where } fn local_transactions(&self) -> Result<BTreeMap<H256, LocalTransactionStatus>, Error> { - self.active()?; - let transactions = take_weak!(self.miner).local_transactions(); Ok(transactions .into_iter() @@ -350,8 +292,6 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where } fn signer_port(&self) -> Result<u16, Error> { - self.active()?; - self.signer .clone() .and_then(|signer| signer.address()) @@ -360,21 +300,16 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where } fn dapps_port(&self) -> Result<u16, Error> { - self.active()?; - self.dapps_port .ok_or_else(|| errors::dapps_disabled()) } fn dapps_interface(&self) -> Result<String, Error> { - self.active()?; - self.dapps_interface.clone() .ok_or_else(|| errors::dapps_disabled()) } fn next_nonce(&self, address: H160) -> Result<U256, Error> { - self.active()?; let address: Address = address.into(); let miner = take_weak!(self.miner); let client = take_weak!(self.client); @@ -400,26 +335,21 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where } fn consensus_capability(&self) -> Result<ConsensusCapability, Error> { - self.active()?; let updater = take_weak!(self.updater); Ok(updater.capability().into()) } fn version_info(&self) -> Result<VersionInfo, Error> { - self.active()?; let updater = take_weak!(self.updater); Ok(updater.version_info().into()) } fn releases_info(&self) -> Result<Option<OperationsInfo>, Error> { - self.active()?; let updater = take_weak!(self.updater); Ok(updater.info().map(Into::into)) } fn chain_status(&self) -> Result<ChainStatus, Error> { - self.active()?; - let chain_info = take_weak!(self.client).chain_info(); let gap = chain_info.ancient_block_number.map(|x| U256::from(x + 1)) diff --git a/rpc/src/v1/impls/parity_accounts.rs b/rpc/src/v1/impls/parity_accounts.rs index 05ac7e5e2..1034b1df5 100644 --- a/rpc/src/v1/impls/parity_accounts.rs +++ b/rpc/src/v1/impls/parity_accounts.rs @@ -21,7 +21,6 @@ use util::{Address}; use ethkey::{Brain, Generator, Secret}; use ethcore::account_provider::AccountProvider; -use ethcore::client::MiningBlockChainClient; use jsonrpc_core::Error; use v1::helpers::errors; @@ -29,30 +28,21 @@ use v1::traits::ParityAccounts; use v1::types::{H160 as RpcH160, H256 as RpcH256, DappId}; /// Account management (personal) rpc implementation. -pub struct ParityAccountsClient<C> where C: MiningBlockChainClient { +pub struct ParityAccountsClient { accounts: Weak<AccountProvider>, - client: Weak<C>, } -impl<C> ParityAccountsClient<C> where C: MiningBlockChainClient { +impl ParityAccountsClient { /// Creates new PersonalClient - pub fn new(store: &Arc<AccountProvider>, client: &Arc<C>) -> Self { + pub fn new(store: &Arc<AccountProvider>) -> Self { ParityAccountsClient { accounts: Arc::downgrade(store), - client: Arc::downgrade(client), } } - - fn active(&self) -> Result<(), Error> { - // TODO: only call every 30s at most. - take_weak!(self.client).keep_alive(); - Ok(()) - } } -impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlockChainClient { +impl ParityAccounts for ParityAccountsClient { fn all_accounts_info(&self) -> Result<BTreeMap<RpcH160, BTreeMap<String, String>>, Error> { - self.active()?; let store = take_weak!(self.accounts); let info = store.accounts_info().map_err(|e| errors::account("Could not fetch account info.", e))?; let other = store.addresses_info(); @@ -75,7 +65,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock } fn new_account_from_phrase(&self, phrase: String, pass: String) -> Result<RpcH160, Error> { - self.active()?; let store = take_weak!(self.accounts); let brain = Brain::new(phrase).generate().unwrap(); @@ -85,7 +74,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock } fn new_account_from_wallet(&self, json: String, pass: String) -> Result<RpcH160, Error> { - self.active()?; let store = take_weak!(self.accounts); store.import_presale(json.as_bytes(), &pass) @@ -95,7 +83,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock } fn new_account_from_secret(&self, secret: RpcH256, pass: String) -> Result<RpcH160, Error> { - self.active()?; let store = take_weak!(self.accounts); let secret = Secret::from_slice(&secret.0) @@ -106,7 +93,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock } fn test_password(&self, account: RpcH160, password: String) -> Result<bool, Error> { - self.active()?; let account: Address = account.into(); take_weak!(self.accounts) @@ -115,7 +101,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock } fn change_password(&self, account: RpcH160, password: String, new_password: String) -> Result<bool, Error> { - self.active()?; let account: Address = account.into(); take_weak!(self.accounts) .change_password(&account, password, new_password) @@ -124,7 +109,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock } fn kill_account(&self, account: RpcH160, password: String) -> Result<bool, Error> { - self.active()?; let account: Address = account.into(); take_weak!(self.accounts) .kill_account(&account, &password) @@ -133,7 +117,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock } fn remove_address(&self, addr: RpcH160) -> Result<bool, Error> { - self.active()?; let store = take_weak!(self.accounts); let addr: Address = addr.into(); @@ -142,7 +125,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock } fn set_account_name(&self, addr: RpcH160, name: String) -> Result<bool, Error> { - self.active()?; let store = take_weak!(self.accounts); let addr: Address = addr.into(); @@ -152,7 +134,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock } fn set_account_meta(&self, addr: RpcH160, meta: String) -> Result<bool, Error> { - self.active()?; let store = take_weak!(self.accounts); let addr: Address = addr.into(); @@ -216,7 +197,6 @@ impl<C: 'static> ParityAccounts for ParityAccountsClient<C> where C: MiningBlock } fn geth_accounts(&self) -> Result<Vec<RpcH160>, Error> { - self.active()?; let store = take_weak!(self.accounts); Ok(into_vec(store.list_geth_accounts(false))) diff --git a/rpc/src/v1/impls/parity_set.rs b/rpc/src/v1/impls/parity_set.rs index b5d864a72..a6f1129ba 100644 --- a/rpc/src/v1/impls/parity_set.rs +++ b/rpc/src/v1/impls/parity_set.rs @@ -23,7 +23,7 @@ use ethcore::client::MiningBlockChainClient; use ethcore::mode::Mode; use ethsync::ManageNetwork; use fetch::{self, Fetch}; -use futures::{self, BoxFuture, Future}; +use futures::{BoxFuture, Future}; use util::sha3; use updater::{Service as UpdateService}; @@ -62,12 +62,6 @@ impl<C, M, U, F> ParitySetClient<C, M, U, F> where fetch: fetch, } } - - fn active(&self) -> Result<(), Error> { - // TODO: only call every 30s at most. - take_weak!(self.client).keep_alive(); - Ok(()) - } } impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where @@ -78,63 +72,46 @@ impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where { fn set_min_gas_price(&self, gas_price: U256) -> Result<bool, Error> { - self.active()?; - take_weak!(self.miner).set_minimal_gas_price(gas_price.into()); Ok(true) } fn set_gas_floor_target(&self, target: U256) -> Result<bool, Error> { - self.active()?; - take_weak!(self.miner).set_gas_floor_target(target.into()); Ok(true) } fn set_gas_ceil_target(&self, target: U256) -> Result<bool, Error> { - self.active()?; - take_weak!(self.miner).set_gas_ceil_target(target.into()); Ok(true) } fn set_extra_data(&self, extra_data: Bytes) -> Result<bool, Error> { - self.active()?; - take_weak!(self.miner).set_extra_data(extra_data.into_vec()); Ok(true) } fn set_author(&self, author: H160) -> Result<bool, Error> { - self.active()?; - take_weak!(self.miner).set_author(author.into()); Ok(true) } fn set_engine_signer(&self, address: H160, password: String) -> Result<bool, Error> { - self.active()?; take_weak!(self.miner).set_engine_signer(address.into(), password).map_err(Into::into).map_err(errors::from_password_error)?; Ok(true) } fn set_transactions_limit(&self, limit: usize) -> Result<bool, Error> { - self.active()?; - take_weak!(self.miner).set_transactions_limit(limit); Ok(true) } fn set_tx_gas_limit(&self, limit: U256) -> Result<bool, Error> { - self.active()?; - take_weak!(self.miner).set_tx_gas_limit(limit.into()); Ok(true) } fn add_reserved_peer(&self, peer: String) -> Result<bool, Error> { - self.active()?; - match take_weak!(self.net).add_reserved_peer(peer) { Ok(()) => Ok(true), Err(e) => Err(errors::invalid_params("Peer address", e)), @@ -142,8 +119,6 @@ impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where } fn remove_reserved_peer(&self, peer: String) -> Result<bool, Error> { - self.active()?; - match take_weak!(self.net).remove_reserved_peer(peer) { Ok(()) => Ok(true), Err(e) => Err(errors::invalid_params("Peer address", e)), @@ -151,15 +126,11 @@ impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where } fn drop_non_reserved_peers(&self) -> Result<bool, Error> { - self.active()?; - take_weak!(self.net).deny_unreserved_peers(); Ok(true) } fn accept_non_reserved_peers(&self) -> Result<bool, Error> { - self.active()?; - take_weak!(self.net).accept_unreserved_peers(); Ok(true) } @@ -186,10 +157,6 @@ impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where } fn hash_content(&self, url: String) -> BoxFuture<H256, Error> { - if let Err(e) = self.active() { - return futures::failed(e).boxed(); - } - self.fetch.process(self.fetch.fetch(&url).then(move |result| { result .map_err(errors::from_fetch_error) @@ -201,13 +168,11 @@ impl<C, M, U, F> ParitySet for ParitySetClient<C, M, U, F> where } fn upgrade_ready(&self) -> Result<Option<ReleaseInfo>, Error> { - self.active()?; let updater = take_weak!(self.updater); Ok(updater.upgrade_ready().map(Into::into)) } fn execute_upgrade(&self) -> Result<bool, Error> { - self.active()?; let updater = take_weak!(self.updater); Ok(updater.execute_upgrade()) } diff --git a/rpc/src/v1/impls/personal.rs b/rpc/src/v1/impls/personal.rs index b29cc41ee..44e854412 100644 --- a/rpc/src/v1/impls/personal.rs +++ b/rpc/src/v1/impls/personal.rs @@ -54,12 +54,6 @@ impl<C, M> PersonalClient<C, M> where allow_perm_unlock: allow_perm_unlock, } } - - fn active(&self) -> Result<(), Error> { - // TODO: only call every 30s at most. - take_weak!(self.client).keep_alive(); - Ok(()) - } } impl<C, M> Personal for PersonalClient<C, M> where @@ -69,15 +63,12 @@ impl<C, M> Personal for PersonalClient<C, M> where type Metadata = Metadata; fn accounts(&self) -> Result<Vec<RpcH160>, Error> { - self.active()?; - let store = take_weak!(self.accounts); let accounts = store.accounts().map_err(|e| errors::account("Could not fetch accounts.", e))?; Ok(accounts.into_iter().map(Into::into).collect::<Vec<RpcH160>>()) } fn new_account(&self, pass: String) -> Result<RpcH160, Error> { - self.active()?; let store = take_weak!(self.accounts); store.new_account(&pass) @@ -86,7 +77,6 @@ impl<C, M> Personal for PersonalClient<C, M> where } fn unlock_account(&self, account: RpcH160, account_pass: String, duration: Option<RpcU128>) -> Result<bool, Error> { - self.active()?; let account: Address = account.into(); let store = take_weak!(self.accounts); let duration = match duration { @@ -117,7 +107,6 @@ impl<C, M> Personal for PersonalClient<C, M> where fn send_transaction(&self, meta: Metadata, request: TransactionRequest, password: String) -> BoxFuture<RpcH256, Error> { let sign_and_send = move || { - self.active()?; let client = take_weak!(self.client); let miner = take_weak!(self.miner); let accounts = take_weak!(self.accounts); diff --git a/rpc/src/v1/impls/signer.rs b/rpc/src/v1/impls/signer.rs index a25a5bbc1..90a811e37 100644 --- a/rpc/src/v1/impls/signer.rs +++ b/rpc/src/v1/impls/signer.rs @@ -55,17 +55,9 @@ impl<C: 'static, M: 'static> SignerClient<C, M> where C: MiningBlockChainClient, } } - fn active(&self) -> Result<(), Error> { - // TODO: only call every 30s at most. - take_weak!(self.client).keep_alive(); - Ok(()) - } - fn confirm_internal<F>(&self, id: U256, modification: TransactionModification, f: F) -> Result<WithToken<ConfirmationResponse>, Error> where F: FnOnce(&C, &M, &AccountProvider, ConfirmationPayload) -> Result<WithToken<ConfirmationResponse>, Error>, { - self.active()?; - let id = id.into(); let accounts = take_weak!(self.accounts); let signer = take_weak!(self.signer); @@ -104,7 +96,6 @@ impl<C: 'static, M: 'static> SignerClient<C, M> where C: MiningBlockChainClient, impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockChainClient, M: MinerService { fn requests_to_confirm(&self) -> Result<Vec<ConfirmationRequest>, Error> { - self.active()?; let signer = take_weak!(self.signer); Ok(signer.requests() @@ -135,8 +126,6 @@ impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockC } fn confirm_request_raw(&self, id: U256, bytes: Bytes) -> Result<ConfirmationResponse, Error> { - self.active()?; - let id = id.into(); let signer = take_weak!(self.signer); let client = take_weak!(self.client); @@ -187,7 +176,6 @@ impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockC } fn reject_request(&self, id: U256) -> Result<bool, Error> { - self.active()?; let signer = take_weak!(self.signer); let res = signer.request_rejected(id.into()); @@ -195,7 +183,6 @@ impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockC } fn generate_token(&self) -> Result<String, Error> { - self.active()?; let signer = take_weak!(self.signer); signer.generate_token() @@ -203,7 +190,6 @@ impl<C: 'static, M: 'static> Signer for SignerClient<C, M> where C: MiningBlockC } fn generate_web_proxy_token(&self) -> Result<String, Error> { - try!(self.active()); let signer = take_weak!(self.signer); Ok(signer.generate_web_proxy_access_token()) diff --git a/rpc/src/v1/impls/signing.rs b/rpc/src/v1/impls/signing.rs index 2d5fdbba7..0db90436c 100644 --- a/rpc/src/v1/impls/signing.rs +++ b/rpc/src/v1/impls/signing.rs @@ -74,12 +74,6 @@ impl<C, M> SigningQueueClient<C, M> where } } - fn active(&self) -> Result<(), Error> { - // TODO: only call every 30s at most. - take_weak!(self.client).keep_alive(); - Ok(()) - } - fn handle_dispatch<OnResponse>(&self, res: Result<DispatchResult, Error>, on_response: OnResponse) where OnResponse: FnOnce(Result<RpcConfirmationResponse, Error>) + Send + 'static { @@ -131,7 +125,6 @@ impl<C: 'static, M: 'static> ParitySigning for SigningQueueClient<C, M> where type Metadata = Metadata; fn post_sign(&self, address: RpcH160, data: RpcBytes) -> Result<RpcEither<RpcU256, RpcConfirmationResponse>, Error> { - self.active()?; self.dispatch(RpcConfirmationPayload::Signature((address.clone(), data).into()), DefaultAccount::Provided(address.into())) .map(|result| match result { DispatchResult::Value(v) => RpcEither::Or(v), @@ -145,7 +138,6 @@ impl<C: 'static, M: 'static> ParitySigning for SigningQueueClient<C, M> where fn post_transaction(&self, meta: Metadata, request: RpcTransactionRequest) -> BoxFuture<RpcEither<RpcU256, RpcConfirmationResponse>, Error> { let post_transaction = move || { - self.active()?; self.dispatch(RpcConfirmationPayload::SendTransaction(request), meta.into()) .map(|result| match result { DispatchResult::Value(v) => RpcEither::Or(v), @@ -160,7 +152,6 @@ impl<C: 'static, M: 'static> ParitySigning for SigningQueueClient<C, M> where } fn check_request(&self, id: RpcU256) -> Result<Option<RpcConfirmationResponse>, Error> { - self.active()?; let mut pending = self.pending.lock(); let id: U256 = id.into(); let res = match pending.get(&id) { @@ -176,8 +167,7 @@ impl<C: 'static, M: 'static> ParitySigning for SigningQueueClient<C, M> where } fn decrypt_message(&self, address: RpcH160, data: RpcBytes) -> BoxFuture<RpcBytes, Error> { - let res = self.active() - .and_then(|_| self.dispatch(RpcConfirmationPayload::Decrypt((address.clone(), data).into()), address.into())); + let res = self.dispatch(RpcConfirmationPayload::Decrypt((address.clone(), data).into()), address.into()); let (ready, p) = futures::oneshot(); // TODO [todr] typed handle_dispatch @@ -200,8 +190,7 @@ impl<C: 'static, M: 'static> EthSigning for SigningQueueClient<C, M> where type Metadata = Metadata; fn sign(&self, address: RpcH160, data: RpcBytes) -> BoxFuture<RpcH520, Error> { - let res = self.active() - .and_then(|_| self.dispatch(RpcConfirmationPayload::Signature((address.clone(), data).into()), address.into())); + let res = self.dispatch(RpcConfirmationPayload::Signature((address.clone(), data).into()), address.into()); let (ready, p) = futures::oneshot(); self.handle_dispatch(res, |response| { @@ -216,8 +205,7 @@ impl<C: 'static, M: 'static> EthSigning for SigningQueueClient<C, M> where } fn send_transaction(&self, meta: Metadata, request: RpcTransactionRequest) -> BoxFuture<RpcH256, Error> { - let res = self.active() - .and_then(|_| self.dispatch(RpcConfirmationPayload::SendTransaction(request), meta.into())); + let res = self.dispatch(RpcConfirmationPayload::SendTransaction(request), meta.into()); let (ready, p) = futures::oneshot(); self.handle_dispatch(res, |response| { @@ -232,7 +220,7 @@ impl<C: 'static, M: 'static> EthSigning for SigningQueueClient<C, M> where } fn sign_transaction(&self, meta: Metadata, request: RpcTransactionRequest) -> BoxFuture<RpcRichRawTransaction, Error> { - let res = self.active().and_then(|_| self.dispatch(RpcConfirmationPayload::SignTransaction(request), meta.into())); + let res = self.dispatch(RpcConfirmationPayload::SignTransaction(request), meta.into()); let (ready, p) = futures::oneshot(); self.handle_dispatch(res, |response| { diff --git a/rpc/src/v1/impls/signing_unsafe.rs b/rpc/src/v1/impls/signing_unsafe.rs index a76ea00a5..b43362c76 100644 --- a/rpc/src/v1/impls/signing_unsafe.rs +++ b/rpc/src/v1/impls/signing_unsafe.rs @@ -62,14 +62,7 @@ impl<C, M> SigningUnsafeClient<C, M> where } } - fn active(&self) -> Result<(), Error> { - // TODO: only call every 30s at most. - take_weak!(self.client).keep_alive(); - Ok(()) - } - fn handle(&self, payload: RpcConfirmationPayload, account: DefaultAccount) -> Result<RpcConfirmationResponse, Error> { - self.active()?; let client = take_weak!(self.client); let miner = take_weak!(self.miner); let accounts = take_weak!(self.accounts); diff --git a/rpc/src/v1/impls/traces.rs b/rpc/src/v1/impls/traces.rs index fe3d5358e..9fe92518b 100644 --- a/rpc/src/v1/impls/traces.rs +++ b/rpc/src/v1/impls/traces.rs @@ -66,17 +66,10 @@ impl<C, M> TracesClient<C, M> where C: BlockChainClient, M: MinerService { data: request.data.map_or_else(Vec::new, |d| d.to_vec()) }.fake_sign(from)) } - - fn active(&self) -> Result<(), Error> { - // TODO: only call every 30s at most. - take_weak!(self.client).keep_alive(); - Ok(()) - } } impl<C, M> Traces for TracesClient<C, M> where C: BlockChainClient + 'static, M: MinerService + 'static { fn filter(&self, filter: TraceFilter) -> Result<Vec<LocalizedTrace>, Error> { - self.active()?; let client = take_weak!(self.client); let traces = client.filter_traces(filter.into()); let traces = traces.map_or_else(Vec::new, |traces| traces.into_iter().map(LocalizedTrace::from).collect()); @@ -84,7 +77,6 @@ impl<C, M> Traces for TracesClient<C, M> where C: BlockChainClient + 'static, M: } fn block_traces(&self, block_number: BlockNumber) -> Result<Vec<LocalizedTrace>, Error> { - self.active()?; let client = take_weak!(self.client); let traces = client.block_traces(block_number.into()); let traces = traces.map_or_else(Vec::new, |traces| traces.into_iter().map(LocalizedTrace::from).collect()); @@ -92,7 +84,6 @@ impl<C, M> Traces for TracesClient<C, M> where C: BlockChainClient + 'static, M: } fn transaction_traces(&self, transaction_hash: H256) -> Result<Vec<LocalizedTrace>, Error> { - self.active()?; let client = take_weak!(self.client); let traces = client.transaction_traces(TransactionId::Hash(transaction_hash.into())); let traces = traces.map_or_else(Vec::new, |traces| traces.into_iter().map(LocalizedTrace::from).collect()); @@ -100,7 +91,6 @@ impl<C, M> Traces for TracesClient<C, M> where C: BlockChainClient + 'static, M: } fn trace(&self, transaction_hash: H256, address: Vec<Index>) -> Result<Option<LocalizedTrace>, Error> { - self.active()?; let client = take_weak!(self.client); let id = TraceId { transaction: TransactionId::Hash(transaction_hash.into()), @@ -113,7 +103,6 @@ impl<C, M> Traces for TracesClient<C, M> where C: BlockChainClient + 'static, M: } fn call(&self, request: CallRequest, flags: Vec<String>, block: Trailing<BlockNumber>) -> Result<Option<TraceResults>, Error> { - self.active()?; let block = block.0; let request = CallRequest::into(request); @@ -125,7 +114,6 @@ impl<C, M> Traces for TracesClient<C, M> where C: BlockChainClient + 'static, M: } fn raw_transaction(&self, raw_transaction: Bytes, flags: Vec<String>, block: Trailing<BlockNumber>) -> Result<Option<TraceResults>, Error> { - self.active()?; let block = block.0; UntrustedRlp::new(&raw_transaction.into_vec()).as_val() @@ -140,8 +128,6 @@ impl<C, M> Traces for TracesClient<C, M> where C: BlockChainClient + 'static, M: } fn replay_transaction(&self, transaction_hash: H256, flags: Vec<String>) -> Result<Option<TraceResults>, Error> { - self.active()?; - Ok(match take_weak!(self.client).replay(TransactionId::Hash(transaction_hash.into()), to_call_analytics(flags)) { Ok(e) => Some(TraceResults::from(e)), _ => None, diff --git a/rpc/src/v1/mod.rs b/rpc/src/v1/mod.rs index f38e13538..0b0787717 100644 --- a/rpc/src/v1/mod.rs +++ b/rpc/src/v1/mod.rs @@ -29,5 +29,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}; +pub use self::helpers::{SigningQueue, SignerService, ConfirmationsQueue, NetworkSettings, block_import, informant}; pub use self::metadata::{Metadata, Origin}; diff --git a/rpc/src/v1/tests/mocked/parity_accounts.rs b/rpc/src/v1/tests/mocked/parity_accounts.rs index be8cb3f91..e245cb92f 100644 --- a/rpc/src/v1/tests/mocked/parity_accounts.rs +++ b/rpc/src/v1/tests/mocked/parity_accounts.rs @@ -17,7 +17,6 @@ use std::sync::Arc; use ethcore::account_provider::AccountProvider; -use ethcore::client::TestBlockChainClient; use jsonrpc_core::IoHandler; use v1::{ParityAccounts, ParityAccountsClient}; @@ -25,14 +24,6 @@ use v1::{ParityAccounts, ParityAccountsClient}; struct ParityAccountsTester { accounts: Arc<AccountProvider>, io: IoHandler, - // these unused fields are necessary to keep the data alive - // as the handler has only weak pointers. - _client: Arc<TestBlockChainClient>, -} - -fn blockchain_client() -> Arc<TestBlockChainClient> { - let client = TestBlockChainClient::new(); - Arc::new(client) } fn accounts_provider() -> Arc<AccountProvider> { @@ -41,8 +32,7 @@ fn accounts_provider() -> Arc<AccountProvider> { fn setup() -> ParityAccountsTester { let accounts = accounts_provider(); - let client = blockchain_client(); - let parity_accounts = ParityAccountsClient::new(&accounts, &client); + let parity_accounts = ParityAccountsClient::new(&accounts); let mut io = IoHandler::default(); io.extend_with(parity_accounts.to_delegate()); @@ -50,7 +40,6 @@ fn setup() -> ParityAccountsTester { let tester = ParityAccountsTester { accounts: accounts, io: io, - _client: client, }; tester diff --git a/signer/src/ws_server/mod.rs b/signer/src/ws_server/mod.rs index 6983ef2fe..7fc046f49 100644 --- a/signer/src/ws_server/mod.rs +++ b/signer/src/ws_server/mod.rs @@ -17,17 +17,19 @@ //! `WebSockets` server. use ws; -use std; -use std::thread; -use std::path::PathBuf; use std::default::Default; -use std::ops::Drop; -use std::sync::Arc; use std::net::SocketAddr; +use std::ops::Drop; +use std::path::PathBuf; +use std::sync::Arc; +use std::thread; +use std; + use io::{PanicHandler, OnPanicListener, MayPanic}; -use jsonrpc_core::Metadata; +use jsonrpc_core::{Metadata, Middleware}; use jsonrpc_core::reactor::RpcHandler; -use rpc::ConfirmationsQueue; +use rpc::{ConfirmationsQueue}; +use rpc::informant::RpcStats; mod session; @@ -54,6 +56,7 @@ pub struct ServerBuilder { queue: Arc<ConfirmationsQueue>, authcodes_path: PathBuf, skip_origin_validation: bool, + stats: Option<Arc<RpcStats>>, } impl ServerBuilder { @@ -63,6 +66,7 @@ impl ServerBuilder { queue: queue, authcodes_path: authcodes_path, skip_origin_validation: false, + stats: None, } } @@ -73,10 +77,23 @@ impl ServerBuilder { self } + /// Configure statistic collection + pub fn stats(mut self, stats: Arc<RpcStats>) -> Self { + self.stats = Some(stats); + self + } + /// Starts a new `WebSocket` server in separate thread. /// Returns a `Server` handle which closes the server when droped. - pub fn start<M: Metadata>(self, addr: SocketAddr, handler: RpcHandler<M>) -> Result<Server, ServerError> { - Server::start(addr, handler, self.queue, self.authcodes_path, self.skip_origin_validation) + pub fn start<M: Metadata, S: Middleware<M>>(self, addr: SocketAddr, handler: RpcHandler<M, S>) -> Result<Server, ServerError> { + Server::start( + addr, + handler, + self.queue, + self.authcodes_path, + self.skip_origin_validation, + self.stats, + ) } } @@ -97,7 +114,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>(addr: SocketAddr, handler: RpcHandler<M>, queue: Arc<ConfirmationsQueue>, authcodes_path: PathBuf, skip_origin_validation: bool) -> Result<Server, ServerError> { + fn start<M: Metadata, S: Middleware<M>>( + addr: SocketAddr, + handler: RpcHandler<M, S>, + queue: Arc<ConfirmationsQueue>, + authcodes_path: PathBuf, + skip_origin_validation: bool, + stats: Option<Arc<RpcStats>>, + ) -> Result<Server, ServerError> { let config = { let mut config = ws::Settings::default(); // accept only handshakes beginning with GET @@ -111,7 +135,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) + session::Factory::new(handler, origin, port, authcodes_path, skip_origin_validation, stats) )?; let panic_handler = PanicHandler::new_in_arc(); diff --git a/signer/src/ws_server/session.rs b/signer/src/ws_server/session.rs index 9fa80de93..f19e86215 100644 --- a/signer/src/ws_server/session.rs +++ b/signer/src/ws_server/session.rs @@ -21,8 +21,9 @@ use authcode_store::AuthCodes; use std::path::{PathBuf, Path}; use std::sync::Arc; use std::str::FromStr; -use jsonrpc_core::{Metadata}; +use jsonrpc_core::{Metadata, Middleware}; use jsonrpc_core::reactor::RpcHandler; +use rpc::informant::RpcStats; use util::{H256, version}; #[cfg(feature = "parity-ui")] @@ -124,17 +125,24 @@ fn add_headers(mut response: ws::Response, mime: &str) -> ws::Response { response } -pub struct Session<M: Metadata> { +pub struct Session<M: Metadata, S: Middleware<M>> { out: ws::Sender, skip_origin_validation: bool, self_origin: String, self_port: u16, authcodes_path: PathBuf, - handler: RpcHandler<M>, + handler: RpcHandler<M, S>, file_handler: Arc<ui::Handler>, + stats: Option<Arc<RpcStats>>, } -impl<M: Metadata> ws::Handler for Session<M> { +impl<M: Metadata, S: Middleware<M>> Drop for Session<M, S> { + 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))] fn on_request(&mut self, req: &ws::Request) -> ws::Result<(ws::Response)> { trace!(target: "signer", "Handling request: {:?}", req); @@ -221,17 +229,25 @@ impl<M: Metadata> ws::Handler for Session<M> { } } -pub struct Factory<M: Metadata> { - handler: RpcHandler<M>, +pub struct Factory<M: Metadata, S: Middleware<M>> { + handler: RpcHandler<M, S>, skip_origin_validation: bool, self_origin: String, self_port: u16, authcodes_path: PathBuf, file_handler: Arc<ui::Handler>, + stats: Option<Arc<RpcStats>>, } -impl<M: Metadata> Factory<M> { - pub fn new(handler: RpcHandler<M>, self_origin: String, self_port: u16, authcodes_path: PathBuf, skip_origin_validation: bool) -> Self { +impl<M: Metadata, S: Middleware<M>> Factory<M, S> { + pub fn new( + handler: RpcHandler<M, S>, + self_origin: String, + self_port: u16, + authcodes_path: PathBuf, + skip_origin_validation: bool, + stats: Option<Arc<RpcStats>>, + ) -> Self { Factory { handler: handler, skip_origin_validation: skip_origin_validation, @@ -239,14 +255,17 @@ impl<M: Metadata> Factory<M> { self_port: self_port, authcodes_path: authcodes_path, file_handler: Arc::new(ui::Handler::default()), + stats: stats, } } } -impl<M: Metadata> ws::Factory for Factory<M> { - type Handler = Session<M>; +impl<M: Metadata, S: Middleware<M>> ws::Factory for Factory<M, S> { + type Handler = Session<M, S>; fn connection_made(&mut self, sender: ws::Sender) -> Self::Handler { + self.stats.as_ref().map(|stats| stats.open_session()); + Session { out: sender, handler: self.handler.clone(), @@ -255,6 +274,7 @@ impl<M: Metadata> ws::Factory for Factory<M> { self_port: self.self_port, authcodes_path: self.authcodes_path.clone(), file_handler: self.file_handler.clone(), + stats: self.stats.clone(), } } }