diff --git a/__tests__/e2e/blocking_requests.spec.js b/__tests__/e2e/blocking_requests.spec.js
new file mode 100644
index 0000000..c6acf1c
--- /dev/null
+++ b/__tests__/e2e/blocking_requests.spec.js
@@ -0,0 +1,67 @@
+//TODO work in this file is paused/block.
+// More details and follow up on: https://github.com/lfilho/sample-webextension/issues/38
+
+/*
+import browserWrapper from 'playwright-firefox';
+
+import child_process from 'child_process';
+import webExt from 'web-ext';
+import pptr from 'puppeteer-core';
+
+import { LOCAL_TEST_PAGE_URL } from '../shared/__url_fixtures.js';
+*/
+let page;
+/*
+let browser;
+
+beforeAll(async () => {
+ webExt.default.util.logger.consoleStream.makeVerbose();
+ const runningInfo = await webExt.default.cmd
+ .run(
+ {
+ sourceDir: `${process.cwd()}/src`,
+ firefox: 'nightly'
+ },
+ { shouldExitProgram: false }
+ )
+ .then((runner) => runner.extensionRunners[0].runningInfo);
+
+ // Needed because `webExt.cmd.run` returns before the DevTools agent starts running.
+ // Alternative would be to wrap the call to pptr.connect() with some custom retry logic
+ child_process.execSync('sleep 5');
+
+ const browserURL = `ws://127.0.0.1:${runningInfo.debuggerPort}`;
+
+ browser = await browserWrapper.firefox.connect({
+ wsEndpoint: browserURL,
+ logger: {
+ isEnabled: () => true,
+ log: (name, severity, message, args) => {
+ console.log(`[${severity}] ${name} ${message}. Args: ${args}`);
+ },
+ },
+ });
+ const context = await browser.newContext();
+ page = await context.newPage();
+
+ await page.goto(LOCAL_TEST_PAGE_URL);
+});
+
+afterAll(async () => {
+ await browser.close();
+});
+*/
+
+describe('request to known tracker urls should be blocked', () => {
+ it.skip('loads the test page', async () => {
+ const text = await page.evaluate(() => {
+ return document.querySelector('[data-testid="test-page-header"]');
+ });
+ expect(text).toBeTruthy();
+ });
+
+ it.skip('blocks a known bad url by not loading its iframe', async () => {
+ //TODO waiting on resolution for: https://github.com/mozilla/web-ext/issues/1927
+ // More details and follow up on: https://github.com/lfilho/sample-webextension/issues/38
+ });
+});
diff --git a/__tests__/shared/test_page.html b/__tests__/shared/test_page.html
new file mode 100644
index 0000000..ed6429c
--- /dev/null
+++ b/__tests__/shared/test_page.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+ Test page
+
+
+
+
+ ✅ Test page
+
+ Bad page iframe (should be blank):
+
+
+ Good page iframe (should not be blank):
+
+
+
+
diff --git a/__tests__/src/lib/__request_fixtures.js b/__tests__/src/lib/__request_fixtures.js
new file mode 100644
index 0000000..03d323d
--- /dev/null
+++ b/__tests__/src/lib/__request_fixtures.js
@@ -0,0 +1,7 @@
+import { A_URL } from '../../../src/shared/__url_fixtures.js';
+
+export const RANDOM_REQUEST_DETAILS = { url: A_URL };
+export const BLOCKING_RESPONSE = {
+ CANCELLED: { cancel: true },
+ NOT_CANCELLED: { cancel: false },
+};
diff --git a/__tests__/src/lib/list.spec.js b/__tests__/src/lib/list.spec.js
new file mode 100644
index 0000000..56210a4
--- /dev/null
+++ b/__tests__/src/lib/list.spec.js
@@ -0,0 +1,62 @@
+import List from '../../../src/lib/model/list.js';
+import {
+ A_URL,
+ ANOTHER_URL,
+ SOME_URLS,
+} from '../../../src/shared/__url_fixtures.js';
+
+let list;
+
+beforeEach(() => {
+ const RANDOM_TYPE = List.types.DENY_LIST;
+ list = new List(RANDOM_TYPE);
+});
+
+describe('List', () => {
+ it('should add an url to the list', () => {
+ const originalListSize = list.size;
+ list.add(A_URL);
+ expect(list.size).toBe(originalListSize + 1);
+ });
+
+ it('should not add duplicate urls to the list', () => {
+ const originalListSize = list.size;
+ list.add(A_URL);
+ list.add(A_URL);
+ expect(list.size).toBe(originalListSize + 1);
+ });
+
+ it('should remove an url from the list', () => {
+ const originalListSize = list.size;
+ list.add(A_URL);
+ list.remove(A_URL);
+ expect(list.size).toBe(originalListSize);
+ });
+
+ it('should tell if an url is on the list', () => {
+ list.add(A_URL);
+ expect(list.has(A_URL)).toBe(true);
+ expect(list.has(ANOTHER_URL)).toBe(false);
+ });
+
+ it('should clear the list', () => {
+ list.add(A_URL);
+ list.add(ANOTHER_URL);
+ list.clear();
+ expect(list.size).toBe(0);
+ });
+
+ it('should return the size of the list', () => {
+ expect(list.size).toBe(0);
+ list.add(A_URL);
+ expect(list.size).toBe(1);
+ list.add(ANOTHER_URL);
+ expect(list.size).toBe(2);
+ });
+
+ it('should add several urls at once', () => {
+ expect(list.size).toBe(0);
+ list.bulkAdd(SOME_URLS);
+ expect(list.size).toBe(SOME_URLS.length);
+ });
+});
diff --git a/__tests__/src/lib/request_listener.spec.js b/__tests__/src/lib/request_listener.spec.js
new file mode 100644
index 0000000..4c0c66d
--- /dev/null
+++ b/__tests__/src/lib/request_listener.spec.js
@@ -0,0 +1,25 @@
+import { jest } from '@jest/globals';
+
+import requestListener from '../../../src/lib/request_listener.js';
+import RequestMatcher from '../../../src/lib/request_matcher.js';
+
+import {
+ RANDOM_REQUEST_DETAILS,
+ BLOCKING_RESPONSE,
+} from './__request_fixtures.js';
+
+jest.mock('../../../src/lib/request_matcher.js');
+
+describe('Request Listener', () => {
+ it('should return a BlockingResponse with cancel: true for a tracker url', () => {
+ RequestMatcher.isDenied = jest.fn().mockReturnValue(true);
+ const result = requestListener(RANDOM_REQUEST_DETAILS);
+ expect(result).toEqual(BLOCKING_RESPONSE.CANCELLED);
+ });
+
+ it('should return a BlockingResponse with cancel: false for a non-tracker url', () => {
+ RequestMatcher.isDenied = jest.fn().mockReturnValue(false);
+ const result = requestListener(RANDOM_REQUEST_DETAILS);
+ expect(result).toEqual(BLOCKING_RESPONSE.NOT_CANCELLED);
+ });
+});
diff --git a/__tests__/src/lib/request_matcher.spec.js b/__tests__/src/lib/request_matcher.spec.js
new file mode 100644
index 0000000..46bdb82
--- /dev/null
+++ b/__tests__/src/lib/request_matcher.spec.js
@@ -0,0 +1,17 @@
+import RequestMatcher from '../../../src/lib/request_matcher.js';
+import { BAD_URL, GOOD_URL } from '../../../src/shared/__url_fixtures.js';
+
+beforeAll(() => RequestMatcher.denyList.add(BAD_URL));
+afterAll(() => RequestMatcher.denyList.clear());
+
+describe('Request Matcher', () => {
+ it('should deny urls in the deny list', () => {
+ const result = RequestMatcher.isDenied(BAD_URL);
+ expect(result).toBe(true);
+ });
+
+ it('should not deny urls if they are not in the deny list', () => {
+ const result = RequestMatcher.isDenied(GOOD_URL);
+ expect(result).toBe(false);
+ });
+});
diff --git a/bin/change-ext-version.js b/bin/change-ext-version.js
old mode 100644
new mode 100755
index 830fe2e..8ab410d
--- a/bin/change-ext-version.js
+++ b/bin/change-ext-version.js
@@ -1,3 +1,5 @@
+#!/usr/bin/env node
+
import fs from 'fs';
const MANITFEST_FILE = 'src/manifest.json';
diff --git a/package.json b/package.json
index 878a6d2..e619a14 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
},
"homepage": "https://github.com/lfilho/sample-webextension#readme",
"scripts": {
- "develop": "npm run web-ext -- run",
+ "develop": "npm run web-ext -- run --bc --url $(pwd)/__tests__/shared/test_page.html",
"format": "prettier --write \"**/*.{js,json,css,md}\"",
"//eslint is configured with prettier, so it will check and fix js formatting too": "",
"lint": "npm run lint:js && npm run lint:ext",
@@ -25,18 +25,19 @@
"lint:js:fix": "npm run lint:js -- --fix",
"pretest": "npm run lint",
"start": "npm run develop",
- "test": "NODE_ENV=test jest",
+ "test": "NODE_ENV=test node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test:watch": "npm test -- --watch",
"web-ext": "web-ext",
"//Release related scripts:": "",
"release": "semantic-release",
"build": "npm run web-ext -- build",
"get-version": "echo $npm_package_version",
- "change-ext-version": "node bin/change-ext-version.js"
+ "change-ext-version": "bin/change-ext-version.js"
},
"devDependencies": {
"@commitlint/cli": "8.3.5",
"@commitlint/config-conventional": "8.3.4",
+ "@jest/globals": "26.0.1",
"@semantic-release/commit-analyzer": "8.0.1",
"@semantic-release/exec": "5.0.0",
"@semantic-release/github": "7.0.7",
@@ -59,7 +60,7 @@
"jest": {
"displayName": "test",
"testRegex": "\\.spec\\.js$",
- "testEnvironment": "jsdom",
+ "testEnvironment": "jest-environment-node",
"testURL": "http://localhost",
"watchPlugins": [
"jest-watch-typeahead/filename",
@@ -67,7 +68,8 @@
],
"coveragePathIgnorePatterns": [
"/node_modules/"
- ]
+ ],
+ "transform": {}
},
"husky": {
"hooks": {
@@ -145,5 +147,6 @@
"webExt": {
"sourceDir": "src",
"artifactsDir": "dist"
- }
+ },
+ "dependencies": {}
}
diff --git a/src/background_script.html b/src/background_script.html
new file mode 100644
index 0000000..439caac
--- /dev/null
+++ b/src/background_script.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/background_script.js b/src/background_script.js
index 863bd10..adf33df 100644
--- a/src/background_script.js
+++ b/src/background_script.js
@@ -1 +1,4 @@
-console.log('This is coming from background script!');
+import RequestBlocker from './lib/request_blocker.js';
+
+console.log('Extension is ready to start blocking!');
+RequestBlocker.startMonitoring();
diff --git a/src/browserAction/script.js b/src/browserAction/script.js
index b472979..7133b56 100644
--- a/src/browserAction/script.js
+++ b/src/browserAction/script.js
@@ -1 +1 @@
-window.alert('This is coming from the extension button!');
+console.info('This is coming from the extension button!');
diff --git a/src/content_script.js b/src/content_script.js
index f1890e0..151c69d 100644
--- a/src/content_script.js
+++ b/src/content_script.js
@@ -1 +1 @@
-window.alert('This is comming from content script!');
+console.info('This is comming from content script!');
diff --git a/src/lib/list_populator.js b/src/lib/list_populator.js
new file mode 100644
index 0000000..1943344
--- /dev/null
+++ b/src/lib/list_populator.js
@@ -0,0 +1,20 @@
+import List from './model/list.js';
+import { BAD_URLS } from '../shared/__url_fixtures.js';
+
+// TODO for now, just populating it with the examples from fixtures
+// TODO real deal: fetch from local files shipped with the extension, augment it with live sources...
+// https://github.com/lfilho/sample-webextension/issues/24
+// https://github.com/lfilho/sample-webextension/issues/18
+// https://github.com/lfilho/sample-webextension/issues/17
+
+export default class ListPopulator {
+ static async populateList(list) {
+ const listTypeToUrlMapper = {
+ [List.types.DENY_LIST]: BAD_URLS,
+ };
+ const urls = listTypeToUrlMapper[list.type];
+ list.bulkAdd(urls);
+
+ //TODO https://github.com/lfilho/sample-webextension/issues/8
+ }
+}
diff --git a/src/lib/model/deny_list.js b/src/lib/model/deny_list.js
new file mode 100644
index 0000000..1b3864d
--- /dev/null
+++ b/src/lib/model/deny_list.js
@@ -0,0 +1,7 @@
+import List from './list.js';
+
+export default class DenyList extends List {
+ constructor() {
+ super(List.types.DENY_LIST);
+ }
+}
diff --git a/src/lib/model/list.js b/src/lib/model/list.js
new file mode 100644
index 0000000..7fa8114
--- /dev/null
+++ b/src/lib/model/list.js
@@ -0,0 +1,50 @@
+export default class List {
+ /*
+ * Using a Set and mainly proxying the methods to their native ones.
+ * So if we change the underlaying data structure in the future,
+ * the API stays the same and the impact on the codebase is minimal.
+ */
+
+ constructor(type) {
+ if (!type) {
+ throw new Error('List constructor needs a type');
+ //TODO Refactor Error strategy later on to contain an unique error code and both technical and user friendly messages
+ }
+ this.list = new Set();
+ this.type = type;
+ }
+
+ static get types() {
+ return Object.freeze({
+ DENY_LIST: 'DENY',
+ ALLOW_LIST: 'ALLOW',
+ });
+ }
+
+ has(url) {
+ // Look kid, one day there will be a complex algorithm here.
+ // For now, full matches only. In future iterations,
+ // we should support some sort of pattern instead: *evil-tracker.com/*
+ return this.list.has(url);
+ }
+
+ add(url) {
+ this.list.add(url);
+ }
+
+ remove(url) {
+ this.list.delete(url);
+ }
+
+ clear() {
+ this.list.clear();
+ }
+
+ get size() {
+ return this.list.size;
+ }
+
+ bulkAdd(values) {
+ values.forEach(this.list.add.bind(this.list));
+ }
+}
diff --git a/src/lib/request_blocker.js b/src/lib/request_blocker.js
new file mode 100644
index 0000000..02e9f16
--- /dev/null
+++ b/src/lib/request_blocker.js
@@ -0,0 +1,19 @@
+import requestListener from './request_listener.js';
+
+/**
+ * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onBeforeRequest
+ */
+export default class RequestBlocker {
+ static startMonitoring() {
+ const BLOCKING_FLAG = 'blocking';
+ const URL_FILTER = { urls: [''] };
+
+ browser.webRequest.onErrorOccurred.addListener(console.error, URL_FILTER);
+
+ browser.webRequest.onBeforeRequest.addListener(
+ requestListener,
+ URL_FILTER,
+ [BLOCKING_FLAG]
+ );
+ }
+}
diff --git a/src/lib/request_listener.js b/src/lib/request_listener.js
new file mode 100644
index 0000000..7da01ac
--- /dev/null
+++ b/src/lib/request_listener.js
@@ -0,0 +1,14 @@
+import RequestMatcher from './request_matcher.js';
+
+export default function requestListener(requestDetails) {
+ const url = requestDetails.url;
+ const isUrlDenied = RequestMatcher.isDenied(url);
+ const shouldBlock = { cancel: isUrlDenied };
+
+ if (isUrlDenied) {
+ console.log(`Blocking request for: ${url}. Nice try!`);
+ } else {
+ console.log(`URL is good to go: ${url}.`);
+ }
+ return shouldBlock;
+}
diff --git a/src/lib/request_matcher.js b/src/lib/request_matcher.js
new file mode 100644
index 0000000..eb47375
--- /dev/null
+++ b/src/lib/request_matcher.js
@@ -0,0 +1,18 @@
+import DenyList from './model/deny_list.js';
+import ListPopulator from './list_populator.js';
+
+let denyList;
+
+export default class RequestMatcher {
+ static get denyList() {
+ if (!denyList) {
+ denyList = new DenyList();
+ ListPopulator.populateList(denyList);
+ }
+ return denyList;
+ }
+
+ static isDenied(url) {
+ return this.denyList.has(url);
+ }
+}
diff --git a/src/manifest.json b/src/manifest.json
index 36f98cf..efcd2f4 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -6,8 +6,9 @@
"icons": {
"64": "icons/icon.png"
},
+ "permissions": ["webRequest", "webRequestBlocking", ""],
"background": {
- "scripts": ["background_script.js"]
+ "page": "background_script.html"
},
"content_scripts": [
{
diff --git a/src/shared/__url_fixtures.js b/src/shared/__url_fixtures.js
new file mode 100644
index 0000000..17be928
--- /dev/null
+++ b/src/shared/__url_fixtures.js
@@ -0,0 +1,23 @@
+const A_URL = 'https://example.com';
+const GOOD_URL = 'https://luiz.dev';
+const BAD_URL = 'https://evil-tracker.com';
+const ANOTHER_URL = GOOD_URL;
+const SOME_URLS = [A_URL, ANOTHER_URL, BAD_URL];
+const TEST_PAGE_GOOD_URL =
+ 'https://itty.bitty.site/#good_page/data:text/html;charset=utf-8;bxze64,XQAAAALrAQAAAAAAAAAeGInmWR9D5qtrM4PFJv4W1okR98bzFbE2QlIiIqEKRNhWozLGaVxy2UBVi6vb5PjLiS+KmhnoBI2zbVEi38FFqGt0V2dZ/n48NtEOjSTkOFXBuNLAKC6rlcwmvnHnUnMAWA2l/QImsEbNvvf1bv40vbtBzNp9F3TGp/HpcdlmwUSp59tjbjdUlRkOVnMxBaTPI6tnqjg9UBREwBH6Y4c6xLg53hJodPJyK8AysLdOEqC5OPFdGrHq7n6ViwKq90juHDM+UhFD8ug4iSu0Yo74yBMAo7Rtj+Jd5h9AkkjDCs/m4RMIP7KQKT4AldOuVvxaNDd4LcfbH/7lFxzMpv2FPyYxeR5ZmDMwE6422v7jh2OnV4nTcu42kWkhVBP7U06PxG1bBjmd5+5p11z/jK399tAktg==';
+const TEST_PAGE_BAD_URL =
+ 'https://itty.bitty.site/#bad_page/data:text/html;charset=utf-8;bxze64,XQAAAAI1AgAAAAAAAAAeGInmWR9D5qtrM4PFJv4W1okR98bzFbE2QlIWuIKxrmOKcpOUCz+bgN13tm1YwI65ckLaaMk97I0eK4ZJmz5rEyi5AYIrCihWPyWsU8imdwCjsAu9mVY4Uz3NDJSwS2tBy4fpBWIzlJM+PEIF1Dz7uHdB5dd0JkIYoHoNI51/xPcOHjaLtkdq929hapYOYARvUkxhKCAYZqoeWl/azB+/iRrItZhXoN2Xhb7cEouQ4ySG85i7cH0nZAJxZezxYYPzjYOKj1QECrYU2ufsndUGql9zsUXyBzVv3UCYzraVIpXLzEWabESPgYK1HY3biPhAiXzCU1i3msUdqKm0jl/HwqnVgJjhKaCsQvuv8pyy+VVkaz/XPVibLpmrbELCVUdG/jgKW2ig1pqjIAIoFb7YROo5+5026HlBBKlBJSJtJPiB5wAAbZMivZGwt72qmdA5R4jjQVr+djo2';
+const BAD_URLS = [BAD_URL, TEST_PAGE_BAD_URL, 'https://facebook.com'];
+const GOOD_URLS = [GOOD_URL, TEST_PAGE_GOOD_URL, 'https://eff.org'];
+
+export {
+ A_URL,
+ GOOD_URL,
+ BAD_URL,
+ ANOTHER_URL,
+ SOME_URLS,
+ TEST_PAGE_BAD_URL,
+ TEST_PAGE_GOOD_URL,
+ BAD_URLS,
+ GOOD_URLS,
+};
diff --git a/test/index.spec.js b/test/index.spec.js
deleted file mode 100644
index 34511b9..0000000
--- a/test/index.spec.js
+++ /dev/null
@@ -1,7 +0,0 @@
-describe('hello world', () => {
- it('is round', () => {
- const world = 'round';
- const shape = 'round';
- expect(world).toBe(shape);
- });
-});