Repository: privacy-tech-lab/gpc-optmeowt
Branch: main
Commit: 5a29d0efe0bb
Files: 53
Total size: 258.2 KB
Directory structure:
gitextract_4uj713gw/
├── .github/
│ └── workflows/
│ └── node.js.yml
├── .gitignore
├── FUNDING.yml
├── LICENSE.md
├── README.md
├── README_ARCHITECTURE.md
├── package.json
├── src/
│ ├── assets/
│ │ └── icon.psd
│ ├── background/
│ │ ├── control.js
│ │ ├── protection/
│ │ │ ├── background.js
│ │ │ ├── listeners-chrome.js
│ │ │ ├── listeners-firefox.js
│ │ │ ├── protection-ff.js
│ │ │ └── protection.js
│ │ └── storage.js
│ ├── common/
│ │ ├── editDomainlist.js
│ │ ├── editRules.js
│ │ └── settings.js
│ ├── content-scripts/
│ │ ├── contentScript.js
│ │ ├── injection/
│ │ │ └── gpc-dom.js
│ │ └── registration/
│ │ └── gpc-dom.js
│ ├── data/
│ │ ├── complianceData.js
│ │ ├── defaultSettings.js
│ │ └── headers.js
│ ├── manifests/
│ │ ├── chrome/
│ │ │ ├── manifest-dev.json
│ │ │ └── manifest-dist.json
│ │ └── firefox/
│ │ ├── manifest-dev.json
│ │ └── manifest-dist.json
│ ├── options/
│ │ ├── components/
│ │ │ ├── scaffold-component.html
│ │ │ └── util.js
│ │ ├── dark-mode.css
│ │ ├── options.html
│ │ ├── options.js
│ │ ├── styles.css
│ │ └── views/
│ │ ├── about-view/
│ │ │ ├── about-view.html
│ │ │ └── about-view.js
│ │ ├── domainlist-view/
│ │ │ ├── domainlist-view.html
│ │ │ └── domainlist-view.js
│ │ ├── main-view/
│ │ │ ├── main-view.html
│ │ │ └── main-view.js
│ │ └── settings-view/
│ │ ├── settings-view.html
│ │ └── settings-view.js
│ ├── popup/
│ │ ├── popup.html
│ │ ├── popup.js
│ │ └── styles.css
│ ├── rules/
│ │ ├── gpc_exceptions_rules.json
│ │ └── universal_gpc_rules.json
│ └── theme/
│ └── darkmode.js
├── test/
│ └── background/
│ ├── cookieRemoval.test.js
│ └── gpc.test.js
├── ui-mockup/
│ ├── Mockup v1.0.xd
│ └── Popup designs/
│ └── Popup.xd
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/node.js.yml
================================================
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Tests
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
================================================
FILE: .gitignore
================================================
# Local user files
.DS_Store
.idea
.vscode
# Node.js
node_modules
# Distribution files
dist
dev
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: FUNDING.yml
================================================
github: privacy-tech-lab
================================================
FILE: LICENSE.md
================================================
MIT License
Copyright (c) 2021 privacy-tech-lab
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# OptMeowt 🐾
OptMeowt ("Opt Me Out") is a browser extension for opting you out from web tracking. OptMeowt works by sending Global Privacy Control (GPC) signals to visited websites per the [GPC spec](https://privacycg.github.io/gpc-spec/) that we are developing [at the W3C](https://github.com/privacycg/gpc-spec). In addition, OptMeowt also opts you out from Google's Topics API.
OptMeowt is developed and maintained by Ambrose Vannier (@avan36), Ruby Friedman (@RubyFri), Matthew Rich (@mcrich921), Austin Bosch (@Spongebosch) and Sebastian Zimmeck (@SebastianZimmeck) of the [privacy-tech-lab](https://privacytechlab.org/).
Former contributors are Francisca Wijaya (@franciscawijaya), Sage Altman (@sagealtman), Matt May (@Mattm27), Ebuka Akubilo (@eakubilo), Samir Cerrato (@samir-cerrato), Nate Levinson (@natelevinson10), Oliver Wang (@OliverWang13), Sophie Eng (@sophieeng), Kate Hausladen (@katehausladen), Jocelyn Wang (@Jocelyn0830), Kuba Alicki (@kalicki1), Stanley Markman (@stanleymarkman), Kiryl Beliauski (@kbeliauski), Daniel Knopf (@dknopf) and Abdallah Salia (@asalia-1).
[1. Research Publications](#1-research-publications)
[2. Promo Video](#2-promo-video)
[3. How Does OptMeowt Work?](#3-how-does-optmeowt-work)
[4. Installing OptMeowt from Source](#4-installing-optmeowt-from-source)
[5. Installing OptMeowt for Developers](#5-installing-optmeowt-for-developers)
[6. Installing the OptMeowt PETS 2023 Version](#6-installing-the-optmeowt-pets-2023-version)
[7. Testing](#7-testing)
[8. OptMeowt's Permission Use](#8-optmeowts-permission-use)
[9. OptMeowt's Architecture](#9-optmeowts-architecture)
[10. Directories in this Repo](#10-directories-in-this-repo)
[11. Third Party Libraries](#11-third-party-libraries)
[12. Developer Guide](#12-developer-guide)
[13. Thank You!](#13-thank-you)
## 1. Research Publications
- Sebastian Zimmeck, [Remarks on the Relevance of Privacy Expectations for Default Opt-out Settings](https://sebastianzimmeck.de/zimmeckEtAlRemarks2026.pdf), IEEE Symposium on Privacy Expectations (ISoPE), New York, New York, 2026, [BibTeX](https://sebastianzimmeck.de/citations.html#zimmeckEtAlGPCRemarks2026Bibtex).
- Sebastian Zimmeck, Nishant Aggarwal, Zachary Liu, Sage Altman and Konrad Kollnig, [Exercising the CCPA Opt-out Right on Android: Legally Mandated but Practically Challenging](https://sebastianzimmeck.de/zimmeckEtAlGPCAndroid2026.pdf), 26th Privacy Enhancing Technologies Symposium (PETS), Calgary, Canada, July 2026, [BibTeX](https://sebastianzimmeck.de/citations.html#zimmeckEtAlGPCAndroid2026Bibtex)
- Katherine Hausladen, Oliver Wang, Sophie Eng, Jocelyn Wang, Francisca Wijaya, Matt May and Sebastian Zimmeck, [Websites' Global Privacy Control Compliance at Scale and over Time](https://sebastianzimmeck.de/hausladenEtAlGPCWeb2025.pdf), 34th USENIX Security Symposium (USENIX Security), Seattle, CA, August 2025, [BibTeX](https://sebastianzimmeck.de/citations.html#hausladenEtAlGPCWeb2025Bibtex)
- Francisca Wijaya, Katherine Hausladen, Matt May, Oliver Wang, Sophie Eng and Sebastian Zimmeck, [Crawl for GPC: An Investigation of CCPA Compliance on the Internet](https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/research/wijayaEtAlCrawlForGPC2024Poster.pdf), Summer Research 2024 Poster Session, Wesleyan University, July 2024
- Sebastian Zimmeck, Nishant Aggarwal, Zachary Liu and Konrad Kollnig, [From Ad Identifiers to Global Privacy Control: The Status Quo and Future of Opting Out of Ad Tracking on Android](https://arxiv.org/abs/2407.14938), Under Review
- Sebastian Zimmeck, Eliza Kuller, Chunyue Ma, Bella Tassone and Joe Champeau, [Generalizable Active Privacy Choice: Designing a Graphical User Interface for Global Privacy Control](https://sebastianzimmeck.de/zimmeckEtAlGPC2024.pdf), 24th Privacy Enhancing Technologies Symposium (PETS), Bristol, UK and Online Event, July 2024, [BibTeX](https://sebastianzimmeck.de/citations.html#zimmeckEtAlGPC2024Bibtex)
- Katherine Hausladen, [Investigating the Current State of CCPA Compliance on the Internet](https://doi.org/10.14418/wes01.2.451), Master's Thesis, Wesleyan University, May 2024
- Nishant Aggarwal, Wesley Tan, Konrad Kollnig and Sebastian Zimmeck, [The Invisible Threat: Exploring Mobile Privacy Concerns](https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/research/aggarwalEtAlInvisibleThreat2023Poster.pdf), Summer Research 2023 Poster Session, Wesleyan University, July 2023
- Eliza Kuller, [Privacy Choice Mechanisms and Online Advertising: Can Generalizable Active Privacy Choices and Online Advertising Coexist?](https://doi.org/10.14418/wes01.1.2797), Undergraduate Honors Thesis, Wesleyan University, April 2023
- Sebastian Zimmeck, Oliver Wang, Kuba Alicki, Jocelyn Wang and Sophie Eng, [Usability and Enforceability of Global Privacy Control](https://sebastianzimmeck.de/zimmeckEtAlGPC2023.pdf), 23rd Privacy Enhancing Technologies Symposium (PETS)
Lausanne, Switzerland and Online Event, July 2023, [BibTeX](https://sebastianzimmeck.de/citations.html#zimmeckEtAlGPC2023Bibtex). For installing the OptMeowt version used in this paper, see the [instructions below](https://github.com/privacy-tech-lab/gpc-optmeowt#6-installing-the-optmeowt-pets-2023-version).
- Isabella Tassone, Chunyue Ma, Eliza Kuller, Joe Champeau and Sebastian Zimmeck, [Enhancing Online Privacy: The Development of Practical Privacy Choice Mechanisms](https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/research/tassoneEtAlEnhancingOnlinePrivacy2022Poster.pdf), Summer Research 2022 Poster Session, Wesleyan University, July 2022
- Sebastian Zimmeck, [Improving Internet Privacy with Global Privacy Control (GPC)](https://sebastianzimmeck.de/SaTC_PI_Meeting_2022_Poster_GPC_Zimmeck.pdf), 5th NSF Secure and Trustworthy Cyberspace Principal Investigator Meeting (2022 SaTC PI Meeting), Arlington, Virginia, USA, June 2022
- Kuba Alicki, [Don't Sell Our Data: Exploring CCPA Compliance via Automated Privacy Signal Detection](https://digitalcollections.wesleyan.edu/islandora/dont-sell-our-data-exploring-ccpa-compliance-automated-privacy-signal-detection), Undergraduate Honors Thesis, Wesleyan University, April 2022
- Eliza Kuller, Chunyue Ma, Isabella Tassone and Sebastian Zimmeck, [Making Online Privacy Choice Mechanisms Effective and Usable](http://summer21.research.wesleyan.edu/2021/07/22/balancing-usability-and-active-choice-while-developing-privacy-permission-schemes/), Summer Research 2021 Poster Session, Wesleyan University, Online, July 2021
- Sebastian Zimmeck and Kuba Alicki, [Standardizing and Implementing Do Not Sell (Short Paper)](https://sebastianzimmeck.de/zimmeckAndAlicki2020DoNotSell.pdf), 19th Workshop on Privacy in the Electronic Society (WPES), Online Event, November 2020, [BibTeX](https://sebastianzimmeck.de/citations.html#zimmeckAndAlicki2020DoNotSellBibtex)
## 2. Promo Video
[](https://drive.google.com/file/d/1eto77EV13WazpJN1hGXiKKsP2l7oMEu1/view?usp=share_link)
## 3. How Does OptMeowt Work?
OptMeowt sends GPC signals to websites when you browse the web. Such signals must be respected for California consumers per the California Consumer Privacy Act (CCPA), [Regs Section 999.315(d)](https://oag.ca.gov/sites/all/files/agweb/pdfs/privacy/oal-sub-final-text-of-regs.pdf). The number of jurisdictions that require websites to respect GPC signals is increasing. Some websites also respect them even if they are not required to do so.
In detail, OptMeowt uses the following methods to opt you out:
1. The [GPC header and JS property](https://privacycg.github.io/gpc-spec/).
2. A `Permissions-Policy` header that opts sites out of Google's [Topics API](https://developer.mozilla.org/en-US/docs/Web/API/Topics_API) on Chromium-based browsers.
**Opting Out of the Topics API:** As all browser vendors are phasing out the use of third-party cookies. In this context Google introduced the [Topics API](https://developer.mozilla.org/en-US/docs/Web/API/Topics_API). The Topics API identifies users' general areas of interest which are then used for personalized advertising. These topics are generated through observing and recording a users' browsing activity. Websites will then receive access to these topics that are stored on users' browsers. To opt you out of the Topics API OptMeowt sends a `Permissions-Policy` header to all the sites you visit. This approach follows [Google's documentation](https://developer.chrome.com/en/docs/privacy-sandbox/topics/#site-opt-out) on how to opt a site out of the Topics API. Note that this functionality of OptMeowt is only available for Chromium browsers as other browsers do not implement the Topics API.
**Customizing which sites receive GPC signals:** For every site you visit OptMeowt will automatically add its domain to the `domain list`. Each newly added domain will receive GPC signals by default. However, you can exclude domains that should not receive GPC signals. This functionality is available on OptMeowt's popup window and settings page.
For a more in-depth look at how OptMeowt works, check out our [Beginners Guide to OptMeowt](https://docs.google.com/document/d/1H0sA6hK0Q0OLT4Tz_Yp-byHi0U4Ue5DO8k7K-NXnY2Q/edit?usp=sharing). (The document is up to date as of its date. Later changes to OptMeowt are not reflected.)
## 4. Installing OptMeowt from Source
Here are the instructions for installing OptMeowt from the source files in this repo.
### Chrome and Firefox
1. Clone this repo locally with:
```bash
git clone https://github.com/privacy-tech-lab/gpc-optmeowt.git
```
You can also download a zipped copy and unzip it.
2. Install [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
3. From within your local `/gpc-optmeowt/` directory install OptMeowt's dependencies with:
```bash
npm ci
```
4. Build the project by running:
```bash
npm run build
```
This command will create a built for both Chrome and Firefox in `.../gpc-optmeowt/dist/chrome/` and `.../gpc-optmeowt/dist/firefox/`, respectively. `npm run build` will also create packaged versions of OptMeowt in `.../gpc-optmeowt/packages` for distribution on the Chrome Web Store and on Firefox Add-Ons.
### Chrome
5. In Chrome, navigate to the extensions page at `chrome://extensions/`.
6. Enable `Developer mode` with the slider on the top right corner of the extension page.
7. Click the `Load unpacked` button in the top left of the page.
8. Select the directory where you built OptMeowt, by default `/gpc-optmeowt/dist/chrome/` (the directory that contains the `manifest.json`).
### Firefox
5. In Firefox, navigate to the addons page with developer privileges at `about:debugging#/runtime/this-firefox`.
6. Under `Temporary extensions`, click `Load Temporary Add-on...`.
7. Select the manifest from the directory where you built OptMeowt, by default `/gpc-optmeowt/dist/firefox/manifest.json/`.
**Note**: OptMeowt is in active development and new features are being added, some of which may cause errors. You can always get the stable release version on the [Chrome Web Store](https://chrome.google.com/webstore/detail/optmeowt/hdbnkdbhglahihjdbodmfefogcjbpgbo) and on [Firefox Add-Ons](https://addons.mozilla.org/en-US/firefox/addon/optmeowt/). You can also disable sending GPC signals to a site in case OptMeowt causes it to break.
## 5. Installing OptMeowt for Developers
To build the development versions of OptMeowt follow the directions above but replace `npm run build` with:
```bash
npm run start
```
This command will run the npm script (referenced in `package.json`) that will call Webpack in development mode (Webpack settings are in `webpack.config.js`). `npm run start` will also initiate Webpack servers for both the Firefox and Chrome versions, which will listen for changes as you work and rebuild as necessary.
### 5.1 Webpack
Webpack will build the development versions of OptMeowt into the `dev` subdirectory instead of the `dist` subdirectory. The subdirectories for Chrome and Firefox are `dev/chrome` and `dev/firefox`, respectively.
Also, when you build for development, the development manifest (in `src/manifest-dev.json`) will be used instead of the distribution manifest (in `src/manifest-dist.json`). The development manifest contains an unsafe eval that we use for our source maps during development. The distribution manifest does not contain this eval. Webpack will select the correct manifest depending on whether you build for development or distribution.
To include new dependencies you can run:
```bash
npm install
```
Running this command instead of `npm ci` will include new dependencies in the `package-lock.json`, which is generated from the `package.json`.
### 5.2 Debugging
We like to use the [Debugger for Firefox](https://marketplace.visualstudio.com/items?itemName=firefox-devtools.vscode-firefox-debug) from within [Visual Studio Code](https://code.visualstudio.com/) when in development to help automating the development and build processes. The default behavior is `F5` to launch and load the extension in the browser. There is a similar extension that you can use for Chrome, [JavaScript Debugger](https://marketplace.visualstudio.com/items?itemName=ms-vscode.js-debug), which is already included in Visual Studio Code by default. Make sure to follow the online documentation on writing the correct `.vscode/launch.json` file, or other necessary settings files, in order to properly load OptMeowt with the debugger.
### 5.3 Developing on Windows
We have built most of our codebase in macOS, so path variables and similar code may cause the build to break in other OSs, in particular Windows. We recommend using macOS or installing a Linux OS if you will be working with the codebase in any significant manner.
## 6. Installing the OptMeowt PETS 2023 Version
The version of OptMeowt used in our 2023 PETS paper, [Usability and Enforceability of Global Privacy Control](https://sebastianzimmeck.de/zimmeckEtAlGPC2023.pdf), can be found in our [v3.0.0-paper release](https://github.com/privacy-tech-lab/gpc-optmeowt/releases/tag/v3.0.0-paper). To view the v3.0.0-paper code, you can [look at the repo here](https://github.com/privacy-tech-lab/gpc-optmeowt/tree/v3.0.0-paper). Instructions for building the extension locally are the same as stated above per our [Firefox instructions](https://github.com/privacy-tech-lab/gpc-optmeowt/tree/main#firefox). To activate Analysis mode in the v3.0.0-paper release press the `Protection Mode` label in the popup. In addition, Analysis mode requires other privacy extensions or browsers to be disabled. For further detailed information on how to use analysis mode, please refer to [our methodology](https://github.com/privacy-tech-lab/gpc-optmeowt/tree/v4.0.1/#4-analysis-mode-firefox-only).
Analysis mode used to be part of the OptMeowt extension but is now part of the [GPC Web Crawler](https://github.com/privacy-tech-lab/gpc-web-crawler), which you can use to analyze websites' GPC compliance at scale.
## 7. Testing
OptMeowt uses the [Mocha](https://mochajs.org/) framework as well as [Puppeteer](https://pptr.dev/) to execute its testing and continuous integration. The continuous integration is built into the OptMeowt repo with Github Actions. The [Actions tab](https://github.com/privacy-tech-lab/gpc-optmeowt/actions) shows all workflows and past unit test checks for previous PRs.
The test responsible for checking OptMeowt's ability to set the GPC signal can not be run with GitHub Actions. You can run it locally with:
```bash
npm test
```
Using Puppeteer this command will launch an automated headful browser on Chromium testing the Chrome GPC signal against the [GPC reference server](https://global-privacy-control.vercel.app/).
### 7.1 Running Automated Unit Tests
**Locally:**
You can run unit tests locally.
1. Clone this repo locally or download a zipped copy and unzip it.
2. Make sure npm is up to date by running `npm -v` to check the version and updating follow [the instructions on the npm site](https://docs.npmjs.com/try-the-latest-stable-version-of-npm), depending on your operating system.
3. Run tests with:
```bash
npm test
```
4. If Puppeteer is not installed, run:
```bash
npm install
```
**Continuous Integration:**
The continuous integration is built into the OptMeowt repo. Therefore, no changes to the extension environment are needed to run new tests.
### 7.2 Manual UI testing
The following procedure is for testing the OptMeowt extension UI, which cannot be automated. They are recommended to be performed manually as follows:
1. Download the version of the extension you want to test through `npm run start`. Then, download the unpacked dev version for your browser.
2. Navigate to a site with the well-known file, like
3. Click on the OptMeowt symbol in the top right of your browser.
- [ ] TEST 1: The symbol for the cat should be solid green.
- [ ] TEST 2: The URL of the website should be written under the "Protection Mode" banner.
- [ ] TEST 3: Global Privacy Control should be enabled.
- [ ] TEST 4: There should be a blue number detailing the number of domains receiving signals.
4. Click on the drop down for "3rd Party Domains".
- [ ] TEST 5: There should be sites that show up with Global Privacy Control switched on.
5. Navigate out of the "3rd Party Domains" drop down and click on the "Website Response" drop down
- [ ] TEST 6: There should be text showing that GPC Signals were accepted.
- [ ] TEST 7: Switch "Dark Mode" on and off and ensure the popup is correctly changing colors.
6. Navigate to the top of the popup and click on the "More" symbol (image: Sliders) to go to the Settings page.
7. In the main settings page, click on "Disable" and open the popup.
- [ ] TEST 8: The popup should be fully grayed out and showing the popup disabled.
8. In the website, move to the Domainlist page.
- [ ] TEST 9: There should be multiple domains showing in the Domainlist tab.
9. Go back to the main settings page and export Domainlist.
- [ ] TEST 10: Check the exported Domainlist and the Domainlist in the settings page to make sure the websites match up.
### 7.3 Creating a New Test
1. Navigate to `.../gpc-optmeowt/test/`. Then navigate to the folder in the test directory that corresponds to the tested function's location in the extension source code.
2. Create a new file in the matching folder. Name the file with the format `FUNCTION_NAME.test.js`.
For example, if testing a function named `sum` located in the folder `.../src/math`, create the test called `sum.test.js` in the folder `.../test/math`
3. Write test using [ECMAScript formatting](https://nodejs.org/api/esm.html).
## 8. OptMeowt's Permission Use
**Note**: We do not collect any data from you. Third parties will also not receive your data. The permissions OptMeowt is using are required for opting you out. To that end, OptMeowt uses the following permissions:
```json
"permissions": [
"declarativeNetRequest",
"webRequest",
"webRequestBlocking",
"webNavigation",
"",
"storage",
"activeTab",
"tabs",
"scripting"
]
```
- `declarativeNetRequest`: Allows OptMeowt to modify rules, allowing us to send the GPC header
- `webRequest`: Pauses outgoing HTTP requests to append opt out headers
- `webRequestBlocking`: Allows an extension to intercept and potentially block, modify, or redirect web requests before they are completed
- `webNavigation`: Similar to `webRequest`, allows OptMeowt to check when navigation requests are made to reset processes
- ``: Gives OptMeowt permission to access and interact with the content and data of any website visited by the browser
- `storage`: Allows OptMeowt to save your opt out preferences in your browser
- `activeTab`: Allows OptMeowt to set opt out signals on your active browser tab
- `tabs`: Allows OptMeowt to keep track of HTTP headers per tab to show you the opt out status of the current site in a popup
- `scripting`: Allows OptMeowt to declare content scripts and send the GPC DOM signal
## 9. OptMeowt's Architecture
Detailed information on OptMeowt's architecture is available in a [separate readme](https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/README_ARCHITECTURE.md).
**Note**: The architecture readme is only current as of its commit date.
## 10. Directories in this Repo
Here are the main directories in this repo:
- `src/`: Main contents of the OptMeowt browser extension.
- `src/assets`: Graphical elements of the extension, including logos and button images.
- `src/background`: Listeners for events and logic for sending privacy signals.
- `src/data`: Definitions of headers and privacy flags.
- `src/options`: UI elements and scripts for the supplemental options page.
- `src/popup`: UI elements and scripts for the popup inside the extensions bar.
- `src/theme`: Dark and light mode themes.
- `ui-mockup`: Contains PDF and XD files demonstrating the preliminary mockup of OptMeowt.
## 11. Third Party Libraries
OptMeowt uses various [third party libraries](https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/package.json). We thank the developers.
## 12. Developer Guide
If you have questions about OptMeowt's functionality or have found a bug, please check out our [FAQ \ Known quirks](https://github.com/privacy-tech-lab/gpc-optmeowt/wiki/FAQ-%5C-Known-quirks) page on the [Wiki](https://github.com/privacy-tech-lab/gpc-optmeowt/wiki). If you cannot find what you are looking for, feel free to open an issue, and we will address it.
**Note**: When viewing your browser's console on a site, a 404 error status code regarding the domain's GPC status file (`/.well-known/gpc.json`) may be shown. This behavior is normal and will occur (1) on domains that do not support GPC and (2) on domains that support GPC but do not host a `/.well-known/gpc.json` file.
## 13. Thank You!
We would like to thank our supporters!
Major financial support provided by the National Science Foundation.
Additional financial support provided by the Alfred P. Sloan Foundation, Wesleyan University, and the Anil Fernando Endowment.
Conclusions reached or positions taken are our own and not necessarily those of our financial supporters, its trustees, officers, or staff.
##
================================================
FILE: README_ARCHITECTURE.md
================================================
# Architecture Overview
```txt
src
├── assets # Static images & files
├── background # Manages the background script processes
│ ├── protection
│ │ ├── background.js
│ │ ├── listeners-chrome.js
│ │ ├── listeners-firefox.js
│ │ ├── protection-ff.js
│ │ └── protection.js
│ ├── control.js
│ └── storage.js
├── common # Manages header sending and rules
│ ├── editDomainlist.js
│ └── editRules.js
├── content-scripts # Runs processes on site on adds DOM signal
│ ├── injection
│ │ └── gpc-dom.js
│ ├── registration
│ │ └── gpc-dom.js
│ └── contentScript.js
├── data # Stores constant data (DNS signals, settings, etc.)
│ ├── defaultSettings.js
│ ├── headers.js
│ └── regex.js
├── manifests # Stores manifests
│ ├── chrome
│ │ ├── manifest-dev.json
│ │ └── manifest-dist.json
│ ├── firefox
│ │ ├── manifest-dev.json
│ │ └── manifest-dist.json
├── options # Options page frontend
│ ├── components
│ │ ├── scaffold-component.html
│ │ └── util.js
│ ├── views
│ │ ├── about-view
│ │ │ ├── about-view.html
│ │ │ └── about-view.js
│ │ ├── domainlist-view
│ │ │ ├── domainlist-view.html
│ │ │ └── domainlist-view.js
│ │ ├── main-view
│ │ │ ├── main-view.html
│ │ │ └── main-view.js
│ │ └── settings-view
│ │ ├── settings-view.html
│ │ └── settings-view.js
│ ├── dark-mode.css
│ ├── options.html
│ ├── options.js
│ └── styles.css
├── popup # Popup page frontend
│ ├── popup.html
│ ├── popup.js
│ └── styles.css
├── rules # Manages universal rules
│ ├── gpc_exceptions_rules.json
│ └── universal_gpc_rules.json
└── theme # Contains darkmode
└── darkmode.js
test
└── background
└── gpc.test.js
```
The following source folders have detailed descriptions further in the document.
[background](#background)\
[common](#common)\
[content-scripts](#content-scripts)\
[data](#data)\
[manifests](#manifests)\
[options](#options)\
[popup](#popup)\
[rules](#rules)\
[theme](#theme)
## background
1. `protection`
2. `control.js`
3. `storage.js`
### `src/background/protection`
1. `background.js`
2. `listeners-chrome.js`
3. `listeners-firefox.js`
4. `protection.js`
5. `protection-ff.js`
#### `protection/background.js`
Initializes the protection mode listeners.
#### `protection/listeners-chrome.js` and `protection/listeners-firefox.js`
Creates listeners for Chrome and Firefox, respectively.
#### `protection/protection.js`
Manages the domain list with functions like `logData();`, `updateDomainlistAndSignal();`, `pullToDomainlistCache();`, `syncDomainlists();`. Also responsible for supplying the popup with the proper information with `dataToPopup();`. Also creates listeners to watch the popup for domain list changes.
#### `protection/protection-ff.js`
Manages the domain list for Firefox.
### `background/control.js`
Uses `protection.js` to turn the extension on and off.
### `background/storage.js`
Handles storage uploads and downloads.
## common
1. `editDomainlist.js`
2. `editRules.js`
This folder holds common internal API's to be used throughout the extension.
### `common/editDomainlist.js`
Is an internal API to be used for editing a users domain list.
### `common/editRules.js`
Is an internal API to be used for editing rules that allow us to send the GPC header.
## content-scripts
1. `injection`
2. `registration`
3. `contentScript.js`
This folder contains our main content script and methods for injecting the GPC signal into the DOM.
### `src/content-scripts/injection`
1. `gpc-dom.js`
`gpc-dom.js` injects the DOM signal.
### `src/content-scripts/registration`
1. `gpc-dom.js`
This file injects `injection/gpc-dom.js` into the page using a static script. (Based on [this stack overflow thread](https://stackoverflow.com/questions/9515704/use-a-content-script-to-access-the-page-context-variables-and-functions))
### `content-scripts/contentScript.js`
This runs on every page and sends information to signal background processes.
## data
1. `defaultSettings.js`
2. `headers.js`
3. `regex.js`
This folder contains static data.
### `data/defaultSettings.js`
Contains the default OptMeowt settings.
### `data/headers.js`
Contains the default headers to be attached to online requests.
### `data/regex.js`
Contains regular expressions for finding "do not sell" links and related privacy signals.
## manifests
1. `chrome`
2. `firefox`
Contains the extension manifests
### `manifests/chrome`
1. `manifest-dev.json`
2. `manifest-dist.json`
Contains the development and distribution manifests for Chrome
### `manifests/firefox`
1. `manifest-dev.json`
2. `manifest-dist.json`
Contains the development and distribution manifests for Firefox
## options
1. `components`
2. `views`
3. `dark-mode.css`
4. `options.html`
This folder contains all of the frontend code
### `options/components`
1. `scaffold-component.html`
2. `util.js`
This folder contains the basic layout of every options page and helper functions to help render the pages.
### `options/views`
1. `about-view`
2. `domainlist-view`
3. `main-view`
4. `settings-view`
Contains all frontend and implementation of the settings pages.
#### `views/about-view`
1. `about-view.html`
2. `about-view.js`
Builds the "about" page
#### `views/domainlist-view`
1. `domainlist-view.html`
2. `domainlist-view.js`
Builds the domain list page
#### `views/main-view`
1. `main-view.html`
2. `main-view.js`
Builds the main options page
#### `views/settings-view`
1. `settings-view.html`
2. `settings-view.js`
Builds the settings page
### `options/dark-mode.css`
Contains the dark-mode styles for OptMeowt.
### `options/options.html` and `options/options.js`
Is the entry point for the main options page.
### `options/styles.css`
Contains the basic styles for OptMeowt.
## popup
1. `popup.html`
2. `popup.js`
3. `styles.css`
Contains the frontend and implementation for the OptMeowt popup.
## rules
1. `gpc_exception_rules.json`
2. `universal_gpc_rules.json`
Contains rule framework for sending GPC headers to sites.
## theme
1. `darkmode.js`
Contains the dark mode functionality.
**Links to APIs:**
Chrome: [webRequest](https://developer.chrome.com/docs/extensions/reference/webRequest/) and [webNavigation](https://developer.chrome.com/docs/extensions/reference/webNavigation/)
Firefox: [webRequest](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest) and [webNavigation](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webNavigation)
================================================
FILE: package.json
================================================
{
"name": "optmeowt",
"version": "6.1.0",
"description": "A privacy extension that allows users to exercise rights under GPC",
"main": "index.js",
"type": "module",
"scripts": {
"prestart": "rimraf dev",
"start": "concurrently -k npm:start:firefox npm:start:chrome",
"start:firefox": "webpack --watch --mode development --env firefox",
"start:chrome": "webpack --watch --mode development --env chrome",
"prebuild": "rimraf dist && mkdir dist && mkdir dist/packages",
"build": "npm run build:firefox && npm run build:chrome",
"build:firefox": "webpack --mode production --env firefox",
"build:chrome": "webpack --mode production --env chrome",
"postbuild:firefox": "cd dist/firefox && zip -rFSX ../packages/ff-optmeowt-$npm_package_version.zip * -x '*.git*' -x '*.DS_Store*' -x '*.txt*'",
"postbuild:chrome": "cd dist/chrome && zip -rFSX ../packages/chrome-optmeowt-$npm_package_version.zip * -x '*.git*' -x '*.DS_Store*' -x '*.txt*'",
"test": "mocha $(find test -name '*.js')"
},
"repository": {
"type": "git",
"url": "git+https://github.com/privacy-tech-lab/gpc-optmeowt.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/privacy-tech-lab/gpc-optmeowt/issues"
},
"homepage": "https://github.com/privacy-tech-lab/gpc-optmeowt#readme",
"dependencies": {
"animate.css": "^4.1.1",
"darkmode-js": "^1.5.7",
"file-saver": "^2.0.5",
"idb": "^7.1.1",
"mocha": "^10.8.2",
"mustache": "^4.2.0",
"path": "^0.12.7",
"psl": "^1.8.0",
"puppeteer": "^22.15.0",
"rimraf": "^3.0.2",
"tippy.js": "^6.3.7",
"uikit": "3.6.9"
},
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/preset-env": "^7.20.2",
"babel-loader": "^9.1.2",
"clean-webpack-plugin": "^4.0.0",
"concurrently": "^6.2.1",
"copy-webpack-plugin": "^14.0.0",
"css-loader": "^5.2.7",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.3.2",
"prettier": "^2.3.2",
"string-replace-loader": "^3.0.3",
"style-loader": "^2.0.0",
"wait-on": "^7.2.0",
"webpack": "^5.105.0",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^5.2.4",
"workbox-webpack-plugin": "^7.3.0"
},
"overrides": {
"serialize-javascript": "^7.0.4"
},
"resolutions": {
"ws": "^8.17.1"
}
}
================================================
FILE: src/background/control.js
================================================
/*
Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md
privacy-tech-lab, https://privacytechlab.org/
*/
/*
control.js
================================================================================
control.js manages persistent data, message liseteners, in particular
to manage the state & functionality mode of the extension
*/
import {
init as initProtection_ff,
halt as haltProtection_ff,
} from "./protection/protection-ff.js";
import {
init as initProtection_cr,
halt as haltProtection_cr,
} from "./protection/protection.js";
import { defaultSettings } from "../data/defaultSettings.js";
import { stores, storage } from "./storage.js";
import { reloadDynamicRules } from "../common/editRules.js";
import {
debug_domainlist_and_dynamicrules,
updateRemovalScript,
} from "../common/editDomainlist.js";
async function enable() {
var initProtection = initProtection_cr;
initProtection();
}
function disable() {
var haltProtection = haltProtection_cr;
haltProtection();
}
/******************************************************************************/
// Initializers
// This is the very first thing the extension runs
(async () => {
chrome.scripting.registerContentScripts([
{
id: "1",
matches: [""],
excludeMatches:["https://example.com/"],
js: ["content-scripts/registration/gpc-dom.js"],
runAt: "document_start",
}
]);
// Check if the browser is Firefox
if ("$BROWSER" == "firefox") {
chrome.runtime.onInstalled.addListener(function (details) {
if (details.reason === 'install') {
chrome.runtime.openOptionsPage((result) => {});
}
});
}
// Initializes the default settings
let settingsDB = await storage.getStore(stores.settings);
for (let setting in defaultSettings) {
if (typeof settingsDB[setting] === "undefined") {
await storage.set(stores.settings, defaultSettings[setting], setting);
}
}
const localSettings = await chrome.storage.local.get(
"WELLKNOWN_CHECK_ENABLED"
);
if (typeof localSettings.WELLKNOWN_CHECK_ENABLED === "undefined") {
await chrome.storage.local.set({
WELLKNOWN_CHECK_ENABLED: defaultSettings["WELLKNOWN_CHECK_ENABLED"],
});
}
let isEnabled = await storage.get(stores.settings, "IS_ENABLED");
if (isEnabled) {
// Turns on the extension
enable();
updateRemovalScript();
reloadDynamicRules();
}
})();
/******************************************************************************/
// Mode listeners
// (1) Handle extension activeness is changed by calling all halt
// - Make sure that I switch extensionmode and separate it from mode.domainlist
// (2) Handle extension functionality with listeners and message passing
/**
* Listeners for information from --POPUP-- or --OPTIONS-- page
* This is the main "hub" for message passing between the extension components
* https://developer.chrome.com/docs/extensions/mv3/messaging/
*/
chrome.runtime.onMessage.addListener(async function (
message
) {
if (message.msg === "TURN_ON_OFF") {
let isEnabled = message.data.isEnabled; // can be undefined
if (isEnabled) {
await storage.set(stores.settings, true, "IS_ENABLED");
enable();
} else {
await storage.set(stores.settings, false, "IS_ENABLED");
disable();
}
}
if (message.msg === "CHANGE_IS_DOMAINLISTED") {
let isDomainlisted = message.data.isDomainlisted; // can be undefined // not used
}
});
================================================
FILE: src/background/protection/background.js
================================================
/*
Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md
privacy-tech-lab, https://privacytechlab.org/
*/
/*
background.js
================================================================================
background.js is the main background script handling OptMeowt's
main opt-out functionality
*/
import { enableListeners, disableListeners } from "./listeners-$BROWSER.js";
import { stores, storage } from "../storage.js";
import { defaultSettings } from "../../data/defaultSettings.js";
// We could alt. use this in place of "building" for chrome/ff, just save it to settings in storage
var userAgent =
window.navigator.userAgent.indexOf("Firefox") > -1 ? "moz" : "chrome";
/******************************************************************************/
/**
* Enables extension functionality and sets site listeners
* Information regarding the functionality and timing of webRequest and webNavigation
* can be found on Mozilla's & Chrome's API docuentation sites (also linked above)
*
* The actual listeners are located in `listeners-(chosen browser).js`
* The functions called on event occurance are located in `events.js`
*
* HIERARCHY: manifest.json --> protection --> background.js --> listeners-$BROWSER.js --> events.js
*/
/******************************************************************************/
/**
* Initializes the extension
* Place all initialization necessary, as high level as can be, here.
*/
async function init() {
enableListeners();
}
function halt() {
disableListeners();
}
/******************************************************************************/
export const background = {
init,
halt,
};
================================================
FILE: src/background/protection/listeners-chrome.js
================================================
/*
Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md
privacy-tech-lab, https://privacytechlab.org/
*/
/*
listeners-chrome.js
================================================================================
listeners-chrome.js holds the on-page-visit listeners for chrome that activate
our main functionality
*/
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onBeforeRequest
// https://developer.chrome.com/docs/extensions/reference/webRequest/
// This is the extraInfoSpec array of strings
const CHROME_REQUEST_SPEC = ["requestHeaders", "extraHeaders"];
const CHROME_RESPONSE_SPEC = ["responseHeaders", "extraHeaders"];
// This is the filter object
const FILTER = { urls: [""] };
/**
* Enables extension functionality and sets site listeners
* Information regarding the functionality and timing of webRequest and webNavigation
* can be found on Mozilla's & Chrome's API docuentation sites (also linked above)
*
* The functions called on event occurance are located in `events.js`
*/
function enableListeners(callbacks) {
const {
onBeforeSendHeaders,
onHeadersReceived,
onBeforeNavigate,
onCommitted,
onCompleted,
} = callbacks;
// (4) global Chrome listeners
chrome.webRequest.onBeforeSendHeaders.addListener(
onBeforeSendHeaders,
FILTER,
CHROME_REQUEST_SPEC
);
chrome.webRequest.onHeadersReceived.addListener(
onHeadersReceived,
FILTER,
CHROME_RESPONSE_SPEC
);
chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigate, FILTER);
chrome.webNavigation.onCommitted.addListener(onCommitted, FILTER);
chrome.webNavigation.onCompleted.addListener(onCompleted, FILTER);
}
/**
* Disables background listeners
*/
function disableListeners(callbacks) {
const {
onBeforeSendHeaders,
onHeadersReceived,
onBeforeNavigate,
onCommitted,
onCompleted,
} = callbacks;
chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders);
chrome.webRequest.onHeadersReceived.removeListener(onHeadersReceived);
chrome.webNavigation.onBeforeNavigate.removeListener(onBeforeNavigate);
chrome.webNavigation.onCommitted.removeListener(onCommitted);
chrome.webNavigation.onCompleted.removeListener(onCompleted);
}
export { enableListeners, disableListeners };
================================================
FILE: src/background/protection/listeners-firefox.js
================================================
/*
Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md
privacy-tech-lab, https://privacytechlab.org/
*/
/*
listeners-firefox.js
================================================================================
listeners-firefox.js holds the on-page-visit listeners for firefox that activate
our main functionality
*/
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onBeforeRequest
// https://developer.chrome.com/docs/extensions/reference/webRequest/
// This is the extraInfoSpec array of strings
const MOZ_REQUEST_SPEC = ["requestHeaders", "blocking"];
const MOZ_RESPONSE_SPEC = ["responseHeaders", "blocking"];
// This is the filter object
const FILTER = { urls: [""] };
/**
* Enables extension functionality and sets site listeners
* Information regarding the functionality and timing of webRequest and webNavigation
* can be found on Mozilla's & Chrome's API docuentation sites (also linked above)
*
* The functions called on event occurance are located in `events.js`
*/
function enableListeners(callbacks) {
const {
onBeforeSendHeaders,
onHeadersReceived,
onBeforeNavigate,
onCommitted,
onCompleted,
} = callbacks;
// (4) global Firefox listeners
chrome.webRequest.onBeforeSendHeaders.addListener(
onBeforeSendHeaders,
FILTER,
MOZ_REQUEST_SPEC
);
chrome.webRequest.onHeadersReceived.addListener(
onHeadersReceived,
FILTER,
MOZ_RESPONSE_SPEC
);
chrome.webNavigation.onBeforeNavigate.addListener(onBeforeNavigate);
chrome.webNavigation.onCommitted.addListener(onCommitted);
chrome.webNavigation.onCompleted.addListener(onCompleted);
}
/**
* Disables background listeners
*/
function disableListeners(callbacks) {
const {
onBeforeSendHeaders,
onHeadersReceived,
onBeforeNavigate,
onCommitted,
onCompleted,
} = callbacks;
chrome.webRequest.onBeforeSendHeaders.removeListener(onBeforeSendHeaders);
chrome.webRequest.onHeadersReceived.removeListener(onHeadersReceived);
chrome.webNavigation.onBeforeNavigate.removeListener(onBeforeNavigate);
chrome.webNavigation.onCommitted.removeListener(onCommitted);
chrome.webNavigation.onCompleted.removeListener(onCompleted);
}
export { enableListeners, disableListeners };
================================================
FILE: src/background/protection/protection-ff.js
================================================
/*
Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md
privacy-tech-lab, https://privacytechlab.org/
*/
/*
protection.js
================================================================================
protection.js (1) Implements our per-site functionality for the background listeners
(2) Handles cached values & message passing to popup & options page
*/
import { stores, storage } from "../storage.js";
import { defaultSettings } from "../../data/defaultSettings.js";
import { headers } from "../../data/headers.js";
import { enableListeners, disableListeners } from "./listeners-$BROWSER.js";
import psl from "psl";
import { isWellknownCheckEnabled } from "../../common/settings.js";
/******************************************************************************/
/******************************************************************************/
/********** # Initializers (cached values) **********/
/******************************************************************************/
/******************************************************************************/
var domainlist = {}; // Caches & mirrors domainlist in storage
var isDomainlisted = defaultSettings["IS_DOMAINLISTED"];
var tabs = {}; // Caches all tab infomration, i.e. requests, etc.
var wellknown = {}; // Caches wellknown info to be sent to popup
var signalPerTab = {}; // Caches if a signal is sent to render the popup icon
var activeTabID = 0; // Caches current active tab id
var sendSignal = true; // Caches if the signal can be sent to the curr domain
var domPrev3rdParties = {}; //stores all the 3rd parties by domain (resets when you quit chrome)
var globalParsedDomain;
async function reloadVars() {
let storedDomainlisted = await storage.get(
stores.settings,
"IS_DOMAINLISTED"
);
if (storedDomainlisted) {
isDomainlisted = storedDomainlisted;
}
}
reloadVars();
/******************************************************************************/
/******************************************************************************/
/********** # Lisetener callbacks - Main functionality **********/
/******************************************************************************/
/******************************************************************************/
/*
* The four following functions are all related to the four main listeners in
* `background.js`. These four functions implement all the other helper
* functions below
*/
const listenerCallbacks = {
/**
* Handles all signal processessing prior to sending request headers
* @param {object} details - retrieved info passed into callback
* @returns {array} details.requestHeaders from addHeaders
*/
onBeforeSendHeaders: async (details) => {
await updateDomainlist(details);
if (true) {
signalPerTab[details.tabId] = true;
return addHeaders(details);
}
},
/**
* @param {object} details - retrieved info passed into callback
*/
onHeadersReceived: (details) => {
logData(details);
},
/**
* @param {object} details - retrieved info passed into callback
*/
onBeforeNavigate: (details) => {
// Resets certain cached info
if (details.frameId === 0) {
wellknown[details.tabId] = null;
signalPerTab[details.tabId] = false;
tabs[activeTabID].REQUEST_DOMAINS = {};
}
},
/**
* Adds DOM property
* @param {object} details - retrieved info passed into callback
*/
onCommitted: async (details) => {
if (true) {
addDomSignal(details);
updatePopupIcon(details);
}
},
}; // closes listenerCallbacks object
/******************************************************************************/
/******************************************************************************/
/********** # Listener helper fxns - Main functionality **********/
/******************************************************************************/
/******************************************************************************/
/**
* Attaches headers from `headers.js` to details.requestHeaders
* @param {object} details - retrieved info passed into callback
* @returns {array} details.requestHeaders
*/
function addHeaders(details) {
console.log("addHeaders called");
for (let signal in headers) {
let s = headers[signal];
details.requestHeaders.push({ name: s.name, value: s.value });
}
return { requestHeaders: details.requestHeaders };
}
/**
* Runs `dom.js` to attach DOM signal
* @param {object} details - retrieved info passed into callback
*/
function addDomSignal(details) {
console.log("addDomSignal called");
chrome.scripting.executeScript(details.tabId, {
file: "../../content-scripts/injection/gpc-dom.js",
frameId: details.frameId, // Supposed to solve multiple injections
// as opposed to allFrames: true
runAt: "document_start",
});
}
function getCurrentParsedDomain() {
return new Promise((resolve, reject) => {
try {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
let tab = tabs[0];
let url = new URL(tab.url);
let parsed = psl.parse(url.hostname);
let domain = parsed.domain;
globalParsedDomain = domain; // for global scope variable
resolve(domain);
});
} catch(e) {
reject();
}
})
}
/**
* Checks whether a particular domain should receive a DNS signal
* (1) Parse url to get domain for domainlist
* (2) Update domains by adding current domain to domainlist in storage.
* (3) Updates the 3rd party list for the currentDomain
* (4) Check to see if we should send signal.
*
* Currently, it only adds to domainlist store as NULL if it doesnt exist
* @param {Object} details - callback object according to Chrome API
*/
async function updateDomainlist(details) {
let url = new URL(details.url);
let parsedUrl = psl.parse(url.hostname);
let parsedDomain = parsedUrl.domain;
if (parsedDomain == null || parsedDomain == undefined) {
return;
}
let parsedDomainVal = domainlist[parsedDomain];
if (parsedDomainVal === undefined) {
storage.set(stores.domainlist, null, parsedDomain); // Sets to storage async
domainlist[parsedDomain] = null; // Sets to cache
parsedDomainVal = null;
}
//get the current parsed domain--this is used to store 3rd parties (using globalParsedDomain variable)
let currentDomain = await getCurrentParsedDomain();
//initialize the objects
if (!(activeTabID in domPrev3rdParties)){
domPrev3rdParties[activeTabID] = {};
}
if (!(currentDomain in domPrev3rdParties[activeTabID]) ){
domPrev3rdParties[activeTabID][currentDomain] = {};
}
//as they come in, add the parsedDomain to the object with null value (just a placeholder)
domPrev3rdParties[activeTabID][currentDomain][parsedDomain] = null;
(isDomainlisted)
? ((parsedDomainVal === null) ? sendSignal = true : sendSignal = false)
: sendSignal = true;
}
function updatePopupIcon(details) {
if (wellknown[details.tabId] === undefined) {
wellknown[details.tabId] = null;
}
if (wellknown[details.tabId] === null) {
chrome.browserAction.setIcon({
tabId: details.tabId,
path: "assets/face-icons/optmeow-face-circle-green-ring-128.png",
});
}
}
function logData(details) {
let url = new URL(details.url);
let parsed = psl.parse(url.hostname);
if (tabs[details.tabId] === undefined) {
tabs[details.tabId] = { DOMAIN: null, REQUEST_DOMAINS: {}, TIMESTAMP: 0 };
tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] = {
URLS: {},
RESPONSE: details.responseHeaders,
TIMESTAMP: details.timeStamp,
};
tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS = {
URL: details.url,
RESPONSE: details.responseHeaders,
};
} else {
if (tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] === undefined) {
tabs[details.tabId].REQUEST_DOMAINS[parsed.domain] = {
URLS: {},
RESPONSE: details.responseHeaders,
TIMESTAMP: details.timeStamp,
};
tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS[details.url] = {
RESPONSE: details.responseHeaders,
};
} else {
tabs[details.tabId].REQUEST_DOMAINS[parsed.domain].URLS[details.url] = {
RESPONSE: details.responseHeaders,
};
}
}
}
async function pullToDomainlistCache() {
let domain;
let domainlistKeys = await storage.getAllKeys(stores.domainlist);
let domainlistValues = await storage.getAll(stores.domainlist);
for (let key in domainlistKeys) {
domain = domainlistKeys[key];
domainlist[domain] = domainlistValues[key];
}
}
async function syncDomainlists() {
// (1) Reconstruct a domainlist indexedDB object from storage
// (2) Iterate through local domainlist
// --- If item in cache NOT in domainlistKeys/domainlistDB, add to storage
// via storage.set()
// (3) Iterate through all domain keys in indexedDB domainlist
// --- If key NOT in cached domainlist, add to cached domainlist
let domainlistKeys = await storage.getAllKeys(stores.domainlist);
let domainlistValues = await storage.getAll(stores.domainlist);
let domainlistDB = {};
let domain;
for (let key in domainlistKeys) {
domain = domainlistKeys[key];
domainlistDB[domain] = domainlistValues[key];
}
for (let domainKey in domainlist) {
if (!domainlistDB[domainKey]) {
await storage.set(stores.domainlist, domainlist[domainKey], domainKey);
}
}
for (let domainKey in domainlistDB) {
if (!domainlist[domainKey]) {
domainlist[domainKey] = domainlistDB[domainKey];
}
}
}
/**
* whether the curr site should get privacy signals
* (We need to try and make a synchronous version, esp. for DOM issue & related
* message passing with the contentscript which injects the DOM signal)
* @returns {bool} sendSignal
*/
async function sendPrivacySignal(domain) {
let sendSignal;
const extensionEnabled = await storage.get(stores.settings, "IS_ENABLED");
const extensionDomainlisted = await storage.get(
stores.settings,
"IS_DOMAINLISTED"
);
const domainDomainlisted = await storage.get(stores.domainlist, domain);
if (extensionEnabled) {
if (extensionDomainlisted) {
// Recall we must flip the value of the domainlisted domain
// due to how to how defined domainlisted values, corresponding to MV3
// declarativeNetRequest rule exceptions
// (i.e., null => no rule exists, valued => exception rule exists)
sendSignal = !domainDomainlisted ? true : false;
} else {
sendSignal = true;
}
} else {
sendSignal = false;
}
return sendSignal;
}
/******************************************************************************/
/******************************************************************************/
/********** # Message Passing - Popup helper fxns **********/
/******************************************************************************/
/******************************************************************************/
function handleSendMessageError() {
const error = chrome.runtime.lastError;
if (error) {
console.warn(error.message);
}
}
// Info back to popup
function dataToPopup() {
let requestsData = {};
if (tabs[activeTabID] !== undefined) {
requestsData = domPrev3rdParties[activeTabID][globalParsedDomain];
console.log("requests by tabID:", domPrev3rdParties);
}
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
let tabID = tabs[0]["id"];
let wellknownData = wellknown[tabID];
let popupData = {
requests: requestsData,
wellknown: wellknownData,
};
chrome.runtime.sendMessage(
{
msg: "POPUP_PROTECTION_DATA",
data: popupData,
},
handleSendMessageError
);
});
}
/******************************************************************************/
/******************************************************************************/
/********** # Message passing **********/
/******************************************************************************/
/******************************************************************************/
/**
* Currently only handles syncing domainlists between storage and memory
* This runs when the popup disconnects from the background page
* @param {Port} port
*/
function onConnectHandler(port) {
if (port.name === "POPUP") {
port.onDisconnect.addListener(function () {
syncDomainlists();
});
}
}
/**
* This is currently only to handle adding the GPC DOM signal.
* I'm not sure how to fit it into an async call, it doesn't want to connect.
* It would be nice to merge the two onMessage handlers.
* TODO: This method still seems to have a timing issue. Doesn't always show DOM signal as thumbs up on reference site.
* @returns {Bool} true (lets us send asynchronous responses to senders)
*/
function onMessageHandlerSynchronous(message, sender, sendResponse) {
if (message.msg === "APPEND_GPC_PROP") {
let url = new URL(sender.origin);
let parsed = psl.parse(url.hostname);
let domain = parsed.domain;
const r = sendPrivacySignal(domain);
r.then((r) => {
const response = {
msg: "APPEND_GPC_PROP_RESPONSE",
sendGPC: r,
};
sendResponse(response);
});
}
return true;
}
/**
* Listeners for information from --POPUP-- or --OPTIONS-- page
* This is the main "hub" for message passing between the extension components
* https://developer.chrome.com/docs/extensions/mv3/messaging/
*/
async function onMessageHandlerAsync(message, sender, sendResponse) {
if (message.msg === "GET_WELLKNOWN_CHECK_ENABLED") {
const enabled = await isWellknownCheckEnabled();
await chrome.storage.local.set({ WELLKNOWN_CHECK_ENABLED: enabled });
sendResponse({ enabled });
return true;
}
if (message.msg === "TOGGLE_WELLKNOWN_CHECK") {
const enabled = message.data?.enabled !== false;
await storage.set(stores.settings, enabled, "WELLKNOWN_CHECK_ENABLED");
await chrome.storage.local.set({ WELLKNOWN_CHECK_ENABLED: enabled });
if (!enabled) {
await storage.clear(stores.wellknownInformation);
wellknown = {};
}
}
if (message.msg === "CHANGE_IS_DOMAINLISTED") {
isDomainlisted = message.data.isDomainlisted;
storage.set(stores.settings, isDomainlisted, "IS_DOMAINLISTED");
}
if (message.msg === "SET_TO_DOMAINLIST") {
let { domain, key } = message.data;
domainlist[domain] = key; // Sets to cache
storage.set(stores.domainlist, key, domain); // Sets to long term storage
}
if (message.msg === "REMOVE_FROM_DOMAINLIST") {
let domain = message.data;
delete domainlist[domain];
}
if (message.msg === "POPUP_PROTECTION") {
dataToPopup();
}
if (message.msg === "CONTENT_SCRIPT_WELLKNOWN") {
const wellknownCheckEnabled = await isWellknownCheckEnabled();
if (!wellknownCheckEnabled) {
return true;
}
let tabID = sender.tab.id;
wellknown[tabID] = message.data;
if (wellknown[tabID]["gpc"] === true) {
setTimeout(() => {}, 10000);
if (signalPerTab[tabID] === true) {
chrome.browserAction.setIcon({
tabId: tabID,
path: "assets/face-icons/optmeow-face-circle-green-128.png",
});
}
}
}
if (message.msg === "CONTENT_SCRIPT_TAB") {
let url = new URL(sender.origin);
let parsed = psl.parse(url.hostname);
let domain = parsed.domain;
let tabID = sender.tab.id;
if (tabs[tabID] === undefined) {
tabs[tabID] = {
DOMAIN: domain,
REQUEST_DOMAINS: {},
TIMESTAMP: message.data,
};
} else if (tabs[tabID].DOMAIN !== domain) {
tabs[tabID].DOMAIN = domain;
let urls = tabs[tabID]["REQUEST_DOMAINS"];
for (let key in urls) {
if (urls[key]["TIMESTAMP"] >= message.data) {
tabs[tabID]["REQUEST_DOMAINS"][key] = urls[key];
} else {
delete tabs[tabID]["REQUEST_DOMAINS"][key];
}
}
tabs[tabID]["TIMESTAMP"] = message.data;
}
}
if (message.msg === "FORCE_RELOAD") {
pullToDomainlistCache();
}
return true; // Async callbacks require this
}
function initMessagePassing() {
chrome.runtime.onConnect.addListener(onConnectHandler);
chrome.runtime.onMessage.addListener(onMessageHandlerAsync);
chrome.runtime.onMessage.addListener(onMessageHandlerSynchronous);
}
function closeMessagePassing() {
chrome.runtime.onConnect.removeListener(onConnectHandler);
chrome.runtime.onMessage.removeListener(onMessageHandlerAsync);
chrome.runtime.onMessage.removeListener(onMessageHandlerSynchronous);
}
/******************************************************************************/
/******************************************************************************/
/********** # Other initializers - run once per enable **********/
/******************************************************************************/
/******************************************************************************/
/**
* Listener for tab switch that updates the cached current tab variable
*/
function onActivatedProtectionMode(info) {
activeTabID = info.tabId;
}
// Handles misc. setup & setup listeners
function initSetup() {
pullToDomainlistCache();
// Runs on startup to initialize the cached current tab variable
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
if (tabs.id) {
activeTabID = tabs.id;
}
});
chrome.tabs.onActivated.addListener(onActivatedProtectionMode);
}
function closeSetup() {
chrome.tabs.onActivated.removeListener(onActivatedProtectionMode);
}
/**
* Inteded to facilitate transitioning between analysis & protection modes
*/
function wipeLocalVars() {
domainlist = {}; // Caches & mirrors domainlist in storage
tabs = {}; // Caches all tab infomration, i.e. requests, etc.
wellknown = {}; // Caches wellknown info to be sent to popup
signalPerTab = {}; // Caches if a signal is sent to render the popup icon
activeTabID = 0; // Caches current active tab id
sendSignal = false; // Caches if the signal can be sent to the curr domain
}
/******************************************************************************/
/******************************************************************************/
/********** # Exportable init / halt functions **********/
/******************************************************************************/
/******************************************************************************/
export function init() {
reloadVars();
enableListeners(listenerCallbacks);
initMessagePassing();
initSetup();
}
export function halt() {
disableListeners(listenerCallbacks);
closeMessagePassing();
closeSetup();
wipeLocalVars();
}
================================================
FILE: src/background/protection/protection.js
================================================
/*
Licensed per https://github.com/privacy-tech-lab/gpc-optmeowt/blob/main/LICENSE.md
privacy-tech-lab, https://privacytechlab.org/
*/
/*
protection.js
================================================================================
protection.js (1) Implements our per-site functionality for the background listeners
(2) Handles cached values & message passing to popup & options page
*/
import { stores, storage } from "./../storage.js";
import { defaultSettings } from "../../data/defaultSettings.js";
import { enableListeners, disableListeners } from "./listeners-$BROWSER.js";
import psl from "psl";
import {
addDynamicRule,
deleteDynamicRule,
reloadDynamicRules,
} from "../../common/editRules.js";
import { isWellknownCheckEnabled, isComplianceCheckEnabled, getUserState } from "../../common/settings.js";
import { fetchComplianceData, isCacheValid } from "../../data/complianceData.js";
/******************************************************************************/
/******************************************************************************/
/********** # Initializers (cached values) **********/
/******************************************************************************/
/******************************************************************************/
var domainlist = {}; // Caches & mirrors domainlist in storage
var isDomainlisted = defaultSettings["IS_DOMAINLISTED"];
var tabs = {}; // Caches all tab infomration, i.e. requests, etc.
var wellknown = {}; // Caches wellknown info to be sent to popup
var signalPerTab = {}; // Caches if a signal is sent to render the popup icon
var activeTabID = 0; // Caches current active tab id
var sendSignal = true; // Caches if the signal can be sent to the curr domain
var domPrev3rdParties = {}; //stores all the 3rd parties by domain (resets when you quit chrome)
var globalParsedDomain;
var setup = false;
// complianceData cache is now handled by the IndexedDB store "stores.complianceData"
const DEFAULT_NO_DATA_STATUS = {
status: 'no_data',
details: 'We do not have data for this site.',
lastChecked: null
};
async function reloadVars() {
let storedDomainlisted = await storage.get(
stores.settings,
"IS_DOMAINLISTED"
);
if (storedDomainlisted) {
isDomainlisted = storedDomainlisted;
}
}
reloadVars();
/******************************************************************************/
/******************************************************************************/
/********** # Lisetener callbacks - Main functionality **********/
/******************************************************************************/
/******************************************************************************/
/*
* The four following functions are all related to the four main listeners in
* `background.js`. These four functions implement all the other helper
* functions below
*/
const listenerCallbacks = {
/**
* Handles all signal processessing prior to sending request headers
* @param {object} details - retrieved info passed into callback
* @returns {array}
*/
onBeforeSendHeaders: async (details) => {
await updateDomainlist(details);
},
/**
* @param {object} details - retrieved info passed into callback
*/
onHeadersReceived: async (details) => {
//if (!setup){
//initSetup();
//}
await logData(details);
await sendData();
},
/**
* @param {object} details - retrieved info passed into callback
*/
onBeforeNavigate: (details) => {
// Resets certain cached info
},
/**
* Adds DOM property
* @param {object} details - retrieved info passed into callback
*/
onCommitted: async (details) => {
await updateDomainlist(details);
},
onCompleted: async (details) => {
await sendData();
await handleComplianceCheck(details);
}
}; // closes listenerCallbacks object
/******************************************************************************/
/******************************************************************************/
/********** # Listener helper fxns - Main functionality **********/
/******************************************************************************/
/******************************************************************************/
/**
* Fetches compliance data if not cached or cache is stale
* @returns {Promise