diff --git a/pending/fabo_56-bind-requests-to-tabs b/pending/fabo_56-bind-requests-to-tabs new file mode 100644 index 0000000000..6302b3beb0 --- /dev/null +++ b/pending/fabo_56-bind-requests-to-tabs @@ -0,0 +1 @@ +[Changed] Remove pending sign requests if a tab closes or changes the URL @faboweb \ No newline at end of file diff --git a/src/background.js b/src/background.js index 30e06717d1..629ee64739 100644 --- a/src/background.js +++ b/src/background.js @@ -1,5 +1,7 @@ import 'babel-polyfill' import { signMessageHandler, walletMessageHandler } from './messageHandlers' +import SignRequestQueue from './requests' +import { bindRequestsToTabs } from './tabsHandler' global.browser = require('webextension-polyfill') @@ -9,6 +11,9 @@ if (process.env.NODE_ENV === 'development') { whitelisted.push('https://localhost') } +const signRequestQueue = new SignRequestQueue() +signRequestQueue.unqueueSignRequest('') + // main message handler chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (!senderAllowed(sender)) { @@ -17,7 +22,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { } try { - signMessageHandler(message, sender, sendResponse) + signMessageHandler(signRequestQueue, message, sender, sendResponse) walletMessageHandler(message, sender, sendResponse) } catch (error) { // Return this as rejected @@ -27,6 +32,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return true }) +bindRequestsToTabs(signRequestQueue, whitelisted) // only allow whitelisted websites to send us messages function senderAllowed(sender) { diff --git a/src/manifest.json b/src/manifest.json index b3d60ddf61..065cf44938 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -31,5 +31,8 @@ "contentScript.js" ] } + ], + "permissions": [ + "tabs" ] } \ No newline at end of file diff --git a/src/messageHandlers.js b/src/messageHandlers.js index 6acb4c6dca..27d65a59bb 100644 --- a/src/messageHandlers.js +++ b/src/messageHandlers.js @@ -9,10 +9,12 @@ const { getSeed } = require('@lunie/cosmos-keys') -let signRequestQueue = [] -unqueueSignRequest('') // restart icons on restart - -export function signMessageHandler(message, sender, sendResponse) { +export function signMessageHandler( + signRequestQueue, + message, + sender, + sendResponse +) { switch (message.type) { case 'LUNIE_SIGN_REQUEST': { const { signMessage, senderAddress } = message.payload @@ -20,14 +22,18 @@ export function signMessageHandler(message, sender, sendResponse) { if (!wallet) { throw new Error('No wallet found matching the sender address.') } - queueSignRequest({ signMessage, senderAddress, tabID: sender.tab.id }) + signRequestQueue.queueSignRequest({ + signMessage, + senderAddress, + tabID: sender.tab.id + }) break } case 'SIGN': { const { signMessage, senderAddress, password, id } = message.payload const wallet = getStoredWallet(senderAddress, password) - const { tabID } = unqueueSignRequest(id) + const { tabID } = signRequestQueue.unqueueSignRequest(id) const signature = signWithPrivateKey( signMessage, Buffer.from(wallet.privateKey, 'hex') @@ -43,9 +49,7 @@ export function signMessageHandler(message, sender, sendResponse) { break } case 'GET_SIGN_REQUEST': { - sendResponse( - signRequestQueue.length > 0 ? signRequestQueue[0] : undefined - ) + sendResponse(signRequestQueue.getSignRequest()) break } case 'REJECT_SIGN_REQUEST': { @@ -54,7 +58,7 @@ export function signMessageHandler(message, sender, sendResponse) { type: 'LUNIE_SIGN_REQUEST_RESPONSE', payload: { rejected: true } }) - unqueueSignRequest(id) + signRequestQueue.unqueueSignRequest(id) sendResponse() // to popup break } @@ -107,21 +111,3 @@ function getWalletFromIndex(walletIndex, address) { ({ address: storedAddress }) => storedAddress === address ) } - -function queueSignRequest({ signMessage, senderAddress, tabID }) { - signRequestQueue.push({ signMessage, senderAddress, id: Date.now(), tabID }) - chrome.browserAction.setIcon({ path: 'icons/128x128-alert.png' }) -} - -function unqueueSignRequest(id) { - const signRequest = signRequestQueue.find( - ({ id: storedId }) => storedId === id - ) - signRequestQueue = signRequestQueue.filter( - ({ id: storedId }) => storedId !== id - ) - if (signRequestQueue.length === 0) { - chrome.browserAction.setIcon({ path: 'icons/128x128.png' }) - } - return signRequest -} diff --git a/src/requests.js b/src/requests.js new file mode 100644 index 0000000000..8649814e45 --- /dev/null +++ b/src/requests.js @@ -0,0 +1,32 @@ +export default class SignRequestQueue { + constructor() { + this.queue = [] + this.unqueueSignRequest('') // to reset the icon in the beginning + } + + queueSignRequest({ signMessage, senderAddress, tabID }) { + this.queue.push({ signMessage, senderAddress, id: Date.now(), tabID }) + chrome.browserAction.setIcon({ path: 'icons/128x128-alert.png' }) + } + + unqueueSignRequest(id) { + const signRequest = this.queue.find(({ id: storedId }) => storedId === id) + this.queue = this.queue.filter(({ id: storedId }) => storedId !== id) + if (this.queue.length === 0) { + chrome.browserAction.setIcon({ path: 'icons/128x128.png' }) + } + return signRequest + } + + unqueueSignRequestForTab(tabID) { + this.queue + .filter(({ tabID: storedtabID }) => storedtabID === tabID) + .map(({ id }) => { + this.unqueueSignRequest(id) + }) + } + + getSignRequest() { + return this.queue.length > 0 ? this.queue[0] : undefined + } +} diff --git a/src/tabsHandler.js b/src/tabsHandler.js new file mode 100644 index 0000000000..c141cb5742 --- /dev/null +++ b/src/tabsHandler.js @@ -0,0 +1,22 @@ +// requests always reference a tab so that a response finds the right listener +// if a tab is killed or it's url changes the request is not useful anymore +export function bindRequestsToTabs(signRequestQueue, whitelisted) { + // check if tab got removed + chrome.tabs.onRemoved.addListener(function(tabID) { + signRequestQueue.unqueueSignRequestForTab(tabID) + }) + // check if url changed + chrome.tabs.onUpdated.addListener(function(tabID, changeInfo) { + // if the url doesn't change, ignore the update + if (!changeInfo.url) { + return + } + if ( + !whitelisted.find(whitelistedUrl => + changeInfo.url.startsWith(whitelistedUrl) + ) + ) { + signRequestQueue.unqueueSignRequestForTab(tabID) + } + }) +} diff --git a/test/unit/requests.spec.js b/test/unit/requests.spec.js new file mode 100644 index 0000000000..564a2b3ace --- /dev/null +++ b/test/unit/requests.spec.js @@ -0,0 +1,59 @@ +import SignRequestQueue from '../../src/requests' + +const mockSignRequest = { + signMessage: 'HALLOOOOO', + senderAddress: 'cosmos1234', + tabID: 42, + id: expect.any(Number) +} + +describe('Sign request queue', () => { + let instance + + beforeEach(() => { + global.chrome = { + browserAction: { + setIcon: jest.fn() + } + } + instance = new SignRequestQueue() + global.chrome.browserAction.setIcon.mockClear() + }) + + it('should queue a sign request', () => { + instance.queueSignRequest(mockSignRequest) + expect(instance.getSignRequest()).toEqual(mockSignRequest) + }) + + it('should unqueue a sign request', () => { + instance.queueSignRequest(mockSignRequest) + const { id } = instance.queue[0] + instance.unqueueSignRequest(id) + + expect(instance.getSignRequest()).toEqual(undefined) + }) + + it('should unqueueSignRequestForTab', () => { + instance.queueSignRequest(mockSignRequest) + instance.unqueueSignRequestForTab(42) + + expect(instance.getSignRequest()).toEqual(undefined) + }) + + describe('icons', () => { + it('shows a pending sign request icon', () => { + instance.queueSignRequest(mockSignRequest) + expect(global.chrome.browserAction.setIcon).toHaveBeenCalled() + }) + + it('removed the pending sign request icon', () => { + instance.queueSignRequest(mockSignRequest) + expect(global.chrome.browserAction.setIcon).toHaveBeenCalled() + + global.chrome.browserAction.setIcon.mockClear() + + instance.unqueueSignRequestForTab(42) + expect(global.chrome.browserAction.setIcon).toHaveBeenCalled() + }) + }) +}) diff --git a/test/unit/tabsHandler.spec.js b/test/unit/tabsHandler.spec.js new file mode 100644 index 0000000000..ac2e97c969 --- /dev/null +++ b/test/unit/tabsHandler.spec.js @@ -0,0 +1,48 @@ +import { bindRequestsToTabs } from '../../src/tabsHandler' + +describe('Sign request queue', () => { + let signRequestQueue + + beforeEach(() => { + signRequestQueue = { + unqueueSignRequestForTab: jest.fn() + } + }) + + it('kills on tab removal', () => { + global.chrome = { + tabs: { + onRemoved: { addListener: callback => callback(42) }, + onUpdated: { addListener: () => {} } + } + } + bindRequestsToTabs(signRequestQueue, []) + + expect(signRequestQueue.unqueueSignRequestForTab).toHaveBeenCalledWith(42) + expect(signRequestQueue.unqueueSignRequestForTab).toHaveBeenCalledTimes(1) + }) + + it('kills on tab url not accepted', () => { + global.chrome = { + tabs: { + onRemoved: { addListener: () => {} }, + onUpdated: { addListener: callback => callback(42, {}) } + } + } + bindRequestsToTabs(signRequestQueue, ['https://lunie.io']) + expect(signRequestQueue.unqueueSignRequestForTab).not.toHaveBeenCalled() + + global.chrome = { + tabs: { + onRemoved: { addListener: () => {} }, + onUpdated: { + addListener: callback => callback(42, { url: 'https://funkytown.io' }) + } + } + } + bindRequestsToTabs(signRequestQueue, ['https://lunie.io']) + + expect(signRequestQueue.unqueueSignRequestForTab).toHaveBeenCalledWith(42) + expect(signRequestQueue.unqueueSignRequestForTab).toHaveBeenCalledTimes(1) + }) +}) diff --git a/test/unit/test.spec.js b/test/unit/test.spec.js deleted file mode 100644 index 4bb1f6a9af..0000000000 --- a/test/unit/test.spec.js +++ /dev/null @@ -1,5 +0,0 @@ -describe('TEST', () => { - it('TEST', () => { - expect(true).toBe(true); - }); -});