From 381ed505a069468061d8b55ba7149b6a505a31d7 Mon Sep 17 00:00:00 2001 From: kaikun213 Date: Mon, 24 Jul 2017 16:21:54 +0200 Subject: [PATCH] postMessage and store (merge..) --- js/src/api/provider/postMessage.js | 100 +++++++++++ js/src/shell/DappRequests/store.js | 278 +++++++++++++++++++++++++++++ 2 files changed, 378 insertions(+) create mode 100644 js/src/api/provider/postMessage.js create mode 100644 js/src/shell/DappRequests/store.js diff --git a/js/src/api/provider/postMessage.js b/js/src/api/provider/postMessage.js new file mode 100644 index 000000000..86d2d031f --- /dev/null +++ b/js/src/api/provider/postMessage.js @@ -0,0 +1,100 @@ +// 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 class PostMessage { + id = 0; + _messages = {}; + + constructor (token, destination) { + this._token = token; + this._destination = destination; + + window.addEventListener('message', this.receiveMessage, false); + } + + addMiddleware () { + } + + _send (data) { + this._destination.postMessage(data, '*'); + } + + send = (method, params, callback) => { + const id = ++this.id; + + this._messages[id] = { callback }; + this._send({ + id, + from: this._token, + method, + params, + token: this._token + }); + } + + subscribe = (api, callback, params) => { + console.log('paritySubscribe', JSON.stringify(params), api, callback); + return new Promise((resolve, reject) => { + const id = ++this.id; + + this._messages[id] = { callback, resolve, reject, subscription: true, initial: true }; + this._send({ + id, + from: this._token, + api, + params, + token: this._token + }); + }); + } + + unsubscribe = (subId) => { + return new Promise((resolve, reject) => { + const id = ++this.id; + + this._messages[id] = { callback: (e, v) => e ? reject(e) : resolve(v) }; + this._send({ + id, + from: this._token, + subId, + token: this._token + }); + }); + } + + unsubscribeAll () { + return this.unsubscribe('*'); + } + + receiveMessage = ({ data: { id, error, from, token, result }, origin, source }) => { + if (from !== 'shell' || token !== this._token) { + return; + } + + if (error) { + console.error(from, error); + } + + if (this._messages[id].subscription) { + console.log('subscription', result, 'initial?', this._messages[id].initial); + this._messages[id].initial ? this._messages[id].resolve(result) : this._messages[id].callback(error && new Error(error), result); + this._messages[id].initial = false; + } else { + this._messages[id].callback(error && new Error(error), result); + this._messages[id] = null; + } + } +} diff --git a/js/src/shell/DappRequests/store.js b/js/src/shell/DappRequests/store.js new file mode 100644 index 000000000..e6082dd52 --- /dev/null +++ b/js/src/shell/DappRequests/store.js @@ -0,0 +1,278 @@ +// 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 { flatten } from 'lodash'; +import { action, computed, observable } from 'mobx'; +import store from 'store'; + +import { sha3 } from '@parity/api/util/sha3'; + +import VisibleStore from '../Dapps/dappsStore'; +import filteredRequests from './filteredRequests'; + +const LS_PERMISSIONS = '_parity::dapps::methods'; + +let nextQueueId = 0; + +export default class Store { + @observable permissions = {}; + @observable requests = []; + @observable tokens = {}; + + constructor (provider) { + this.provider = provider; + this.permissions = store.get(LS_PERMISSIONS) || {}; + + window.addEventListener('message', this.receiveMessage, false); + } + + @computed get hasRequests () { + return this.requests.length !== 0; + } + + @computed get squashedRequests () { + const duplicates = {}; + + return this.requests.filter(({ request: { data: { method, token } } }) => { + const section = this.getFilteredSectionName(method); + const id = `${token}:${section}`; + + if (!duplicates[id]) { + duplicates[id] = true; + return true; + } + + return false; + }); + } + + @action createToken = (appId) => { + const token = sha3(`${appId}:${Date.now()}`); + + this.tokens = Object.assign({}, this.tokens, { + [token]: appId + }); + + return token; + } + + @action removeRequest = (_queueId) => { + this.requests = this.requests.filter(({ queueId }) => queueId !== _queueId); + } + + @action queueRequest = (request) => { + const appId = this.tokens[request.data.from]; + let queueId = ++nextQueueId; + + this.requests = this.requests.concat([{ appId, queueId, request }]); + } + + @action addTokenPermission = (method, token) => { + const id = `${method}:${this.tokens[token]}`; + + this.permissions = Object.assign({}, this.permissions, { + [id]: true + }); + this.savePermissions(); + } + + @action approveSingleRequest = ({ queueId, request: { data, source } }) => { + this.removeRequest(queueId); + if (data.api) { + this.executePubsubCall(data, source); + } else { + this.executeMethodCall(data, source); + } + } + + @action approveRequest = (queueId, approveAll) => { + const queued = this.findRequest(queueId); + + if (approveAll) { + const { request: { data: { method, token, params } } } = queued; + + this.getFilteredSection(method || params[0]).methods.forEach((m) => { + this.addTokenPermission(m, token); + this.findMatchingRequests(m, token).forEach(this.approveSingleRequest); + }); + } else { + this.approveSingleRequest(queued); + } + } + + @action rejectRequest = (queueId) => { + const { request: { data: { id, method, token }, source } } = this.findRequest(queueId); + + this.removeRequest(queueId); + source.postMessage({ + error: `Method ${method} not allowed`, + id, + from: 'shell', + result: null, + token + }, '*'); + } + + @action setPermissions = (_permissions) => { + const permissions = {}; + + Object.keys(_permissions).forEach((id) => { + permissions[id] = !!_permissions[id]; + }); + + this.permissions = Object.assign({}, this.permissions, permissions); + this.savePermissions(); + + return true; + } + + hasTokenPermission = (method, token) => { + return this.hasAppPermission(method, this.tokens[token]); + } + + hasAppPermission = (method, appId) => { + return this.permissions[`${method}:${appId}`] || false; + } + + savePermissions = () => { + store.set(LS_PERMISSIONS, this.permissions); + } + + findRequest (_queueId) { + return this.requests.find(({ queueId }) => queueId === _queueId); + } + + findMatchingRequests (_method, _token) { + return this.requests.filter(({ request: { data: { method, token, params } } }) => (method === _method || (params && params[0] === _method)) && token === _token); + } + + _methodCallbackPost = (id, source, token) => { + return (error, result) => { + source.postMessage({ + error: error + ? error.message + : null, + id, + from: 'shell', + result, + token + }, '*'); + }; + } + + executePubsubCall = ({ api, id, token, params }, source) => { + const callback = this._methodCallbackPost(id, source, token); + + // TODO: enable security pubsub + this.provider.subscribe(api, callback, params).then((v, e) => { + console.log('Error and result', v, e); + this._methodCallbackPost(id, source, token)(null, v); + }); + } + + executeMethodCall = ({ id, from, method, params, token }, source) => { + const visibleStore = VisibleStore.get(); + const callback = this._methodCallbackPost(id, source, token); + + switch (method) { + case 'shell_getApps': + const [displayAll] = params; + + return callback(null, displayAll + ? visibleStore.allApps.slice() + : visibleStore.visibleApps.slice() + ); + + case 'shell_getFilteredMethods': + return callback(null, flatten( + Object + .keys(filteredRequests) + .map((key) => filteredRequests[key].methods) + )); + + case 'shell_getMethodPermissions': + return callback(null, this.permissions); + + case 'shell_setAppVisibility': + const [appId, visibility] = params; + + return callback(null, visibility + ? visibleStore.showApp(appId) + : visibleStore.hideApp(appId) + ); + + case 'shell_setMethodPermissions': + const [permissions] = params; + + return callback(null, this.setPermissions(permissions)); + + default: + return this.provider.send(method, params, callback); + } + } + + getFilteredSectionName = (method) => { + return Object.keys(filteredRequests).find((key) => { + return filteredRequests[key].methods.includes(method); + }); + } + + getFilteredSection = (method) => { + return filteredRequests[this.getFilteredSectionName(method)]; + } + + receiveMessage = ({ data, origin, source }) => { + if (!data) { + return; + } + + const { from, method, token, params, api, subId, id } = data; + + if (!from || from === 'shell' || from !== token) { + return; + } + + if ((method && this.getFilteredSection(method) && !this.hasTokenPermission(method, token)) || + (api && this.getFilteredSection(params[0]) && !this.hasTokenPermission(method, token))) { + this.queueRequest({ data, origin, source }); + return; + } + if (api) { + console.log('apiCall', data); + this.executePubsubCall(data, source); + } else if (subId) { + subId === '*' + ? this.provider.unsubscribeAll().then(v => this._methodCallbackPost(id, source, token)(null, v)) + : this.provider.unsubscribe(subId).then(v => this._methodCallbackPost(id, source, token)(null, v)); + } else { + this.executeMethodCall(data, source); + } + } + + static instance = null; + + static create (provider) { + if (!Store.instance) { + Store.instance = new Store(provider, {}); + } + + return Store.instance; + } + + static get () { + return Store.instance; + } +}