From 1c5f87e0cb7dc0cf1aea8481f4ce18997a47c413 Mon Sep 17 00:00:00 2001 From: Zhiting Lin Date: Tue, 25 Jun 2019 09:53:58 +0800 Subject: [PATCH] update enable function for bytom --- src/assets/language/cn.js | 7 ++ src/assets/language/en.js | 7 ++ src/background.js | 131 ++++++++++++++++++++++++++--------- src/content.js | 24 ++++--- src/dapp.js | 8 +++ src/messages/types.js | 6 ++ src/prompts/Prompt.js | 18 +++++ src/prompts/PromptTypes.js | 6 ++ src/router.js | 8 +++ src/services/NotificationService.js | 103 +++++++++++++++++++++++++++ src/services/StorageService.js | 97 ++++++++++++++++++++++++++ src/utils/BrowserApis.js | 22 ++++++ src/utils/Bytom.js | 17 +++++ src/utils/GenericTools.js | 9 +++ src/utils/Settings.js | 14 ++++ src/utils/errors/Error.js | 4 +- src/views/prompts/authentication.vue | 124 +++++++++++++++++++++++++++++++++ 17 files changed, 564 insertions(+), 41 deletions(-) create mode 100644 src/prompts/Prompt.js create mode 100644 src/prompts/PromptTypes.js create mode 100644 src/services/NotificationService.js create mode 100644 src/services/StorageService.js create mode 100644 src/utils/BrowserApis.js create mode 100644 src/utils/Bytom.js create mode 100644 src/utils/GenericTools.js create mode 100644 src/utils/Settings.js create mode 100644 src/views/prompts/authentication.vue diff --git a/src/assets/language/cn.js b/src/assets/language/cn.js index 9108044..c01a02c 100644 --- a/src/assets/language/cn.js +++ b/src/assets/language/cn.js @@ -51,6 +51,13 @@ const cn = { message:'签名消息', confirmSignature:'确认签名' }, + enable:{ + title:'请求授权', + domain: '域名', + message: '请求获取你的钱包地址,是否同意?', + cancel:'取消', + confirm:'确认授权' + }, receive:{ address: '地址', tips:'提示:点击地址进行拷贝。' diff --git a/src/assets/language/en.js b/src/assets/language/en.js index a7fe5cf..953c8a1 100644 --- a/src/assets/language/en.js +++ b/src/assets/language/en.js @@ -51,6 +51,13 @@ const en = { message:'Sign Message', confirmSignature:'Sign' }, + enable:{ + title:'Connect Request', + domain: 'Domain', + message: 'would like to connect your account', + cancel:'Cancel', + confirm:'Connect' + }, receive:{ address: 'Address', tips:'Tips: Click address to copy directly.' diff --git a/src/background.js b/src/background.js index a101823..fd85f89 100644 --- a/src/background.js +++ b/src/background.js @@ -1,11 +1,17 @@ import { LocalStream } from 'extension-streams' import InternalMessage from '@/messages/internal' import * as MsgTypes from './messages/types' +import NotificationService from './services/NotificationService' +import StorageService from './services/StorageService' +import Prompt from './prompts/Prompt'; +import * as PromptTypes from './prompts/PromptTypes' import Error from './utils/errors/Error' import accountAction from "@/models/account"; import bytom from "@/models/bytom"; +let prompt = null; + export default class Background { constructor() { this.setupInternalMessaging() @@ -54,17 +60,32 @@ export default class Background { this.signMessage(sendResponse, message.payload) break case MsgTypes.REQUEST_CURRENT_ACCOUNT: - this.requestCurrentAccount(sendResponse) + this.requestCurrentAccount(sendResponse, message.payload) break case MsgTypes.REQUEST_CURRENT_NETWORK: this.requestCurrentNetwork(sendResponse) break - case MsgTypes.REQUEST_ACCOUNT_LIST: - this.requestAccountList(sendResponse) + case MsgTypes.ENABLE: + Background.authenticate(sendResponse, message.payload) break + case MsgTypes.SET_PROMPT: + Background.setPrompt(sendResponse, message.payload); + break; + case MsgTypes.GET_PROMPT: + Background.getPrompt(sendResponse); + break; } } + static setPrompt(sendResponse, notification){ + prompt = notification; + sendResponse(true); + } + + static getPrompt(sendResponse){ + sendResponse(prompt); + } + signMessage(sendResponse, payload) { var promptURL = chrome.extension.getURL('pages/prompt.html') var requestBody = payload @@ -105,12 +126,12 @@ export default class Background { } }); - chrome.windows.onRemoved.addListener(function(windowId){ - if(windowId === window.id) { - sendResponse(Error.promptClosedWithoutAction()); - return false; - } - }); + // chrome.windows.onRemoved.addListener(function(windowId){ + // if(windowId === window.id) { + // sendResponse(Error.promptClosedWithoutAction()); + // return false; + // } + // }); } ) } @@ -227,35 +248,30 @@ export default class Background { ) } - requestCurrentAccount(sendResponse){ - const currentAccount = JSON.parse(localStorage.currentAccount) - delete(currentAccount['label']) - delete(currentAccount['net']) - currentAccount['accountId'] = currentAccount['guid'] - delete(currentAccount['guid']) - delete(currentAccount['balance']) + requestCurrentAccount(sendResponse, payload){ + Background.load(bytom => { + const domain = payload.domain; + if(bytom.settings.domains.find(_domain => _domain === domain)) { + const currentAccount = JSON.parse(localStorage.currentAccount) + delete(currentAccount['label']) + delete(currentAccount['net']) + currentAccount['accountId'] = currentAccount['guid'] + delete(currentAccount['guid']) + delete(currentAccount['balance']) + + sendResponse(currentAccount); + } else{ + sendResponse(null); + return false; + } + }) - sendResponse(currentAccount) } requestCurrentNetwork(sendResponse){ sendResponse(localStorage.bytomNet) } - requestAccountList(sendResponse){ - accountAction.list().then(resp=>{ - const accountList = resp - accountList.forEach(function(account) { - delete(account['label']) - delete(account['net']) - account['accountId'] = account['guid'] - delete(account['guid']) - delete(account['balance']) - }) - sendResponse(accountList) - }) - } - send(sendResponse, payload) { const action = payload.action if(action){ @@ -273,6 +289,59 @@ export default class Background { } } } + + /*** + * Returns the saved instance of Bytom from the storage + * @param sendResponse - Delegating response handler + * @returns {Bytom} + */ + static load(sendResponse){ + StorageService.get().then(bytom => { + sendResponse(bytom) + }) + } + + /*** + * Updates the Scatter instance inside persistent storage + * @param sendResponse - Delegating response handler + * @param bytom - The updated cleartext Scatter instance + * @returns {boolean} + */ + static update(sendResponse, bytom){ + StorageService.save(bytom).then(saved => { + sendResponse(bytom) + }) + } + + static authenticate(sendResponse, payload){ + Background.load(bytom => { + const domain = payload.domain; + const currentAccount = JSON.parse(localStorage.currentAccount) + delete(currentAccount['label']) + delete(currentAccount['net']) + currentAccount['accountId'] = currentAccount['guid'] + delete(currentAccount['guid']) + delete(currentAccount['balance']) + + if(bytom.settings.domains.find(_domain => _domain === domain)) { + sendResponse(currentAccount); + } else{ + NotificationService.open(new Prompt(PromptTypes.REQUEST_AUTH, payload.domain, approved => { + if(approved === false || approved.hasOwnProperty('isError')) sendResponse(approved); + else { + bytom.settings.domains.unshift(domain); + if(approved === true){ + this.update(() => sendResponse(currentAccount), bytom); + }else{ + this.update(() => sendResponse(approved), bytom); + } + } + })); + } + }) + } + + } new Background() diff --git a/src/content.js b/src/content.js index 37c3fe4..cb918ae 100644 --- a/src/content.js +++ b/src/content.js @@ -4,6 +4,8 @@ import NetworkMessage from '@/messages/network' import InternalMessage from '@/messages/internal' import * as MsgTypes from './messages/types' import * as EventNames from '@/messages/event' +import {strippedHost} from '@/utils/GenericTools' + let stream = new WeakMap() let INJECTION_SCRIPT_FILENAME = 'js/inject.js' @@ -31,11 +33,10 @@ class Content { stream.onSync(async () => { const defaultAccount = await this.getDefaultAccount(); const net = await this.getDefaultNetwork(); - const accountList = await this.getAccountList(); // Pushing an instance of Bytomdapp to the web application stream.send( - NetworkMessage.payload(MsgTypes.PUSH_BYTOM, {defaultAccount, net, accountList}), + NetworkMessage.payload(MsgTypes.PUSH_BYTOM, {defaultAccount, net}), EventNames.INJECT ) @@ -95,6 +96,9 @@ class Content { case MsgTypes.SEND: this.transfer(msg.type, networkMessage) break + case MsgTypes.ENABLE: + this.enable(msg.type, networkMessage) + break default: stream.send(networkMessage.error('errtest'), EventNames.INJECT) break @@ -104,7 +108,7 @@ class Content { getVersion() {} getDefaultAccount(){ - return InternalMessage.signal(MsgTypes.REQUEST_CURRENT_ACCOUNT) + return InternalMessage.payload(MsgTypes.REQUEST_CURRENT_ACCOUNT,{domain:strippedHost()}) .send() } @@ -113,11 +117,6 @@ class Content { .send() } - getAccountList(){ - return InternalMessage.signal(MsgTypes.REQUEST_ACCOUNT_LIST) - .send() - } - respond(message, payload) { if (!isReady) return @@ -142,6 +141,15 @@ class Content { .send() .then(res => this.respond(message, res)) } + + enable(type, networkMessage) { + networkMessage.payload ={ + domain: strippedHost() + } + + this.transfer(type, networkMessage) + } + } const content = new Content() diff --git a/src/dapp.js b/src/dapp.js index e248159..58d44d7 100644 --- a/src/dapp.js +++ b/src/dapp.js @@ -66,6 +66,14 @@ export default class Bytomdapp { _subscribe() } + enable(){ + return _send(MsgTypes.ENABLE) + .then(async default_account =>{ + this.default_account = default_account; + return default_account; + }) + } + send_transaction(params) { return _send(MsgTypes.TRANSFER, params) } diff --git a/src/messages/types.js b/src/messages/types.js index d6b9a4e..de70fae 100644 --- a/src/messages/types.js +++ b/src/messages/types.js @@ -4,6 +4,7 @@ export const PUSH_BYTOM = 'pushBytom' export const UPDATE_BYTOM = 'updateBytom' export const AUTHENTICATE = 'authenticate' export const TRANSFER = 'transfer' +export const ENABLE = 'enable' export const ADVTRANSFER = 'advTransfer' export const SIGNMESSAGE = 'signMessage' export const SEND = 'send' @@ -12,3 +13,8 @@ export const SEND = 'send' export const REQUEST_CURRENT_ACCOUNT = 'defaultAccount'; export const REQUEST_CURRENT_NETWORK = 'currentNetwork'; export const REQUEST_ACCOUNT_LIST = 'accountList'; + + +//Internal Message +export const SET_PROMPT = 'setPrompt'; +export const GET_PROMPT = 'getPrompt'; diff --git a/src/prompts/Prompt.js b/src/prompts/Prompt.js new file mode 100644 index 0000000..b87e806 --- /dev/null +++ b/src/prompts/Prompt.js @@ -0,0 +1,18 @@ +export default class Prompt { + + constructor(_type = '', _domain = '', _responder = null){ + this.type = _type; + this.domain = _domain; + // this.network = _network; + // this.data = _data; + this.responder = _responder; + } + + static placeholder(){ return new Prompt(); } + static fromJson(json){ return Object.assign(this.placeholder(), json); } + + routeName(){ + return `${this.type}`; + } + +} diff --git a/src/prompts/PromptTypes.js b/src/prompts/PromptTypes.js new file mode 100644 index 0000000..3438561 --- /dev/null +++ b/src/prompts/PromptTypes.js @@ -0,0 +1,6 @@ +export const REQUEST_AUTH = 'enable'; +export const REQUEST_SIGNATURE = 'requestSignature'; +export const REQUEST_ARBITRARY_SIGNATURE = 'signatureArbitrary'; +export const REQUEST_ADD_NETWORK = 'requestAddNetwork'; +export const REQUEST_UNLOCK = 'requestUnlock'; +export const UPDATE_VERSION = 'updateVersion'; diff --git a/src/router.js b/src/router.js index dac05cc..16758bb 100644 --- a/src/router.js +++ b/src/router.js @@ -42,6 +42,14 @@ const routers = [ } }, { + path: '/enable', + name: 'enable', + meta: { title: '授权' }, + component: resolve => { + require(['@/views/prompts/authentication.vue'], resolve) + } + }, + { path: '/transfer/info', name: 'transfer-info', meta: { title: '交易详情' }, diff --git a/src/services/NotificationService.js b/src/services/NotificationService.js new file mode 100644 index 0000000..98169b2 --- /dev/null +++ b/src/services/NotificationService.js @@ -0,0 +1,103 @@ +import Error from '../utils/errors/Error' +import {apis} from '../utils/BrowserApis'; +import InternalMessage from '../messages/internal' +import * as InternalMessageTypes from '../messages/types' + +let openWindow = null; + +export default class NotificationService { + + /*** + * Opens a prompt window outside of the extension + * @param notification + */ + static async open(notification){ + if(openWindow){ + // For now we're just going to close the window to get rid of the error + // that is caused by already open windows swallowing all further requests + openWindow.close(); + openWindow = null; + + // Alternatively we could focus the old window, but this would cause + // urgent 1-time messages to be lost, such as after dying in a game and + // uploading a high-score. That message will be lost. + // openWindow.focus(); + // return false; + + // A third option would be to add a queue, but this could cause + // virus-like behavior as apps overflow the queue causing the user + // to have to quit the browser to regain control. + } + + + const height = 623; + const width = 360; + let middleX = window.screen.availWidth/2 - (width/2); + let middleY = window.screen.availHeight/2 - (height/2); + + const getPopup = async () => { + try { + const url = `${apis.runtime.getURL('pages/prompt.html')}#${notification.routeName()}`; + + // Notifications get bound differently depending on browser + // as Firefox does not support opening windows from background. + if(typeof chrome !== 'undefined') { + window.notification = notification; + apis.windows.create({ + url, + height, + width, + type:'popup' + },(_window) => { + apis.windows.onRemoved.addListener(function(windowId){ + if(windowId === _window.id) { + notification.responder(Error.promptClosedWithoutAction()); + return false; + } + }); + return _window; + }); + } + else { + const win = window.open(url, 'BytomPrompt', `width=${width},height=${height},resizable=0,top=${middleY},left=${middleX},titlebar=0`); + win.data = notification; + openWindow = win; + return win; + } + } catch (e) { + console.log('notification error', e); + return null; + } + } + + await InternalMessage.payload(InternalMessageTypes.SET_PROMPT, JSON.stringify(notification)).send(); + + let popup = await getPopup(); + + if(popup){ + popup.onbeforeunload = () => { + notification.responder(Error.promptClosedWithoutAction()); + + // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload + // Must return undefined to bypass form protection + openWindow = null; + return undefined; + }; + } + } + + /*** + * Always use this method for closing notification popups. + * Otherwise you will double send responses and one will always be null. + */ + static async close(){ + if(typeof browser !== 'undefined') { + const {id: windowId,} = (await apis.windows.getCurrent()); + apis.windows.remove(windowId); + } else { + window.onbeforeunload = () => {}; + window.close(); + } + } + +} diff --git a/src/services/StorageService.js b/src/services/StorageService.js new file mode 100644 index 0000000..2f2e08a --- /dev/null +++ b/src/services/StorageService.js @@ -0,0 +1,97 @@ +import BytomObj from '../utils/Bytom' +import {apis} from '../utils/BrowserApis'; + +export default class StorageService { + + constructor(){} + + /*** + * Saves an instance of Bytom in the extension's local storage + * The keychain will always be encrypted when in storage + * @param bytom + * @returns {Promise} + */ + static save(_bytom){ + return new Promise(resolve => { + apis.storage.local.set({bytom:_bytom}, () => { + resolve(_bytom); + }); + }) + }; + + /*** + * Gets an instance of Bytom from the extension's local storage + * Will return a new Bytom instance if not found. + * @returns {Promise} + */ + static get() { + return new Promise(resolve => { + apis.storage.local.get('bytom', (possible) => { + (possible && Object.keys(possible).length && possible.hasOwnProperty('bytom')) + ? resolve(BytomObj.fromJson(possible.bytom)) + : resolve(BytomObj.placeholder()); + }); + }) + } + + /*** + * Removes the instance of Bytom. + * This will delete all user data. + * @returns {Promise} + */ + static remove(){ + return new Promise(resolve => { + apis.storage.local.remove('bytom', () => { + resolve(); + }); + }) + } + + /*** + * Caches an ABI + * @param contractName + * @param chainId + * @param abi + * @returns {Promise} + */ + static cacheABI(contractName, chainId, abi){ + return new Promise(resolve => { + apis.storage.local.set({[`abi:${contractName}:${chainId}`]:abi}, () => { + resolve(abi); + }); + }); + } + + /*** + * Fetches an ABI from cache + * @param contractName + * @param chainId + * @returns {Promise} + */ + static getABI(contractName, chainId){ + return new Promise(resolve => { + const prop = `abi:${contractName}:${chainId}`; + apis.storage.local.get(prop, (possible) => { + if(JSON.stringify(possible) !== '{}') resolve(possible[prop]); + else resolve('no cache'); + }); + }) + } + + static getSalt(){ + return new Promise(resolve => { + apis.storage.local.get('salt', (possible) => { + if(JSON.stringify(possible) !== '{}') resolve(possible.salt); + else resolve('SALT_ME'); + }); + }) + } + + static setSalt(salt){ + return new Promise(resolve => { + apis.storage.local.set({salt}, () => { + resolve(salt); + }); + }) + } +} diff --git a/src/utils/BrowserApis.js b/src/utils/BrowserApis.js new file mode 100644 index 0000000..af7e482 --- /dev/null +++ b/src/utils/BrowserApis.js @@ -0,0 +1,22 @@ + +const swallow = fn => {try { fn() } catch(e){}}; + +class ApiGenerator { + constructor(){ + [ + 'app', + 'storage', + 'extension', + 'runtime', + 'windows' + ] + .map(api => { + if(typeof chrome !== 'undefined') swallow(() => {if(chrome[api]) this[api] = chrome[api]}); + if(typeof browser !== 'undefined') swallow(() => {if(browser[api]) this[api] = browser[api]}); + }); + + if(typeof browser !== 'undefined') swallow(() => {if (browser && browser.runtime) this.runtime = browser.runtime}); + } +} + +export const apis = new ApiGenerator(); \ No newline at end of file diff --git a/src/utils/Bytom.js b/src/utils/Bytom.js new file mode 100644 index 0000000..f464e93 --- /dev/null +++ b/src/utils/Bytom.js @@ -0,0 +1,17 @@ +import Settings from './Settings'; + +export default class BytomObj { + + constructor(){ + this.settings = Settings.placeholder(); + } + + static placeholder(){ return new BytomObj(); } + static fromJson(json){ + let p = Object.assign(this.placeholder(), json); + if(json.hasOwnProperty('settings')) p.settings = Settings.fromJson(json.settings); + return p; + } + + clone(){ return BytomObj.fromJson(JSON.parse(JSON.stringify(this))) } +} diff --git a/src/utils/GenericTools.js b/src/utils/GenericTools.js new file mode 100644 index 0000000..a56b67e --- /dev/null +++ b/src/utils/GenericTools.js @@ -0,0 +1,9 @@ + +export const strippedHost = () => { + let host = location.hostname; + + // Replacing www. only if the domain starts with it. + if(host.indexOf('www.') === 0) host = host.replace('www.', ''); + + return host; +}; \ No newline at end of file diff --git a/src/utils/Settings.js b/src/utils/Settings.js new file mode 100644 index 0000000..224a99c --- /dev/null +++ b/src/utils/Settings.js @@ -0,0 +1,14 @@ +export default class Settings { + + constructor(){ + this.domains = []; + this.language = 'ENGLISH'; + } + + static placeholder(){ return new Settings(); } + static fromJson(json){ + let p = Object.assign(this.placeholder(), json); + if(json.hasOwnProperty('domains')) p.domains = json.domains; + return p; + } +} diff --git a/src/utils/errors/Error.js b/src/utils/errors/Error.js index 1c69768..3fc980b 100644 --- a/src/utils/errors/Error.js +++ b/src/utils/errors/Error.js @@ -19,7 +19,7 @@ export default class Error { } static locked(){ - return new Error(ErrorTypes.LOCKED, "The user's Scatter is locked. They have been notified and should unlock before continuing.") + return new Error(ErrorTypes.LOCKED, "The user's Bytom is locked. They have been notified and should unlock before continuing.") } static promptClosedWithoutAction(){ @@ -49,7 +49,7 @@ export default class Error { static usedKeyProvider(){ return new Error( ErrorTypes.MALICIOUS, - "Do not use a `keyProvider` with a Scatter. Use a `signProvider` and return only signatures to this object. A malicious person could retrieve your keys otherwise.", + "Do not use a `keyProvider` with a Bytom. Use a `signProvider` and return only signatures to this object. A malicious person could retrieve your keys otherwise.", ErrorCodes.NO_SIGNATURE ) } diff --git a/src/views/prompts/authentication.vue b/src/views/prompts/authentication.vue new file mode 100644 index 0000000..0f827f3 --- /dev/null +++ b/src/views/prompts/authentication.vue @@ -0,0 +1,124 @@ + + + + + -- 2.11.0