Skip to content

Commit

Permalink
Get script injection working for Chrome MV3 builds of the extension (#…
Browse files Browse the repository at this point in the history
…1389)

Chrome Manifest V3 introduces the new scripting API[1] that can inject
scripts directly into the "main world" of a website. Along with that,
Manifest V3 prevents the old pattern of injecting a script element
into the DOM. Adjust to those changes here, to get our injected
protections working again.

Note: about:blank frame script injection is not yet working[2].

1 - https://developer.chrome.com/docs/extensions/reference/scripting
2 - https://crbug.com/1360392
  • Loading branch information
kzar committed Sep 7, 2022
1 parent 13e58a4 commit e921e6a
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 26 deletions.
2 changes: 1 addition & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ module.exports = function (grunt) {
exec: {
copyjs: `cp shared/js/*.js build/${browser}/${buildType}/js/ && rm build/${browser}/${buildType}/js/*.es6.js`,
installContentScope: contentScopeInstall,
copyContentScope: `${contentScopeBuild} cp ${ddgContentScope}/build/${browserSimilar}/inject.js build/${browser}/${buildType}/public/js/inject.js`,
copyContentScope: `${contentScopeBuild} cp ${ddgContentScope}/build/${browser}/inject.js build/${browser}/${buildType}/public/js/inject.js`,
copyContentScripts: `cp shared/js/content-scripts/*.js build/${browser}/${buildType}/public/js/content-scripts/`,
copyData: `cp -r shared/data build/${browser}/${buildType}/`,
copyAutofillJs: `mkdir -p build/${browser}/${buildType}/public/js/content-scripts/ && cp ${ddgAutofill}/*.js build/${browser}/${buildType}/public/js/content-scripts/`,
Expand Down
32 changes: 20 additions & 12 deletions integration-test/background/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ const thirdPartyDomain = 'good.third-party.site'
const thirdPartyTracker = 'broken.third-party.site'

async function setup () {
const { browser, bgPage, teardown } = await harness.setup()
const { browser, bgPage, teardown, manifestVersion } = await harness.setup()
const page = await browser.newPage()

await backgroundWait.forAllConfiguration(bgPage)
await loadTestConfig(bgPage, 'storage-blocking.json')

return { browser, page, teardown, bgPage }
return { browser, page, teardown, bgPage, manifestVersion }
}

async function waitForAllResults (page) {
Expand All @@ -26,9 +26,12 @@ async function waitForAllResults (page) {
describe('Storage blocking Tests', () => {
describe(`On https://${testPageDomain}/privacy-protections/storage-blocking/`, () => {
let cookies = []
let manifestVersion

beforeAll(async () => {
const { page, teardown } = await setup()
let page
let teardown
({ page, teardown, manifestVersion } = await setup())
try {
// Load the test pages home first to give some time for the extension background to start
// and register the content-script-message handler
Expand All @@ -51,15 +54,20 @@ describe('Storage blocking Tests', () => {
expect(headerCookie.expires).toBeGreaterThan(Date.now() / 1000)
})

it('blocks 3rd party HTTP cookies not on block list', () => {
const headerCookie = cookies.find(({ name, domain }) => name === 'headerdata' && domain === thirdPartyDomain)
expect(headerCookie).toBeUndefined()
})

it('blocks 3rd party HTTP cookies for trackers', () => {
const headerCookie = cookies.find(({ name, domain }) => name === 'headerdata' && domain === thirdPartyTracker)
expect(headerCookie).toBeUndefined()
})
// FIXME - Once Cookie header blocking is working in the experimental
// Chrome MV3 build of the extension we should remove this
// condition.
if (manifestVersion === 2) {
it('blocks 3rd party HTTP cookies not on block list', () => {
const headerCookie = cookies.find(({ name, domain }) => name === 'headerdata' && domain === thirdPartyDomain)
expect(headerCookie).toBeUndefined()
})

it('blocks 3rd party HTTP cookies for trackers', () => {
const headerCookie = cookies.find(({ name, domain }) => name === 'headerdata' && domain === thirdPartyTracker)
expect(headerCookie).toBeUndefined()
})
}

it('does not block 1st party JS cookies', () => {
const jsCookie = cookies.find(({ name, domain }) => name === 'jsdata' && domain === testPageDomain)
Expand Down
5 changes: 4 additions & 1 deletion integration-test/config-mv3.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"spec_dir": "integration-test",
"spec_files": [
"background/onboarding.js",
"background/request-blocking.js"
"background/request-blocking.js",
"background/test-fingerprint.js",
"background/storage.js",
"content-scripts/gpc.js"
]
}
25 changes: 15 additions & 10 deletions integration-test/content-scripts/gpc.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ const frameTests = [
let server
let server2
let teardown
let manifestVersion

describe('Ensure GPC is injected into frames', () => {
beforeAll(async () => {
({ browser, bgPage, teardown } = await harness.setup())
({ browser, bgPage, teardown, manifestVersion } = await harness.setup())
server = setupServer({}, 8080)
server2 = setupServer({}, 8081)

Expand All @@ -72,16 +73,20 @@ describe('Ensure GPC is injected into frames', () => {
expect(gpc).toEqual(gpc2)
})

it(`${iframeHost} should work with about:blank injected frames`, async () => {
const page = await browser.newPage()
await pageWait.forGoto(page, 'http://127.0.0.1:8080/blank_framer.html')
const gpc = await getGPCValueOfContext(page)
// FIXME - chrome.scripting API is not yet injecting into about:blank
// frames correctly. See https://crbug.com/1360392.
if (manifestVersion === 2) {
it(`${iframeHost} should work with about:blank injected frames`, async () => {
const page = await browser.newPage()
await pageWait.forGoto(page, 'http://127.0.0.1:8080/blank_framer.html')
const gpc = await getGPCValueOfContext(page)

const iframeInstance = page.frames().find(iframe => iframe.url() === 'about:blank')
const gpc2 = await getGPCValueOfContext(iframeInstance)
const iframeInstance = page.frames().find(iframe => iframe.url() === 'about:blank')
const gpc2 = await getGPCValueOfContext(iframeInstance)

expect(gpc).toEqual(true)
expect(gpc).toEqual(gpc2)
})
expect(gpc).toEqual(true)
expect(gpc).toEqual(gpc2)
})
}
})
})
7 changes: 5 additions & 2 deletions integration-test/helpers/harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ const setup = async (ops) => {
`--user-data-dir=${dataDir}`
]

const manifestVersion =
process.env.npm_lifecycle_event === 'test-int-mv3' ? 3 : 2

if (loadExtension) {
let extensionPath = 'build/chrome/dev'
if (process.env.npm_lifecycle_event === 'test-int-mv3') {
if (manifestVersion === 3) {
extensionPath = extensionPath.replace('chrome', 'chrome-mv3')
}
args.push('--disable-extensions-except=' + extensionPath)
Expand Down Expand Up @@ -90,7 +93,7 @@ const setup = async (ops) => {
spawnSync('rm', ['-rf', dataDir])
}

return { browser, bgPage, requests, teardown }
return { browser, bgPage, requests, teardown, manifestVersion }
}

module.exports = {
Expand Down
1 change: 1 addition & 0 deletions shared/js/background/background.es6.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require('./events.es6')
const settings = require('./settings.es6')
const { onStartup } = require('./startup.es6')
require('./declarative-net-request')
require('./script-injection')

settings.ready().then(() => {
onStartup()
Expand Down
30 changes: 30 additions & 0 deletions shared/js/background/script-injection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const browserWrapper = require('./wrapper.es6')

// With Manifest V2 the content scripts are run directly from the manifest and
// they inject into the main world of websites by adding a <script> element to
// the DOM. With Manifest V3 however the chrome.scripting API must be used to
// inject scripts into the main world.
// Note: It's important that the isolated world script runs first. The order the
// scripts are added by registerContentScripts is based on the script ID,
// but note that's an implementation detail (from use of std::set for new
// script IDs[1]) and could change in the future.
// 1 - https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/extensions/api/scripting/scripting_api.cc;l=929
if (browserWrapper.getManifestVersion() === 3) {
// @ts-ignore - The chrome type does not yet know about
// registerContentScripts.
chrome.scripting.registerContentScripts([{
id: '1-script-injection-isolated-world',
allFrames: true,
js: ['public/js/content-scripts/content-scope-messaging.js'],
runAt: 'document_start',
world: 'ISOLATED',
matches: ['<all_urls>']
}, {
id: '2-script-injection-main-world',
allFrames: true,
js: ['public/js/inject.js'],
runAt: 'document_start',
world: 'MAIN',
matches: ['<all_urls>']
}])
}
47 changes: 47 additions & 0 deletions shared/js/content-scripts/content-scope-messaging.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
function getSecret () {
return new Promise(resolve => {
window.addEventListener('ddg-secret', event => {
event.stopImmediatePropagation()
resolve(event.detail)
}, { once: true })
})
}

async function init () {
const secret = await getSecret()

chrome.runtime.onMessage.addListener((message) => {
window.dispatchEvent(new CustomEvent(secret, {
detail: message
}))
})

chrome.runtime.sendMessage({
messageType: 'registeredContentScript',
options: {
documentUrl: window.location.href
}
}, argumentsObject => {
// Setup debugging messages if necessary.
if (argumentsObject.debug) {
window.addEventListener('message', message => {
if (message.data.action && message.data.message) {
chrome.runtime.sendMessage({
messageType: 'debuggerMessage',
options: message.data
})
}
})
}

// Init the content-scope-scripts with the argumentsObject.
window.dispatchEvent(new CustomEvent(secret, {
detail: {
type: 'register',
argumentsObject
}
}))
})
}

init()

0 comments on commit e921e6a

Please sign in to comment.