├── .eslintrc.json ├── .github └── issue_template.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── PRIVACY_POLICY.md ├── README.md ├── docs ├── cydec-anti-fingerprinting-comparison.md ├── moz-validation-results ├── notes.md └── panopticlick.md ├── media ├── chrome-icon.png ├── chrome-large-title.png ├── chrome-marquee.png ├── chrome-small-title.png ├── firefox-large-icon.png ├── firefox-small-icon.png ├── original-drawing.jpg └── promotional-images.svg ├── scripts ├── getpsl.py ├── npm_install.sh ├── release.sh ├── selenium_test.sh ├── source_me.sh └── test.sh ├── selenium ├── cookies.js ├── etags.js ├── integration_tests.js ├── package.json └── utils.js └── src ├── js ├── bootstrap.js ├── browser_compat.js ├── browser_utils.js ├── constants.js ├── contentscripts │ ├── fingercounting.cjs │ └── twitter.js ├── disk_map.js ├── domains │ ├── basedomain.js │ ├── mdfp.js │ ├── parties.js │ └── psl.js ├── external │ ├── react-dom │ │ ├── package.json │ │ └── react-dom.production.min.js │ └── react │ │ ├── package.json │ │ └── react.production.min.js ├── fakes.js ├── initialize.js ├── initialize_contentscripts.js ├── initialize_popup.js ├── package.json ├── popup.js ├── popup_components.js ├── popup_server.js ├── possum.js ├── reasons │ ├── etag.js │ ├── fingerprinting.js │ ├── handlers.js │ ├── headers.js │ ├── reasons.js │ ├── referer.js │ ├── user_url_deactivate.js │ └── utils.js ├── schemes.js ├── shim.js ├── store.js ├── suffixtree.js ├── tabs.js ├── test │ ├── .eslintrc │ ├── basedomain_test.js │ ├── constants_test.js │ ├── disk_map_test.js │ ├── fakes_tests.js │ ├── fingercounting_test.js │ ├── helper.js │ ├── mdfp_test.js │ ├── messages_test.js │ ├── parties_test.js │ ├── popup_test.js │ ├── possum_test.js │ ├── reasons │ │ ├── etag_test.js │ │ └── referer_test.js │ ├── reasons_test.js │ ├── schemes_test.js │ ├── shim_test.js │ ├── store_test.js │ ├── suffixtree_test.js │ ├── tabs_test.js │ ├── testing_utils.js │ ├── utils_test.js │ └── webrequest_test.js ├── utils.js └── webrequest.js ├── manifest.json ├── media ├── block-icon.png ├── etag-icon.png ├── etag.svg ├── fingerprinting-icon.png ├── icon-inactive.svg ├── icon-inactive256.png ├── icon-inactive48.png ├── icon-inactive96.png ├── icon.svg ├── icon128.png ├── icon16.png ├── icon256.png ├── icon32.png ├── icon48.png ├── icon64.png ├── icon96.png ├── logo-active-100.png ├── logo-inactive-100.png ├── logo-med256.png ├── med-possum.svg ├── onOff.svg ├── popup-icon.png ├── popup-icon.svg └── possum.svg └── skin ├── background.html ├── popup.css └── popup.html /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "webextensions": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2017, 9 | "sourceType": "script" 10 | }, 11 | "rules": { 12 | "no-unused-vars": "error", 13 | "no-undef": "error", 14 | "no-trailing-spaces": "error", 15 | "no-debugger": "error", 16 | "no-console": "error", 17 | "strict": ["error", "global"] 18 | }, 19 | "globals": { 20 | "exports": true, 21 | "define": true, 22 | "require": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | If you are reporting a bug, then providing the following is helpful: 2 | * browser name and version 3 | * privacy possum version 4 | 5 | If a website is broken, please provide the URL and describe the problem. 6 | 7 | If you would like to be extra helpful, and know how to access the Javascript console for the extension, please provide a debug log. The debug log may contain information about your browsing, such as URLS. Please read through it and redact information as you see fit. 8 | 9 | You can access the debug log from the popup console by running `popup.debug()`, or from the extension's background page by running `possum.prettyLog()`. 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # we only use package-lock.json in dev, so we ignore it 61 | package-lock.json 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: node_js 4 | node_js: 5 | - "10" 6 | install: make npm_install 7 | matrix: 8 | include: 9 | - name: "Node Unit Tests" 10 | install: make npm_install_node 11 | script: make test_node 12 | - name: "Selenium Unit Tests" 13 | addons: 14 | chrome: stable 15 | hosts: 16 | - firstparty.local 17 | - thirdparty.local 18 | before_install: 19 | - export DISPLAY=:99.0 20 | - sh -e /etc/init.d/xvfb start 21 | install: make npm_install_selenium 22 | script: make test_selenium 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | toplevel := $(shell git rev-parse --show-toplevel) 2 | possum_pem := $(toplevel)/possum.pem 3 | possum_zip := $(toplevel)/possum.zip 4 | possum_crx := $(toplevel)/possum.crx 5 | 6 | clean: 7 | rm -rf src/js/node_modules 8 | rm -f src/js/package-lock.json 9 | rm -rf selenium/node_modules 10 | rm -f selenium/package-lock.json 11 | rm -f possum.zip 12 | rm -f possum.crx 13 | 14 | test_node: src/js/node_modules 15 | ./scripts/test.sh 16 | 17 | test_selenium: selenium/node_modules 18 | ./scripts/selenium_test.sh 19 | 20 | npm_install_node: 21 | ./scripts/npm_install.sh src/js/. 22 | 23 | npm_install_selenium: 24 | ./scripts/npm_install.sh selenium/. 25 | 26 | psl: 27 | ./scripts/getpsl.py > src/js/domains/psl.js 28 | 29 | scripts/release.sh: scripts/source_me.sh 30 | 31 | possum.zip: $(shell git ls-files src) 32 | cd src/ && git ls-files | zip -q -9 -X $(possum_zip) -@ 33 | 34 | dev: src/js/node_modules selenium/node_modules 35 | src/js/node_modules: src/js/package.json 36 | cd src/js && npm install 37 | 38 | selenium/node_modules: selenium/package.json 39 | cd selenium && npm install 40 | 41 | possum.crx: possum.zip src/js/node_modules $(shell git ls-files src) 42 | src/js/node_modules/.bin/crx3-new $(possum_pem) < $(possum_zip) > $(possum_crx) 43 | 44 | git_tag_release: 45 | today=$$(date '+%Y.%-m.%-d'); \ 46 | manifest_version=$$(jq ".version" src/manifest.json); \ 47 | if [ $${manifest_version} != "\"$${today}\"" ]; then \ 48 | echo "bad version in manifest.json change $${manifest_version} to \"$${today}\""; \ 49 | exit 1;\ 50 | fi; \ 51 | echo "tagging version: \"$${today}\""; \ 52 | git tag $${today} 53 | 54 | release: clean git_tag_release possum.zip possum.crx 55 | 56 | .PHONY: clean test_node test_selenium npm_install_node npm_install_selenium psl release 57 | -------------------------------------------------------------------------------- /PRIVACY_POLICY.md: -------------------------------------------------------------------------------- 1 | ## Privacy Policy 2 | 3 | Privacy Possum does not collect or send data to any server. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/cowlicks/privacypossum.svg?branch=master)](https://travis-ci.org/cowlicks/privacypossum) 2 | 3 | ![logo](/src/media/logo-med256.png) 4 | 5 | Install for [Chrome](https://chrome.google.com/webstore/detail/privacy-possum/ommfjecdpepadiafbnidoiggfpbnkfbj) and for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/privacy-possum/). 6 | 7 | Privacy Possum makes tracking you less profitable. 8 | Companies gobble up data about you to create an asymmetry of information that they leverage for profit in ever expanding ways. 9 | Their profit comes from your informational disadvantage. 10 | Privacy Possum monkey wrenches common commercial tracking methods by reducing and falsifying the data gathered by tracking companies. 11 | 12 | # Current Features 13 | 14 | * Blocks cookies that let trackers uniquely identify you across websites 15 | * Blocks `refer` headers that reveal your browsing location 16 | * Blocks `etag` tracking which leverages browser caching to uniquely identify you 17 | * Blocks browser fingerprinting which tracks the inherent uniqueness of your browser 18 | 19 | # Threat Model 20 | 21 | __Privacy Possum does not have a threat model.__ Weird huh? We prioritize costing tracking companies money over protecting you. When considering some anti-tracking measure we do not ask "Is it possible to circumvent this?". Instead we ask "Is it cost-effective for a tracking company to circumvent this?". If the answer is "yes, no" we accept it. 22 | 23 | Tracking companies are growing, they own more infrastructure, and make more money than ever. This means they have a growing economic, technical, and political influence. And they are guiding the internet into a ever less private place. 24 | 25 | We think tackling the problem from an economic angle is extremely important, and will ultimately help shift the internet into more private place. 26 | 27 | 28 | # Related Projects 29 | 30 | ## Why not Privacy Badger? 31 | 32 | [Privacy Badger](https://github.com/EFForg/privacybadger) is another privacy focused browser extension maintained by the Electronic Frontier Foundation. 33 | I worked for the EFF on the project full time for 6 months, and found that its current privacy benefits to be limited. 34 | Adding new privacy protections was difficult, or impossible with the current architecture. 35 | And the project maintainers were not interested in fixing these issues. 36 | 37 | Comparisons with other anti-tracking projects can be found [here](docs/). 38 | 39 | # Tracker Blocking 40 | 41 | ## Browser Fingerprinting 42 | 43 | Sites can inspect aspects of your browser itself to determine its uniqueness, and therefore track you. This tracking technique is widely used. 44 | 45 | Privacy Badger's fingerprinting blocking has a large deficiency, when fingerprinting is detected, the *origin* is marked as tracking (not the URL). So everything from that origin is blocked in a 3rd party context. This is a problem because it can lead you to block everything from a cdn. To get around this, Privacy Badger adds CDN's to the "cookieblock list". This prevents cookies from being sent to origin's on the list. However, it then *prevents* fingerprinting scripts from being blocked, thus allowing fingerprinting. 46 | 47 | For example [many sites](https://publicwww.com/websites/cdn.jsdelivr.net%2Fnpm%2Ffingerprintjs2/) load fingerprintjs2 from the jsdelivr CDN, but this is on Privacy Badger's [cookie block list](https://github.com/EFForg/privacybadger/blob/08b61e85e5c361fe8b535ec9e33950431e28632a/src/data/yellowlist.txt#L314). So Privacy Badger will allow sites to load this script fingerprint you. 48 | 49 | Fingerprinting usually aggregates information across many esoteric browser API's, so we watch for this behavior. When we detect it, we block it. 50 | 51 | However many sites load first party fingerprinting code alongside other necessary code, like on reddit.com, so we can't simply block the script, or it will break the page. Instead when we see first party fingerprinting, we inject random data to spoil the fingerprint. Visit [valve.github.io/fingerprintjs2](https://valve.github.io/fingerprintjs2/) to see this. "get your fingerprint" multiple times, and see it change each time. 52 | 53 | ## Cookie Tracking 54 | 55 | Most online tracking happens through cookies. 56 | 57 | Privacy Possum blocks all 3rd party cookies. 58 | 59 | ## Etag Tracking 60 | 61 | Etags are a well known tracking vector, commonly used in lieu of cookies. 62 | 63 | We detect and block third party etags as follows: 64 | * The first time you see a request to a 3rd party url with an etag, strip the etag header and store its value. 65 | * The second time you see a 3rd party request to this url, compare the new etag you get with the old one. 66 | - If they are the same, this is not a tracking etag, allow etags for this url now and in the future. 67 | - If they are different, do not allow etags for this url now or in the future. 68 | 69 | Chrome withholds the `if-none-match` headers from `onBeforeSendHeaders` (https://developer.chrome.com/extensions/webRequest#Life_cycle_of_requests). 70 | So we can't prevent the browser from revealing some data via sending cache information, we are only able to intercept incoming etags from sources that are not already cached. 71 | 72 | ## Referer headers 73 | 74 | Referer headers are not exactly used for tracking themselves. But they are used in conjunction with other methods to track you. So we block them, and use a simple algorithm to unblock them when this causes problems. 75 | * Block `referer` headers to 3rd party sources 76 | * If the source responds with bad status code, we retry with the header added back in 77 | 78 | ## 301 Moved Permanent Redirect Tracking 79 | 80 | If you visit a site, it might load a resource that has a 301 redirect. The resource can redirect you to url that is *unique* to you. Then, the next time you see the original resource, your browser will load the unique url from the cache, and fetch the resource from there. Making you uniquely identified. 81 | 82 | This is a well known technique, but its pervasiveness is unknown to me. 83 | 84 | One solution to this would be to use cached 301 redirects from 3rd party sources. 85 | 86 | However we have not found a way to disable the cache like this in chrome's extension api. It is possible to intercept the redirect, but if you redirect back to the original url, you fetch from the cache again. 87 | One hack to disable the cache is to append a dummy query parameter, like `?` or `&`. With this you can re-try the url redirect to determine if it is unique per request. 88 | 89 | If this tests positive for a tracking redirect, we can't simply bust the cache every time by appending dummy query parameters because we'd end up with urls like `https://foo.com/?&&&&&&&&&&&&&&&....`. 90 | 91 | The next best solution would be to just block the request. This is yet to be implemented. 92 | 93 | # Development 94 | 95 | ## Dependencies 96 | 97 | The packaged extension contains *no* external dependencies. However we have several local dependencies we manually update for development purposes. First, we maintain our own copy of Mozilla's Public Suffix List, and Privacy Badger's Multi-domain First Parties list. These are used to determine if a given domain is "first party" or "third party". We also have a copy of React and ReactDOM. 98 | 99 | There are dependencies for development. These are all installed by running `npm install` inside `src/js`. 100 | 101 | ## Module System 102 | 103 | We use a lightweight implementation of nodes `require` function to implement modules without requiring a compilation step between running in the browser, and running in node. 104 | 105 | For this to work, we wrap each module in code like this (from `src/js/reasons/utils.js`): 106 | 107 | ```js 108 | "use strict"; 109 | 110 | [(function(exports) { 111 | 112 | ... 113 | 114 | Object.assign(exports, {sendUrlDeactivate, ...}); 115 | 116 | })].map(func => typeof exports == 'undefined' ? define('/reasons/utils', func) : func(exports)); 117 | ``` 118 | Note the path to this module must be passed in as a string to the `define` function. 119 | Exported stuff is assigned to properties on `exports` just like in node. 120 | 121 | ## Releases 122 | 123 | * edit the manifest.json version number to the form year.month.day with no leading zeros. 124 | * save and commit 125 | * run `make release`, this tags the repo with the manifest version and builds a zip file 126 | * test the zip file in a fresh instances of supported browsers. 127 | - for chrome run `google-chrome --user-data-dir=$(mktemp -d)` install the zip by dragging it to the chrome://extensions/ page. 128 | - for firefox run `firefox --profile $(mktemp -d) --no-remote --new-instance`. Go to `about:debugging` and click load temporary addon. Navigate to the zip file. 129 | - Do some basic Q&A tests, visit https://valve.github.io/fingerprintjs2/ https://reddit.com/ https://twitch.tv/ https://duckduckgo.com/ 130 | * upload the zip. 131 | - for chrome visit https://chrome.google.com/webstore/developer/edit/ommfjecdpepadiafbnidoiggfpbnkfbj record any other edits to the chrome store profile in this repo 132 | - for firefox visit https://addons.mozilla.org/en-US/developers/addon/privacy-possum/edit 133 | * notify users 134 | 135 | ## Testing 136 | 137 | From inside `src/js/` you can run node tests with `npm test`, check coverage with `npm run cover`, and run selenium tests inside `selenium/` by running `npm test`. 138 | -------------------------------------------------------------------------------- /docs/cydec-anti-fingerprinting-comparison.md: -------------------------------------------------------------------------------- 1 | Comparison with CyDec Platform Anti-Fingerprinting 2 | 3 | A brief review of the codebase showed spoofing of: 4 | * `window.screen.*` 5 | * `Date().getTimezoneOffset` 6 | * `HTMLCanvasElement.prototype.getContext` 7 | * `HTMLCanvasElement.prototype.toDataURL` 8 | * and the user agent sent with each request, but not in each page's javascript context 9 | 10 | This spoofing is done for *every page* and request. 11 | 12 | Privacy Possum spoofs all these except `toDataUrl` (more on this later). But the way PP spoofs is very different. We spoof only when fingerprinting is detected, and then we only spoof the specific fingerprinting scripts. We do this because commercial fingerprinting has been easy to detect with our heuristic, and because spoofing has the potential to break websites. Then when we do spoof, we spoof a lot more data. Testing on [fingerprintjs2's test page](https://valve.github.io/fingerprintjs2/), PP spoofs 11 fields, CyDec spoofs 3 (and notably not the User Agent). 13 | 14 | It is possible a website could configure fingerprintjs2 to disable these 3 fingerprinting vectors to circumvent CyDec's anti-fingerprinting measures. For the website to do this with PP they would have to disable 11 vectors. Since fingerprinting tools add many fingerprinting vectors into one fingerprint to increase its uniqueness. So disabling as many as possible decreases your uniqueness. 15 | 16 | Privacy Possum probably should spoof `toDataUrl`, since this is usually used to dump the results of some canvas fingerprinting into a hashable format. 17 | 18 | There is also a very odd & suspicious thing CyDec does, it frequently makes requests to localhost:61006, it has something to do with spoofing useragents. Does anyone know why? 19 | 20 | One simple improvement CyDec could make is to also spoof the user agent in the webpage itself. 21 | 22 | Privacy Possum does several things that CyDec does not. Among other things it blocks 3rd party cookie tracking, etag tracking, and refer data collection. On top of current tracker blocking methods, it is built on top of an extensible framework that allows for heuristics for other tracking techniques to be added. 23 | -------------------------------------------------------------------------------- /docs/moz-validation-results: -------------------------------------------------------------------------------- 1 | Comments on validation results: 2 | 3 | Unsafe assignment to innerHTML Warning 4 | js/test/testing_utils.js 5 | 6 | The refers to code in a unittest, which is not exposed to functioning addon at all. We use the code to clear fake dom between unit tests. 7 | 8 | Known JS library detected warnings: 9 | js/external/react/react.production.min.js 10 | js/external/react-dom/react-dom.production.min.js 11 | 12 | I use react for the popup. I don't use jsx so I can avoid transpiling. 13 | 14 | Violation of Mozilla conditions of use Warning: Words found that violate the Mozilla conditions of use 15 | js/domains/psl.js 16 | 17 | This is referring to some domain name in Mozilla's own Public Suffix list. I use this list to determine what is a 3rd party. 18 | -------------------------------------------------------------------------------- /docs/notes.md: -------------------------------------------------------------------------------- 1 | # Tracking Feature Roadmap 2 | 3 | * ad blocker blocking? 4 | * surrogates 5 | * widgets 6 | * rules like: 7 | - youtube -> youtube-nocookie 8 | - inject header for twitter 9 | 10 | * evercookie/supercookie protection, start with localstorage read/write in 3rd party frames 11 | 12 | * tracking pixels 13 | -reddit sets pixels specific per ad on reddit.com. Advertisers embed a pixel in their home pages. Reddit tracks conversion between these. 14 | * 304 Not Modified tracking - if you load a script once, `foo.com/script.js` and the server sends it with some unique id embedded in it, then the next time you load that script the server can respond that you already have that script stored. Then you will load the script, it will check the unique id. 15 | * pixel cache tracking 16 | 17 | # Testing roadmap 18 | 19 | * integration testing with selenium 20 | * travis-ci unittesting 21 | * travis-ci selenium 22 | 23 | # todo 24 | 25 | * move todos to issues on github 26 | * add promotional titles to chrome Small tile - 440x280, Large tile - 920x680, Marquee - 1400x560 27 | * handlers should just be for dispatching stuff, not starting listeners 28 | 29 | * update psl script to reflect new location 30 | * error getting fingerprinting message from tabId=-1? from extension? 31 | * error when inspect popup creates it, make currentTab work with this 32 | * change badge text color to grey 33 | * make icon not blurry in chrome store? 34 | * add screenshots for chrome store, resize current ones to 1280 pixels wide and 800 pixels high 35 | * add video of fingerprint blocking 36 | * add a help me link to github issues or email me 37 | 38 | 39 | ## techniques 40 | 41 | We block *all* thirdparty cookies. PB spends a lot of energy inspecting third 42 | party coookies, then trying to detect if they are tracking, then blocking the 43 | domains they use. Howevever we can almost *always* block *all* third party 44 | cookies and not break anything. This eliminates a lot of tracking. 45 | 46 | We also strip referer headers, with cookies already blocked, this adds little 47 | to *your* privacy. But it does effect the 3rd parties analytics so they will 48 | know less about where their content is being used. 49 | 50 | We detect fingerprinting and block it in a first, and third party context. 51 | 52 | for first parties, we don't outright block the request, because it is more 53 | likely bundled with code that is essential to functionality of the website. 54 | 55 | t.co urls get unwrapped on twitter and tweetdeck 56 | 57 | ## goals 58 | 59 | Keep the codebase small with few external dependencies. 60 | 61 | Keep the codebase easy to build and develop. 62 | We prefer to have the extension runnable without any build tools. So it can be through the extension menu. 63 | Put fewer layers of abstraction between developer and install. 64 | 65 | ## threats 66 | 67 | The "threat model" of this project is not purely induvidual. The threat it 68 | adresses is the tracking industry. So not all measures in this project are 69 | intended to protect you from all tracking, but they are intended to protect you 70 | from common commercial tracking. 71 | 72 | We want to threaten tracking on a large scale, we want to be the threat model 73 | of the tracking industry. 74 | 75 | ## architecture 76 | 77 | ### reasons 78 | The ways we change normal browser behavior are defined by "reasons.js". Each way we change behavior has its own "reason". 79 | reasons are loaded into various "handlers". Handlers intercept browser behaviour and modify them based on whatever reasons they have loaded. 80 | 81 | ### data structures 82 | 83 | We store domain-name related data in a heirarchical tree data-structure, TLD's 84 | at the root, like the real DNS system. This lets us store aggregate data 85 | easily. So we can get all urls with a certain hostname which we store data for, 86 | or all subdomains of a given hostname, etc. The data structure has synchronouse 87 | gets, and asynchronous sets. 88 | 89 | We also have a datastructure for monitoring tabs, and getting informationg 90 | about them in a synchronous way. This is defined in tabs.js. 91 | 92 | ### shims 93 | 94 | We extensively test the project headlessly. To do this, we shim all the browser extension interfaces in shim.js. We define the fake interfaces in fakes.js. 95 | -------------------------------------------------------------------------------- /docs/panopticlick.md: -------------------------------------------------------------------------------- 1 | Why doesn't panopticlick trigger? 2 | 3 | TL;DR Panopticlick and Am I Unique use a homerolled assortment of tracking code that is impractical for commercial tracking. 4 | 5 | I'll go into a little detail about Panopticlick to explain more. Panopticlick uses a deployment of the open source fingerprinting tool Fingerprintjs2, along with their own unique fingerprinting code. 6 | 7 | I added some debug code and visited Panopticlick I see Privacy Possum detects the page accessing 12 API's that are marked for watching for fingerprinting. Except this is split over 3 different scripts: 8 | https://panopticlick.eff.org/static/fp2.js 9 | https://panopticlick.eff.org/static/fetch_whorls.js 10 | https://panopticlick.eff.org/static/deployJava.js 11 | 12 | Privacy watches for fingerprinting on *per script basis*, this is a reasonable assumption because, normally a websites tracking code is bundled into one place, so that the tracking info can be easily aggregated and used. I'm not aware of a real deployment where tracking is split up like this. It is practical for panopticlick (and Am I Unique) because they want to present information about your tracking independently, and manage the code to do that in a more practical way. 13 | 14 | For a demonstration of the fingerprinting detection code, I usually point folks to: 15 | https://github.com/Valve/fingerprintjs2 16 | 17 | I think it is worth considering cases like Panopticlick, or Am I Unique, because they can be used to evade PP's novel detection. But I have not seen a case like this in the wild. 18 | -------------------------------------------------------------------------------- /media/chrome-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/media/chrome-icon.png -------------------------------------------------------------------------------- /media/chrome-large-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/media/chrome-large-title.png -------------------------------------------------------------------------------- /media/chrome-marquee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/media/chrome-marquee.png -------------------------------------------------------------------------------- /media/chrome-small-title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/media/chrome-small-title.png -------------------------------------------------------------------------------- /media/firefox-large-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/media/firefox-large-icon.png -------------------------------------------------------------------------------- /media/firefox-small-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/media/firefox-small-icon.png -------------------------------------------------------------------------------- /media/original-drawing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/media/original-drawing.jpg -------------------------------------------------------------------------------- /scripts/getpsl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | Based on a Privacy Badger pull request: 4 | https://github.com/cowlicks/privacybadgerchrome/blob/300d41eb1de22493aabdb46201a148c028a6228d/scripts/convertpsl.py 5 | ''' 6 | 7 | # script based on 8 | # https://github.com/adblockplus/buildtools/blob/d090e00610a58cebc78478ae33e896e6b949fc12/publicSuffixListUpdater.py 9 | 10 | import json 11 | 12 | import urllib.request 13 | 14 | psl_url = 'https://publicsuffix.org/list/public_suffix_list.dat' 15 | 16 | file_text = '''/* eslint-disable */ 17 | const publicSuffixes = new Map( 18 | %s 19 | ); 20 | 21 | export {publicSuffixes};''' 22 | 23 | 24 | def get_psl_text(): 25 | return urllib.request.urlopen(psl_url).read() 26 | 27 | 28 | def punycode(x): 29 | return x.encode('idna').decode() 30 | 31 | 32 | def convert(psl_lines): 33 | suffixes = [] 34 | 35 | for line in psl_lines: 36 | if line.startswith('//') or '.' not in line: 37 | continue 38 | if line.startswith('*.'): 39 | suffixes.append([punycode(line[2:]), ]) 40 | elif line.startswith('!'): 41 | suffixes.append([punycode(line[1:]), 0]) 42 | else: 43 | suffixes.append([punycode(line), 1]) 44 | 45 | entries = sorted(suffixes, key=lambda x: x[0]) 46 | return file_text % '],\n'.join(json.dumps(entries).split('], ')) 47 | 48 | 49 | if __name__ == '__main__': 50 | psl_lines = get_psl_text().decode().split('\n') 51 | print(convert(psl_lines)) 52 | -------------------------------------------------------------------------------- /scripts/npm_install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | source $(git rev-parse --show-toplevel)/scripts/source_me.sh 3 | 4 | run_in_dir $1 "npm install" 5 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | source $(git rev-parse --show-toplevel)/scripts/source_me.sh 3 | manifest=${src_dir}/manifest.json 4 | today=$(date '+%Y.%-m.%-d') 5 | out_file=${toplevel}/possum.zip 6 | 7 | manifest_version=$(jq ".version" ${manifest}) 8 | if [ ${manifest_version} != "\"${today}\"" ]; then 9 | echo "bad version in manifest.json change ${manifest_version} to \"${today}\"" 10 | exit 1 11 | fi 12 | 13 | echo "tagging version: \"${today}\"" 14 | git tag ${today} 15 | 16 | pushd ${src_dir} > /dev/null 17 | trap "popd > /dev/null" EXIT 18 | 19 | echo "packaging extension to: ${out_file}" 20 | git ls-files | zip -q -9 -X ${out_file} -@ 21 | -------------------------------------------------------------------------------- /scripts/selenium_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | source $(git rev-parse --show-toplevel)/scripts/source_me.sh 3 | 4 | run_in_dir $selenium_dir "npm test" 5 | -------------------------------------------------------------------------------- /scripts/source_me.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Load this by running 3 | # `source $(git rev-parse --show-toplevel)/scripts/source_me.sh` 4 | # in a script. 5 | toplevel=$(git rev-parse --show-toplevel) 6 | src_dir=${toplevel}/src 7 | js_dir=${src_dir}/js 8 | selenium_dir=${toplevel}/selenium 9 | 10 | run_in_dir() { 11 | pushd $1 > /dev/null 12 | trap "popd > /dev/null" EXIT 13 | $2 14 | } 15 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | source $(git rev-parse --show-toplevel)/scripts/source_me.sh 3 | 4 | run_in_dir $js_dir "npm test" 5 | -------------------------------------------------------------------------------- /selenium/cookies.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'), 4 | cookieParser = require('cookie-parser'), 5 | {requestRecorderMiddleware, baseTestApp} = require('./utils'); 6 | 7 | let fpcookie = {name: '1pname', value: '1pvalue'}, 8 | tpcookie = {name: '3pname', value: '3pvalue'}; 9 | 10 | 11 | function cookieSetterApp(cookieName, cookieValue, app = express()) { 12 | app.use(cookieParser()); 13 | app.use((req, res, next) => { 14 | res.cookie(cookieName, cookieValue); 15 | next(); 16 | }); 17 | requestRecorderMiddleware(app); 18 | return app; 19 | } 20 | 21 | function cookieApp() { 22 | let fpApp = cookieSetterApp(fpcookie.name, fpcookie.value), 23 | tpApp = cookieSetterApp(tpcookie.name, tpcookie.value); 24 | return baseTestApp(fpApp, tpApp); 25 | } 26 | 27 | Object.assign(module.exports, {cookieApp, fpcookie, tpcookie}); 28 | -------------------------------------------------------------------------------- /selenium/etags.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let express = require('express'), 4 | {requestRecorderMiddleware, baseTestApp} = require('./utils'); 5 | 6 | const ifNoneMatch = 'if-none-match'; 7 | 8 | function etagTracker(app = express()) { 9 | let tracked = new Map(), 10 | etagCount = 0; 11 | app.set('etag', false); 12 | Object.assign(app, {tracked, etagCount}); 13 | app.use((req, res, next) => { 14 | if (req.headers.hasOwnProperty(ifNoneMatch) && tracked.has(req.headers[ifNoneMatch])) { 15 | let key = req.headers[ifNoneMatch]; 16 | tracked.set(key, 1 + tracked.get(key)); 17 | res.statusCode = 304; 18 | } else { 19 | res.setHeader('etag', `W/"${String(etagCount += 1)}"`); 20 | } 21 | next(); 22 | }); 23 | return app; 24 | } 25 | 26 | function etagApp() { 27 | let fpApp = requestRecorderMiddleware(etagTracker()), 28 | tpApp = requestRecorderMiddleware(etagTracker()); 29 | return baseTestApp(fpApp, tpApp); 30 | } 31 | 32 | Object.assign(module.exports, {etagApp}); 33 | -------------------------------------------------------------------------------- /selenium/integration_tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const {assert} = require('chai'); 4 | 5 | const {newDriver, startApp, stopApp, firstPartyHost} = require('./utils'), 6 | {cookieApp, fpcookie} = require("./cookies"), 7 | {etagApp} = require('./etags'); 8 | 9 | async function sleep(ms) { 10 | return new Promise(resolve => setTimeout(resolve, ms)); 11 | } 12 | 13 | beforeEach(async function() { 14 | this.driver = await newDriver(); 15 | await sleep(250); 16 | }); 17 | 18 | afterEach(async function() { 19 | await this.driver.quit(); 20 | }); 21 | 22 | describe('etag tests', function() { 23 | beforeEach(async function() { 24 | this.app = etagApp(); 25 | startApp(this.app); 26 | }); 27 | afterEach(function() { 28 | stopApp(this.app); 29 | }); 30 | it('blocks etags', async function() { 31 | let {app, driver} = this; 32 | await driver.get(firstPartyHost); // etag gets set 33 | await driver.get(firstPartyHost); // browser sends if-none-match to check if etag still valid 34 | let req = await app.firstParty.requests.next(); 35 | //let req3 = await app.thirdParty.requests.next(); 36 | assert.isTrue(req.headers.hasOwnProperty('if-none-match'), 'allows 1st party etags on first visit'); 37 | // known failure on chrome due to lack of access to caching headers in chrome webrquest api 38 | //assert.isFalse(req3.headers.hasOwnProperty('if-none-match'), 'blocks 3rd party etags headers on first visit'); 39 | }); 40 | }); 41 | 42 | describe('cookie tests', function() { 43 | beforeEach(async function() { 44 | this.app = cookieApp(); 45 | startApp(this.app); 46 | }); 47 | afterEach(function() { 48 | stopApp(this.app); 49 | }); 50 | 51 | it('blocks cookies', async function() { 52 | let {app, driver} = this; 53 | 54 | await driver.get(firstPartyHost); 55 | let request = await app.firstParty.requests.next(); 56 | // no cookies initially 57 | assert.deepEqual(request.cookies, {}); 58 | request = await app.thirdParty.requests.next(); 59 | assert.deepEqual(request.cookies, {}); 60 | 61 | await driver.get(firstPartyHost); 62 | request = await app.firstParty.requests.next(); 63 | // now we have first party cookies set 64 | assert.deepEqual(request.cookies, {[fpcookie.name]: fpcookie.value}); 65 | request = await app.thirdParty.requests.next(); 66 | // but not third party cookies 67 | assert.deepEqual(request.cookies, {}); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /selenium/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "privacy-possum-selenium-tests", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "integration_tests.js", 6 | "scripts": { 7 | "test": "mocha ./integration_tests.js", 8 | "debug": "mocha debug ./integration_tests.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "chai": "^4.2.0", 14 | "chromedriver": "^2.42.0", 15 | "cookie-parser": "^1.4.3", 16 | "express": "^4.16.3", 17 | "mocha": "^5.2.0", 18 | "selenium-webdriver": "^4.0.0-alpha.1", 19 | "vhost": "^3.0.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /selenium/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sw = require('selenium-webdriver'), 4 | {createServer} = require('http'), 5 | path = require('path'), 6 | vhost = require('vhost'), 7 | express = require('express'); 8 | 9 | function startApp(app, port=PORT) { 10 | app.server = createServer(app); 11 | app.server.listen(port); 12 | } 13 | 14 | const srcDir = '../src/.', 15 | PORT = 8000, 16 | host = (hostname, port) => `${hostname}:${port}`, 17 | firstPartyHostname = 'firstparty.local', 18 | thirdPartyHostname = 'thirdparty.local', 19 | firstPartyHost = host(firstPartyHostname, PORT), 20 | thirdPartyHost = host(thirdPartyHostname, PORT); 21 | 22 | function startApp(app, port=PORT) { 23 | app.server = createServer(app); 24 | app.server.listen(port); 25 | } 26 | 27 | function stopApp(app) { 28 | app.server.close(); 29 | } 30 | 31 | /* 32 | * in /etc/hosts this requires: 33 | * 127.0.0.1 firstparty.local 34 | * 127.0.0.1 thirdparty.local 35 | */ 36 | 37 | async function loadDriverWithExtension(extPath) { 38 | let chromeOptions = sw.Capabilities.chrome(); 39 | chromeOptions.set("chromeOptions", {"args": [ 40 | `--load-extension=${extPath}`, 41 | '--no-sandbox', 42 | ]}); 43 | return new sw.Builder() 44 | .forBrowser('chrome') 45 | .withCapabilities(chromeOptions) 46 | .build(); 47 | } 48 | 49 | async function newDriver() { 50 | const srcPath = path.resolve(__dirname, srcDir); 51 | return await loadDriverWithExtension(srcPath); 52 | } 53 | 54 | class Channel { 55 | // async stack datastructure 56 | constructor() { 57 | this.items = []; 58 | this.waiting = []; 59 | } 60 | async popQueue() { 61 | if (this.items.length > 0) { 62 | return this.items.pop(); 63 | } else { 64 | return new Promise((resolve) => { 65 | this.waiting.push(resolve); 66 | }); 67 | } 68 | } 69 | 70 | // Get the item from the top of the stack, or wait for an item if there are none. 71 | async next() { 72 | return await this.popQueue(); 73 | } 74 | 75 | // Push an item onto the stack. 76 | push(item) { 77 | if (this.waiting.length > 0) { 78 | this.waiting.pop()(item); 79 | } else { 80 | this.items.push(item); 81 | } 82 | } 83 | } 84 | 85 | function requestRecorderMiddleware(app = express()) { 86 | app.requests = new Channel(); 87 | app.responses = new Channel(); 88 | app.use((req, res, next) => { 89 | app.requests.push(req); 90 | app.responses.push(res); 91 | next(); 92 | }); 93 | return app; 94 | } 95 | 96 | function firstPartyApp(app = express(), tpHost = thirdPartyHost) { 97 | app.get('*', (req, res) => { 98 | return res.send( 99 | `` 100 | ); 101 | }); 102 | return app; 103 | } 104 | 105 | function thirdPartyApp(app = express()) { 106 | app.get('*', (req, res) => { 107 | return res.send('console.log("third party script")'); 108 | }); 109 | return app; 110 | } 111 | 112 | 113 | function baseTestApp(fpApp, tpApp, app = express(), fpHostname = firstPartyHostname, tpHostname = thirdPartyHostname) { 114 | let firstParty = firstPartyApp(fpApp), 115 | thirdParty = thirdPartyApp(tpApp); 116 | app.all('/', vhost(fpHostname, firstParty)); 117 | app.all('/tracker.js', vhost(tpHostname, thirdParty)); 118 | Object.assign(app, {firstParty, thirdParty}); 119 | return app; 120 | } 121 | 122 | Object.assign(module.exports, {newDriver, startApp, stopApp, PORT, firstPartyHostname, thirdPartyHostname, firstPartyHost, thirdPartyHost, Channel, requestRecorderMiddleware, firstPartyApp, thirdPartyApp, baseTestApp}); 123 | -------------------------------------------------------------------------------- /src/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple commonjs-like module system that is compatible with node. 3 | * 4 | * We wrap each module in: 5 | * 6 | * [(function(exports) { 7 | * ... 8 | * 9 | * })].map(func => typeof exports == 'undefined' ? define('/path/to/module', func) : func(exports)); 10 | * 11 | * Then in the module, exported objects are assigned to properties on `exports`. 12 | * 13 | * We use `require` to import modules relative to the current location, like so: 14 | * 15 | * const {someFunc} = require('./name'); 16 | * 17 | * someFunc(...); 18 | * 19 | * Just like you would in node. 20 | * 21 | * We use this wrapping (instead of a IIAFE) because it lets us load the module lazily, or 22 | * immediately, based on the environment. It seemed to be the most concise 23 | * anonymous expression. 24 | * 25 | * New files need to be added to the manifest.json. And to appropriate html files, like popup.html. 26 | */ 27 | 28 | function require(module) { 29 | let before = require.loc; 30 | while (!module.startsWith('./')) { 31 | module = module.substr(1); 32 | require.loc = require.loc['..']; 33 | } 34 | 35 | let arr = module.substr(2).split('/'); 36 | 37 | let part = arr.reduce( 38 | (obj, part) => { 39 | if (typeof obj[part] === 'function') { 40 | obj[part](obj[part] = {}); 41 | } 42 | require.loc = obj[part]; 43 | return obj[part]; 44 | }, 45 | require.loc 46 | ); 47 | require.loc = before; 48 | return part; 49 | } 50 | 51 | // ('/path/to/module', func) 52 | function define(name, moduleFunc) { // eslint-disable-line 53 | let arr = name.substr(1).split('/'), 54 | lastName = arr.pop(); 55 | 56 | let baseObj = arr.reduce( 57 | (obj, part) => obj.hasOwnProperty(part) ? obj[part] : obj[part] = {'..': obj}, 58 | require.scopes 59 | ); 60 | 61 | baseObj[lastName] = moduleFunc; 62 | } 63 | require.scopes = {}; 64 | require.loc = require.scopes; 65 | -------------------------------------------------------------------------------- /src/js/browser_compat.js: -------------------------------------------------------------------------------- 1 | import {shim} from './shim.js'; 2 | 3 | /* 4 | * Firefox & Chrome now take different values in opt_extraInfoSpec 5 | */ 6 | function getOnBeforeRequestOptions({OnBeforeRequestOptions} = shim) { 7 | return ["BLOCKING"].map(x => OnBeforeRequestOptions[x]).filter(x => x); 8 | } 9 | 10 | function getOnBeforeSendHeadersOptions({OnBeforeSendHeadersOptions} = shim) { 11 | return ["BLOCKING", "REQUEST_HEADERS", "REQUESTHEADERS", "EXTRA_HEADERS"].map(x => OnBeforeSendHeadersOptions[x]).filter(x => x); 12 | } 13 | 14 | function getOnHeadersReceivedOptions({OnHeadersReceivedOptions} = shim) { 15 | return ["BLOCKING", "RESPONSE_HEADERS", "RESPONSEHEADERS", "EXTRA_HEADERS"].map(x => OnHeadersReceivedOptions[x]).filter(x => x); 16 | } 17 | 18 | export {getOnBeforeRequestOptions, getOnBeforeSendHeadersOptions, getOnHeadersReceivedOptions}; 19 | -------------------------------------------------------------------------------- /src/js/browser_utils.js: -------------------------------------------------------------------------------- 1 | import {activeIcons, inactiveIcons} from './constants.js'; 2 | 3 | import {shim} from './shim.js'; 4 | const {tabsQuery, tabsGet, setIcon, setBadgeText} = shim; 5 | 6 | 7 | async function currentTab() { 8 | const active = true, lastFocusedWindow = true; 9 | return new Promise(resolve => { 10 | tabsQuery({active, lastFocusedWindow}, tabsFirstTry => { 11 | if (tabsFirstTry.length > 0) { 12 | resolve(tabsFirstTry[0]); 13 | } else { // tab not focused 14 | tabsQuery({active}, tabsSecondTry => { 15 | resolve(tabsSecondTry[0]); 16 | }); 17 | } 18 | }); 19 | }); 20 | } 21 | 22 | function errorOccurred(cb = ()=>{}) { 23 | if (typeof chrome !== 'undefined' && chrome.runtime.lastError) { 24 | cb(chrome.runtime.lastError); 25 | return true; 26 | } else { 27 | return false; 28 | } 29 | } 30 | 31 | async function tabExists(tabId) { 32 | if (tabId >= 0) { 33 | return await new Promise(resolve => { 34 | tabsGet(tabId, () => { 35 | if (!errorOccurred()) { 36 | resolve(true); 37 | } else { 38 | resolve(false); 39 | } 40 | }); 41 | }); 42 | } else { 43 | return true; 44 | } 45 | } 46 | 47 | // todo after setIcon return's a promise, make this return a promise 48 | async function setTabIconActive(tabId, active) { 49 | if (await tabExists(tabId)) { 50 | let icons = active ? activeIcons : inactiveIcons; 51 | setIcon({tabId: tabId, path: icons}); 52 | } 53 | } 54 | 55 | async function safeSetBadgeText(tabId, text) { 56 | if (await tabExists(tabId)) { 57 | setBadgeText({text, tabId}); 58 | } 59 | } 60 | 61 | export { 62 | currentTab, 63 | errorOccurred, 64 | tabExists, 65 | setTabIconActive, 66 | safeSetBadgeText, 67 | } 68 | -------------------------------------------------------------------------------- /src/js/constants.js: -------------------------------------------------------------------------------- 1 | // disk name 2 | const DISK_NAME = 'p055um'; 3 | 4 | // BlockingResponse types 5 | const NO_ACTION = {}, 6 | CANCEL = {cancel: true}; 7 | const responses = {CANCEL, NO_ACTION}; 8 | 9 | // suffix of main_frame & sub_frame 10 | const TYPES = { 11 | main_frame : 'main_frame', 12 | sub_frame : 'sub_frame', 13 | }; 14 | 15 | const FRAME_END = '_frame'; 16 | 17 | const inactiveIcons = { 18 | 48: "/media/icon-inactive48.png", 19 | 96: "/media/icon-inactive96.png", 20 | 256: "/media/icon-inactive256.png", 21 | } 22 | 23 | const activeIcons = { 24 | 48: "/media/icon48.png", 25 | 64: "/media/icon64.png", 26 | 96: "/media/icon96.png", 27 | 256: "/media/icon256.png", 28 | } 29 | 30 | const request_methods = { 31 | ON_BEFORE_REQUEST: 'onBeforeRequest', 32 | ON_HEADERS_RECEIVED: 'onHeadersReceived', 33 | ON_BEFORE_SEND_HEADERS: 'onBeforeSendHeaders', 34 | }; 35 | 36 | const http_headers = { 37 | REQUEST: 'requestHeaders', 38 | RESPONSE: 'responseHeaders', 39 | }; 40 | 41 | const header_methods = new Map([ 42 | [request_methods.ON_HEADERS_RECEIVED, http_headers.RESPONSE], 43 | [request_methods.ON_BEFORE_SEND_HEADERS, http_headers.REQUEST], 44 | ]); 45 | 46 | // reasons 47 | // todo move these into their own namespace 48 | const FINGERPRINTING = 'fingerprinting', 49 | USER_HOST_DEACTIVATE = 'user_host_deactivate', 50 | USER_URL_DEACTIVATE = 'user_url_deactivate', 51 | BLOCK = 'block', 52 | HEADER_DEACTIVATE_ON_HOST = 'header_deactivate_on_host'; 53 | 54 | const CONTENTSCRIPTS = new Set([ 55 | '/js/bootstrap.js', 56 | '/js/contentscripts/fingercounting.cjs', 57 | '/js/initialize_contentscripts.js', 58 | ]); 59 | 60 | const etag = { 61 | ETAG_TRACKING: 'etag_tracking', 62 | ETAG_SAFE: 'etag_safe', 63 | } 64 | 65 | const TAB_DEACTIVATE = 'tab_deactivate', 66 | TAB_DEACTIVATE_HEADERS = 'tab_deactivate_headers'; 67 | 68 | const reasons = {FINGERPRINTING, USER_HOST_DEACTIVATE, USER_URL_DEACTIVATE, USER_HOST_DEACTIVATE}; 69 | 70 | // ports 71 | const POPUP = 'popup'; 72 | 73 | const REMOVE_ACTION = 'remove_action'; 74 | 75 | const GET_DEBUG_LOG = 'get_debug_log'; 76 | 77 | export { 78 | DISK_NAME, 79 | responses, 80 | NO_ACTION, 81 | CANCEL, 82 | TYPES, 83 | FRAME_END, 84 | inactiveIcons, 85 | activeIcons, 86 | reasons, 87 | request_methods, 88 | header_methods, 89 | FINGERPRINTING, 90 | CONTENTSCRIPTS, 91 | USER_HOST_DEACTIVATE, 92 | USER_URL_DEACTIVATE, 93 | BLOCK, 94 | HEADER_DEACTIVATE_ON_HOST, 95 | etag, 96 | TAB_DEACTIVATE, 97 | TAB_DEACTIVATE_HEADERS, 98 | POPUP, 99 | REMOVE_ACTION, 100 | GET_DEBUG_LOG, 101 | }; 102 | -------------------------------------------------------------------------------- /src/js/contentscripts/twitter.js: -------------------------------------------------------------------------------- 1 | let query_param, 2 | tcos_with_destination, 3 | fixes = {}; 4 | 5 | function setQuery() { 6 | if (/https?:\/\/tweetdeck.twitter.com\//.test(window.location.href)) { 7 | query_param = 'data-full-url'; // tweetdeck 8 | } else { 9 | query_param = 'data-expanded-url'; // twitter and tests 10 | } 11 | tcos_with_destination = `a[${query_param}][href^='https://t.co/'], a[${query_param}][href^='http://t.co/']`; 12 | } 13 | 14 | function maybeAddNoreferrer(link) { 15 | let rel = link.rel ? link.rel : ""; 16 | if (!rel.includes("noreferrer")) {rel += " noreferrer";} 17 | link.rel = rel; 18 | } 19 | 20 | function unwrapTco(tco, destination) { 21 | if (!destination) { 22 | return; 23 | } 24 | tco.href = destination; 25 | tco.addEventListener("click", function (e) { 26 | e.stopPropagation(); 27 | }); 28 | maybeAddNoreferrer(tco); 29 | } 30 | 31 | function findInAllFrames(query) { 32 | let out = []; 33 | document.querySelectorAll(query).forEach((node) => { 34 | out.push(node); 35 | }); 36 | Array.from(document.getElementsByTagName('iframe')).forEach((iframe) => { 37 | try { 38 | iframe.contentDocument.querySelectorAll(query).forEach((node) => { 39 | out.push(node); 40 | }); 41 | } catch(e) { 42 | // pass on cross origin iframe errors 43 | } 44 | }); 45 | return out; 46 | } 47 | 48 | function unwrapTwitterURLs() { 49 | findInAllFrames(tcos_with_destination).forEach((link) => { 50 | let attr = link.getAttribute(query_param); 51 | if (attr && (attr.startsWith("https://") || attr.startsWith("http://"))) { 52 | fixes[link.href] = attr; 53 | unwrapTco(link, attr); 54 | } 55 | }); 56 | findInAllFrames("a[href^='https://t.co/'], a[href^='http://t.co/'").forEach((link) => { 57 | if (fixes.hasOwnProperty(link.href)) { 58 | unwrapTco(link, fixes[link.href]); 59 | } 60 | }); 61 | } 62 | 63 | setQuery(); 64 | unwrapTwitterURLs(); 65 | setInterval(unwrapTwitterURLs, 2000); 66 | -------------------------------------------------------------------------------- /src/js/disk_map.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DiskMap wraps an asynchronous get/set api, and prefixes its keys with a 3 | * string, effectively giving each instance of DiskMap a namespace. This allows 4 | * us to store arbitrary keys without having to worry about collisions with 5 | * keys from other things. 6 | * 7 | * Another way to acheive namespacing like this is to store a nested object. 8 | * But everytime you update an entry in the object this way, it updates the 9 | * whole thing. So storing large frequently updated objects can potentially be 10 | * resource intensive. So instead we do this. 11 | */ 12 | 13 | 14 | class DiskMap { 15 | constructor(name, disk) { 16 | this.disk = disk; 17 | this.name = name; 18 | this.keys_key = 'keys_for_' + name; 19 | this.keys = new Set(); 20 | } 21 | 22 | static async load(name, disk) { 23 | let out = new DiskMap(name, disk); 24 | await out.loadKeys(); 25 | return out; 26 | } 27 | 28 | async loadKeys() { 29 | return this.keys = await this.getKeys(); 30 | } 31 | 32 | async getKeys() { 33 | return new Promise(resolve => { 34 | this.disk.get(this.keys_key, keys => { 35 | keys = (typeof keys === 'undefined') ? [] : keys; 36 | resolve(new Set(keys)); 37 | }); 38 | }); 39 | } 40 | 41 | maybeAddKey(key) { 42 | return new Promise(resolve => { 43 | if (this.keys.has(key)) { 44 | return resolve(); 45 | } 46 | this.keys.add(key); 47 | return this.disk.set(this.keys_key, Array.from(this.keys), resolve); 48 | }); 49 | } 50 | 51 | async toMap() { 52 | let out = new Map(); 53 | for (let key of this.keys) { 54 | out.set(key, await this.get(key)); 55 | } 56 | return out; 57 | } 58 | 59 | async set(key, value) { 60 | await this.maybeAddKey(key); 61 | return new Promise(resolve => { 62 | this.disk.set((this.name + key), [key, value], resolve); 63 | }); 64 | } 65 | 66 | get(key) { 67 | return new Promise(resolve => { 68 | this.disk.get((this.name + key), keyValue => { 69 | return resolve(typeof keyValue !== 'undefined' ? keyValue[1]: undefined); 70 | }); 71 | }); 72 | } 73 | 74 | has(key) { 75 | return this.keys.has(key); 76 | } 77 | 78 | async delete(key) { 79 | if (this.keys.has(key)) { 80 | this.keys.delete(key); 81 | await new Promise(resolve => this.disk.set(this.keys_key, Array.from(this.keys), resolve)); 82 | return new Promise(resolve => { 83 | this.disk.remove((this.name + key), r => resolve(r)); 84 | }); 85 | } 86 | return false; 87 | } 88 | } 89 | 90 | export {DiskMap}; 91 | -------------------------------------------------------------------------------- /src/js/domains/basedomain.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on https://github.com/cowlicks/privacybadgerchrome/300d41eb1de22493aabdb46201a148c028a6228d/src/lib/basedomain.js 3 | */ 4 | 5 | /** 6 | * Parts of original code from ipv6.js 7 | * Copyright 2011 Beau Gunderson 8 | * Available under MIT license 9 | */ 10 | 11 | 12 | import {publicSuffixes} from './psl.js'; 13 | import {memoize} from '../utils.js'; 14 | 15 | const re_ipv4 = /[0-9]$/; 16 | 17 | function isIPv4(hostname) { 18 | return !!hostname.match(re_ipv4); 19 | } 20 | 21 | function isIPv6(hostname) { 22 | return hostname.endsWith(']'); 23 | } 24 | 25 | /** 26 | * Returns base domain for specified host based on Public Suffix List. 27 | * @param {String} hostname The name of the host to get the base domain for 28 | */ 29 | function getBaseDomain(/**String*/ hostname) { 30 | // remove trailing dot(s) 31 | hostname = hostname.replace(/\.+$/, ''); 32 | 33 | // return IP address untouched 34 | if (isIPv6(hostname) || isIPv4(hostname)) { 35 | return hostname; 36 | } 37 | 38 | // search through PSL 39 | var prevDomains = []; 40 | var curDomain = hostname; 41 | var nextDot = curDomain.indexOf('.'); 42 | var tld = 0; 43 | 44 | while (true) { 45 | var suffix = publicSuffixes.get(curDomain); 46 | if (typeof(suffix) != 'undefined') { 47 | tld = suffix; 48 | break; 49 | } 50 | 51 | if (nextDot < 0) { 52 | tld = 1; 53 | break; 54 | } 55 | 56 | prevDomains.push(curDomain.substring(0,nextDot)); 57 | curDomain = curDomain.substring(nextDot+1); 58 | nextDot = curDomain.indexOf('.'); 59 | } 60 | 61 | while (tld > 0 && prevDomains.length > 0) { 62 | curDomain = prevDomains.pop() + '.' + curDomain; 63 | tld--; 64 | } 65 | 66 | return curDomain; 67 | } 68 | getBaseDomain = memoize(getBaseDomain, (x) => x, 1000); 69 | 70 | export {getBaseDomain}; 71 | -------------------------------------------------------------------------------- /src/js/domains/parties.js: -------------------------------------------------------------------------------- 1 | import {memoize} from '../utils.js'; 2 | import {getBaseDomain} from './basedomain.js'; 3 | import {isMdfp} from './mdfp.js'; 4 | 5 | function isThirdParty(d1, d2) { 6 | let b1 = getBaseDomain(d1), 7 | b2 = getBaseDomain(d2); 8 | 9 | if (b1 == b2 || isMdfp(b1, b2)) { 10 | return false; 11 | } 12 | return true; 13 | } 14 | isThirdParty = memoize(isThirdParty, ([a, b]) => a + ' ' + b, 1000); 15 | 16 | export {isThirdParty}; 17 | -------------------------------------------------------------------------------- /src/js/external/react-dom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-dom", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "react-dom.production.min.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /src/js/external/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "react.production.min.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /src/js/external/react/react.production.min.js: -------------------------------------------------------------------------------- 1 | /** @license React v16.4.1 2 | * react.production.min.js 3 | * 4 | * Copyright (c) 2013-present, Facebook, Inc. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 'use strict';(function(p,h){"object"===typeof exports&&"undefined"!==typeof module?module.exports=h():"function"===typeof define&&define.amd?define(h):p.React=h()})(this,function(){function p(a){for(var b=arguments.length-1,f="https://reactjs.org/docs/error-decoder.html?invariant="+a,d=0;du.length&&u.push(a)}function t(a,b,f,d){var e=typeof a;if("undefined"===e||"boolean"===e)a=null;var k=!1;if(null===a)k=!0;else switch(e){case "string":case "number":k=!0;break;case "object":switch(a.$$typeof){case r:case Q:k=!0}}if(k)return f(d,a,""===b?"."+y(a,0):b),1;k=0;b=""===b?".":b+":";if(Array.isArray(a))for(var c=0;ca;a++)b["_"+String.fromCharCode(a)]=a;if("0123456789"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(""))return!1;var f={};"abcdefghijklmnopqrst".split("").forEach(function(a){f[a]=a});return"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},f)).join("")?!1:!0}catch(d){return!1}}()?Object.assign:function(a,b){if(null===a||void 0===a)throw new TypeError("Object.assign cannot be called with null or undefined");var f=Object(a);for(var d,e=1;e setTimeout(() => resolve(func()))); 5 | } 6 | 7 | class FakeDisk extends Map { 8 | constructor() { 9 | super(...arguments); 10 | this.getter = super.get.bind(this); 11 | this.setter = super.set.bind(this); 12 | this.deleter = super.delete.bind(this); 13 | } 14 | 15 | async get(key, cb) { 16 | return asyncify(() => cb(this.getter(key))); 17 | } 18 | 19 | async set(key, value, cb) { 20 | return asyncify(() => cb(this.setter(key, value))); 21 | } 22 | 23 | async delete(key, cb) { 24 | return asyncify(() => cb(this.deleter(key))); 25 | } 26 | async remove(key, cb) { 27 | return this.delete(key, cb); 28 | } 29 | } 30 | 31 | class FakeMessages { 32 | constructor() { 33 | this.funcs = []; 34 | this.messages = []; 35 | } 36 | 37 | clear() { 38 | this.funcs = []; 39 | this.messages = []; 40 | } 41 | 42 | addListener(func) { 43 | this.funcs.push(func); 44 | } 45 | 46 | async sendMessage() { 47 | this.messages.push(Array.from(arguments).slice(0, arguments.length)) 48 | for (let func of this.funcs) { 49 | await func(...arguments); 50 | } 51 | } 52 | } 53 | 54 | class Port { 55 | constructor(name) { 56 | this.name = name; 57 | this.onMessage = new Listener(); 58 | this.onDisconnect = new Listener(); 59 | } 60 | 61 | setOther(other) { 62 | this.otherPort = other; 63 | } 64 | 65 | async disconnect() { 66 | for (let func of this.otherPort.onDisconnect.funcs) { 67 | await func(...arguments); 68 | } 69 | } 70 | 71 | async postMessage() { 72 | for (let func of this.otherPort.onMessage.funcs) { 73 | await func(...arguments); 74 | } 75 | } 76 | } 77 | 78 | // this should be cleaned up, and probably moved into shim.js or testing_utils.js 79 | function fakePort(name) { 80 | let a = new Port(name), b = new Port(name); 81 | a.setOther(b); 82 | b.setOther(a); 83 | return [a, b]; 84 | } 85 | 86 | class Connects extends Function { 87 | // returs fake [connect, onConnect] 88 | static create() { 89 | let onConnect = new Connects(); 90 | let connect = new Proxy(onConnect, { 91 | apply: function(target, thisArg, argList) { 92 | return target.connect.apply(target, argList); 93 | } 94 | }); 95 | return [connect, onConnect]; 96 | } 97 | 98 | constructor() { 99 | super(); 100 | this.funcs = []; 101 | } 102 | 103 | addListener(func) { 104 | this.funcs.push(func); 105 | } 106 | 107 | clear() { 108 | this.funcs = [] 109 | } 110 | 111 | connect({name}) { 112 | let [port, otherPort] = fakePort(name); 113 | for (let func of this.funcs) { 114 | func(otherPort); 115 | } 116 | return port; 117 | } 118 | } 119 | 120 | export {FakeDisk, FakeMessages, fakePort, Connects}; 121 | -------------------------------------------------------------------------------- /src/js/initialize.js: -------------------------------------------------------------------------------- 1 | // Initialize the extension. This is only run in the browser. 2 | 3 | import {Possum} from './possum.js'; 4 | import {shim} from './shim.js'; 5 | 6 | Possum.load(shim.Disk).then(possum => window['possum'] = possum); 7 | -------------------------------------------------------------------------------- /src/js/initialize_contentscripts.js: -------------------------------------------------------------------------------- 1 | let {makeFingerCounting} = require('./contentscripts/fingercounting'); 2 | let event_id = Math.random(); 3 | 4 | // listen for messages from the script we are about to insert 5 | document.addEventListener(event_id, function (e) { 6 | if (e.detail.type === 'fingerprinting') { 7 | chrome.runtime.sendMessage(e.detail); 8 | } 9 | }); 10 | 11 | // we wait for the script to say its ready 12 | let ready = new Promise(resolve => { 13 | document.addEventListener(event_id, function (e) { 14 | if (e.detail.type === 'ready') { 15 | resolve(); // script is ready to receive data 16 | } 17 | }); 18 | 19 | // insert script now that ready listener is listening. 20 | const scriptTag = document.createElement('script'), 21 | blob = new Blob([`(${makeFingerCounting.toString()})(${event_id})`], {type: 'text/javascript'}), 22 | url = URL.createObjectURL(blob); 23 | scriptTag.src = url; 24 | scriptTag.onload = function() { 25 | this.remove(); 26 | URL.revokeObjectURL(url); 27 | }; 28 | (document.head || document.documentElement).appendChild(scriptTag); 29 | }); 30 | 31 | const clean = message => { 32 | try { 33 | return cloneInto(message, document.defaultView); // eslint-disable-line 34 | } catch (unused) { 35 | return message; 36 | } 37 | } 38 | 39 | chrome.runtime.onMessage.addListener(message => { 40 | if (message.type === 'firstparty-fingerprinting') { 41 | message = clean(message); 42 | ready.then(() => { 43 | document.dispatchEvent(new CustomEvent(event_id, {detail: message})); 44 | }); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /src/js/initialize_popup.js: -------------------------------------------------------------------------------- 1 | import {Popup} from './popup.js'; 2 | import {currentTab} from './browser_utils.js'; 3 | 4 | (async () => { 5 | const tab = await currentTab(); 6 | const popup = new Popup(tab.id); 7 | await popup.connect(); 8 | })(); 9 | -------------------------------------------------------------------------------- /src/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "privacy-possum", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "", 6 | "main": "utils.js", 7 | "dependencies": {}, 8 | "devDependencies": { 9 | "chai": "^4.1.2", 10 | "coveralls": "^3.0.1", 11 | "crx3-utils": "0.0.3", 12 | "eslint": "^4.19.1", 13 | "jsdom": "^11.10.0", 14 | "mocha": "^7.1.0", 15 | "nyc": "^11.7.3", 16 | "react": "file:./external/react", 17 | "react-dom": "file:./external/react-dom" 18 | }, 19 | "scripts": { 20 | "test": "mocha --file test/helper.js --recursive", 21 | "lint": "git ls-files | grep \".js$\" | grep -v \"external\" | xargs eslint", 22 | "loc": "git ls-files | grep -v 'psl.js' | xargs cat | wc -l", 23 | "cover": "nyc --reporter=html --reporter=text mocha", 24 | "report": "nyc report --reporter=text-lcov | coveralls" 25 | }, 26 | "keywords": [], 27 | "author": "", 28 | "license": "GPL-2.0+" 29 | } 30 | -------------------------------------------------------------------------------- /src/js/popup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `possum` is created with a property `possum.popup` which is an instance of 3 | * `Server` here. When a popup is openened, it creates an instance of `Popup`, 4 | * and connects to `possum.popup`. Once connected, the server sends the data of 5 | * that tab to the popup. Changes on the server are pushed to the popup 6 | * automatically. 7 | */ 8 | 9 | import {shim} from './shim.js'; 10 | const {connect, document, sendMessage, ReactDOM} = shim; 11 | import {PopupHandler} from './reasons/handlers.js'; 12 | import {View, Counter} from './utils.js'; 13 | import {Action} from './schemes.js'; 14 | import {popupTitleBar, popupBody} from './popup_components.js'; 15 | import {GET_DEBUG_LOG, POPUP, USER_URL_DEACTIVATE, USER_HOST_DEACTIVATE, HEADER_DEACTIVATE_ON_HOST} from './constants.js'; 16 | 17 | const $ = (id) => document.getElementById(id); 18 | const asyncRender = (component, anchor) => new Promise(async (resolve) => (await ReactDOM).render(component, anchor, resolve)); 19 | 20 | class Popup { 21 | constructor(tabId) { 22 | this.urlActions = new Map(); 23 | this.handler = new PopupHandler(); 24 | this.tabId = tabId; 25 | 26 | $('debug-link').onclick = this.debug.bind(this); 27 | 28 | this.renderHeader(false); 29 | } 30 | 31 | async renderHeader(active) { 32 | return asyncRender((await popupTitleBar)({onOff: this.onOff.bind(this), active}), $('title-bar')); 33 | } 34 | 35 | async renderBody({active, urlActions, headerCounts, headerCountsActive} = this) { 36 | let pb = (await popupBody)({active, urlActions, headerCounts, headerCountsActive, headerHandler: this.headerHandler.bind(this)}); 37 | return asyncRender(pb, $('base')); 38 | } 39 | 40 | connect() { 41 | this.port = connect({name: POPUP}); 42 | this.view = new View(this.port, async ({active, actions, headerCounts, headerCountsActive}) => { 43 | if (typeof active !== 'undefined') { 44 | this.active = active; 45 | } 46 | if (actions) { 47 | this.updateUrlActions(actions); 48 | } 49 | if (headerCounts) { 50 | this.headerCounts = new Counter(headerCounts); 51 | } 52 | if (typeof headerCountsActive !== 'undefined') { 53 | this.headerCountsActive = headerCountsActive; 54 | } 55 | await this.show(); 56 | }); 57 | return this.view.ready; 58 | } 59 | 60 | getActionInfo(url, action) { 61 | action = Action.coerce(action); 62 | let {message: title, icon: iconPath, attribution} = this.handler.getInfo(action.reason), 63 | handler = this.handler.getFunc(action.reason, [url, this.tabId]); 64 | 65 | if (action.reason == USER_URL_DEACTIVATE) { 66 | let {reason} = action.getData('deactivatedAction'); 67 | ({icon: iconPath, attribution} = this.handler.getInfo(reason)); 68 | } 69 | return [url, {action, iconPath, title, attribution, handler}]; 70 | } 71 | 72 | updateUrlActions(actions) { 73 | this.urlActions = new Map( 74 | actions.map(([url, action]) => this.getActionInfo(url, action)) 75 | ); 76 | } 77 | 78 | async onOff() { 79 | await sendMessage({type: USER_HOST_DEACTIVATE, tabId: this.tabId}); 80 | } 81 | 82 | async headerHandler() { 83 | await sendMessage({ 84 | type: HEADER_DEACTIVATE_ON_HOST, 85 | tabId: this.tabId, 86 | checked: $('header-checkbox').checked 87 | }); 88 | } 89 | 90 | async debug() { 91 | await sendMessage({ 92 | type: GET_DEBUG_LOG, 93 | tabId: this.tabId, 94 | }, 95 | debugString => { 96 | console.log(debugString); // eslint-disable-line 97 | }); 98 | } 99 | 100 | async show() { 101 | await this.renderHeader(this.active); 102 | await this.renderBody(); 103 | } 104 | 105 | getHandlers(actionsUrls) { 106 | let out = []; 107 | actionsUrls.forEach((action, url) => { 108 | out.push([action, url, this.getClickHandler(action.reason, [url])]); 109 | }); 110 | return out; 111 | } 112 | } 113 | 114 | export {Popup, $}; 115 | -------------------------------------------------------------------------------- /src/js/popup_components.js: -------------------------------------------------------------------------------- 1 | import {shim} from './shim.js'; 2 | import {USER_URL_DEACTIVATE} from './constants.js'; 3 | 4 | const {getURL, React} = shim; 5 | 6 | const pathToReact = './external/react/react.production.min.js'; 7 | const text = {enabled: 'ENABLED', disabled: 'DISABLED'}; 8 | 9 | const merge = (...objs) => Object.assign({}, ...objs); 10 | 11 | function Deferred() { 12 | const o = {}; 13 | const p = new Promise((resolve, reject) => Object.assign(o, {resolve, reject})); 14 | return Object.assign(p, o); 15 | } 16 | 17 | 18 | const popupTitleBar = Deferred(), 19 | popupBody = Deferred(); 20 | 21 | (async () => { 22 | const {createElement, Fragment} = (await React); 23 | 24 | 25 | const e = createElement, 26 | eFactory = (name) => (props, ...children) => e(name, props, ...children), 27 | span = eFactory('span'), 28 | div = eFactory('div'), 29 | img = eFactory('img'), 30 | code = eFactory('code'), 31 | label = eFactory('label'), 32 | ul = eFactory('ul'), 33 | li = eFactory('li'), 34 | input = eFactory('input'), 35 | checkbox = (props, ...children) => input(merge({type: 'checkbox'}, props), ...children); 36 | 37 | function branding() { 38 | return div( 39 | {id: 'branding'}, 40 | img({id: 'logo', src: '/media/popup-icon.png', alt: "I'm a possum"}), 41 | span({id: 'title'}, 'Privacy Possum') 42 | ); 43 | } 44 | 45 | function onOffButton({active, onClick}) { 46 | return div( 47 | {id: 'on-off', title: `click to ${active ? 'disable' : 'enable'} for this site`, onClick}, 48 | span({id: 'on-off-text', className: 'grey'}, active ? text.enabled : text.disabled), 49 | div({id: 'on-off-button'}, img({src: getURL(`/media/logo-${active ? 'active' : 'inactive'}-100.png`)})) 50 | ); 51 | } 52 | 53 | popupTitleBar.resolve( 54 | ({onOff: onClick, active}) => { 55 | return e(Fragment, 56 | null, 57 | branding(), 58 | onOffButton({onClick, active}) 59 | ); 60 | } 61 | ); 62 | 63 | function headerHtml({name, count, key}) { 64 | return li({key}, 65 | code(null, name), 66 | ` headers blocked from ${count} sources` 67 | ); 68 | } 69 | 70 | function headerList({headerCounts}) { 71 | return ul({id: 'headers-count-list'}, 72 | Array.from(headerCounts, ([name, count], index) => { 73 | return headerHtml({name, count, key: index.toString()}); 74 | }) 75 | ) 76 | } 77 | 78 | function headersSection({headerCounts, headerCountsActive: active, headerHandler: onChange}) { 79 | let children = []; 80 | children.push(checkbox({id: 'header-checkbox', checked: active, onChange})); 81 | if (active) { 82 | children.push('Blocked tracking headers:'); 83 | children.push(headerList({headerCounts})); 84 | } else { 85 | children.push('Blocking tracking headers disabled'); 86 | } 87 | return div(null, ...children); 88 | } 89 | 90 | function actionIcon({iconPath, attribution}) { 91 | return img({className: 'action-icon', src: getURL(iconPath), attribution}); 92 | } 93 | 94 | function actionHtml({action, handler: onChange, title, iconPath, attribution, url, key}) { 95 | const checked = action.reason != USER_URL_DEACTIVATE; 96 | 97 | return li({className: 'action', key}, 98 | label({title, ['data-reason']: action.reason}, 99 | checkbox({checked, onChange}), 100 | actionIcon({action, iconPath, attribution}), 101 | url 102 | ) 103 | ); 104 | } 105 | 106 | function allActions({actions}) { 107 | return ul(null, 108 | Array.from(actions, ([url, actionInfo], index) => { 109 | return actionHtml(merge(actionInfo, {url, key: index.toString()})); 110 | }) 111 | ); 112 | } 113 | 114 | popupBody.resolve( 115 | ({active, urlActions, headerCounts, headerCountsActive, headerHandler}) => { 116 | let children = []; 117 | if (!active) { 118 | children.push('Disabled for this site'); 119 | } else if (urlActions.size === 0 && headerCounts.size === 0 && headerCountsActive) { 120 | children.push('Nothing to do'); 121 | } else { 122 | children.push(div({id: 'headers'}, 123 | headersSection({headerCounts, headerCountsActive, headerHandler}) 124 | )); 125 | children.push(div({id: 'actions'}, 126 | allActions({actions: urlActions}) 127 | )); 128 | } 129 | return e(Fragment, null, ...children); 130 | } 131 | ); 132 | })(); 133 | 134 | export {popupTitleBar, popupBody}; 135 | -------------------------------------------------------------------------------- /src/js/popup_server.js: -------------------------------------------------------------------------------- 1 | import {shim} from './shim.js'; 2 | const {onConnect} = shim; 3 | 4 | import {POPUP} from './constants.js'; 5 | import {Model, log} from './utils.js'; 6 | import {currentTab} from './browser_utils.js'; 7 | 8 | class Server { 9 | constructor(tabs) { 10 | this.tabs = tabs; 11 | this.connections = new Map(); 12 | } 13 | 14 | start() { 15 | onConnect.addListener(port => { 16 | if (port.name === POPUP) { 17 | currentTab().then(tab => { 18 | log(`Opening popup for tab: ${tab.id}`); 19 | let model = new Model(port, this.tabs.getTab(tab.id)); 20 | this.connections.set(tab.id, model); 21 | port.onDisconnect.addListener(() => { 22 | log(`Removing popup connection for tab: ${tab.id}`); 23 | this.connections.delete(tab.id); 24 | model.delete(); 25 | }); 26 | }); 27 | } 28 | }); 29 | } 30 | } 31 | 32 | export {Server}; 33 | -------------------------------------------------------------------------------- /src/js/possum.js: -------------------------------------------------------------------------------- 1 | import * as constants from './constants.js'; 2 | import {Tabs} from './tabs.js'; 3 | import {Store} from './store.js'; 4 | import {Reasons, reasonsArray} from './reasons/reasons.js'; 5 | import {Handler, MessageHandler} from './reasons/handlers.js'; 6 | import {WebRequest} from './webrequest.js'; 7 | import {Server as PopupServer} from './popup_server.js'; 8 | import {prettyLog, log} from './utils.js'; 9 | 10 | class Possum { 11 | constructor(store = new Store(constants.DISK_NAME)) { 12 | const tabs = new Tabs(), 13 | reasons = Reasons.fromArray(reasonsArray), 14 | handler = new Handler(tabs, store, reasons), 15 | webRequest = new WebRequest(tabs, store, handler), 16 | messageListener = new MessageHandler(tabs, store, reasons), 17 | popup = new PopupServer(tabs); 18 | 19 | tabs.startListeners(); 20 | webRequest.startListeners() 21 | messageListener.startListeners(); 22 | popup.start(); 23 | 24 | Object.assign(this, {store, tabs, reasons, handler, webRequest, messageListener, popup}); 25 | log('Woop woop possum party!!!'); 26 | } 27 | 28 | static async load(disk) { 29 | return new Possum(await Store.load(constants.DISK_NAME, disk)); 30 | } 31 | 32 | prettyLog() { 33 | return prettyLog(); 34 | } 35 | } 36 | 37 | export {Possum}; 38 | -------------------------------------------------------------------------------- /src/js/reasons/etag.js: -------------------------------------------------------------------------------- 1 | import {etag} from '../constants.js'; 2 | import {log, LruMap} from '../utils.js'; 3 | import {sendUrlDeactivate} from './utils.js'; 4 | import {Action} from '../schemes.js'; 5 | 6 | const {ETAG_TRACKING} = etag; 7 | 8 | async function setEtagAction(store, url, reason, data={etagValue: null}) { 9 | log(`etag update with: 10 | reason: ${reason} 11 | url: ${url} 12 | etag value: ${data.etagValue}`); 13 | data.time = Date.now(); 14 | return await store.setUrl(url, new Action(reason, data)); 15 | } 16 | 17 | function newEtagHeaderFunc(store) { 18 | let unknownEtags = new LruMap(2000), 19 | safeEtags = new LruMap(5000); 20 | return etagHeader.bind(undefined, {store, unknownEtags, safeEtags}); 21 | } 22 | 23 | function etagHeader({store, unknownEtags, safeEtags}, details, header) { 24 | const {href} = details.urlObj, 25 | etagValue = header.value, 26 | action = store.getUrl(href); 27 | if (action && (action.reason === ETAG_TRACKING)) { // known tracking etag 28 | Object.assign(details, {action}) 29 | return true; 30 | } else if (safeEtags.has(href)) { // known safe etag 31 | return false; 32 | } else if (unknownEtags.has(href)) { // 2nd time seeing this etag 33 | let oldEtagValue = unknownEtags.get(href).etagValue; 34 | unknownEtags.delete(href) 35 | if (etagValue === oldEtagValue) { 36 | safeEtags.set(href, {etagValue}); 37 | return false; 38 | } else { // mark ETAG_TRACKING 39 | setEtagAction(store, href, ETAG_TRACKING, {etagValue}); 40 | Object.assign(details, {action: new Action(ETAG_TRACKING, {etagValue})}); 41 | return true; 42 | } 43 | } else { // unknown etag 44 | unknownEtags.set(href, {etagValue}); 45 | return true; 46 | } 47 | } 48 | 49 | const reason = { 50 | name: ETAG_TRACKING, 51 | props: { 52 | popupHandler: sendUrlDeactivate, 53 | popup_info: { 54 | icon: '/media/etag-icon.png', 55 | message: 'blocked tracking etag', 56 | attribution: "CCBY Privacy Possum, US", 57 | } 58 | } 59 | } 60 | 61 | export {reason, etagHeader, setEtagAction, newEtagHeaderFunc}; 62 | -------------------------------------------------------------------------------- /src/js/reasons/fingerprinting.js: -------------------------------------------------------------------------------- 1 | import {Action} from '../schemes.js'; 2 | import {log} from '../utils.js'; 3 | import {sendUrlDeactivate} from './utils.js'; 4 | import {FINGERPRINTING, USER_URL_DEACTIVATE, CANCEL} from '../constants.js'; 5 | import {shim} from '../shim.js'; 6 | 7 | const {URL, tabsSendMessage} = shim; 8 | 9 | function isDeactivated(action) { 10 | return action && action.reason && action.reason === USER_URL_DEACTIVATE; 11 | } 12 | 13 | function fingerPrintingRequestHandler({tabs}, details) { 14 | const {url, tabId, frameId} = details; 15 | 16 | log(`request for fingerprinting script seen at 17 | tabId: ${tabId}, url: ${url}, and frameId ${frameId}`); 18 | if (tabs.isRequestThirdParty(details)) { 19 | log(`blocking 3rd party fingerprinting request`); 20 | Object.assign(details, {response: CANCEL, shortCircuit: false}); 21 | } else { 22 | // send set fp signal 23 | if (tabId >= 0) { 24 | log(`intercepting 1st party fingerprinting script`); 25 | tabs.markAction({reason: FINGERPRINTING}, url, tabId); 26 | tabsSendMessage(tabId, {type: 'firstparty-fingerprinting', url}, {frameId}); 27 | } else { 28 | log(`Error: 1st party fingerprinting request from negative tabId, probably from a cache thing`); 29 | } 30 | } 31 | } 32 | 33 | async function onFingerPrinting({store, tabs}, message, sender) { 34 | let tabId = sender.tab.id, 35 | {frameId} = sender, 36 | {url} = message, 37 | type = 'script'; 38 | 39 | log(`received fingerprinting message from tab '${sender.tab.url}' for url '${url}'`); 40 | // NB: the url could be dangerous user input, so we check it is an existing resource. 41 | if (tabs.hasResource({tabId, frameId, url, type})) { 42 | let reason = FINGERPRINTING, 43 | frameUrl = tabs.getFrameUrl(tabId, frameId), 44 | tabUrl = tabs.getTabUrl(sender.tab.id), 45 | {href} = new URL(url), 46 | currentAction = store.getUrl(href); 47 | 48 | if (!isDeactivated(currentAction)) { 49 | log(`store fingerprinting data`); 50 | tabs.markAction({reason: FINGERPRINTING}, href, sender.tab.id); 51 | await store.setUrl(href, new Action(reason, {href, frameUrl, tabUrl})); 52 | } else { 53 | log(`ignoring fingerprinting message because this url is deactivated`); 54 | } 55 | } 56 | } 57 | 58 | const fingerPrintingReason = { 59 | name: FINGERPRINTING, 60 | props: { 61 | requestHandler: fingerPrintingRequestHandler, 62 | messageHandler: onFingerPrinting, 63 | popupHandler: sendUrlDeactivate, 64 | popup_info: { 65 | icon: '/media/fingerprinting-icon.png', 66 | message: 'fingerprinting detected and blocked', 67 | attribution: "CCBY Ciprian Popescu, RO", 68 | } 69 | }, 70 | }; 71 | 72 | export {fingerPrintingReason}; 73 | -------------------------------------------------------------------------------- /src/js/reasons/handlers.js: -------------------------------------------------------------------------------- 1 | import {shim} from '../shim.js'; 2 | import {HeaderHandler} from './headers.js'; 3 | import {Reasons, reasonsArray} from './reasons.js'; 4 | 5 | 6 | /* 7 | * Handler superclass 8 | * holds functions and dispatches them 9 | * 10 | */ 11 | class Dispatcher { 12 | constructor(name, dependencies, reasons) { 13 | Object.assign(this, {name, reasons}); 14 | Object.assign(this, dependencies); 15 | this.funcs = new Map(); 16 | this.info = new Map(); 17 | 18 | this.addReason = this.addReason.bind(this, [dependencies]); 19 | this.reasons.map(this.addReason.bind(this)); 20 | } 21 | 22 | dispatcher(type, args) { 23 | if (this.funcs.has(type)) { 24 | return (this.funcs.get(type)).apply(undefined, args); 25 | } 26 | } 27 | 28 | // same as dispatcher but returns the function with bound arguments. 29 | getFunc() { 30 | return this.dispatcher.bind(this, ...arguments); 31 | } 32 | 33 | addReason(args, reason) { 34 | if (reason[this.name]) { 35 | args = args ? args : []; 36 | return this.addListener(reason.name, reason[this.name].bind(this, ...args)); 37 | } 38 | } 39 | addListener(name, func) { 40 | return this.funcs.set(name, func); 41 | } 42 | } 43 | 44 | class PopupHandler extends Dispatcher { 45 | constructor(reasons = Reasons.fromArray(reasonsArray)) { 46 | super('popupHandler', {}, reasons); // no tabs or store in popup 47 | } 48 | isInPopup(reasonName) { 49 | return this.funcs.has(reasonName); 50 | } 51 | getInfo(reasonName) { 52 | if (this.info.has(reasonName)) { 53 | return this.info.get(reasonName); 54 | } 55 | return { 56 | icon: '/media/block-icon.png', 57 | attribution: "CCBY ProSymbols, US", 58 | }; 59 | } 60 | addReason(args, reason) { 61 | let {name, popup_info} = reason; 62 | super.addReason(args, reason); 63 | if (popup_info) { 64 | this.info.set(name, popup_info); 65 | } 66 | } 67 | } 68 | 69 | class MessageHandler extends Dispatcher { 70 | constructor(tabs, store, reasons = Reasons.fromArray(reasonsArray)) { 71 | super('messageHandler', {tabs, store}, reasons); 72 | } 73 | 74 | dispatcher(messenger, sender, sendResponse) { 75 | return super.dispatcher(messenger.type, [messenger, sender, sendResponse]); 76 | } 77 | 78 | startListeners(onMessage = shim.onMessage) { 79 | Object.assign(this, {onMessage}); 80 | this.onMessage.addListener(this.dispatcher.bind(this)); 81 | } 82 | } 83 | 84 | // todo wrap handler requests to assure main_frame's are not blocked. 85 | class RequestHandler extends Dispatcher { 86 | constructor(tabs, store, reasons) { 87 | super('requestHandler', {tabs, store}, reasons); 88 | } 89 | 90 | dispatcher(obj, details) { 91 | return super.dispatcher(obj.action.reason, [details]); 92 | } 93 | 94 | handleRequest(obj, details) { 95 | if (obj.hasOwnProperty('action')) { 96 | details.action = obj.action; 97 | this.dispatcher(obj, details); 98 | } 99 | } 100 | } 101 | 102 | class TabHandler extends Dispatcher { 103 | constructor(tabs, store, reasons) { 104 | super('tabHandler', {tabs, store}, reasons); 105 | } 106 | 107 | startListeners(onUpdated = shim.onUpdated) { 108 | onUpdated.addListener(this.handleUpdated.bind(this)); 109 | } 110 | 111 | dispatcher({tab, info}) { 112 | return super.dispatcher(tab.action.reason, [{tab, info}]); 113 | } 114 | 115 | handleUpdated(tabId, info) { 116 | if (this.tabs.hasTab(tabId)) { 117 | let tab = this.tabs.getTab(tabId); 118 | if (tab.hasOwnProperty('action')) { 119 | return this.dispatcher({tab, info}); 120 | } 121 | } 122 | } 123 | } 124 | 125 | class Handler { 126 | constructor(tabs, store, reasons = Reasons.fromArray(reasonsArray)) { 127 | this.reasons = reasons; 128 | this.requestHandler = new RequestHandler(tabs, store, reasons); 129 | this.handleRequest = this.requestHandler.handleRequest.bind(this.requestHandler); 130 | this.headerHandler = new HeaderHandler(store); 131 | this.removeHeaders = this.headerHandler.removeHeaders.bind(this.headerHandler); 132 | 133 | this.tabHandler = new TabHandler(tabs, store, reasons); 134 | this.tabHandler.startListeners(); 135 | 136 | this.popupHandler = new PopupHandler(reasons); 137 | this.isInPopup = this.popupHandler.isInPopup.bind(this.popupHandler); 138 | } 139 | addReason(name, reason) { 140 | this.requestHandler.addReason(name, reason); 141 | this.tabHandler.addReason(name, reason); 142 | this.popupHandler.addReason(name, reason); 143 | } 144 | } 145 | 146 | export {PopupHandler, MessageHandler, RequestHandler, TabHandler, Handler}; 147 | -------------------------------------------------------------------------------- /src/js/reasons/headers.js: -------------------------------------------------------------------------------- 1 | import {Action} from '../schemes.js'; 2 | import {shim} from '../shim.js'; 3 | import {hasAction} from '../utils.js'; 4 | import {newEtagHeaderFunc} from './etag.js'; 5 | import {Referer} from './referer.js'; 6 | import {HEADER_DEACTIVATE_ON_HOST, header_methods, NO_ACTION, TAB_DEACTIVATE_HEADERS} from '../constants.js'; 7 | 8 | const {URL} = shim; 9 | 10 | const alwaysTrue = () => true; 11 | 12 | class HeaderHandler { 13 | constructor(store) { 14 | this.referer = new Referer(); 15 | this.badHeaders = new Map([ 16 | ['cookie', alwaysTrue], 17 | ['set-cookie', alwaysTrue], 18 | ['referer', this.referer.shouldRemoveHeader.bind(this.referer)], 19 | ['etag', newEtagHeaderFunc(store)], 20 | ['if-none-match', alwaysTrue] 21 | ]); 22 | } 23 | 24 | removeHeaders(details, headers) { 25 | let removed = []; 26 | for (let i = 0; i < headers.length; i++) { 27 | while (i < headers.length && this.shouldRemoveHeader(details, headers[i])) { 28 | removed.push(...headers.splice(i, 1)); 29 | } 30 | } 31 | return removed; 32 | } 33 | 34 | shouldRemoveHeader(details, header) { 35 | let name = header.name.toLowerCase(); 36 | if (this.badHeaders.has(name)) { 37 | return this.badHeaders.get(name)(details, header); 38 | } 39 | return false; 40 | } 41 | } 42 | 43 | function isHeaderRequest(details) { 44 | return header_methods.has(details.requestType); 45 | } 46 | 47 | function requestHandler({tabs}, details) { 48 | if (details.type === 'main_frame') { 49 | Object.assign(details, {shortCircuit: true, response: NO_ACTION}); 50 | let tab = tabs.getTab(details.tabId); 51 | tab.action = new Action(TAB_DEACTIVATE_HEADERS); 52 | tab.headerCountsActive = false; 53 | tab.onChange(); 54 | } 55 | } 56 | 57 | function tabHeaderHandler({}, details) { 58 | if (isHeaderRequest(details)) { 59 | return Object.assign(details, { 60 | response: NO_ACTION, 61 | shortCircuit: true, 62 | }); 63 | } 64 | } 65 | 66 | async function messageHandler({tabs, store}, {tabId, checked}) { 67 | let url = new URL(tabs.getTabUrl(tabId)); 68 | await store.updateDomain(url.href, (domain) => { 69 | if (hasAction(domain, HEADER_DEACTIVATE_ON_HOST) && checked) { 70 | store.deleteDomain(url.href); 71 | } else if (!checked) { 72 | return Object.assign(domain, { 73 | action: new Action( 74 | HEADER_DEACTIVATE_ON_HOST, 75 | {href: url.href}, 76 | ), 77 | }) 78 | } 79 | }); 80 | let tab = tabs.getTab(tabId); 81 | tab.headerCountsActive = checked; 82 | if (!checked) { 83 | tab.action = new Action(TAB_DEACTIVATE_HEADERS); 84 | } else if (checked && tab.action && tab.action.reason === TAB_DEACTIVATE_HEADERS) { 85 | delete tab.action; 86 | } 87 | tab.onChange(); 88 | } 89 | 90 | const reason = { 91 | name: HEADER_DEACTIVATE_ON_HOST, 92 | props: { 93 | requestHandler, 94 | messageHandler, 95 | }, 96 | } 97 | 98 | const tabReason = { 99 | name: TAB_DEACTIVATE_HEADERS, 100 | props: { 101 | requestHandler: tabHeaderHandler, 102 | } 103 | } 104 | 105 | export {HeaderHandler, requestHandler, tabHeaderHandler, messageHandler, reason, tabReason}; 106 | -------------------------------------------------------------------------------- /src/js/reasons/reasons.js: -------------------------------------------------------------------------------- 1 | import {Action} from '../schemes.js'; 2 | import {setResponse, sendUrlDeactivate} from './utils.js'; 3 | import {Listener, log, logger, hasAction} from '../utils.js'; 4 | import {setTabIconActive} from '../browser_utils.js'; 5 | import {shim} from '../shim.js'; 6 | import * as constants from '../constants.js'; 7 | 8 | const {URL} = shim; 9 | const {NO_ACTION, CANCEL, BLOCK, GET_DEBUG_LOG, 10 | USER_HOST_DEACTIVATE, TAB_DEACTIVATE, REMOVE_ACTION} = constants; 11 | 12 | /** 13 | * `name` is the string name of this reasons, see constants.reasons.* 14 | * `messageHandler` function with signature ({store, tabs}, message, sender) 15 | * `requestHandler` function with signature ({store, tabs}, details) 16 | */ 17 | class Reason { 18 | constructor(name, {messageHandler, requestHandler, tabHandler, popup_info, popupHandler}) { 19 | Object.assign(this, {name, messageHandler, requestHandler, tabHandler, popup_info, popupHandler}); 20 | } 21 | } 22 | 23 | class Reasons extends Listener { 24 | constructor(reasons = []) { 25 | super(); 26 | this.reasons = new Set(); 27 | reasons.map(this.addReason.bind(this)); 28 | } 29 | 30 | static fromArray(reasonsArray) { 31 | return new Reasons(reasonsArray.map(({name, props}) => new Reason(name, props))); 32 | } 33 | 34 | map(func) { 35 | return Array.from(this.reasons).map(func); 36 | } 37 | 38 | addReason(reason) { 39 | if (!this.reasons.has(reason)) { 40 | this.reasons.add(reason); 41 | this.onEvent(reason); 42 | } 43 | } 44 | } 45 | 46 | const tabDeactivate = new Action(TAB_DEACTIVATE), // should these go elsewhere? 47 | removeAction = new Action(REMOVE_ACTION), 48 | blockAction = new Action(BLOCK); 49 | 50 | function onRemoveAction({store, tabs}, message) { // sent from popup so no `sender` 51 | tabs.markAction(removeAction, message.url, message.tabId); 52 | return store.deleteUrl(message.url); 53 | } 54 | 55 | function setActiveState(possumTab, active) { 56 | if (possumTab.active === active) { 57 | return; 58 | } 59 | toggleActiveState(possumTab); 60 | } 61 | 62 | function toggleActiveState(possumTab) { 63 | if (hasAction(possumTab, constants.TAB_DEACTIVATE)) { 64 | possumTab.setActiveState(true); 65 | delete possumTab.action; 66 | } else { 67 | possumTab.setActiveState(false); 68 | possumTab.action = tabDeactivate; 69 | } 70 | } 71 | 72 | function userHostDeactivateRequestHandler({tabs}, details) { 73 | details.shortCircuit = true; 74 | details.response = NO_ACTION; 75 | setActiveState(tabs.getTab(details.tabId), false); 76 | } 77 | 78 | async function onUserHostDeactivate({tabs, store}, {tabId}) { 79 | let active, 80 | url = new URL(tabs.getTabUrl(tabId)); 81 | await store.updateDomain(url.href, (domain) => { 82 | if (hasAction(domain, constants.USER_HOST_DEACTIVATE)) { 83 | active = true; 84 | delete domain.action 85 | } else { 86 | active = false; 87 | Object.assign(domain, { 88 | action: new Action( 89 | constants.USER_HOST_DEACTIVATE, 90 | {href: url.href}, 91 | ), 92 | }); 93 | } 94 | return domain; 95 | }); 96 | return setActiveState(tabs.getTab(tabId), active); 97 | } 98 | 99 | const reasonsArray = [ 100 | { 101 | name: TAB_DEACTIVATE, 102 | props: { 103 | requestHandler: setResponse(NO_ACTION, true), 104 | tabHandler: ({}, {tab}) => { 105 | setTabIconActive(tab.id, !!tab.active); 106 | }, 107 | }, 108 | }, 109 | { 110 | name: USER_HOST_DEACTIVATE, 111 | props: { 112 | requestHandler: userHostDeactivateRequestHandler, 113 | messageHandler: onUserHostDeactivate, 114 | }, 115 | }, 116 | { 117 | name: BLOCK, 118 | props: { 119 | requestHandler: setResponse(CANCEL, true), 120 | popupHandler: sendUrlDeactivate, 121 | popup_info: { 122 | icon: '/media/block-icon.png', 123 | message: 'url blocked', 124 | attribution: "CCBY ProSymbols, US", 125 | } 126 | }, 127 | }, 128 | { 129 | name: REMOVE_ACTION, 130 | props: { 131 | messageHandler: onRemoveAction, 132 | }, 133 | }, 134 | { 135 | name: GET_DEBUG_LOG, 136 | props: { 137 | messageHandler: ({}, messenger, sender, sendResponse) => { 138 | log('got GET_DEBUG_LOG msg'); 139 | return sendResponse(logger.prettyLog()); 140 | }, 141 | }, 142 | }, 143 | ]; 144 | 145 | import {fingerPrintingReason} from './fingerprinting.js'; 146 | import {urlDeactivateReason} from './user_url_deactivate.js'; 147 | import {reason as headerReason} from './headers.js'; 148 | import {tabReason} from './headers.js'; 149 | import {reason as etagReason} from './etag.js'; 150 | 151 | reasonsArray.push( 152 | fingerPrintingReason, 153 | urlDeactivateReason, 154 | headerReason, 155 | tabReason, 156 | etagReason, 157 | ); 158 | 159 | export {Reasons, reasonsArray, tabDeactivate, blockAction, Reason}; 160 | -------------------------------------------------------------------------------- /src/js/reasons/referer.js: -------------------------------------------------------------------------------- 1 | import {LruMap, log} from '../utils.js'; 2 | 3 | function is4xx(statusCode) { 4 | return (400 <= statusCode) && (statusCode < 500); 5 | } 6 | 7 | class Referer { 8 | constructor() { 9 | this.requestIdCache = new LruMap(1000); 10 | this.badRedirects = new LruMap(1000); 11 | } 12 | 13 | removeRefererFailedOnce({statusCode, requestId}) { 14 | return (is4xx(statusCode) && this.requestIdCache.has(requestId)) && !this.badRedirects.has(requestId); 15 | } 16 | 17 | failedAlready({requestId}) { 18 | return this.badRedirects.has(requestId); 19 | } 20 | 21 | shouldRemoveHeader(details, header) { 22 | if (!this.requestIdCache.has(details.requestId)) { 23 | this.requestIdCache.set(details.requestId, header.value); 24 | } 25 | 26 | if (this.failedAlready(details)) { 27 | log('failed referer already'); 28 | return false; 29 | } 30 | return true; 31 | } 32 | 33 | onHeadersReceived(details) { 34 | if (this.removeRefererFailedOnce(details)) { 35 | this.badRedirects.set(details.requestId); 36 | log(`failed referer removal, redirecting ${details.url}`); 37 | details.shortCircuit = true; 38 | return details.response = {redirectUrl: details.url}; 39 | } 40 | } 41 | } 42 | 43 | export {Referer}; 44 | -------------------------------------------------------------------------------- /src/js/reasons/user_url_deactivate.js: -------------------------------------------------------------------------------- 1 | import {USER_URL_DEACTIVATE, NO_ACTION} from '../constants.js'; 2 | import {log} from '../utils.js'; 3 | import {setResponse, sendUrlDeactivate} from './utils.js'; 4 | import {Action} from '../schemes.js'; 5 | 6 | async function onUserUrlDeactivate({store, tabs}, {url, tabId}) { 7 | await store.updateUrl(url, currentAction => { 8 | let action; 9 | log(`got user deactivate message for action: '${currentAction.reason}' with url: '${url}'`); 10 | if (currentAction.reason === USER_URL_DEACTIVATE) { 11 | action = currentAction.getData('deactivatedAction'); 12 | log(`reactivating action: '${action.reason}' for url: '${url}'`); 13 | } else { 14 | action = new Action(USER_URL_DEACTIVATE, { 15 | href: url, 16 | deactivatedAction: currentAction, 17 | }); 18 | log(`deactivating action: '${currentAction.reason}' for url: '${url}'`); 19 | } 20 | tabs.markAction(action, url, tabId); 21 | return action; 22 | }); 23 | } 24 | 25 | const urlDeactivateReason = { 26 | name: USER_URL_DEACTIVATE, 27 | props: { 28 | requestHandler: setResponse(NO_ACTION, true), 29 | messageHandler: onUserUrlDeactivate, 30 | popupHandler: sendUrlDeactivate, 31 | }, 32 | } 33 | 34 | export {onUserUrlDeactivate, urlDeactivateReason}; 35 | -------------------------------------------------------------------------------- /src/js/reasons/utils.js: -------------------------------------------------------------------------------- 1 | import {shim} from '../shim.js'; 2 | import {REMOVE_ACTION, USER_URL_DEACTIVATE} from '../constants.js'; 3 | 4 | const {sendMessage} = shim; 5 | 6 | function setResponse(response, shortCircuit) { 7 | return ({}, details) => Object.assign(details, {response, shortCircuit}); 8 | } 9 | 10 | function makeSendAction(type) { 11 | return function({}, url, tabId) { 12 | return sendMessage({type, url, tabId}); 13 | }; 14 | } 15 | 16 | const sendUrlDeactivate = makeSendAction(USER_URL_DEACTIVATE), 17 | sendRemoveAction = makeSendAction(REMOVE_ACTION); 18 | 19 | export {sendUrlDeactivate, sendRemoveAction, setResponse}; 20 | -------------------------------------------------------------------------------- /src/js/schemes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * These things get written to disk, so we can't use Map/Set & stuff that doesn't serialize there. 3 | * 4 | * Maybe each `reason should be in charge of creating its own action` 5 | * 6 | * domain { 7 | * paths { 8 | * path { 9 | * action { 10 | * respnose: webrequest blockingResponse 11 | * reason: reason for action 12 | * href: the full url in question (this domain + path) (what abt port, protocol, etc?) 13 | * frameUrl: url of frame this happened in 14 | * tabUrl: url of the tab this happened in 15 | * 16 | */ 17 | 18 | 19 | /* `reason` is from constants.reasons* */ 20 | class Action { 21 | static coerce(obj) { 22 | return obj instanceof Action ? obj : new Action(obj.reason, obj.data); 23 | } 24 | constructor(reason, data) { 25 | Object.assign(this, {reason, data}); 26 | } 27 | setData(key, val) { 28 | return this.data[key] = val; 29 | } 30 | getData(key) { 31 | return this.data[key]; 32 | } 33 | deleteData(key) { 34 | return delete this.data[key]; 35 | } 36 | } 37 | 38 | class Domain { 39 | constructor(data = {paths: {}}) { 40 | Object.assign(this, data); 41 | } 42 | 43 | setPath(path, action) { 44 | this.paths[path] = action; 45 | return this; 46 | } 47 | setPathAction(path, action) { 48 | action = Action.coerce(action); 49 | return this.setPath(path, {action}); 50 | } 51 | 52 | hasPath(path) { 53 | return this.paths && this.paths.hasOwnProperty(path); 54 | } 55 | 56 | getPath(path) { 57 | return this.paths[path]; 58 | } 59 | getPathAction(path) { 60 | if (this.hasPath(path)) { 61 | return Action.coerce(this.getPath(path).action); 62 | } 63 | } 64 | 65 | deletePath(path) { 66 | delete this.paths[path]; 67 | return this; 68 | } 69 | 70 | updatePath(path, callback) { 71 | return this.setPath(path, callback(this.getPath(path))); 72 | } 73 | updatePathAction(path, callback) { 74 | return this.setPathAction(path, callback(this.getPathAction(path))); 75 | } 76 | } 77 | 78 | export {Action, Domain}; 79 | -------------------------------------------------------------------------------- /src/js/shim.js: -------------------------------------------------------------------------------- 1 | /** 2 | * We shim browser API's so our code can be compatible with both Node and the 3 | * Browser without transpiling stuff. 4 | */ 5 | 6 | const exports = {}; 7 | /** 8 | * we load these lazily. 9 | */ 10 | 11 | import {FakeMessages, FakeDisk, Connects} from './fakes.js'; 12 | import {BrowserDisk} from './utils.js'; 13 | 14 | function wrapObject(base) { 15 | let mutableBase = null; 16 | return new Proxy(base, { 17 | construct(base, thisArg, argumentList) { 18 | mutableBase ?? (mutableBase = base); 19 | return new mutableBase(...argumentsList); 20 | }, 21 | apply(base, thisArg, argumentsList) { 22 | mutableBase ?? (mutableBase = base); 23 | return mutableBase.apply(thisArg, argumentsList); 24 | }, 25 | get(base, prop, receiver) { 26 | mutableBase ?? (mutableBase = base); 27 | const value = mutableBase[prop]; 28 | return typeof value === 'function' ? value.bind(mutableBase) : value; 29 | }, 30 | set(base, prop, value, receiver) { 31 | mutableBase ?? (mutableBase = base); 32 | if (prop === 'setBase') { 33 | mutableBase = value; 34 | return true; 35 | } 36 | mutableBase[prop] = value; 37 | return true; 38 | } 39 | }); 40 | } 41 | 42 | let globalObj = (typeof window === 'object') ? window : global; // eslint-disable-line 43 | let getter = (name, obj) => name.split('.').reduce((o, i) => o[i], obj); 44 | let passThru = (x) => x; 45 | let makeFakeMessages = () => { 46 | return new FakeMessages(); 47 | }; 48 | let makeFakeSendMessage = () => { 49 | let fm = makeFakeMessages(), 50 | sendMessage = fm.sendMessage.bind(fm) 51 | Object.assign(sendMessage, {clear: fm.clear.bind(fm), onMessage: fm}); 52 | return sendMessage; 53 | } 54 | let makeFakeDisk = () => { 55 | let out = FakeDisk; 56 | out.newDisk = () => new FakeDisk(); 57 | return out; 58 | } 59 | 60 | function assign(name, definition) { 61 | return Object.defineProperty(exports, name, { 62 | get: function() { 63 | return definition; 64 | } 65 | }); 66 | } 67 | 68 | function lazyDef(base, name, define) { 69 | Object.defineProperty(base, name, { 70 | configurable: true, 71 | get: function() { 72 | let out = define(); 73 | Object.defineProperty(base, name, { 74 | get: function() { 75 | return out; 76 | } 77 | }); 78 | return out; 79 | } 80 | }); 81 | } 82 | 83 | function shimmer(out_name, real_name, onSuccess, onFail) { 84 | lazyDef(exports, out_name, () => { 85 | let out; 86 | try { 87 | out = getter(real_name, globalObj); 88 | out = (typeof out !== 'undefined') ? onSuccess(out) : onFail(out_name); 89 | } catch(e) { 90 | out = onFail(out_name); 91 | } 92 | return out; 93 | }); 94 | } 95 | 96 | /** 97 | * shim for api's that share state; // todo use proxy's for these? 98 | */ 99 | let onAndSendMessage = (name) => { 100 | let fm = makeFakeMessages(); 101 | assign('onMessage', fm); 102 | assign('sendMessage', fm.sendMessage.bind(fm)); 103 | return getter(name, exports); 104 | }; 105 | 106 | let tabsOnAndSendMessage = (name) => { 107 | let fm = makeFakeMessages(); 108 | assign('tabsOnMessage', fm); 109 | assign('tabsSendMessage', fm.sendMessage.bind(fm)); 110 | return getter(name, exports); 111 | }; 112 | 113 | let connectAndOnConnect = (name) => { 114 | let [con, onCon] = Connects.create(); 115 | assign('onConnect', onCon); 116 | assign('connect', con); 117 | return getter(name, exports); 118 | }; 119 | 120 | // todo wrap these callbacks with promises 121 | // todo DRY these with stuff above 122 | let setAndGetBadgeText = (name) => { 123 | let setBadgeText = ({text, tabId}) => { 124 | setBadgeText.data[tabId] = text; 125 | }; 126 | setBadgeText.data = {}; 127 | let getBadgeText = ({tabId}, callback) => { 128 | callback(setBadgeText.data[tabId]); 129 | }; 130 | assign('setBadgeText', setBadgeText); 131 | assign('getBadgeText', getBadgeText); 132 | return getter(name, exports); 133 | } 134 | 135 | let shim = [ 136 | ['onMessage', 'chrome.runtime.onMessage', passThru, onAndSendMessage], 137 | ['sendMessage', 'chrome.runtime.sendMessage', passThru, onAndSendMessage], 138 | ['onBeforeRequest', 'chrome.webRequest.onBeforeRequest', passThru, makeFakeMessages], 139 | ['onBeforeRedirect', 'chrome.webRequest.onBeforeRedirect', passThru, makeFakeMessages], 140 | ['onBeforeSendHeaders', 'chrome.webRequest.onBeforeSendHeaders', passThru, makeFakeMessages], 141 | ['onHeadersReceived', 'chrome.webRequest.onHeadersReceived', passThru, makeFakeMessages], 142 | ['onCompleted', 'chrome.webRequest.onCompleted', passThru, makeFakeMessages], 143 | ['OnBeforeRequestOptions', 'chrome.webRequest.OnBeforeRequestOptions', passThru, () => { 144 | return {'BLOCKING': 'blocking'}; 145 | }], 146 | ['OnBeforeSendHeadersOptions', 'chrome.webRequest.OnBeforeSendHeadersOptions', passThru, () => { 147 | return {'BLOCKING': 'blocking', 'REQUEST_HEADERS': "requestHeaders"}; 148 | }], 149 | ['OnHeadersReceivedOptions', 'chrome.webRequest.OnHeadersReceivedOptions', passThru, ()=> { 150 | return {'BLOCKING': 'blocking', 'RESPONSE_HEADERS': "responseHeaders"}; 151 | }], 152 | ['onRemoved', 'chrome.tabs.onRemoved', passThru, makeFakeMessages], 153 | ['onActivated', 'chrome.tabs.onActivated', passThru, makeFakeMessages], 154 | ['onUpdated', 'chrome.tabs.onUpdated', passThru, makeFakeMessages], 155 | ['tabsOnMessage', 'chrome.tabs.onMessage', passThru, tabsOnAndSendMessage], 156 | ['tabsSendMessage', 'chrome.tabs.sendMessage', passThru, tabsOnAndSendMessage], 157 | ['tabsExecuteScript', 'chrome.tabs.executeScript', passThru, makeFakeSendMessage], 158 | ['onNavigationCommitted', 'chrome.webNavigation.onCommitted', passThru, makeFakeMessages], 159 | ['onErrorOccurred', 'chrome.webNavigation.onErrorOccurred', passThru, makeFakeMessages], 160 | ['getAllFrames', 'chrome.webNavigation.getAllFrames', passThru, 161 | () => { 162 | let out = (obj, callback) => callback(out.data); 163 | out.data = []; 164 | out.clear = () => out.data = []; 165 | return out; 166 | }, 167 | ], 168 | ['connect', 'chrome.runtime.connect', passThru, connectAndOnConnect], 169 | ['onConnect', 'chrome.runtime.onConnect', passThru, connectAndOnConnect], 170 | ['tabsQuery', 'chrome.tabs.query', passThru, 171 | () => { 172 | let out = (obj, callback) => callback(out.tabs); 173 | out.tabs = []; 174 | out.clear = () => out.tabs = []; 175 | return out; 176 | }, 177 | ], 178 | ['tabsGet', 'chrome.tabs.get', passThru, () => (tabId, callback) => callback(tabId)], 179 | ['setBadgeText', 'chrome.browserAction.setBadgeText', passThru, setAndGetBadgeText], 180 | ['getBadgeText', 'chrome.browserAction.getBadgeText', passThru, setAndGetBadgeText], 181 | ['setIcon', 'chrome.browserAction.setIcon', passThru, () => () => {}], 182 | ['getURL', 'chrome.extension.getURL', passThru, () => () => {}], 183 | ['React', 'React', passThru, () => { 184 | return import('./external/react/react.production.min.js').then(({default: React}) => React); 185 | }], 186 | ['ReactDOM', 'ReactDOM', passThru, () => { 187 | return import('./external/react-dom/react-dom.production.min.js').then(({default: ReactDOM}) => ReactDOM); 188 | }], 189 | ['document', 'document', passThru, () => { 190 | return wrapObject( 191 | import('jsdom').then(({default: {JSDOM}}) => { 192 | let mainDoc = (new JSDOM()).window.document; 193 | return mainDoc; 194 | })); 195 | }], 196 | ['URL', 'URL', () => URL, () => { 197 | return import('url').then(({URL}) => URL); 198 | }], 199 | ['Disk', 'chrome.storage.local', 200 | () => { 201 | let out = new BrowserDisk(chrome.storage.local); 202 | out.newDisk = () => out; 203 | return out; 204 | }, 205 | makeFakeDisk, 206 | ], 207 | ]; 208 | 209 | shim.forEach(shim => shimmer.apply(undefined, shim)); 210 | 211 | Object.assign(exports, {wrapObject}); 212 | 213 | export {exports as shim}; 214 | -------------------------------------------------------------------------------- /src/js/store.js: -------------------------------------------------------------------------------- 1 | import {DiskMap} from './disk_map.js'; 2 | import {Tree, splitter} from './suffixtree.js'; 3 | import {Domain} from './schemes.js'; 4 | import {shim} from './shim.js'; 5 | const {URL, Disk} = shim; 6 | 7 | class DomainTree extends Tree { 8 | beforeSet(val) { 9 | return (val instanceof Domain) ? val : new Domain(val); 10 | } 11 | 12 | set(key, val) { 13 | return super.set(key, this.beforeSet(val)); 14 | } 15 | } 16 | 17 | class Store { 18 | constructor(name, disk = Disk.newDisk(), splitter_ = splitter) { 19 | this.init(name, disk, splitter_); 20 | } 21 | 22 | init(name, disk, splitter) { 23 | this.tree = new DomainTree(splitter); 24 | this.diskMap = new DiskMap(name, disk); 25 | this.attachMethods(); 26 | } 27 | 28 | static async load(name, disk) { 29 | let out = new Store(name, disk); 30 | await out.diskMap.loadKeys(); 31 | for (let key of out.keys) { 32 | out.tree.set(key, new Domain(await out.diskMap.get(key))); 33 | } 34 | return out; 35 | } 36 | 37 | get keys() { 38 | return this.diskMap.keys; 39 | } 40 | 41 | attachMethods() { 42 | this.get = this.tree.get.bind(this.tree); 43 | this.getBranchData = this.tree.getBranchData.bind(this.tree); 44 | } 45 | 46 | has(key) { 47 | return this.keys.has(key); 48 | } 49 | 50 | async delete(key) { 51 | return this.tree.delete(key) && await this.diskMap.delete(key); 52 | } 53 | 54 | async set(key, value) { 55 | this.tree.set(key, value); 56 | await this.diskMap.set(key, value); 57 | } 58 | 59 | async update(key, callback) { 60 | await this.set(key, callback(this.get(key))); 61 | } 62 | 63 | getDomain(url) { 64 | url = new URL(url); 65 | return this.get(url.hostname); 66 | } 67 | 68 | async setDomain(url, value) { 69 | url = new URL(url); 70 | return await this.set(url.hostname, value); 71 | } 72 | 73 | async deleteDomain(url) { 74 | url = new URL(url); 75 | return await this.delete(url.hostname); 76 | } 77 | 78 | async updateDomain(url, callback) { 79 | let domain = this.getDomain(url); 80 | if (typeof domain === 'undefined' || !(domain instanceof Domain)) { 81 | domain = new Domain(); 82 | } 83 | return await this.setDomain(url, callback(domain)); 84 | } 85 | 86 | getUrl(url) { 87 | let {pathname} = new URL(url), 88 | domain = this.getDomain(url); 89 | if (domain instanceof Domain) { 90 | return domain.getPathAction(pathname); 91 | } 92 | } 93 | 94 | async setUrl(url, action) { 95 | await this.updateDomain(url, (domain) => { 96 | let {pathname} = new URL(url); 97 | return domain.setPathAction(pathname, action); 98 | }); 99 | } 100 | 101 | async updateUrl(url, callback) { 102 | return await this.updateDomain(url, (domain) => { 103 | let {pathname} = new URL(url); 104 | return domain.updatePathAction(pathname, callback); 105 | }); 106 | } 107 | 108 | async deleteUrl(url) { 109 | return await this.updateDomain(url, (domain) => { 110 | let {pathname} = new URL(url); 111 | return domain.deletePath(pathname); 112 | }); 113 | } 114 | } 115 | 116 | export {Store}; 117 | -------------------------------------------------------------------------------- /src/js/suffixtree.js: -------------------------------------------------------------------------------- 1 | const SENTINEL = '.', 2 | LABEL = 'label', 3 | root_label = 'root'; 4 | 5 | class Node extends Map { 6 | constructor(label) { 7 | super(); 8 | this[LABEL] = label; 9 | } 10 | 11 | setLabelData(data) { 12 | this[SENTINEL] = data; 13 | } 14 | 15 | getLabelData() { 16 | return this[SENTINEL]; 17 | } 18 | 19 | hasLabelData() { 20 | return this.hasOwnProperty(SENTINEL); 21 | } 22 | 23 | deleteLableData() { 24 | return delete this[SENTINEL]; 25 | } 26 | 27 | dangling() { // no data and no children 28 | return this.size == 0 && !this.hasLabelData(); 29 | } 30 | } 31 | 32 | function setAgg(node, part) { 33 | if (!node.has(part)) { 34 | node.set(part, new Node(part)); 35 | } 36 | return node.get(part); 37 | } 38 | 39 | function getAgg(node, part) { 40 | return node.get(part); 41 | } 42 | 43 | function deleteAgg(node, part, aggregator) { 44 | let next = node.get(part); 45 | aggregator.push(next); 46 | return next; 47 | } 48 | 49 | function branchAgg(node, part, agg) { 50 | node = node.get(part); 51 | if (typeof node !== 'undefined' && node.hasLabelData()) { 52 | agg.set(part, node.getLabelData()); 53 | } 54 | return node; 55 | }; 56 | 57 | class Tree { 58 | constructor(splitter) { 59 | this.splitter = splitter; 60 | this._root = new Node(root_label); 61 | } 62 | 63 | aggregate(key, aggFunc, aggregator) { 64 | let parts = this.splitter(key), 65 | len = parts.length, 66 | node = this._root; 67 | 68 | for (let i = 0; i < len; i++) { 69 | let part = parts[i]; 70 | node = aggFunc(node, part, aggregator); 71 | if (typeof node === 'undefined') { 72 | return undefined; 73 | } 74 | } 75 | return node 76 | } 77 | 78 | set(key, val) { 79 | let node = this.aggregate(key, setAgg); 80 | node.setLabelData(val); 81 | } 82 | 83 | get(key) { 84 | let node = this.aggregate(key, getAgg); 85 | return (typeof node === 'undefined') ? undefined : node.getLabelData(); 86 | } 87 | 88 | delete(key) { 89 | let branch = [], deleted = false; 90 | this.aggregate(key, deleteAgg, branch); 91 | let cur = branch.pop(); 92 | if (typeof cur !== 'undefined' && cur.hasLabelData()) { 93 | cur.deleteLableData(); 94 | deleted = true; 95 | while (branch.length > 0 && cur.dangling()) { 96 | let parent_ = branch.pop(); 97 | parent_.delete(cur[LABEL]); 98 | cur = parent_; 99 | } 100 | } 101 | return deleted; 102 | } 103 | 104 | getBranchData(key) { 105 | let aggregator = new Map(); 106 | let node = this.aggregate(key, branchAgg, aggregator) 107 | return (typeof node == 'undefined') ? undefined : aggregator; 108 | } 109 | }; 110 | 111 | function splitter(splitme) { 112 | return splitme.split('.').reverse(); 113 | } 114 | 115 | export {Tree, splitter}; 116 | -------------------------------------------------------------------------------- /src/js/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/js/test/basedomain_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * based on https://github.com/cowlicks/privacybadgerchrome/blob/300d41eb1de22493aabdb46201a148c028a6228d/src/tests/tests/baseDomain.js 3 | */ 4 | /* * This file is part of Adblock Plus , * Copyright (C) 2006-2013 Eyeo GmbH * 5 | * Adblock Plus is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License version 3 as 7 | * published by the Free Software Foundation. 8 | * 9 | * Adblock Plus is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with Adblock Plus. If not, see . 16 | */ 17 | 18 | import chai from 'chai'; const {assert} = chai; 19 | import {getBaseDomain} from '../domains/basedomain.js'; 20 | 21 | describe('basedomain.js', function() { 22 | it("Determining base domain", function () { 23 | var tests = [ 24 | ["com", "com"], 25 | ["example.com", "example.com"], 26 | ["www.example.com", "example.com"], 27 | ["www.example.com.", "example.com"], 28 | ["www.example.co.uk", "example.co.uk"], 29 | ["www.example.co.uk.", "example.co.uk"], 30 | ["www.example.bl.uk", "bl.uk"], 31 | ["foo.bar.example.co.uk", "example.co.uk"], 32 | ["1.2.3.4", "1.2.3.4"], 33 | ["[::1]", "[::1]"], 34 | ["[2001:db8:1f70:0:999:de8:7648:6e8]", "[2001:db8:1f70:0:999:de8:7648:6e8]"], 35 | ["www.example.sande.xn--mre-og-romsdal-qqb.no", "example.sande.xn--mre-og-romsdal-qqb.no"], 36 | ["test.xn--e1aybc.xn--p1ai", "xn--e1aybc.xn--p1ai"], 37 | ["www.example.xn--0trq7p7nn.jp", "example.xn--0trq7p7nn.jp"], 38 | ]; 39 | 40 | for (var i = 0; i < tests.length; i++) { 41 | assert.equal(getBaseDomain(tests[i][0]), tests[i][1], tests[i][0]); 42 | } 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/js/test/constants_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | 3 | import * as constants from '../constants.js'; 4 | 5 | describe('constants.js', () => { 6 | it('has constants attached', () => { 7 | assert.equal([...Object.keys(constants)].length > 0, true); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/js/test/disk_map_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | import {DiskMap} from '../disk_map.js'; 3 | import {shim} from '../shim.js'; 4 | const {Disk} = shim; 5 | 6 | describe('disk_map.js', function() { 7 | describe('DiskMap', function() { 8 | beforeEach(function() { 9 | let disk = Disk.newDisk(), 10 | name = 'name'; 11 | this.dmap = new DiskMap(name, disk); 12 | }); 13 | 14 | it('gets and sets', async function() { 15 | let keys = 'abcdefg'.split(''); 16 | for (let i = 0; i < keys.length; i++) { 17 | await this.dmap.set(keys[i], i); 18 | assert.equal(await this.dmap.get(keys[i]), i); 19 | } 20 | }); 21 | 22 | it('deletes', async function() { 23 | let key = 'key', val = 'val'; 24 | await this.dmap.set(key, val); 25 | assert.equal(await this.dmap.get(key), val); 26 | 27 | assert.isTrue(await this.dmap.delete(key)); 28 | assert.isFalse(this.dmap.has(key)); 29 | }); 30 | 31 | it('loads from disk', async function() { 32 | let keys = 'abcdefg'.split(''); 33 | 34 | for (let i = 0; i < keys.length; i++) { 35 | await this.dmap.set(keys[i], i); 36 | assert.equal(await this.dmap.get(keys[i]), i); 37 | } 38 | 39 | let newDisk = await DiskMap.load(this.dmap.name, this.dmap.disk); 40 | assert.deepEqual(newDisk.keys, this.dmap.keys, 'same keys'); 41 | assert.deepEqual(await newDisk.toMap(), await this.dmap.toMap(), 'same map'); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/js/test/fakes_tests.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | import * as fakes from '../fakes.js'; 3 | 4 | describe('fakes.js', function() { 5 | describe('Connects', function() { 6 | it('make connect & onConnect', function() { 7 | let called = false, 8 | name = 'name', 9 | [connect, onConnect] = fakes.Connects.create(); 10 | onConnect.addListener(port => { 11 | assert.equal(port.name, name); 12 | called = true; 13 | }); 14 | 15 | let port = connect({name}); 16 | assert.equal(port.name, name); 17 | assert.isTrue(called); 18 | 19 | connect.clear(); 20 | }); 21 | }) 22 | describe('FakeDisk', function() { 23 | beforeEach(async function() { 24 | this.key = 'key', this.value = 'value'; 25 | this.fd = new fakes.FakeDisk(); 26 | await new Promise(resolve => this.fd.set(this.key, this.value, resolve)); 27 | }); 28 | it('sets & gets', async function() { 29 | // key with no data returns undefined 30 | assert.isUndefined(await new Promise(resolve => this.fd.get('does not exist', resolve))); 31 | assert.equal(await new Promise(resolve => this.fd.get(this.key, resolve)), this.value); 32 | }); 33 | it('remove', async function() { 34 | await new Promise(resolve => this.fd.remove(this.key, resolve)); 35 | assert.isUndefined(await new Promise(resolve => this.fd.get(this.key, resolve))); 36 | }); 37 | }); 38 | }) 39 | -------------------------------------------------------------------------------- /src/js/test/helper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Clear global state between each test 3 | * 4 | * The node extension api's carry state by design (since browsers do) so we 5 | * clear them between each stateful interface between each test. 6 | */ 7 | 8 | import {clearState, useJSDOM} from './testing_utils.js'; 9 | import {logger} from '../utils.js'; 10 | import mochaBase from 'mocha/lib/reporters/base.js'; 11 | 12 | const {colors} = mochaBase; 13 | import * as jsdom from 'jsdom'; 14 | const {default: {JSDOM}} = jsdom; 15 | 16 | colors['pass'] = '32'; 17 | colors['error stack'] = '31'; 18 | 19 | before(() => { 20 | useJSDOM(JSDOM); 21 | }); 22 | 23 | beforeEach(function() { 24 | logger.print = false; 25 | clearState(); 26 | }); 27 | -------------------------------------------------------------------------------- /src/js/test/mdfp_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | import {isMdfp} from '../domains/mdfp.js'; 3 | 4 | describe('mdfp.js', function() { 5 | it('true cases', function() { 6 | assert.isTrue(isMdfp('reddit.com', 'redditstatic.com')); 7 | assert.isTrue(isMdfp('reddit.com', 'reddit.com')); 8 | assert.isTrue(isMdfp('notonlist.com', 'notonlist.com')); 9 | assert.isTrue(isMdfp('avatars0.githubusercontent.com', 'github.com')); 10 | }) 11 | 12 | it('false cases', function() { 13 | assert.isFalse(isMdfp('reddit.com', 'railnation.de')); 14 | assert.isFalse(isMdfp('notonlist.com', 'othernotonlist.com')); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/js/test/messages_test.js: -------------------------------------------------------------------------------- 1 | // todo messages.js was added reasons/handlers.js, so rename this file, or add it to handlers.js 2 | 3 | import chai from 'chai'; const {assert} = chai; 4 | import * as constants from '../constants.js'; 5 | import {shim} from '../shim.js'; 6 | const {URL, onMessage, sendMessage} = shim; 7 | import {Tabs} from '../tabs.js'; 8 | import {Store} from '../store.js'; 9 | import {Action} from '../schemes.js'; 10 | import {MessageHandler} from '../reasons/handlers.js'; 11 | import {Mock, Details, details} from './testing_utils.js'; 12 | 13 | describe('messages.js', function() { 14 | describe('MessageHandler', function() { 15 | beforeEach(function() { 16 | this.tabs = new Tabs(); 17 | this.store = new Store('name'); 18 | this.ml = new MessageHandler( 19 | this.tabs, 20 | this.store, 21 | ); 22 | }); 23 | 24 | describe('#onMessage', function() { 25 | it('dispatches messages', function() { 26 | let func = new Mock(), 27 | type = 'test msg'; 28 | 29 | this.ml.startListeners(onMessage); 30 | this.ml.addListener(type, func); 31 | 32 | sendMessage({type}); 33 | assert.isTrue(func.called); 34 | }) 35 | }) 36 | 37 | describe('Deactivate', function() { 38 | let {main_frame} = details, 39 | {url, tabId} = main_frame, 40 | urlAction = new Action(constants.USER_URL_DEACTIVATE, {href: url}), 41 | hostAction = new Action(constants.USER_HOST_DEACTIVATE, {href: url}); 42 | beforeEach(function() { 43 | this.tabs.addResource(main_frame); 44 | }); 45 | 46 | it('url deactivate updates storage', async function() { 47 | let beforeAction = new Action('before'); 48 | await this.ml.store.setUrl(url, beforeAction); 49 | 50 | await this.ml.dispatcher( // deactivate initial action 51 | {type: constants.USER_URL_DEACTIVATE, url, tabId}, 52 | undefined 53 | ); 54 | 55 | let action1 = this.ml.store.getUrl(url); 56 | assert.equal(action1.reason, urlAction.reason); 57 | assert.include(action1.data, urlAction.data); 58 | assert.deepEqual(action1.data.deactivatedAction, beforeAction); 59 | 60 | await this.ml.dispatcher( // reactivate initial action 61 | {type: constants.USER_URL_DEACTIVATE, url, tabId}, 62 | undefined 63 | ); 64 | 65 | let action2 = this.ml.store.getUrl(url); 66 | assert.deepEqual(action2, beforeAction); 67 | }); 68 | 69 | 70 | it('host deactivate updates storage', async function() { 71 | await this.ml.dispatcher( 72 | {type: constants.USER_HOST_DEACTIVATE, tabId}, 73 | undefined 74 | ); 75 | let domain = this.ml.store.getDomain(url); 76 | assert.deepEqual(domain.action, hostAction); 77 | }); 78 | }); 79 | 80 | describe('#onFingerPrinting', function() { 81 | let url = new URL(details.script.url), 82 | message = {type: constants.FINGERPRINTING, url: url.href}, 83 | action = new Action( 84 | constants.FINGERPRINTING, 85 | { 86 | href: url.href, 87 | frameUrl: undefined, 88 | tabUrl: undefined, 89 | } 90 | ); 91 | 92 | beforeEach(async function() { 93 | this.ml.tabs.addResource(details.script); // add the resource 94 | await this.ml.dispatcher(message, details.script.toSender()); 95 | }); 96 | 97 | it('updates storage', async function() { 98 | let domain = await this.ml.store.getDomain(url.href); 99 | assert.isTrue(domain.paths.hasOwnProperty(url.pathname), 'path set on domain'); 100 | 101 | let path = domain.paths[url.pathname]; 102 | 103 | assert.deepEqual(path.action, action, 'correct action set'); 104 | }) 105 | 106 | it('adds a second path', async function() { 107 | let url2 = new URL(details.script.url); 108 | url2.pathname = '/otherpath.js'; 109 | 110 | let details2 = new Details(Object.assign({}, details.script, {url: url2.href})) 111 | 112 | this.ml.tabs.addResource(details2); // add the resource 113 | await this.ml.dispatcher({type: constants.FINGERPRINTING, url: details2.url}, details2.toSender()); 114 | 115 | let domain = await this.ml.store.getDomain(url2.href); 116 | assert.isTrue(domain.paths.hasOwnProperty(url2.pathname), 'path set on domain'); 117 | 118 | let path = domain.paths[url2.pathname], 119 | action2 = new Action(action.reason, Object.assign({}, action.data, {href: url2.href})); 120 | 121 | assert.deepEqual(path.action, action2, 'correct action set'); 122 | }); 123 | 124 | it('rejects unknown resources', async function() { 125 | let details2 = new Details(Object.assign({}, details.script, {url: 'https://other.com/foo.js'})); 126 | await this.ml.dispatcher({type: constants.FINGERPRINTING, url: details2.url}, details2.toSender()); 127 | assert.isUndefined(await this.ml.store.getDomain(details2.url), 'no domain gets set'); 128 | }); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /src/js/test/parties_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | import {isThirdParty} from '../domains/parties.js'; 3 | 4 | describe('parties.js', function() { 5 | it('false cases', function() { 6 | assert.isFalse(isThirdParty('reddit.com', 'redditstatic.com')); 7 | assert.isFalse(isThirdParty('github.com', 'avatars2.githubusercontent.com')); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/js/test/popup_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | import {USER_URL_DEACTIVATE} from '../constants.js'; 3 | import {Tab, Tabs} from '../tabs.js'; 4 | import {blockAction} from '../reasons/reasons.js'; 5 | import {setDocumentHtml, useJSDOM} from './testing_utils.js'; 6 | import {Server} from '../popup_server.js'; 7 | import {Popup, $} from '../popup.js'; 8 | import {shim} from '../shim.js'; 9 | const {onMessage, tabsQuery, setAsyncImports, document} = shim; 10 | import * as jsdom from 'jsdom'; 11 | const {default: {JSDOM}} = jsdom; 12 | 13 | before(() => { 14 | useJSDOM(JSDOM); 15 | }); 16 | 17 | describe('popup.js', function() { 18 | beforeEach(async function() { 19 | await setDocumentHtml('../skin/popup.html'); 20 | }); 21 | 22 | describe('header html', function() { 23 | beforeEach(function() { 24 | this.tabId = 1; 25 | this.popup = new Popup(this.tabId); 26 | this.bodyProps = {active: true, urlActions: new Map(), headerCountsActive: true, headerCounts: new Map()}; 27 | Object.assign(this.popup, this.bodyProps); 28 | }); 29 | it('inactive', async function() { 30 | this.popup.headerCountsActive = false; 31 | await this.popup.renderBody(); 32 | assert.isFalse($('header-checkbox').checked); 33 | }); 34 | it('headerHandler', async function() { 35 | this.popup.headerCounts = new Map([[1, 2]]); 36 | await this.popup.renderBody(); 37 | await this.popup.headerHandler(); 38 | assert.isTrue(onMessage.messages.pop().pop().checked); 39 | }); 40 | it('headers active', async function() { 41 | let name = 'headerName', count = 42; 42 | this.popup.headerCounts = new Map([[name, count]]); 43 | await this.popup.renderBody(); 44 | assert.isTrue($('header-checkbox').checked); 45 | assert.include($('headers-count-list').innerHTML, name); 46 | assert.include($('headers-count-list').innerHTML, count); 47 | }); 48 | }); 49 | 50 | describe('Popup and Server', function() { 51 | let url1 = 'https://foo.com/stuff', 52 | url2 = 'https://bar.com/other'; 53 | beforeEach(async function() { 54 | this.tabId = 1; 55 | this.tab = new Tab(this.tabId); 56 | this.tabs = new Tabs(); 57 | 58 | tabsQuery.tabs = [{id: this.tabId}]; // mock current tab 59 | this.tab.markAction(blockAction, url1); 60 | this.tabs.setTab(this.tab.id, this.tab); 61 | 62 | this.server = new Server(this.tabs); 63 | this.popup = new Popup(this.tabId); 64 | 65 | this.server.start(); 66 | await this.popup.connect(); 67 | }); 68 | 69 | describe('action click handlers', function() { 70 | it('active is set', async function() { 71 | assert.isTrue(this.popup.active); 72 | 73 | this.popup.view.onChange({active: false}); 74 | 75 | assert.isFalse(this.popup.active, false); 76 | }); 77 | it('sets click handlers', async function() { 78 | let popup = this.popup; 79 | 80 | assert.isTrue(popup.urlActions.has(url1)); 81 | assert.deepEqual(popup.urlActions.get(url1).action, blockAction); 82 | 83 | popup.urlActions.get(url1).handler(); 84 | 85 | let url = url1, type = USER_URL_DEACTIVATE, 86 | res = onMessage.messages.pop().pop(); 87 | assert.include(res, {url, type}); 88 | }); 89 | }); 90 | 91 | describe('updates are sent', function() { 92 | it('actions are sent', function() { 93 | assert.isTrue(this.popup.urlActions.has(url1), 'initial url is blocked'); 94 | 95 | this.tab.markAction(blockAction, url2); 96 | assert.isTrue(this.popup.urlActions.has(url2), 'added url is blocked'); 97 | }); 98 | 99 | it('active is sent', function() { 100 | assert.isTrue(this.popup.active); 101 | 102 | this.tab.toggleActiveState(); 103 | assert.isFalse(this.popup.active); 104 | }); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/js/test/reasons/etag_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | import {Store} from '../../store.js'; 3 | import {newEtagHeaderFunc, setEtagAction} from '../../reasons/etag.js'; 4 | import {etag} from '../../constants.js'; 5 | const {ETAG_TRACKING} = etag; 6 | 7 | describe('etag.js', function() { 8 | let etagValue ='hi', href = 'https://foo.com/stuff.js'; 9 | beforeEach(function() { 10 | this.store = new Store('name'); 11 | this.etagHeader = newEtagHeaderFunc(this.store); 12 | this.header = {name: 'etag', value: etagValue}; 13 | this.details = {urlObj: {href}}; 14 | 15 | }); 16 | describe('etagHeader', function() { 17 | it('marks new etag headers as uknown', async function() { 18 | let {details, header} = this, 19 | remove = this.etagHeader(details, header), 20 | action = this.store.getUrl(details.urlObj.href); 21 | assert.isTrue(remove); 22 | assert.isUndefined(action); 23 | }); 24 | it('allows and marks as safe on same etag', async function() { 25 | let {details, header} = this; 26 | 27 | this.etagHeader(details, header); 28 | assert.isFalse(this.etagHeader(details, header)); 29 | }) 30 | it('blocks and marks as tracking on diff etag', async function() { 31 | let {details, header} = this, 32 | differentEtagValue = 'different'; 33 | 34 | this.etagHeader(details, header); 35 | let remove = this.etagHeader(details, {value: differentEtagValue}); 36 | assert.isTrue(remove); 37 | assert.equal(details.action.reason, ETAG_TRACKING); 38 | 39 | let action = this.store.getUrl(details.urlObj.href); 40 | assert.equal(action.reason, ETAG_TRACKING); 41 | assert.equal(action.data.etagValue, differentEtagValue); 42 | }) 43 | it('blocks etags from tracking urls', async function() { 44 | let {details, header} = this; 45 | 46 | await setEtagAction(this.store, href, ETAG_TRACKING); 47 | let remove = this.etagHeader(details, header); 48 | 49 | assert.equal(details.action.reason, ETAG_TRACKING); 50 | assert.isTrue(remove); 51 | }) 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/js/test/reasons/referer_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | import {Referer} from '../../reasons/referer.js'; 3 | 4 | describe('referer.js', function() { 5 | let requestId = 1, 6 | url = 'https://whatever.com/nope'; 7 | beforeEach(function() { 8 | this.details = {requestId, url}; 9 | this.header = {name: 'Referer', value: 'https://foo.com/stuff'}; 10 | this.referer = new Referer(); 11 | }); 12 | describe('#shouldRemoveHeader', function() { 13 | it('removes first', function() { 14 | assert.isTrue(this.referer.shouldRemoveHeader(this.details, this.header)); 15 | }); 16 | it('does not remove when failedAlready', function() { 17 | this.referer.badRedirects.set(requestId); 18 | assert.isFalse(this.referer.shouldRemoveHeader(this.details, this.header)); 19 | }); 20 | }); 21 | 22 | describe('#onHeadersReceived', function() { 23 | it('no action for non-400 responses', function() { 24 | assert.isUndefined(this.referer.onHeadersReceived({statusCode: 200})); 25 | }); 26 | describe('sent', function() { 27 | beforeEach(function() { 28 | this.referer.shouldRemoveHeader(this.details, this.header); 29 | }); 30 | it('no actionn for already failed but 400 again responses', function() { 31 | this.referer.badRedirects.set(requestId); 32 | assert.isUndefined(this.referer.onHeadersReceived({requestId, statusCode: 403})); 33 | }); 34 | it('redirects on first 400', function() { 35 | let {details} = this, 36 | statusCode = 403; 37 | Object.assign(details, {statusCode}); 38 | assert.deepEqual(this.referer.onHeadersReceived(details), {redirectUrl: details.url}); 39 | }); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/js/test/reasons_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | import {Tabs, Tab} from '../tabs.js'; 3 | import {Store} from '../store.js'; 4 | import {shim} from '../shim.js'; 5 | const {URL, onMessage, onUpdated} = shim; 6 | import {Action} from '../schemes.js'; 7 | import {cookie, notCookie} from './testing_utils.js'; 8 | import {details} from './testing_utils.js'; 9 | const {main_frame, third_party} = details; 10 | import {Reason, Reasons, reasonsArray, tabDeactivate} from '../reasons/reasons.js'; 11 | import {onUserUrlDeactivate} from '../reasons/user_url_deactivate.js'; 12 | import {PopupHandler, Handler, TabHandler} from '../reasons/handlers.js'; 13 | import {messageHandler, tabHeaderHandler, requestHandler} from '../reasons/headers.js'; 14 | 15 | import {TAB_DEACTIVATE, TAB_DEACTIVATE_HEADERS, NO_ACTION, USER_HOST_DEACTIVATE, CANCEL, USER_URL_DEACTIVATE, BLOCK, FINGERPRINTING, HEADER_DEACTIVATE_ON_HOST, request_methods} from '../constants.js'; 16 | 17 | describe('reasons.js', function() { 18 | beforeEach(function() { 19 | this.tabs = new Tabs(); 20 | this.store = new Store('name'), 21 | this.reasons = Reasons.fromArray(reasonsArray); 22 | }); 23 | 24 | describe('header deactivate', function() { 25 | it('messageHandler', async function() { 26 | let {url, tabId} = main_frame; 27 | this.tabs.addResource(main_frame); 28 | 29 | await messageHandler(this, {tabId, checked: false}); 30 | 31 | let domain = this.store.getDomain(url); 32 | assert.equal(domain.action.reason, HEADER_DEACTIVATE_ON_HOST); 33 | assert.equal(this.tabs.getTab(tabId).action.reason, TAB_DEACTIVATE_HEADERS); 34 | 35 | await messageHandler(this, {tabId, checked: true}); 36 | assert.isUndefined(this.store.getDomain(url).action); 37 | assert.isUndefined(this.tabs.getTab(tabId).action); 38 | }); 39 | 40 | describe('requests', function() { 41 | beforeEach(function() { 42 | let details = main_frame.copy(), 43 | action = new Action(HEADER_DEACTIVATE_ON_HOST); 44 | details.requestHeaders = [cookie, notCookie]; 45 | this.store.setUrl(details.url, action); 46 | this.tabs.addResource(main_frame.copy()); 47 | Object.assign(this, {details, action}); 48 | }); 49 | 50 | it('host requestHandler', function() { 51 | // request gets shorted 52 | let {details} = this; 53 | requestHandler(this, details); 54 | assert.deepEqual(details.response, NO_ACTION); 55 | assert.isTrue(details.shortCircuit); 56 | // tab is set 57 | assert.equal(this.tabs.getTab(details.tabId).action.reason, TAB_DEACTIVATE_HEADERS); 58 | }) 59 | 60 | it('tab requestHandler', function() { 61 | let {details} = this; 62 | details.requestType = request_methods.ON_BEFORE_SEND_HEADERS; 63 | tabHeaderHandler(this, details); 64 | assert.deepEqual(details.response, NO_ACTION); 65 | assert.isTrue(details.shortCircuit); 66 | }); 67 | }); 68 | }); 69 | 70 | describe('user_url_deactivate', function() { 71 | it('saves previous action', async function() { 72 | const {url, tabId} = main_frame, 73 | {store, tabs} = this, 74 | firstAction = new Action('something'); 75 | 76 | tabs.addResource(main_frame.copy()); 77 | await this.store.setUrl(url, firstAction); 78 | 79 | await onUserUrlDeactivate({store, tabs}, {url, tabId}); 80 | let action = store.getUrl(url); 81 | assert.equal(action.reason, USER_URL_DEACTIVATE); 82 | assert.deepEqual(action.getData('deactivatedAction'), firstAction); 83 | }); 84 | }); 85 | 86 | describe('Reasons', function() { 87 | it('#addReason', async function() { 88 | let reasons = Reasons.fromArray(reasonsArray), 89 | result = new Promise(resolve => reasons.addListener(resolve)), 90 | value = 'value'; 91 | reasons.addReason(value); 92 | assert.equal(await result, value); 93 | }); 94 | }); 95 | describe('PopupHandler', function() { 96 | beforeEach(function() { 97 | this.popupHandler = new PopupHandler(this.reasons); 98 | }); 99 | it('fingerprinting popup handler sends url deactivate', function() { 100 | let url = 'some url', 101 | expected = {type: USER_URL_DEACTIVATE, url}; 102 | 103 | this.popupHandler.dispatcher(FINGERPRINTING, [url]); 104 | assert.include(onMessage.messages.pop().pop(), expected); 105 | }); 106 | 107 | it('adds reasons', function() { 108 | let called = false, name = 'test'; 109 | this.popupHandler.addReason({name, popupHandler: () => called = true}); 110 | this.popupHandler.dispatcher(name); 111 | assert.isTrue(called); 112 | }); 113 | 114 | it('#getInfo', function() { 115 | assert.include(this.popupHandler.getInfo(FINGERPRINTING).icon, 'fingerprinting'); 116 | assert.include(this.popupHandler.getInfo(BLOCK).icon, 'block'); 117 | }); 118 | }); 119 | describe('TabHandler', function() { 120 | it('handles tabs', async function() { 121 | let name = 'name', 122 | called = false; // should change to true 123 | 124 | let action = new Action(name), 125 | reason = new Reason(name, {tabHandler: ({}, {}) => called = true}), 126 | reasons = new Reasons(); 127 | 128 | // setup tab 129 | this.tabs.addResource(main_frame); 130 | this.tabs.getTab(main_frame.tabId).action = action; 131 | 132 | // setup handler 133 | let th = new TabHandler(this.tabs, undefined, reasons); 134 | th.addReason(reason); 135 | th.startListeners(); 136 | 137 | await onUpdated.sendMessage(main_frame.tabId, {}); 138 | assert.isTrue(called); 139 | }); 140 | }); 141 | describe('Handler', function() { 142 | beforeEach(function() { 143 | this.handler = new Handler(this.tabs); 144 | }); 145 | 146 | describe('#addReason', function() { 147 | it('handler can add reasons and use them', function() { 148 | let details = {}, 149 | name = 'block', 150 | assignedToDetails = true, 151 | requestHandler = ({}, details) => Object.assign(details, {assignedToDetails}), 152 | reason = new Reason(name, {requestHandler}), 153 | obj = {action: new Action(name)}; 154 | 155 | this.handler.addReason(reason); 156 | this.handler.handleRequest(obj, details); 157 | assert.isTrue(details.assignedToDetails); 158 | }); 159 | }); 160 | 161 | it('#isInPopup', function() { 162 | [FINGERPRINTING, USER_URL_DEACTIVATE].forEach(name => { 163 | assert.isTrue(this.handler.isInPopup(name), `${name} should be in popup`); 164 | }); 165 | [TAB_DEACTIVATE, USER_HOST_DEACTIVATE].forEach(name => { 166 | assert.isFalse(this.handler.isInPopup(name), `${name} should be in popup`); 167 | }); 168 | }); 169 | 170 | describe('#handleRequest', function() { 171 | it('fingerprinting', function() { 172 | let obj = {action: new Action(FINGERPRINTING)}, 173 | details = third_party.copy(); 174 | details.urlObj = new URL(details.url); 175 | this.tabs.addResource(main_frame.copy()); 176 | 177 | this.handler.handleRequest(obj, details); 178 | 179 | assert.equal(details.response, CANCEL); 180 | }); 181 | 182 | it('url deactivate', function() { 183 | let details = {}, obj = {}; 184 | obj.action = new Action(USER_URL_DEACTIVATE); 185 | this.handler.handleRequest(obj, details); 186 | assert.equal(details.response, NO_ACTION); 187 | }); 188 | 189 | it('host deactivate', function() { 190 | let tabId = 1, 191 | obj = {}, details = {tabId}, 192 | tab = new Tab(); 193 | 194 | this.tabs.setTab(tabId, tab); 195 | obj.action = new Action(USER_HOST_DEACTIVATE); 196 | 197 | this.handler.handleRequest(obj, details); 198 | 199 | assert.deepEqual(details.response, NO_ACTION); 200 | assert.isTrue(details.shortCircuit); 201 | assert.deepEqual(tab.action, tabDeactivate); 202 | }); 203 | 204 | it('tab deactivate', function() { 205 | let details = {}, tab = new Tab(); 206 | tab.action = tabDeactivate; 207 | 208 | this.handler.handleRequest(tab, details); 209 | assert.deepEqual(details.response, NO_ACTION); 210 | assert.isTrue(details.shortCircuit); 211 | }); 212 | }) 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /src/js/test/schemes_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | import {Domain, Action} from '../schemes.js'; 3 | import {testGetSetUpdate} from './testing_utils.js'; 4 | 5 | describe('schemes.js', function() { 6 | describe('Domain', function() { 7 | it('get set update path', async function() { 8 | let [initKey1, initVal1] = ['initKey1', 'initVal1'], 9 | domain = new Domain({paths: {[initKey1]: initVal1}}); 10 | 11 | assert.equal(domain.getPath(initKey1), initVal1); 12 | 13 | await testGetSetUpdate(domain, 'Path'); 14 | }); 15 | }); 16 | it('get/set/updatePathAction', async function() { 17 | let domain = new Domain(), 18 | path = 'path', 19 | action = new Action('reason1', 'data1'), 20 | update = new Action('reason2', 'data2'); 21 | await testGetSetUpdate(domain, 'PathAction', [path, action, update]); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/js/test/shim_test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ideally shim should be as simple as possible so as not to require testing. 3 | * Some are a little more complicated so we test them here. 4 | */ 5 | 6 | import chai from 'chai'; const {assert} = chai; 7 | import {shim} from '../shim.js'; 8 | const {connect, onConnect, wrapObject} = shim; 9 | 10 | describe('shim.js', function() { 11 | describe('connect and onConnect', function() { 12 | it('creates port that can send data', async function() { 13 | let name = 'test_name', 14 | called = false; 15 | 16 | onConnect.addListener(port => { 17 | assert.equal(port.name, name); 18 | port.onMessage.addListener(data => { 19 | called = true; 20 | assert.deepEqual(data, {a: 1, b: 2}); 21 | }); 22 | }); 23 | 24 | let port = connect({name}); 25 | assert.equal(port.name, name); 26 | await port.postMessage({a: 1, b: 2}); 27 | assert.isTrue(called); 28 | }); 29 | }); 30 | 31 | describe('wrapObject', ()=> { 32 | it('wraps objects', () => { 33 | let base = {a: 6}, 34 | base2 = {a: 7}; 35 | let wrapped = wrapObject(base); 36 | 37 | assert.equal(wrapped.a, 6); 38 | wrapped.b = 'b1'; 39 | assert.equal(wrapped.b, 'b1'); 40 | 41 | wrapped.setBase = base2; 42 | 43 | assert.equal(wrapped.a, 7); 44 | assert.equal(wrapped.b, undefined); 45 | }); 46 | 47 | it('methods work', () => { 48 | let base = { 49 | a() { 50 | return this.b; 51 | }, 52 | b: 6, 53 | }; 54 | let base2 = { 55 | a() { 56 | return this.b; 57 | }, 58 | b: 7, 59 | }; 60 | let wrapped = wrapObject(base); 61 | assert.equal(wrapped.a(), 6); 62 | 63 | wrapped.setBase = base2; 64 | assert.equal(wrapped.a(), 7); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/js/test/store_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | import {Store} from '../store.js'; 3 | import {Domain, Action} from '../schemes.js'; 4 | 5 | describe('store.js', function() { 6 | describe('Store', function() { 7 | let host = 'bar.foo.example.com', 8 | parts = host.split('.'), 9 | len = parts.length; 10 | 11 | async function loadNewFromTree({diskMap: {name, disk}}) { 12 | return await Store.load(name, disk); 13 | } 14 | 15 | beforeEach(function() { 16 | this.dtree = new Store('name'); 17 | }); 18 | 19 | it('deletes', async function() { 20 | let host = 'some.key.com', path1 = '/val1', path2 = '/val2', 21 | url1 = `https://${host}${path1}`, url2 = `https://${host}${path2}`, 22 | action1 = new Action(path1), action2 = new Action(path2); 23 | 24 | await this.dtree.setUrl(url1, action1); 25 | await this.dtree.setUrl(url2, action2); 26 | 27 | await this.dtree.deleteUrl(url1); 28 | 29 | assert.isUndefined(this.dtree.getUrl(url1)); 30 | assert.equal(this.dtree.getUrl(url2), action2); 31 | assert.deepEqual(this.dtree.getDomain(url2), {paths: {[path2]: {action: action2}}}); 32 | 33 | let newTree = await loadNewFromTree(this.dtree); 34 | 35 | // unchanged after loading 36 | assert.deepEqual(newTree.getDomain(url2), {paths: {[path2]: {action: action2}}}); 37 | 38 | await newTree.deleteDomain(url1); 39 | assert.isUndefined(newTree.getDomain(url1)); 40 | }); 41 | 42 | it('gets and sets', async function(){ 43 | for (let i = 2; i <= len; i++) { 44 | let name = parts.slice(-i).join('.'); 45 | 46 | let val = new Domain(i); 47 | 48 | await this.dtree.set(name, val); 49 | 50 | assert.deepEqual(this.dtree.get(name), val); 51 | } 52 | }); 53 | 54 | it('updates', async function(){ 55 | await this.dtree.set(host, new Domain()); 56 | await this.dtree.update(host, (domain) => domain.setPath('a', 'b')); 57 | 58 | assert.deepEqual(this.dtree.get(host), new Domain().setPath('a', 'b')); 59 | }); 60 | 61 | it('loads from disk', async function() { 62 | for (let i = 2; i <= len; i++) { 63 | let name = parts.slice(-i).join('.'); 64 | await this.dtree.set(name, i); 65 | } 66 | 67 | let loadedTree = await loadNewFromTree(this.dtree); 68 | 69 | assert.deepEqual(loadedTree.keys, this.dtree.keys); 70 | this.dtree.keys.forEach(key => { 71 | assert.deepEqual(loadedTree.get(key), this.dtree.get(key)); 72 | }); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /src/js/test/suffixtree_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import {Tree, splitter} from '../suffixtree.js'; 3 | 4 | const {assert} = chai; 5 | 6 | describe('suffixtree.js', function() { 7 | let host = 'bar.foo.example.com', 8 | parts = host.split('.'), 9 | len = parts.length; 10 | 11 | beforeEach(function() { 12 | this.tree = new Tree(splitter); 13 | }); 14 | 15 | it('get and set', function() { 16 | let {tree} = this; 17 | for (let i = 2; i <= len; i++) { 18 | let name = parts.slice(-i).join('.'); 19 | 20 | tree.set(name, i); 21 | 22 | assert.equal(tree.get(name), i); 23 | } 24 | 25 | }); 26 | 27 | describe('#aggregate', function() { 28 | it('gathers along a path', function() { 29 | let expected = new Map(), 30 | {tree} = this; 31 | for (let i = 2; i <= len; i++) { 32 | let n = parts.slice(-i).join('.'); 33 | 34 | tree.set(n, i); 35 | 36 | expected.set(n.split('.').shift(), i); 37 | } 38 | 39 | let result = tree.getBranchData(host); 40 | assert.deepEqual(result, expected); 41 | expected.forEach(key => assert.equal(result.get(key), expected.get(key))); 42 | }); 43 | it('returns undefined for bad paths', function() { 44 | assert.isUndefined(this.tree.getBranchData('foo.bar.com')); 45 | }); 46 | }); 47 | 48 | describe('#delete', function() { 49 | let host1 = 'sub.mid.tld', 50 | host2 = 'sub2.mid.tld', 51 | host3 = 'mid.tld'; 52 | 53 | beforeEach(function() { 54 | this.tree.set(host1, host1); 55 | }); 56 | 57 | it('deletes item', function() { 58 | let {tree} = this; 59 | assert.equal(tree.get(host1), host1); 60 | 61 | assert.isTrue(tree.delete(host1)); 62 | assert.equal(typeof tree.get(host1), 'undefined'); 63 | 64 | assert.isFalse(tree.delete(host1)); 65 | assert.isFalse(tree.delete(host2)); 66 | }) 67 | 68 | it('does not effect intermediates', function() { 69 | let {tree} = this; 70 | tree.set(host2, host2); 71 | 72 | tree.delete(host2); 73 | assert.equal(tree.get(host1), host1); 74 | }); 75 | 76 | it('deleting intermediate does not effect children', function() { 77 | let {tree} = this; 78 | assert.isFalse(tree.delete(host3), 'cant delete intermediate domain'); 79 | 80 | tree.set(host3, host3); // set the intermediate 81 | assert.isTrue(tree.delete(host3), 'deletes set intermediate'); 82 | assert.isUndefined(tree.get(host3), 'cant get it'); 83 | assert.equal(tree.get(host1), host1, 'subdomains uneffected'); 84 | 85 | tree.set(host3, host3); // set the intermediate 86 | assert.isTrue(tree.delete(host1), 'deletes subdomain'); 87 | assert.isUndefined(tree.get(host1), 'subdomain gone'); 88 | assert.equal(tree.get(host3), host3); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/js/test/tabs_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | import {BLOCK, REMOVE_ACTION} from '../constants.js'; 3 | import {shim} from '../shim.js'; 4 | const {onRemoved, getBadgeText, onNavigationCommitted, getAllFrames, tabsQuery, tabsExecuteScript} = shim; 5 | import {Action} from '../schemes.js'; 6 | import {cookie} from './testing_utils.js'; 7 | import {Tab, Tabs} from '../tabs.js'; 8 | 9 | const tabId = 1, 10 | firstParty = 'https://google.com/', thirdParty = 'https://third.com/', 11 | main_frame = {frameId: 0, url: firstParty, tabId, parentFrameId: -1, type: 'main_frame'}, 12 | sub_frame = {frameId: 1, url: 'about:blank', tabId, parentFrameId: 0, type: 'sub_frame'}; 13 | 14 | describe('tabs.js', function() { 15 | describe('Tabs', function() { 16 | beforeEach(function() { 17 | this.tabs = new Tabs(); 18 | this.tabs.addResource(main_frame); 19 | this.tabs.addResource(sub_frame); 20 | this.tab = this.tabs.getTab(main_frame.tabId); 21 | }); 22 | describe('#getCurrentData', function() { 23 | it('does not get frames from "discarded" tabs', async function() { 24 | let discarded = true, id = 2, url = 'https://url.com/'; 25 | tabsQuery.tabs = [{id, discarded, url}]; 26 | getAllFrames.data = [ 27 | {frameId: 0, parentFrameId: -1, url}, 28 | {frameId: 1, parentFrameId: 0, url: url} 29 | ]; 30 | 31 | await this.tabs.getCurrentData(); 32 | assert.isUndefined(this.tabs.getFrame(id, 1)); 33 | }); 34 | }); 35 | describe('#isRequestThirdParty', function() { 36 | it('is first party if main_frame', function() { 37 | let details = {type: 'main_frame'}; 38 | assert.isFalse(this.tabs.isRequestThirdParty(details)); 39 | }); 40 | it('no initiator', function() { 41 | let details = {tabId: 1, urlObj: new URL(thirdParty)}; 42 | assert.isTrue(this.tabs.isRequestThirdParty(details)); 43 | }); 44 | it('has initiator', function() { 45 | let isFirst = {initiator: firstParty, urlObj: new URL(firstParty)}, 46 | isThird = {initiator: firstParty, urlObj: new URL(thirdParty)}; 47 | assert.isFalse(this.tabs.isRequestThirdParty(isFirst)); 48 | assert.isTrue(this.tabs.isRequestThirdParty(isThird)); 49 | }); 50 | it('no initiator tabId = -1', function() { 51 | assert.isFalse(this.tabs.isRequestThirdParty({tabId: -1})); 52 | }); 53 | it('no initiator, no prexisting data about tab', function() { 54 | this.tabs = new Tabs(); 55 | let details = {tabId: 1, urlObj: new URL(thirdParty)}; 56 | assert.isFalse(this.tabs.isRequestThirdParty(details)); 57 | }); 58 | }); 59 | 60 | it('#getTabUrl', function() { 61 | assert.equal(this.tabs.getTabUrl(1), 'https://google.com/'); 62 | assert.isUndefined(this.tabs.getTabUrl('not present')); 63 | }); 64 | 65 | it('#getFrameUrl', function() { 66 | assert.equal(this.tabs.getFrameUrl(1, 1), 'about:blank'); 67 | }); 68 | 69 | it('#removeTab', function() { 70 | assert.isTrue(this.tabs.removeTab(main_frame.tabId)); 71 | }); 72 | 73 | describe('#onNavigationCommitted', function() { 74 | let {tabId, frameId, url} = main_frame; 75 | beforeEach(async function() { 76 | this.tabs.startListeners(); 77 | }); 78 | 79 | it('injects when onNavigationComitted fires', async function() { 80 | await onNavigationCommitted.sendMessage({tabId, frameId, url}); 81 | assert.isTrue(tabsExecuteScript.onMessage.messages.length > 0); 82 | }); 83 | it('does not inject when tab is deactivated', async function() { 84 | this.tab.setActiveState(false) 85 | await onNavigationCommitted.sendMessage({tabId, frameId, url}); 86 | assert.equal(tabsExecuteScript.onMessage.messages.length, 0); 87 | }); 88 | }); 89 | 90 | describe('#startListeners', function() { 91 | it('removes tabs on message', async function() { 92 | this.tabs.startListeners(); 93 | assert.isTrue(this.tabs.hasTab(main_frame.tabId)); 94 | await onRemoved.sendMessage(main_frame.tabId) 95 | assert.isFalse(this.tabs.hasTab(main_frame.tabId)); 96 | }) 97 | }); 98 | 99 | describe('#hasResource', function() { 100 | let resource = {tabId, frameId: 0, url: 'https://google.com/foo.js', type: 'script'} 101 | it('no resource', function() { 102 | assert.isFalse(this.tabs.hasResource(resource)); 103 | }); 104 | 105 | it('with resource', function() { 106 | this.tabs.addResource(resource); 107 | assert.isTrue(this.tabs.hasResource(resource)); 108 | }); 109 | }) 110 | 111 | describe('Tab', function() { 112 | const tabId = 1, 113 | url = 'https://example.com'; 114 | 115 | beforeEach(function() { 116 | this.tab = new Tab(tabId); 117 | }) 118 | it('merge', async function() { 119 | let block = new Action(BLOCK), 120 | {tab} = this, 121 | removed = [{name: 'foo'}, {name: 'bar'}], 122 | tabId2 = 2, url2 = 'https://other.com', 123 | removed2 = [{name: 'foo'}, {name: 'qux'}], 124 | tab2 = new Tab(tabId2); 125 | 126 | await tab.markAction(block, url); 127 | await tab.markHeaders(removed); 128 | 129 | await tab2.markAction(block, url2); 130 | await tab2.markHeaders(removed2); 131 | 132 | tab.merge(tab2); 133 | assert.deepEqual(Array.from(tab.actions), [[url, block]], 'url isnt overwritten'); 134 | assert.deepEqual(Array.from(tab.headerCounts), [['foo', 2], ['bar', 1], ['qux', 1]]) 135 | }); 136 | describe('#markAction', function() { 137 | it('adds actions', async function() { 138 | await this.tab.markAction(new Action(BLOCK), url); 139 | assert.equal(await new Promise(resolve => getBadgeText({tabId}, resolve)), '1'); 140 | }) 141 | it('removes actions', async function() { 142 | this.tab.markAction(new Action(BLOCK), url); 143 | this.tab.markAction(new Action(REMOVE_ACTION), url); 144 | assert.equal(await new Promise(resolve => getBadgeText({tabId}, resolve)), ''); 145 | }); 146 | }); 147 | it('#markHeaders', function() { 148 | let removed = [cookie, cookie]; 149 | this.tab.markHeaders(removed); 150 | assert.deepEqual(Array.from(this.tab.headerCounts), [['cookie', 2]]); 151 | }); 152 | }) 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /src/js/test/testing_utils.js: -------------------------------------------------------------------------------- 1 | import {shim} from '../shim.js'; 2 | import {Popup} from '../popup.js'; 3 | import {annotateDetails} from '../webrequest.js'; 4 | 5 | 6 | const {tabsExecuteScript, onNavigationCommitted, onConnect, tabsOnMessage, onMessage, tabsQuery, getAllFrames} = shim; 7 | 8 | const notCookie = {name: 'a', value: 'b'}, 9 | cookie = {name: 'Cookie', value: 'c'}; 10 | 11 | function clearState() { 12 | tabsExecuteScript.clear(); 13 | onNavigationCommitted.clear(); 14 | tabsOnMessage.clear(); 15 | onMessage.clear(); 16 | onConnect.clear(); 17 | tabsQuery.clear(); 18 | getAllFrames.clear(); 19 | } 20 | 21 | async function setDocumentHtml(path) { 22 | let {default: {JSDOM}} = await import('jsdom'); 23 | let {shim: {document}} = await import('../shim.js'); 24 | let newDoc = (await JSDOM.fromFile(path)).window.document; 25 | (await document).documentElement.innerHTML = newDoc.documentElement.innerHTML; 26 | } 27 | 28 | function useJSDOM(JSDOM) { 29 | shim.document.setBase = new JSDOM().window.document; 30 | } 31 | 32 | 33 | async function makePopup(tabId) { 34 | tabsQuery.tabs = [{id: tabId}]; 35 | let popup = new Popup(tabId); 36 | await popup.connect(); 37 | return popup; 38 | } 39 | 40 | function clone(val) { 41 | return JSON.parse(JSON.stringify(val)); 42 | } 43 | 44 | function makeGetterSetterUpdater(obj, suffix) { 45 | return ['get', 'set', 'update'].map(prefix => obj[prefix + suffix].bind(obj)); 46 | } 47 | 48 | async function testGetSetUpdate(obj, suffix, [k1, v1, update] = ['k1', 'v1', 'update']) { 49 | const [getter, setter, updater] = makeGetterSetterUpdater(obj, suffix), 50 | {default: {assert}} = await import('chai'); 51 | 52 | setter(k1, v1); 53 | assert.deepEqual(getter(k1), v1); 54 | 55 | let before = await new Promise(resolve => { 56 | updater(k1, value => { 57 | resolve(value); 58 | return update; 59 | }); 60 | }); 61 | assert.equal(before, v1); 62 | assert.equal(getter(k1), update); 63 | } 64 | 65 | function Mock(retval) { 66 | let out = function() { 67 | out.calledWith = Array.from(arguments); 68 | out.called = true; 69 | out.ncalls += 1; 70 | return retval; 71 | } 72 | out.called = false; 73 | out.ncalls = 0; 74 | return out; 75 | } 76 | 77 | function watchFunc(func) { 78 | function outFunc() { 79 | outFunc.inputs.push(Array.from(arguments)); 80 | let res = func.apply(this, arguments); 81 | outFunc.outputs.push(res); 82 | return res; 83 | } 84 | outFunc.inputs = [], outFunc.outputs = []; 85 | return outFunc; 86 | } 87 | 88 | function stub(name, value) { 89 | let parts = name.split('.'), 90 | last = parts.pop(), 91 | part = global; 92 | parts.forEach(partName => { 93 | if (!part.hasOwnProperty(partName)) { 94 | part[partName] = {}; 95 | } 96 | part = part[partName]; 97 | }); 98 | part[last] = value; 99 | } 100 | 101 | function stubber(namesValues) { 102 | namesValues.forEach(nameValue => { 103 | stub(...nameValue); 104 | }); 105 | } 106 | 107 | 108 | function toSender(details, url) { 109 | if (typeof url === 'undefined') { 110 | url = details.url; 111 | } 112 | return {tab: {id: details.tabId}, url, frameId: details.frameId}; 113 | } 114 | 115 | class Details { 116 | constructor (details) { 117 | annotateDetails(details); 118 | Object.assign(this, details); 119 | } 120 | toSender(url) { 121 | return toSender(this, url); 122 | } 123 | copy() { 124 | return new Details(JSON.parse(JSON.stringify(this))); 125 | } 126 | } 127 | 128 | const main_frame = new Details({ 129 | frameId: 0, 130 | url: 'https://firstparty.com/', 131 | tabId: 1, 132 | parentFrameId: -1, 133 | type: 'main_frame', 134 | }), 135 | sub_frame = new Details({ 136 | frameId: 1, 137 | url: 'about:blank', 138 | tabId: 1, 139 | parentFrameId: 0, 140 | type: 'sub_frame', 141 | }), 142 | first_party_script = new Details({ 143 | frameId: 0, 144 | url: 'https://firstparty.com/script.js', 145 | tabId: 1, 146 | type: 'script', 147 | }), 148 | // todo consolidate script and third_party 149 | script = new Details({ 150 | frameId: 0, 151 | url: 'https://foo.com/otherscript.js', 152 | tabId: 1, 153 | type: 'script', 154 | }), 155 | third_party = new Details({ 156 | frameId: 0, 157 | url: 'https://third-party.com/stuff.js', 158 | tabId: 1, 159 | type: 'script', 160 | }); 161 | 162 | const details = {main_frame, sub_frame, first_party_script, script, third_party}; 163 | 164 | export {setDocumentHtml, watchFunc, Mock, stub, stubber, Details, details, clone, cookie, notCookie, toSender, testGetSetUpdate, makePopup, clearState, useJSDOM}; 165 | -------------------------------------------------------------------------------- /src/js/test/utils_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | import {fakePort} from '../fakes.js'; 3 | import {View, Model, Listener, Counter, LruMap, LogBook, wrap, zip} from '../utils.js'; 4 | 5 | describe('utils.js', function() { 6 | describe('LruMap', function() { 7 | let maxSize = 3; 8 | beforeEach(function() { 9 | this.lrumap = new LruMap(maxSize); 10 | this.lrumap.set(1, 11).set(2, 22); 11 | }); 12 | it('does not get bigger than max size', function() { 13 | this.lrumap.set(3, 33).set(4, 44); 14 | assert.equal(this.lrumap.size, maxSize); 15 | assert.isFalse(this.lrumap.has(1)); 16 | }); 17 | it('.has reorders cache', function() { 18 | assert.isTrue(this.lrumap.has(1)); // move 1 ahead of 2 19 | this.lrumap.set(3, 33).set(4, 44); 20 | assert.equal(this.lrumap.size, maxSize); 21 | assert.isFalse(this.lrumap.has(2)); // 2 was purged 22 | }); 23 | it('.get reorders cache', function() { 24 | assert.equal(this.lrumap.get(1), 11); // move 1 ahead of 2 25 | this.lrumap.set(3, 33).set(4, 44); 26 | assert.equal(this.lrumap.size, maxSize); 27 | assert.isUndefined(this.lrumap.get(2), 22); // 2 was purged 28 | }); 29 | }); 30 | describe('Counter', function() { 31 | beforeEach(function() { 32 | this.counter = new Counter(); 33 | [1, 2, 2, 3, 3, 3].forEach(x => this.counter.add(x)); 34 | }); 35 | it('adds', function() { 36 | assert.deepEqual(Array.from(this.counter), [[1, 1], [2, 2], [3, 3]]); 37 | }); 38 | it('merges', function() { 39 | let c2 = new Counter(); 40 | c2.add(1); 41 | c2.add(4); 42 | this.counter.merge(c2); 43 | assert.deepEqual(Array.from(this.counter), [[1, 2], [2, 2], [3, 3], [4, 1]]); 44 | }); 45 | }); 46 | describe('View and Model', function() { 47 | it('they can talk', async function() { 48 | let [aPort, bPort] = fakePort('test'), 49 | result, 50 | data = new Listener(); 51 | 52 | data.getData = () => data.x; 53 | data.x = 'initial'; 54 | 55 | let view = new View(aPort, out => result = out); 56 | new Model(bPort, data), 57 | 58 | assert.equal(result, 'initial'); 59 | 60 | data.x = 'new data'; 61 | data.onChange(); 62 | 63 | assert.equal(result, 'new data'); 64 | 65 | await view.disconnect(); 66 | 67 | data.x = 'should not change to this'; 68 | data.onChange(); 69 | 70 | assert.equal(result, 'new data'); 71 | }); 72 | }); 73 | 74 | it('wraps', function() { 75 | let splitter = (string) => string.split(''), 76 | after = (s) => s.join(''), 77 | before = (s) => ['[' + s + ']']; 78 | let func = wrap(splitter, before, after); 79 | assert.equal(func('abc'), '[abc]'); 80 | }); 81 | 82 | it('zip', function() { 83 | let a = 'abcd'.split(''), 84 | b = 'efgh'.split(''), 85 | expected = 'ae bf cg dh'.split(' ').map(x => x.split('')); 86 | 87 | let result = zip(a, b); 88 | assert.deepEqual(result, expected); 89 | // recombine 90 | assert.deepEqual(zip(...result), [a, b]) 91 | }); 92 | it('logger', function() { 93 | let l = new LogBook(2); 94 | l.log('a'); 95 | assert.deepEqual([[0, 'a']], l.dump()); 96 | l.log('b') 97 | l.log('c'); 98 | // strips entries over maxSize, and returns ordered by most recent 99 | assert.deepEqual([[2, 'c'], [1, 'b']], l.dump()); 100 | }); 101 | }) 102 | -------------------------------------------------------------------------------- /src/js/test/webrequest_test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; const {assert} = chai; 2 | import {WebRequest} from '../webrequest.js'; 3 | import {NO_ACTION} from '../constants.js'; 4 | import {Tabs} from '../tabs.js'; 5 | import {Store} from '../store.js'; 6 | import {details, clone, cookie, notCookie} from './testing_utils.js'; 7 | 8 | 9 | describe('webrequest.js', function() { 10 | beforeEach(function() { 11 | this.tabs = new Tabs(), 12 | this.wr = new WebRequest(this.tabs, new Store()); 13 | }); 14 | describe('WebRequest', function() { 15 | describe('#isThirdParty', function() { 16 | beforeEach(function() { 17 | let tabId = -1, initiator = 'https://firstparty.com', urlObj = {}; 18 | this.details = {tabId, initiator, urlObj}; 19 | }); 20 | describe('tabId is -1', function() { 21 | it('thirdparty', function() { 22 | this.details.urlObj.hostname = 'thirdparty.com'; 23 | assert.isTrue(this.wr.isThirdParty(this.details)); 24 | }); 25 | it('firstparty', function() { 26 | this.details.urlObj.hostname = 'firstparty.com'; 27 | assert.isFalse(this.wr.isThirdParty(this.details)); 28 | }); 29 | it('thirdparty but no initiator', function() { 30 | this.details.urlObj.hostname = 'thirdparty.com'; 31 | delete this.details.initiator; 32 | assert.isFalse(this.wr.isThirdParty(this.details)); 33 | }); 34 | }); 35 | }); 36 | describe('#onBeforeRequest', function() { 37 | it('adds frames', function() { 38 | this.wr.onBeforeRequest(details.main_frame); 39 | assert.equal( 40 | this.tabs.getTabUrl(details.main_frame.tabId), 41 | details.main_frame.url 42 | ); 43 | 44 | this.wr.onBeforeRequest(details.sub_frame); 45 | assert.equal( 46 | this.tabs.getFrameUrl(details.sub_frame.tabId, details.sub_frame.frameId), 47 | details.sub_frame.url 48 | ); 49 | }); 50 | }); 51 | }); 52 | 53 | describe('#onBeforeSendHeaders', function() { 54 | beforeEach(function() { 55 | this.wr.onBeforeRequest(details.main_frame); 56 | this.main_frame = clone(details.main_frame); 57 | this.third_party = clone(details.third_party); 58 | this.third_party.url = 'https://third-party.com/'; 59 | }) 60 | it('removes cookies from thirdparty requests', function() { 61 | this.third_party.requestHeaders = [cookie, notCookie]; 62 | assert.deepEqual(this.wr.onBeforeSendHeaders(this.third_party), {requestHeaders: [notCookie]}); 63 | }); 64 | it('does not effect first party cookies', function() { 65 | let first_party = clone(details.main_frame); 66 | first_party.requestHeaders = [cookie, notCookie]; 67 | assert.deepEqual(this.wr.onBeforeSendHeaders(first_party), NO_ACTION); 68 | }) 69 | it('does not effect thirdparty requests with no cookies', function() { 70 | this.third_party.requestHeaders = [notCookie, notCookie]; 71 | assert.deepEqual(this.wr.onBeforeSendHeaders(this.third_party), {}); 72 | }); 73 | }); 74 | 75 | // todo DRY with ohBeforeSendHeaders 76 | describe('#onHeadersReceived', function() { 77 | beforeEach(function() { 78 | this.wr.onBeforeRequest(details.main_frame); 79 | this.first_party = clone(details.main_frame); 80 | this.third_party = clone(details.third_party); 81 | this.third_party.url = 'https://third-party.com/'; 82 | }) 83 | it('does not effect cookies on main_frame requests', function() { 84 | this.first_party.responseHeaders = [cookie, notCookie]; 85 | assert.deepEqual(this.wr.onHeadersReceived(this.first_party), NO_ACTION); 86 | }); 87 | it('removes cookies from thirdparty requests', function() { 88 | this.third_party.responseHeaders = [cookie, notCookie]; 89 | assert.deepEqual(this.wr.onHeadersReceived(this.third_party), {responseHeaders: [notCookie]}); 90 | }); 91 | it('does not effect first party cookies', function() { 92 | let first_party = clone(details.main_frame); 93 | first_party.responseHeaders = [cookie, notCookie]; 94 | assert.deepEqual(this.wr.onHeadersReceived(first_party), {}); 95 | }) 96 | it('does not effect thirdparty requests with no cookies', function() { 97 | this.third_party.responseHeaders = [notCookie, notCookie]; 98 | assert.deepEqual(this.wr.onHeadersReceived(this.third_party), {}); 99 | }); 100 | }); 101 | 102 | describe('removeHeaders', function() { 103 | it('removes cookie headers', function() { 104 | let data = [ 105 | [[], [], []], 106 | [[cookie, notCookie], [cookie], [notCookie]], 107 | [[notCookie, cookie, cookie, notCookie, cookie], [cookie, cookie, cookie], [notCookie, notCookie]], 108 | ]; 109 | for (let [headers, expectedRemoved, expectedHeaders] of data) { 110 | let resRemoved = this.wr.removeHeaders({}, headers); 111 | assert.deepEqual(headers, expectedHeaders); 112 | assert.deepEqual(resRemoved, expectedRemoved); 113 | } 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /src/js/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * View of some remote data represented by a `Model`. 3 | */ 4 | class View { 5 | constructor(port, onChange) { 6 | Object.assign(this, { 7 | disconnect: port.disconnect.bind(port), 8 | onChange 9 | }); 10 | this.ready = new Promise(resolve => { 11 | port.onMessage.addListener(async (obj) => { 12 | if (obj.change) { 13 | await onChange(obj.change); 14 | resolve(); 15 | } 16 | }); 17 | }); 18 | } 19 | } 20 | 21 | /* 22 | * Model that sends data changes to a corresponding view. 23 | * 24 | * Takes a `port` and an object with an `onChange` and `addListener` 25 | * methods. `onChange` is called directly first to send the initial data. 26 | * 27 | * todo: add a mixin that conforms to changer interface 28 | */ 29 | class Model { 30 | constructor(port, data) { 31 | this.data = data; 32 | this.func = change => port.postMessage({change}); 33 | data.addListener(this.func); 34 | data.onChange(); // send initial data 35 | port.onDisconnect.addListener(() => this.delete()); 36 | } 37 | 38 | delete() { 39 | this.data.removeListener(this.func); 40 | } 41 | } 42 | 43 | class Counter extends Map { 44 | add(name) { 45 | if (!this.has(name)) { 46 | this.set(name, 0); 47 | } 48 | return this.set(name, this.get(name) + 1); 49 | } 50 | merge(other) { 51 | other.forEach((value, key) => { 52 | this.set(key, (this.has(key) ? this.get(key) : 0) + value); 53 | }); 54 | } 55 | } 56 | 57 | class LruMap extends Map { 58 | constructor(maxSize = 2000) { 59 | super(); 60 | Object.assign(this, {maxSize}); 61 | } 62 | 63 | get(key) { 64 | if (super.has(key)) { 65 | let value = super.get(key); 66 | this.delete(key) 67 | super.set(key, value); 68 | return value; 69 | } 70 | } 71 | 72 | set(key, value) { 73 | if ((this.size >= this.maxSize) && !super.has(key)) { 74 | this.delete(this.keys().next().value); 75 | } 76 | super.delete(key); 77 | return super.set(key, value); 78 | } 79 | 80 | has(key) { 81 | if (super.has(key)) { 82 | let value = super.get(key); 83 | this.delete(key) 84 | super.set(key, value); 85 | return true; 86 | } 87 | return false; 88 | } 89 | } 90 | 91 | class FifoMap extends Map { 92 | constructor(maxSize) { 93 | super(); 94 | Object.assign(this, {maxSize}); 95 | } 96 | 97 | set(key, val) { 98 | super.set(key, val); 99 | if (this.size > this.maxSize) { 100 | this.delete(this.keys().next().value); 101 | } 102 | } 103 | } 104 | 105 | class LogBook extends FifoMap { 106 | constructor() { 107 | super(...arguments); 108 | this.print = true; 109 | this.count = 0; 110 | } 111 | 112 | dump() { 113 | return Array.from(this).reverse(); 114 | } 115 | 116 | prettyLog() { 117 | let out = '!!! This log may contain information about your browisng !!!'; 118 | for (let [i, entry] of this.dump()) { 119 | out += ` 120 | ${i}: 121 | ${entry}` 122 | } 123 | return out; 124 | } 125 | 126 | log(entry) { 127 | if (this.print) { 128 | console.log(entry); // eslint-disable-line 129 | } 130 | this.set(this.count, entry); 131 | this.count += 1; 132 | } 133 | } 134 | 135 | function lazyDef(exports_, name, definerFunc) { 136 | Object.assign(exports_, { 137 | get [name]() { 138 | delete this[name]; 139 | return this[name] = Object.assign(exports_, definerFunc())[name]; 140 | } 141 | }); 142 | } 143 | 144 | /* 145 | * Memoize the function `func`. `hash` coneverts the functions arguments into a 146 | * key to reference the result in the cache. `size` is the max size of the 147 | * cache. 148 | */ 149 | function memoize(func, hash, size) { 150 | let cache = new FifoMap(size); 151 | return function() { 152 | let key = hash(arguments); 153 | if (cache.has(key)) { 154 | return cache.get(key); 155 | } 156 | let result = func.apply(undefined, arguments); 157 | cache.set(key, result); 158 | return result; 159 | } 160 | } 161 | 162 | class BrowserDisk { 163 | constructor(disk) { 164 | this.disk = disk; 165 | } 166 | 167 | get(key, cb) { 168 | this.disk.get(key, (res) => { 169 | return (res.hasOwnProperty(key)) ? cb(res[key]) : cb(); 170 | }); 171 | } 172 | 173 | set(key, value, cb) { 174 | return this.disk.set({[key]: value}, cb); 175 | } 176 | 177 | delete(key, cb) { 178 | return this.disk.remove(key, cb); 179 | } 180 | remove(key, cb) { 181 | return this.delete(key, cb); 182 | } 183 | } 184 | 185 | // move to shim 186 | function makeTrap() { 187 | let target = () => {}; 188 | let lol = () => { 189 | return new Proxy(target, descriptor); 190 | }; 191 | let descriptor = { 192 | apply: lol, 193 | get: lol, 194 | }; 195 | return lol(); 196 | } 197 | 198 | /* 199 | * Make a class have an eventListener interface. The base class needs to 200 | * implement a `getData` function and call the `onChange` function when 201 | * appropriate. 202 | */ 203 | let listenerMixin = (Base) => class extends Base { 204 | constructor() { 205 | super(); 206 | this.funcs = new Set(); 207 | this.onChange = this.onEvent; 208 | } 209 | 210 | addListener(func) { 211 | this.funcs.add(func) 212 | } 213 | 214 | removeListener(func) { 215 | this.funcs.delete(func); 216 | } 217 | 218 | onEvent(event_) { 219 | this.funcs.forEach(func => func(this.getData(event_))); 220 | } 221 | 222 | getData(event_) { 223 | return event_; 224 | } 225 | } 226 | 227 | function hasAction(obj, reason) { 228 | return obj.hasOwnProperty('action') && (obj.action.reason === reason); 229 | } 230 | 231 | class Listener extends listenerMixin(Object) {} 232 | 233 | // check if hostname has the given basename 234 | function isBaseOfHostname(base, host) { 235 | return host.endsWith(base) ? 236 | (base.length === host.length || host.substr(-base.length - 1, 1) === '.') : 237 | false; 238 | } 239 | isBaseOfHostname = memoize(isBaseOfHostname, ([base, host]) => base + ' ' + host, 1000); 240 | 241 | function passThrough() { 242 | return Array.from(arguments); 243 | } 244 | 245 | function wrap(func, before = passThrough, after = passThrough) { 246 | return function() { 247 | return after(func.apply(undefined, before.apply(undefined, arguments))); 248 | } 249 | } 250 | 251 | function zip() { 252 | let args = Array.from(arguments), 253 | nargs = args.length, 254 | out = []; 255 | 256 | if (!nargs) return; 257 | 258 | for (let i = 0; i < nargs; i++) { 259 | let arr = args[i], 260 | len = arr.length; 261 | for (let j = 0; j < len; j++) { 262 | if (j >= out.length) out.push([]); 263 | out[j].push(arr[j]); 264 | } 265 | } 266 | return out; 267 | } 268 | 269 | const logger = new LogBook(100), 270 | log = logger.log.bind(logger), 271 | prettyLog = logger.prettyLog.bind(logger); 272 | 273 | export { 274 | View, 275 | Model, 276 | LruMap, 277 | Counter, 278 | memoize, 279 | LogBook, 280 | BrowserDisk, 281 | makeTrap, 282 | listenerMixin, 283 | Listener, 284 | hasAction, 285 | isBaseOfHostname, 286 | lazyDef, 287 | wrap, 288 | zip, 289 | logger, 290 | log, 291 | prettyLog, 292 | }; 293 | -------------------------------------------------------------------------------- /src/js/webrequest.js: -------------------------------------------------------------------------------- 1 | import {shim} from './shim.js'; 2 | import * as constants from './constants.js'; 3 | 4 | const {header_methods, request_methods} = constants; 5 | const {ON_BEFORE_REQUEST, ON_BEFORE_SEND_HEADERS, ON_HEADERS_RECEIVED} = request_methods; 6 | import {getOnBeforeRequestOptions, getOnBeforeSendHeadersOptions, getOnHeadersReceivedOptions} from './browser_compat.js'; 7 | import {Handler} from './reasons/handlers.js'; 8 | 9 | const {URL} = shim; 10 | 11 | function annotateDetails(details, requestType) { 12 | return Object.assign(details, { 13 | requestType, 14 | headerPropName: header_methods.get(requestType), 15 | urlObj: new URL(details.url), 16 | response: constants.NO_ACTION, 17 | }); 18 | } 19 | 20 | 21 | class WebRequest { 22 | constructor(tabs, store, handler = new Handler(tabs, store)) { 23 | Object.assign(this, {tabs, store, handler}); 24 | this.checkRequestAction = this.handler.handleRequest.bind(this.handler); 25 | this.removeHeaders = this.handler.removeHeaders.bind(this.handler); 26 | } 27 | 28 | startListeners({onBeforeRequest, onBeforeSendHeaders, onHeadersReceived} = shim) { 29 | 30 | onBeforeRequest.addListener( 31 | this.onBeforeRequest.bind(this), 32 | {urls: [""]}, 33 | getOnBeforeRequestOptions(), 34 | ); 35 | 36 | onBeforeSendHeaders.addListener( 37 | this.onBeforeSendHeaders.bind(this), 38 | {urls: [""]}, 39 | getOnBeforeSendHeadersOptions(), 40 | ); 41 | 42 | onHeadersReceived.addListener( 43 | this.onHeadersReceived.bind(this), 44 | {urls: [""]}, 45 | getOnHeadersReceivedOptions(), 46 | ); 47 | } 48 | 49 | isThirdParty(details) { 50 | // cache isThirdParty status per details and requestId 51 | return this.tabs.isRequestThirdParty(details); 52 | } 53 | 54 | recordRequest(details) { 55 | this.tabs.addResource(details); 56 | } 57 | 58 | markAction({action, url, tabId}) { 59 | if (action && this.handler.isInPopup(action.reason)) { 60 | return this.tabs.markAction(action, url, tabId); 61 | } 62 | } 63 | 64 | markHeaders(removed, {tabId}) { 65 | return this.tabs.markHeaders(removed, tabId); 66 | } 67 | 68 | checkAllRequestActions(details) { 69 | let {tabId} = details, 70 | {hostname, pathname} = details.urlObj; 71 | 72 | // we check actions in tab -> domain -> path 73 | this.checkRequestAction(this.tabs.getTab(tabId), details); 74 | if (!details.shortCircuit && this.store.has(hostname)) { 75 | let domain = this.store.get(hostname); 76 | this.checkRequestAction(domain, details); 77 | if (!details.shortCircuit && domain.hasPath(pathname)) { 78 | let path = domain.getPath(pathname); 79 | this.checkRequestAction(path, details); 80 | } 81 | } 82 | } 83 | 84 | commitRequest(details) { 85 | this.checkAllRequestActions(details); 86 | this.markAction(details); // record new behavior 87 | return details.response; 88 | } 89 | 90 | onBeforeRequest(details) { 91 | annotateDetails(details, ON_BEFORE_REQUEST); 92 | this.recordRequest(details); 93 | return this.commitRequest(details); 94 | } 95 | 96 | onBeforeSendHeaders(details) { 97 | annotateDetails(details, ON_BEFORE_SEND_HEADERS); 98 | this.headerHandler(details); 99 | this.markAction(details); 100 | return details.response; 101 | } 102 | 103 | onHeadersReceived(details) { 104 | annotateDetails(details, ON_HEADERS_RECEIVED); 105 | this.headerHandler(details); 106 | this.markAction(details); 107 | return details.response; 108 | } 109 | 110 | requestOrResponseAction(details) { 111 | if (!details.shortCircuit) { 112 | if (details.requestType == ON_HEADERS_RECEIVED) { 113 | return this.handler.headerHandler.referer.onHeadersReceived(details); 114 | } 115 | } 116 | } 117 | 118 | headerHandler(details) { 119 | if (this.isThirdParty(details)) { 120 | let headers = details[details.headerPropName], 121 | removed = this.removeHeaders(details, headers); 122 | this.checkAllRequestActions(details); 123 | this.requestOrResponseAction(details); 124 | if (!details.shortCircuit && (removed.length || headers.mutated)) { 125 | details.response = {[details.headerPropName]: headers}; 126 | this.markHeaders(removed, details); 127 | } 128 | } 129 | return details.response; 130 | } 131 | } 132 | 133 | export {WebRequest, annotateDetails}; 134 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2019.7.18", 3 | "name": "Privacy Possum", 4 | "author": "cowlicks@riseup.net", 5 | "manifest_version": 2, 6 | "applications": { 7 | "gecko": { 8 | "id": "woop-NoopscooPsnSXQ@jetpack" 9 | } 10 | }, 11 | "incognito": "spanning", 12 | "permissions": [ 13 | "tabs", 14 | "http://*/*", 15 | "https://*/*", 16 | "webRequest", 17 | "webRequestBlocking", 18 | "webNavigation", 19 | "storage" 20 | ], 21 | "icons": { 22 | "48": "media/icon48.png", 23 | "96": "media/icon96.png", 24 | "256": "media/icon256.png" 25 | }, 26 | "browser_action": { 27 | "default_icon": { 28 | "48": "media/icon48.png", 29 | "64": "media/icon64.png", 30 | "96": "media/icon96.png", 31 | "256": "media/icon256.png" 32 | }, 33 | "default_title": "Privacy Possum", 34 | "default_popup": "skin/popup.html" 35 | }, 36 | "background": { 37 | "page": "skin/background.html" 38 | }, 39 | "content_scripts": [ 40 | { 41 | "js": [ 42 | "js/contentscripts/twitter.js" 43 | ], 44 | "matches": [ 45 | "*://twitter.com/*", 46 | "*://tweetdeck.twitter.com/*" 47 | ], 48 | "run_at": "document_idle" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /src/media/block-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/block-icon.png -------------------------------------------------------------------------------- /src/media/etag-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/etag-icon.png -------------------------------------------------------------------------------- /src/media/etag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/media/fingerprinting-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/fingerprinting-icon.png -------------------------------------------------------------------------------- /src/media/icon-inactive.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 41 | 43 | 44 | 46 | image/svg+xml 47 | 49 | 50 | 51 | 52 | 53 | 55 | 61 | 62 | -------------------------------------------------------------------------------- /src/media/icon-inactive256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/icon-inactive256.png -------------------------------------------------------------------------------- /src/media/icon-inactive48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/icon-inactive48.png -------------------------------------------------------------------------------- /src/media/icon-inactive96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/icon-inactive96.png -------------------------------------------------------------------------------- /src/media/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 45 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 65 | 68 | 75 | 76 | 77 | 83 | 93 | 99 | 100 | -------------------------------------------------------------------------------- /src/media/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/icon128.png -------------------------------------------------------------------------------- /src/media/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/icon16.png -------------------------------------------------------------------------------- /src/media/icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/icon256.png -------------------------------------------------------------------------------- /src/media/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/icon32.png -------------------------------------------------------------------------------- /src/media/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/icon48.png -------------------------------------------------------------------------------- /src/media/icon64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/icon64.png -------------------------------------------------------------------------------- /src/media/icon96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/icon96.png -------------------------------------------------------------------------------- /src/media/logo-active-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/logo-active-100.png -------------------------------------------------------------------------------- /src/media/logo-inactive-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/logo-inactive-100.png -------------------------------------------------------------------------------- /src/media/logo-med256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/logo-med256.png -------------------------------------------------------------------------------- /src/media/med-possum.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 25 | 27 | image/svg+xml 28 | 30 | 31 | 32 | 33 | 34 | 36 | 61 | 67 | 68 | -------------------------------------------------------------------------------- /src/media/onOff.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 55 | 62 | 63 | -------------------------------------------------------------------------------- /src/media/popup-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cowlicks/privacypossum/a328104217e6bebc35ee48f9561255ef83c51c41/src/media/popup-icon.png -------------------------------------------------------------------------------- /src/media/popup-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 25 | 27 | image/svg+xml 28 | 30 | 31 | 32 | 33 | 34 | 36 | 61 | 69 | 70 | -------------------------------------------------------------------------------- /src/skin/background.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/skin/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top: 2px; 3 | background-color: white; 4 | font-family: Cantarell, Arial, sans-serif; 5 | width: 300px; 6 | } 7 | 8 | header { 9 | border-bottom: 1px solid rgb(238, 238, 238); 10 | margin-bottom: 8px; 11 | } 12 | 13 | footer { 14 | margin-top: 8px; 15 | border-top: 1px solid rgb(238, 238, 238); 16 | padding-top: 5px; 17 | } 18 | 19 | .grey { 20 | color: rgb(76, 76, 76); 21 | } 22 | 23 | code { 24 | background-color: rgb(238, 238, 238); 25 | padding-left: 3; 26 | padding-right: 3; 27 | } 28 | 29 | #title-bar { 30 | height: 45; 31 | display: flex; 32 | align-items: center; 33 | justify-content: space-between 34 | } 35 | 36 | #title-bar img { 37 | height: 35; 38 | } 39 | 40 | #branding { 41 | display: flex; 42 | align-items: center; 43 | } 44 | 45 | #title { 46 | font-size: 20; 47 | padding-left: 5; 48 | } 49 | 50 | #on-off { 51 | display: flex; 52 | align-items: center; 53 | } 54 | 55 | #on-off-text { 56 | font-size: 10; 57 | padding-right: 5; 58 | } 59 | 60 | #actions ul { 61 | padding: 0; 62 | } 63 | 64 | .action { 65 | overflow: hidden; 66 | white-space: nowrap; 67 | text-overflow: ellipsis; 68 | } 69 | 70 | .action-icon { 71 | padding-left: 5; 72 | padding-right: 5; 73 | vertical-align: middle; 74 | height: 20px; 75 | width: 20px; 76 | } 77 | -------------------------------------------------------------------------------- /src/skin/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 | 17 | 18 | --------------------------------------------------------------------------------