diff --git a/manifest.json b/manifest.json index 872e73d..869481b 100644 --- a/manifest.json +++ b/manifest.json @@ -2,18 +2,17 @@ "name": "Ethereum Swarm Extension", "short_name": "Swarm Extension", "version": "0.2.1", - "manifest_version": 2, - "permissions": ["webRequest", "webRequestBlocking", "tabs", "storage", "*://*/*", "webNavigation"], + "manifest_version": 3, + "permissions": ["webRequest", "declarativeNetRequest", "tabs", "storage", "webNavigation"], "author": "nugaon", "description": "Web3 framework for Ethereum Swarm dApps", "icons": { "48": "assets/swarm.png" }, "background": { - "scripts": ["background.js"], - "persistent": true + "service_worker": "background.js" }, - "browser_action": { + "action": { "default_title": "Swarm Extension", "default_popup": "popup-page/index.html" }, @@ -24,5 +23,12 @@ "run_at": "document_start", "all_frames": true } + ], + "host_permissions": ["*://*/*"], + "web_accessible_resources": [ + { + "resources": ["swarm-session-id.js", "swarm-html.js", "swarm-library.js", "sandbox-polyfill.js"], + "matches": ["file://*/*", "http://*/*", "https://*/*"] + } ] } diff --git a/package-lock.json b/package-lock.json index a74b58b..504ed59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,9 +32,9 @@ "@types/copy-webpack-plugin": "^6.4.0", "@types/glob": "^7.1.3", "@types/jest": "^26.0.19", - "@types/jest-environment-puppeteer": "^4.4.1", + "@types/jest-environment-puppeteer": "^5.0.0", "@types/node": "^14.14.16", - "@types/puppeteer": "^5.4.2", + "@types/puppeteer": "^5.4.4", "@types/react": "^17.0.2", "@types/react-dom": "^17.0.1", "@types/react-jss": "^10.0.0", @@ -60,7 +60,7 @@ "jest": "^26.6.3", "jest-puppeteer": "^5.0.4", "prettier": "^2.2.1", - "puppeteer": "^5.5.0", + "puppeteer": "^13.2.0", "raw-loader": "^4.0.2", "rimraf": "^3.0.2", "style-loader": "^2.0.0", @@ -3704,14 +3704,14 @@ } }, "node_modules/@types/jest-environment-puppeteer": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@types/jest-environment-puppeteer/-/jest-environment-puppeteer-4.4.1.tgz", - "integrity": "sha512-LiZTD6i63le6QMnxi7pJB0SFv/fWtss6VVEEDm/UaeowBgjduf8txyE//j3WEeDPxngTvioUjbzA7Rc6Wc3cBA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/jest-environment-puppeteer/-/jest-environment-puppeteer-5.0.0.tgz", + "integrity": "sha512-GiwGkJW6462DFfyAIB1LDSVo4CI0tt4ot3+AsXfrrlDhSMe4mxKYz8xaUSGDVKoLfN1mHEvYO8b1h4V/v1J9Tw==", "dev": true, "dependencies": { - "@jest/types": ">=24 <=26", + "@jest/types": ">=24 <=27", "@types/puppeteer": "*", - "jest-environment-node": ">=24 <=26" + "jest-environment-node": ">=24 <=27" } }, "node_modules/@types/json-schema": { @@ -6506,9 +6506,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.818844", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.818844.tgz", - "integrity": "sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg==", + "version": "0.0.960912", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.960912.tgz", + "integrity": "sha512-I3hWmV9rWHbdnUdmMKHF2NuYutIM2kXz2mdXW8ha7TbRlGTVs+PF+PsB5QWvpCek4Fy9B+msiispCfwlhG5Sqg==", "dev": true }, "node_modules/diff": { @@ -13387,61 +13387,47 @@ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "node_modules/puppeteer": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-5.5.0.tgz", - "integrity": "sha512-OM8ZvTXAhfgFA7wBIIGlPQzvyEETzDjeRa4mZRCRHxYL+GNH5WAuYUQdja3rpWZvkX/JKqmuVgbsxDNsDFjMEg==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-13.2.0.tgz", + "integrity": "sha512-OSRcIgPq78Cjysm4AOvGgGN464qugfYZ1bJRpPZ7d6c2P/zVQmACblIiB56frVoSuHpvqo+ZphFJo7kF9V5iEg==", "dev": true, "hasInstallScript": true, "dependencies": { - "debug": "^4.1.0", - "devtools-protocol": "0.0.818844", - "extract-zip": "^2.0.0", - "https-proxy-agent": "^4.0.0", - "node-fetch": "^2.6.1", - "pkg-dir": "^4.2.0", - "progress": "^2.0.1", - "proxy-from-env": "^1.0.0", - "rimraf": "^3.0.2", - "tar-fs": "^2.0.0", - "unbzip2-stream": "^1.3.3", - "ws": "^7.2.3" + "debug": "4.3.3", + "devtools-protocol": "0.0.960912", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.0", + "node-fetch": "2.6.7", + "pkg-dir": "4.2.0", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.2.3" }, "engines": { "node": ">=10.18.1" } }, - "node_modules/puppeteer/node_modules/agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "dev": true, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/puppeteer/node_modules/https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "dev": true, - "dependencies": { - "agent-base": "5", - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/puppeteer/node_modules/node-fetch": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", - "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "dev": true, "dependencies": { "whatwg-url": "^5.0.0" }, "engines": { "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/puppeteer/node_modules/tr46": { @@ -13466,6 +13452,27 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/puppeteer/node_modules/ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -19819,14 +19826,14 @@ } }, "@types/jest-environment-puppeteer": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@types/jest-environment-puppeteer/-/jest-environment-puppeteer-4.4.1.tgz", - "integrity": "sha512-LiZTD6i63le6QMnxi7pJB0SFv/fWtss6VVEEDm/UaeowBgjduf8txyE//j3WEeDPxngTvioUjbzA7Rc6Wc3cBA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/jest-environment-puppeteer/-/jest-environment-puppeteer-5.0.0.tgz", + "integrity": "sha512-GiwGkJW6462DFfyAIB1LDSVo4CI0tt4ot3+AsXfrrlDhSMe4mxKYz8xaUSGDVKoLfN1mHEvYO8b1h4V/v1J9Tw==", "dev": true, "requires": { - "@jest/types": ">=24 <=26", + "@jest/types": ">=24 <=27", "@types/puppeteer": "*", - "jest-environment-node": ">=24 <=26" + "jest-environment-node": ">=24 <=27" } }, "@types/json-schema": { @@ -22028,9 +22035,9 @@ "dev": true }, "devtools-protocol": { - "version": "0.0.818844", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.818844.tgz", - "integrity": "sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg==", + "version": "0.0.960912", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.960912.tgz", + "integrity": "sha512-I3hWmV9rWHbdnUdmMKHF2NuYutIM2kXz2mdXW8ha7TbRlGTVs+PF+PsB5QWvpCek4Fy9B+msiispCfwlhG5Sqg==", "dev": true }, "diff": { @@ -27276,45 +27283,29 @@ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "puppeteer": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-5.5.0.tgz", - "integrity": "sha512-OM8ZvTXAhfgFA7wBIIGlPQzvyEETzDjeRa4mZRCRHxYL+GNH5WAuYUQdja3rpWZvkX/JKqmuVgbsxDNsDFjMEg==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-13.2.0.tgz", + "integrity": "sha512-OSRcIgPq78Cjysm4AOvGgGN464qugfYZ1bJRpPZ7d6c2P/zVQmACblIiB56frVoSuHpvqo+ZphFJo7kF9V5iEg==", "dev": true, "requires": { - "debug": "^4.1.0", - "devtools-protocol": "0.0.818844", - "extract-zip": "^2.0.0", - "https-proxy-agent": "^4.0.0", - "node-fetch": "^2.6.1", - "pkg-dir": "^4.2.0", - "progress": "^2.0.1", - "proxy-from-env": "^1.0.0", - "rimraf": "^3.0.2", - "tar-fs": "^2.0.0", - "unbzip2-stream": "^1.3.3", - "ws": "^7.2.3" + "debug": "4.3.3", + "devtools-protocol": "0.0.960912", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.0", + "node-fetch": "2.6.7", + "pkg-dir": "4.2.0", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.2.3" }, "dependencies": { - "agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "dev": true - }, - "https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "dev": true, - "requires": { - "agent-base": "5", - "debug": "4" - } - }, "node-fetch": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", - "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "dev": true, "requires": { "whatwg-url": "^5.0.0" @@ -27341,6 +27332,13 @@ "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } + }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "dev": true, + "requires": {} } } }, diff --git a/package.json b/package.json index c0bce41..d27014d 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "compile:main": "webpack", "test": "jest --config=jest.config.ts", "test:demo": "npm run test -- --demo=true --runInBand", - "lint": "eslint --fix \"src/**/*.ts\" \"test/**/*.ts\" && prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "lint": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "lint:check": "eslint \"src/**/*.ts\" \"test/**/*.ts\" && prettier --check \"src/**/*.ts\" \"test/**/*.ts\"", "zip": "cd dist/ && bestzip ../extension.zip *", "update:test-bee-js-browser": "cp node_modules/@ethersphere/bee-js/dist/index.browser.min.js test/bzz-test-page/bee-js.min.js" diff --git a/src/background/dapp-session.manager.ts b/src/background/dapp-session.manager.ts index e9b8f0e..fb54ed1 100644 --- a/src/background/dapp-session.manager.ts +++ b/src/background/dapp-session.manager.ts @@ -1,3 +1,5 @@ +import { getItem, setItem } from '../utils/storage' +import { DappSecurityContextData } from './data/DappSecurityContextData' import { senderContentOrigin, senderFrameId, senderFrameOrigin, senderTabId } from './utils' type LocalStorage = { @@ -56,27 +58,40 @@ class DappSecurityContext { } } +type SecurityContextDataMap = { [sessionId: string]: DappSecurityContextData } + export class DappSessionManager { - private securityContexts: { [sessionId: string]: DappSecurityContext } + private securityContextsPromise: Promise | null = null - public constructor() { - this.securityContexts = {} + constructor() { + chrome.runtime.onSuspend.addListener(() => { + setItem('securityContexts', {}) + }) } - public register(sessionId: string, sender: chrome.runtime.MessageSender): void { + + public async register(sessionId: string, sender: chrome.runtime.MessageSender): Promise { const tabId = senderTabId(sender) const frameId = senderFrameId(sender) const frameContentRoot = senderFrameOrigin(sender) const originContentRoot = senderContentOrigin(sender) - this.securityContexts[sessionId] = new DappSecurityContext(tabId, frameId, frameContentRoot, originContentRoot) - console.log(`dApp session "${sessionId}" has been initialized`, this.securityContexts[sessionId]) + const context: DappSecurityContextData = { + tabId, + frameId, + frameContentRoot, + originContentRoot, + } + + await this.setSecurityContext(sessionId, context) + + console.log(`dApp session "${sessionId}" has been initialized`, context) } /** * Checks the given sessionId and sender's data match with the DappSessionManager's records */ - public isValidSession(sessionId: string, sender: chrome.runtime.MessageSender): boolean { - const context = this.securityContexts[sessionId] + public async isValidSession(sessionId: string, sender: chrome.runtime.MessageSender): Promise { + const context = await this.getSecurityContext(sessionId) if (!context) return false @@ -95,11 +110,11 @@ export class DappSessionManager { ) } - public getStorageItem( + public async getStorageItem( sessionId: string, ...getStorageParams: Parameters ): ReturnType { - const context = this.getSecurityContext(sessionId) + const context = await this.getSecurityContext(sessionId) return context.getStorageItem(...getStorageParams) } @@ -108,18 +123,40 @@ export class DappSessionManager { sessionId: string, ...setStorageParams: Parameters ): Promise { - const context = this.getSecurityContext(sessionId) + const context = await this.getSecurityContext(sessionId) await context.setStorageItem(...setStorageParams) } - private getSecurityContext(sessionId: string): DappSecurityContext { - const securityContext = this.securityContexts[sessionId] + private async getSecurityContext(sessionId: string): Promise { + const securityContextMap = await this.getSecurityContextsDataMap() + const securityContext = securityContextMap[sessionId] if (!securityContext) { throw new Error(`DappSessionManager: There is no registered security context for session: ${sessionId}`) } - return securityContext + return new DappSecurityContext( + securityContext.tabId, + securityContext.frameId, + securityContext.frameContentRoot, + securityContext.originContentRoot, + ) + } + + private getSecurityContextsDataMap(): Promise { + if (this.securityContextsPromise) { + return this.securityContextsPromise + } + + return (this.securityContextsPromise = (async () => { + return (await getItem('securityContexts')) || {} + })()) + } + + private async setSecurityContext(sessionId: string, context: DappSecurityContextData): Promise { + const securityContexts = await this.getSecurityContextsDataMap() + securityContexts[sessionId] = context + await setItem('securityContexts', securityContexts) } } diff --git a/src/background/data/DappSecurityContextData.ts b/src/background/data/DappSecurityContextData.ts new file mode 100644 index 0000000..503f4f8 --- /dev/null +++ b/src/background/data/DappSecurityContextData.ts @@ -0,0 +1,6 @@ +export interface DappSecurityContextData { + tabId: number + frameId: number + frameContentRoot: string + originContentRoot: string +} diff --git a/src/background/listener/bee-api.listener.ts b/src/background/listener/bee-api.listener.ts index b2912d8..cc2eeb4 100644 --- a/src/background/listener/bee-api.listener.ts +++ b/src/background/listener/bee-api.listener.ts @@ -1,7 +1,6 @@ -import { subdomainToBzzResource } from '../../utils/bzz-link' -import { fakeUrl } from '../../utils/fake-url' import { getItem, StoreObserver } from '../../utils/storage' -import { SWARM_SESSION_ID_KEY, unpackSwarmSessionIdFromUrl } from '../../utils/swarm-session-id' +import { fakeUrl } from '../../utils/fake-url' +import { subdomainToBzzResource } from '../../utils/bzz-link' export class BeeApiListener { private _beeApiUrl: string @@ -9,83 +8,95 @@ export class BeeApiListener { private _globalPostageBatchId: string private _web2OriginEnabled: boolean - public constructor(private storeObserver: StoreObserver) { + protected static POSTAGE_BATCH_RULE_ID = 1 + protected static WEB2_ORIGIN_RULE_ID = 2 + protected static BZZ_LINK_BLOCKER_ID = 3 + protected static RESOURCE_LOADER_BLOCKER_ID = 4 + protected static RESOURCE_LOADER_REDIRECT_ID = 5 + protected static BEE_API_BLOCKER_ID = 6 + protected static BEE_API_REDIRECT_ID = 7 + + protected static RESOURCE_TYPE_ALL = [ + 'main_frame', + 'sub_frame', + 'stylesheet', + 'script', + 'image', + 'font', + 'object', + 'xmlhttprequest', + 'media', + 'websocket', + ] + constructor(private storeObserver: StoreObserver) { this._beeApiUrl = 'http://localhost:1633' this._globalPostageBatchEnabled = false this._web2OriginEnabled = false this._globalPostageBatchId = 'undefined' // it is not necessary to check later, if it is enabled it will insert this.addStoreListeners() this.asyncInit() + this.setBeeNodeListeners() this.addBzzListeners() } - public get beeApiUrl(): string { - return this._beeApiUrl - } - - /** - * Handles postage batch id header replacement with global batch id - */ - private globalPostageStampHeaderListener = ( - details: chrome.webRequest.WebRequestHeadersDetails, - ): void | chrome.webRequest.BlockingResponse => { - if (!this._globalPostageBatchEnabled || !details.requestHeaders) return - - const postageBatchIdHeader = details.requestHeaders.find(header => header.name === 'swarm-postage-batch-id') - - if (!postageBatchIdHeader) return - - console.log( - `Postage Batch: ${this._globalPostageBatchId} Batch ID will be used instead of ` + postageBatchIdHeader.value, - ) - - postageBatchIdHeader.value = this._globalPostageBatchId - - return { requestHeaders: details.requestHeaders } - } - - private sandboxListener = ( - details: chrome.webRequest.WebResponseHeadersDetails, - ): void | chrome.webRequest.BlockingResponse => { - console.log('web2OriginEnabled', this._web2OriginEnabled) - - if (this._web2OriginEnabled) return { responseHeaders: details.responseHeaders } + private setBeeNodeListeners() { + const rules = [] - const urlArray = details.url.toString().split('/') - - if (urlArray[3] === 'bzz' && urlArray[4]) { - details.responseHeaders?.push({ - name: 'Content-Security-Policy', - value: 'sandbox allow-scripts allow-modals allow-popups allow-forms', + /** + * Handles postage batch id header replacement with global batch id + */ + if (this._globalPostageBatchEnabled) { + rules.push({ + id: BeeApiListener.POSTAGE_BATCH_RULE_ID, + condition: { + urlFilter: `${this._beeApiUrl}/*`, + resourceTypes: BeeApiListener.RESOURCE_TYPE_ALL, + }, + action: { + type: 'modifyHeaders', + requestHeaders: [ + { + header: 'swarm-postage-batch-id', + operation: 'set', + value: this._globalPostageBatchId, + }, + ], + }, }) } - console.log('responseHeaders', details.responseHeaders) - return { responseHeaders: details.responseHeaders } - } + if (this._web2OriginEnabled) { + rules.push({ + id: BeeApiListener.WEB2_ORIGIN_RULE_ID, + condition: { + urlFilter: `${this._beeApiUrl}/bzz/*`, + resourceTypes: BeeApiListener.RESOURCE_TYPE_ALL, + }, + action: { + type: 'modifyHeaders', + responseHeaders: [ + { + header: 'Content-Security-Policy', + operation: 'set', + value: 'sandbox allow-scripts allow-modals allow-popups allow-forms', + }, + ], + }, + }) + } - private addBeeNodeListeners(beeApiUrl: string) { - chrome.webRequest.onBeforeSendHeaders.addListener( - this.globalPostageStampHeaderListener, + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-extra-semi + ;(chrome as any).declarativeNetRequest.updateSessionRules( { - urls: [`${beeApiUrl}/*`], + removeRuleIds: [BeeApiListener.POSTAGE_BATCH_RULE_ID, BeeApiListener.WEB2_ORIGIN_RULE_ID], + addRules: rules, }, - ['blocking', 'requestHeaders'], - ) - chrome.webRequest.onHeadersReceived.addListener( - this.sandboxListener, - { - urls: [`${beeApiUrl}/*`], + () => { + console.log('Bee node listeners updated') }, - ['blocking', 'responseHeaders', 'extraHeaders'], ) } - private removeBeeNodeListeners() { - console.log('remove bee node listeners') - chrome.webRequest.onBeforeSendHeaders.removeListener(this.globalPostageStampHeaderListener) - } - private addBzzListeners() { /** * New Swarm page load request @@ -116,17 +127,6 @@ export class BeeApiListener { * 2. can be referred from dApp */ - /** - * this listener automatically cancels all requests towards .bzz.link URLs - * it relates to the 2nd scenario - */ - chrome.webRequest.onBeforeRequest.addListener( - () => { - return { cancel: true } - }, - { urls: ['https://*.bzz.link/*', 'http://*.bzz.link/*'] }, - ) - /** * it force-redirects to the fakeURL of the bzz resource. * it solves the 1st scenario @@ -151,7 +151,7 @@ export class BeeApiListener { (details: chrome.webRequest.WebRequestBodyDetails) => { console.log('Original BZZ Url', details.url) const urlParams = new URLSearchParams(details.url) - const query = urlParams.get('oq') + const query = decodeURI(urlParams.get('oq') || '') if (!query || !query.startsWith('bzz://')) return @@ -162,65 +162,89 @@ export class BeeApiListener { }, ) - // Used to load page resources like images - // Always have to have session ID in the URL Param - chrome.webRequest.onBeforeRequest.addListener( - details => { - let { url } = details - - let swarmSessionId: string - try { - const { sessionId, originalUrl } = unpackSwarmSessionIdFromUrl(url) - swarmSessionId = sessionId - url = originalUrl - } catch (e) { - console.error(`There is no valid '${SWARM_SESSION_ID_KEY}' passed to the bzz reference: ${url}`) - - return { - cancel: true, - } - } - // get the full referenced BZZ address from the modified url (without bzz address) - const urlArray = url.toString().split(`${fakeUrl.bzzProtocol}/`) - const redirectUrl = `${this._beeApiUrl}/bzz/${urlArray[1]}` - console.log(`bzz redirect to ${redirectUrl} from ${details.url}. Session ID: ${swarmSessionId}`) - - return { - redirectUrl, - } + /** + * this listener automatically cancels all requests towards .bzz.link URLs + * it relates to the 2nd scenario + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(chrome as any).declarativeNetRequest.updateSessionRules( + { + removeRuleIds: [ + BeeApiListener.BZZ_LINK_BLOCKER_ID, + BeeApiListener.RESOURCE_LOADER_BLOCKER_ID, + BeeApiListener.RESOURCE_LOADER_REDIRECT_ID, + BeeApiListener.BEE_API_BLOCKER_ID, + BeeApiListener.BEE_API_REDIRECT_ID, + ], + addRules: [ + { + id: BeeApiListener.BZZ_LINK_BLOCKER_ID, + priority: 1, + condition: { + regexFilter: '^(https?\\://)?.*\\.bzz\\.link/.*', + resourceTypes: ['main_frame'], + }, + action: { + type: 'block', + }, + }, + { + id: BeeApiListener.RESOURCE_LOADER_BLOCKER_ID, + priority: 1, + condition: { + regexFilter: fakeUrl.createRE2Pattern('bzz'), + resourceTypes: ['main_frame'], + }, + action: { + type: 'block', + }, + }, + { + id: BeeApiListener.RESOURCE_LOADER_REDIRECT_ID, + priority: 2, + condition: { + regexFilter: fakeUrl.createRE2Pattern('bzz', true), + resourceTypes: ['main_frame'], + }, + action: { + type: 'redirect', + redirect: { + regexSubstitution: `${this._beeApiUrl}/\\2\\3`, + }, + }, + }, + { + id: BeeApiListener.BEE_API_BLOCKER_ID, + priority: 1, + condition: { + regexFilter: fakeUrl.createRE2Pattern('bee-api'), + resourceTypes: ['main_frame'], + }, + action: { + type: 'block', + }, + }, + { + // Redirect the Bee API calls with swarm-session-id query param + // The swarm-session-id query parameter can be between the path and the host + id: BeeApiListener.BEE_API_REDIRECT_ID, + priority: 2, + condition: { + regexFilter: fakeUrl.createRE2Pattern('bee-api', true), + resourceTypes: ['main_frame'], + }, + action: { + type: 'redirect', + redirect: { + regexSubstitution: `${this._beeApiUrl}/\\2\\3`, + }, + }, + }, + ], }, - { urls: [`${fakeUrl.bzzProtocol}/*`] }, - ['blocking'], - ) - - // Redirect the Bee API calls with swarm-session-id query param - // The swarm-session-id query parameter can be between the path and the host - chrome.webRequest.onBeforeRequest.addListener( - details => { - let { url } = details - - try { - const { originalUrl } = unpackSwarmSessionIdFromUrl(url) - url = originalUrl - } catch (e) { - console.error(`There is no valid '${SWARM_SESSION_ID_KEY}' passed to the bzz reference: ${url}`) - - return { - cancel: true, - } - } - - // get the full referenced BZZ address from the modified url (without bzz address) - const urlArray = url.split(`${fakeUrl.beeApiAddress}/`) - const redirectUrl = `${this._beeApiUrl}/${urlArray[1]}` - console.log(`Bee API client request redirect to ${redirectUrl} from ${url}`) - - return { - redirectUrl, - } + () => { + console.log('Bzz listeners set') }, - { urls: [`${fakeUrl.beeApiAddress}*`] }, - ['blocking'], ) } @@ -237,36 +261,30 @@ export class BeeApiListener { if (storedGlobalPostageBatchId) this._globalPostageBatchId = storedGlobalPostageBatchId if (storedWeb2OriginEnabled) this._web2OriginEnabled = storedWeb2OriginEnabled - - // register listeners that have to be after async init - this.addBeeNodeListeners(this._beeApiUrl) } private addStoreListeners(): void { this.storeObserver.addListener('beeApiUrl', newValue => { console.log('Bee API URL changed to', newValue) this._beeApiUrl = newValue - this.removeBeeNodeListeners() - this.addBeeNodeListeners(this._beeApiUrl) + this.setBeeNodeListeners() }) this.storeObserver.addListener('globalPostageStampEnabled', newValue => { this._globalPostageBatchEnabled = Boolean(newValue) + this.setBeeNodeListeners() }) this.storeObserver.addListener('globalPostageBatch', newValue => { this._globalPostageBatchId = newValue + this.setBeeNodeListeners() }) this.storeObserver.addListener('web2OriginEnabled', newValue => { console.log('web2OriginEnabled changed to', newValue) + this.setBeeNodeListeners() this._web2OriginEnabled = newValue + this.setBeeNodeListeners() }) } - /** - * Redirects the tab or create a new tab for the dApp under the given BZZ reference - * - * @param bzzReference in form of $ROOT_HASH<$PATH><$QUERY> - * @param tabId the tab will be navigated to the dApp page - */ private redirectToBzzReference(bzzReference: string, tabId: number) { const url = `${this._beeApiUrl}/bzz/${bzzReference}` diff --git a/src/contentscript/document-start/index.ts b/src/contentscript/document-start/index.ts index 1276aca..cc0868e 100644 --- a/src/contentscript/document-start/index.ts +++ b/src/contentscript/document-start/index.ts @@ -15,9 +15,12 @@ import { MessengerInterceptor } from './messenger.interceptor' function init(): void { const sessionId = nanoid() dappSessionRegister(sessionId) - injectSessionId(sessionId) - injectSwarmHtml() - injectSwarmLibrary() + + document.addEventListener('DOMContentLoaded', () => { + injectSessionId(sessionId) + injectSwarmLibrary() + injectSwarmHtml() + }) //listen to events which come from inpage side new MessengerInterceptor() diff --git a/src/contentscript/document-start/inject/sandbox-polyfill.ts b/src/contentscript/document-start/inject/sandbox-polyfill.ts index 4662dc0..4f5f0b8 100644 --- a/src/contentscript/document-start/inject/sandbox-polyfill.ts +++ b/src/contentscript/document-start/inject/sandbox-polyfill.ts @@ -5,8 +5,5 @@ import { injectScript } from '../utils' * and as a result some common libraries won't work without polyfilling */ export function injectSandboxPolyfill(): void { - injectScript( - "Object.defineProperty(document, 'cookie', { get: function(){return ''}, set: function(){return true}, });", - 'sandboxPolyfill', - ) + injectScript('sandbox-ployfill.js', 'sandboxPolyfill') } diff --git a/src/contentscript/document-start/inject/session-id.ts b/src/contentscript/document-start/inject/session-id.ts index a7fdb51..d6d67d0 100644 --- a/src/contentscript/document-start/inject/session-id.ts +++ b/src/contentscript/document-start/inject/session-id.ts @@ -1,5 +1,20 @@ +import { filterMessage, MessageType, WindowMessage } from '../../model/WindowMessage' import { injectScript } from '../utils' export function injectSessionId(sessionId: string): void { - injectScript(`window.swarm = {...window.swarm, sessionId: '${sessionId}'}`, 'swarmSessionId') + window.addEventListener( + 'message', + filterMessage(MessageType.GET_SWARM_SESSION_ID, event => { + window.postMessage( + { + type: MessageType.SET_SWARM_SESSION_ID, + data: sessionId, + } as WindowMessage, + '*', + ) + }), + false, + ) + + injectScript(`swarm-session-id.js?swarmSessionId=${sessionId}`, 'swarmSessionId') } diff --git a/src/contentscript/document-start/inject/swarm-html.ts b/src/contentscript/document-start/inject/swarm-html.ts index 7deae07..d1e1f3f 100644 --- a/src/contentscript/document-start/inject/swarm-html.ts +++ b/src/contentscript/document-start/inject/swarm-html.ts @@ -1,7 +1,6 @@ -import SwarmHtml from 'raw-loader!../../../../dist/swarm-html.js' import { injectScript } from '../utils' /** Only does the injection of Swarm library on document start */ export function injectSwarmHtml(): void { - injectScript(SwarmHtml, 'swarmHtml') + injectScript('swarm-html.js', 'swarmHtml') } diff --git a/src/contentscript/document-start/inject/swarm-library.ts b/src/contentscript/document-start/inject/swarm-library.ts index 7f8bb84..9aded0c 100644 --- a/src/contentscript/document-start/inject/swarm-library.ts +++ b/src/contentscript/document-start/inject/swarm-library.ts @@ -1,7 +1,6 @@ -import SwarmLibrary from 'raw-loader!../../../../dist/swarm-library.js' import { injectScript } from '../utils' /** Only does the injection of Swarm library on document start */ export function injectSwarmLibrary(): void { - injectScript(SwarmLibrary, 'swarm') + injectScript('swarm-library.js', 'swarm') } diff --git a/src/contentscript/document-start/utils.ts b/src/contentscript/document-start/utils.ts index cde2022..5d4e2cb 100644 --- a/src/contentscript/document-start/utils.ts +++ b/src/contentscript/document-start/utils.ts @@ -3,14 +3,13 @@ * * @param content - Code to be executed in the current document */ -export function injectScript(content: string, windowObjectName: string): void { +export function injectScript(scriptSrc: string, windowObjectName: string, async = false) { try { - const container = document.head || document.documentElement - const scriptTag = document.createElement('script') - scriptTag.setAttribute('async', 'false') - scriptTag.textContent = content - container.insertBefore(scriptTag, container.children[0]) - container.removeChild(scriptTag) + const script = document.createElement('script') + script.setAttribute('type', 'text/javascript') + script.setAttribute('async', 'false') + script.setAttribute('src', chrome.runtime.getURL(scriptSrc)) + document.body.appendChild(script) console.log(`Swarm-Extension: injected object is available via "window.${windowObjectName}"`) } catch (error) { console.error('Swarm-Extension: Provider injection failed.', error) diff --git a/src/contentscript/model/WindowMessage.ts b/src/contentscript/model/WindowMessage.ts new file mode 100644 index 0000000..84b5fca --- /dev/null +++ b/src/contentscript/model/WindowMessage.ts @@ -0,0 +1,23 @@ +export enum MessageType { + GET_SWARM_SESSION_ID = 'get-swarm-session-id', + SET_SWARM_SESSION_ID = 'set-swarm-session-id', +} + +export interface Message { + type: Type + data?: Data +} +export type WindowMessage = + | Message + | Message + +export function filterMessage( + messageType: MessageType, + callback: (event: MessageEvent) => void, +): (event: MessageEvent) => void { + return (event: MessageEvent) => { + if (event.data?.type === messageType) { + callback(event) + } + } +} diff --git a/src/contentscript/sandbox-polyfill/index.ts b/src/contentscript/sandbox-polyfill/index.ts new file mode 100644 index 0000000..4499d5d --- /dev/null +++ b/src/contentscript/sandbox-polyfill/index.ts @@ -0,0 +1 @@ +import './sandbox-ployfill' diff --git a/src/contentscript/sandbox-polyfill/sandbox-ployfill.ts b/src/contentscript/sandbox-polyfill/sandbox-ployfill.ts new file mode 100644 index 0000000..7802546 --- /dev/null +++ b/src/contentscript/sandbox-polyfill/sandbox-ployfill.ts @@ -0,0 +1,8 @@ +Object.defineProperty(document, 'cookie', { + get: function () { + return '' + }, + set: function () { + return true + }, +}) diff --git a/src/contentscript/swarm-session-id/index.ts b/src/contentscript/swarm-session-id/index.ts new file mode 100644 index 0000000..8e33639 --- /dev/null +++ b/src/contentscript/swarm-session-id/index.ts @@ -0,0 +1 @@ +import './session-id-listener' diff --git a/src/contentscript/swarm-session-id/session-id-listener.ts b/src/contentscript/swarm-session-id/session-id-listener.ts new file mode 100644 index 0000000..5a64a8a --- /dev/null +++ b/src/contentscript/swarm-session-id/session-id-listener.ts @@ -0,0 +1,24 @@ +import { filterMessage, MessageType, WindowMessage } from '../model/WindowMessage' + +window.addEventListener( + 'message', + filterMessage(MessageType.SET_SWARM_SESSION_ID, event => { + const windowObject = window as any + const { swarm } = windowObject + const sessionId = event.data.data + + if (typeof swarm === 'object' && swarm !== null) { + windowObject.swarm = { + ...swarm, + sessionId, + } + } else { + windowObject.swarm = { + sessionId, + } + } + }), + true, +) + +window.postMessage({ type: MessageType.GET_SWARM_SESSION_ID } as WindowMessage, '*') diff --git a/src/utils/fake-url.ts b/src/utils/fake-url.ts index 7d6f34d..6301896 100644 --- a/src/utils/fake-url.ts +++ b/src/utils/fake-url.ts @@ -1,3 +1,5 @@ +import { SWARM_SESSION_ID_KEY } from './swarm-session-id' + /** API endpoints that the extension can serve out */ class FakeUrl { /** @@ -29,6 +31,16 @@ class FakeUrl { this.beeApiAddress = `${this.baseUrl}/bee-api` this.openDapp = `${this.baseUrl}/open-dapp` } + + public createRE2Pattern(type: 'bzz' | 'bee-api', includeSessionIdKey = false): string { + let pattern = `^(https?\\://)?swarm\\.fakeurl\\.localhost/${type}(.*)` + + if (includeSessionIdKey) { + pattern += `(__${SWARM_SESSION_ID_KEY}~.*__.*)` + } + + return pattern + } } export const fakeUrl = new FakeUrl() diff --git a/src/utils/storage.ts b/src/utils/storage.ts index 4ce6e60..16b4669 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -1,4 +1,5 @@ import { EventEmitter } from 'events' +import { DappSecurityContextData } from '../background/data/DappSecurityContextData' interface Store { beeApiUrl: string @@ -6,6 +7,7 @@ interface Store { web2OriginEnabled: boolean globalPostageBatch: string | null globalPostageStampEnabled: boolean + securityContexts: { [sessionId: string]: DappSecurityContextData } } type StoreKey = keyof Store diff --git a/webpack.config.ts b/webpack.config.ts index cce3790..4d56bad 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -240,10 +240,10 @@ const background = (env?: Partial): Configuration => { } const contentscript = ( - scriptType: 'document-start' | 'swarm-library' | 'swarm-html', + scriptType: 'document-start' | 'swarm-library' | 'swarm-html' | 'swarm-session-id' | 'sandbox-polyfill', env?: Partial, ): Configuration => { - const dependencies = ['swarm-library', 'swarm-html'] + const dependencies = ['swarm-library', 'swarm-html', 'swarm-session-id', 'sandbox-polyfill'] const isProduction = env?.mode === 'production' const filename = `${scriptType}.js` const entry = Path.resolve(__dirname, 'src', 'contentscript', scriptType) @@ -479,7 +479,7 @@ export default (env?: Partial): Configuration[] => { if (env?.buildDeps) { if (env?.mode === 'development') liveReloadServer = new Server({ port: DEPS_RELOAD_PORT }) - baseConfig = [contentscript('swarm-library', env), contentscript('swarm-html', env)] + baseConfig = [contentscript('swarm-library', env), contentscript('swarm-html', env), contentscript('swarm-session-id', env), contentscript('sandbox-polyfill', env)] } else { if (env?.mode === 'development') liveReloadServer = new Server({ port: MAIN_RELOAD_PORT })