├── .babelrc ├── .chglog ├── CHANGELOG.tpl.md └── config.yml ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── android │ ├── update_manifest.py │ └── updates.json │ └── build_legacy.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.js ├── scripts └── build-zip.js ├── src ├── _locales │ ├── de │ │ └── messages.json │ ├── en │ │ └── messages.json │ ├── es │ │ └── messages.json │ ├── fi │ │ └── messages.json │ ├── fr │ │ └── messages.json │ ├── index.ts │ ├── it │ │ └── messages.json │ ├── ja │ │ └── messages.json │ ├── ko │ │ └── messages.json │ ├── pl │ │ └── messages.json │ ├── pt_BR │ │ └── messages.json │ ├── pt_PT │ │ └── messages.json │ ├── ru │ │ └── messages.json │ ├── tr │ │ └── messages.json │ └── zh_CN │ │ └── messages.json ├── background.ts ├── browser.d.ts ├── css │ ├── notosans.ttf │ └── tailwind.css ├── icons │ ├── icon.svg │ └── icon_disabled.svg ├── lib │ ├── chameleon.ts │ ├── devices.ts │ ├── inject.ts │ ├── intercept.ts │ ├── language.ts │ ├── profiles.ts │ ├── spoof │ │ ├── audioContext.ts │ │ ├── clientRects.ts │ │ ├── cssExfil.ts │ │ ├── font.ts │ │ ├── history.ts │ │ ├── kbFingerprint.ts │ │ ├── language.ts │ │ ├── media.ts │ │ ├── mediaSpoof.ts │ │ ├── name.ts │ │ ├── navigator.ts │ │ ├── quirks.ts │ │ ├── referer.ts │ │ ├── screen.ts │ │ └── timezone.ts │ ├── tz.ts │ ├── util.ts │ ├── webext.ts │ └── whitelisted.ts ├── manifest.json ├── options │ ├── App.vue │ ├── options.html │ └── options.ts ├── popup │ ├── App.vue │ ├── popup.html │ └── popup.ts └── store │ ├── actions.ts │ ├── index.ts │ ├── mutation-types.ts │ └── mutations.ts ├── tailwind.config.js ├── tests ├── headers.spec.js ├── options.spec.js ├── server │ ├── index.ejs │ └── sha1.js ├── setup.js └── teardown.js ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-proposal-optional-chaining", 4 | "@babel/plugin-proposal-export-default-from" 5 | ], 6 | "presets": [ 7 | ["@babel/preset-env", { 8 | "useBuiltIns": "usage", 9 | "corejs": 3, 10 | "targets": { 11 | "firefox": "68" 12 | } 13 | }] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.chglog/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | {{ range .Versions }} 2 | 3 | 4 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }}) 5 | 6 | {{ range .CommitGroups -}} 7 | 8 | ### {{ .Title }} 9 | 10 | {{ range .Commits -}} 11 | 12 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 13 | {{ end }} 14 | {{ end -}} 15 | 16 | {{- if .MergeCommits -}} 17 | 18 | ### Pull Requests 19 | 20 | {{ range .MergeCommits -}} 21 | 22 | - {{ .Header }} 23 | {{ end }} 24 | {{ end -}} 25 | 26 | {{- if .NoteGroups -}} 27 | {{ range .NoteGroups -}} 28 | 29 | ### {{ .Title }} 30 | 31 | {{ range .Notes }} 32 | {{ .Body }} 33 | {{ end }} 34 | {{ end -}} 35 | {{ end -}} 36 | {{ end -}} 37 | -------------------------------------------------------------------------------- /.chglog/config.yml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: https://github.com/sereneblue/chameleon 6 | options: 7 | commits: 8 | filters: 9 | Type: 10 | - feat 11 | - fix 12 | sort_by: Scope 13 | commit_groups: 14 | title_maps: 15 | feat: Features 16 | fix: Bug Fixes 17 | # perf: Performance Improvements 18 | # refactor: Code Refactoring 19 | header: 20 | pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$" 21 | pattern_maps: 22 | - Type 23 | - Scope 24 | - Subject 25 | notes: 26 | keywords: 27 | - BREAKING CHANGE 28 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | // File taken from https://github.com/vuejs-templates/webpack/blob/1.3.1/template/.eslintrc.js, thanks. 3 | 4 | module.exports = { 5 | root: true, 6 | parserOptions: { 7 | parser: 'babel-eslint', 8 | }, 9 | env: { 10 | browser: true, 11 | webextensions: true, 12 | }, 13 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 14 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 15 | extends: ['plugin:vue/essential', 'airbnb-base', 'plugin:prettier/recommended'], 16 | // required to lint *.vue files 17 | plugins: ['vue'], 18 | // check if imports actually resolve 19 | settings: { 20 | 'import/resolver': { 21 | webpack: { 22 | config: './webpack.config.js', 23 | }, 24 | }, 25 | }, 26 | // add your custom rules here 27 | rules: { 28 | // don't require .vue extension when importing 29 | 'import/extensions': [ 30 | 'error', 31 | 'always', 32 | { 33 | js: 'never', 34 | vue: 'never', 35 | }, 36 | ], 37 | // disallow reassignment of function parameters 38 | // disallow parameter object manipulation except for specific exclusions 39 | 'no-param-reassign': [ 40 | 'error', 41 | { 42 | props: true, 43 | ignorePropertyModificationsFor: [ 44 | 'state', // for vuex state 45 | 'acc', // for reduce accumulators 46 | 'e', // for e.returnvalue 47 | ], 48 | }, 49 | ], 50 | // disallow default export over named export 51 | 'import/prefer-default-export': 'off', 52 | // allow debugger during development 53 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/manifest.json merge=ours 2 | README.md merge=ours -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | 3 | Please use issues for bugs only! Answer the following questions for yourself before submitting an issue: **YOU MAY DELETE THE PREREQUISITES SECTION.** 4 | 5 | - [ ] I am running the latest version 6 | - [ ] I checked the documentation and found no answer 7 | - [ ] I checked to make sure that this issue has not already been filed 8 | 9 | ## Expected Behavior 10 | 11 | ## Current Behavior 12 | 13 | ## Relevant settings 14 | 15 | 16 | 17 | ## Context (Environment) 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/android/update_manifest.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | manifest = {} 4 | 5 | with open('./src/manifest.json') as f: 6 | manifest = json.load(f) 7 | 8 | # increment version for android build 9 | version = manifest['version'].split('.') 10 | version[-1] = str(int(version[-1]) + 1) 11 | 12 | manifest['version'] = ".".join(version) 13 | manifest['version_name'] = ".".join(version[:-1]) 14 | 15 | # add update url 16 | manifest['browser_specific_settings']['gecko']['update_url'] = 'https://raw.githubusercontent.com/sereneblue/chameleon/master/.github/workflows/android/updates.json' 17 | 18 | # remove optional permissions 19 | del manifest['optional_permissions'] 20 | 21 | manifest['permissions'].append('privacy') 22 | 23 | with open('./src/manifest.json', 'w') as f: 24 | json.dump(manifest, f, indent=2) -------------------------------------------------------------------------------- /.github/workflows/android/updates.json: -------------------------------------------------------------------------------- 1 | { 2 | "addons": { 3 | "{3579f63b-d8ee-424f-bbb6-6d0ce3285e6a}": { 4 | "updates": [ 5 | { 6 | "version": "0.22.73.2", 7 | "browser_specific_settings": { 8 | "gecko": { "strict_min_version": "68.0a1" }, 9 | "gecko_android": { "strict_min_version": "68.0a1" } 10 | }, 11 | "update_link": "https://github.com/sereneblue/chameleon/releases/download/v0.22.73/chameleon_v0.22.73.signed.xpi" 12 | } 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/build_legacy.yml: -------------------------------------------------------------------------------- 1 | name: Chameleon Legacy Build 2 | 3 | on: 4 | push: 5 | tags: 6 | - v** 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out repo 13 | uses: actions/checkout@v4 14 | - name: Install NodeJS 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: '18' 18 | - name: Build extension 19 | run: | 20 | npm install 21 | npm install --global web-ext 22 | python3 ./.github/workflows/android/update_manifest.py 23 | npm run build 24 | - name: Sign extension 25 | run: | 26 | web-ext sign -s ./dist --channel unlisted 27 | mv ./web-ext-artifacts/$(ls web-ext-artifacts) ./web-ext-artifacts/chameleon_$(git describe --tags).signed.xpi 28 | env: 29 | WEB_EXT_API_KEY: ${{ secrets.WEBEXT_API_KEY }} 30 | WEB_EXT_API_SECRET: ${{ secrets.WEBEXT_API_SECRET }} 31 | - name: Generate variables 32 | id: chameleon 33 | run: | 34 | echo "VERSION=$(git describe --tags)" >> $GITHUB_OUTPUT 35 | echo "FILENAME=chameleon_$(git describe --tags).signed.xpi" >> $GITHUB_OUTPUT 36 | - name: Create release 37 | id: create_release 38 | uses: softprops/action-gh-release@v2 39 | with: 40 | token: ${{ secrets.GITHUB_TOKEN }} 41 | tag_name: ${{ steps.chameleon.outputs.VERSION }} 42 | name: Chameleon Legacy Build ${{ steps.chameleon.outputs.VERSION }} 43 | body: 'This version of Chameleon is for users using an older version Firefox 75 and would like to have Chameleon control their privacy settings. More info can be found [here](https://sereneblue.github.io/chameleon/wiki/legacy).' 44 | draft: false 45 | prerelease: false 46 | files: ./web-ext-artifacts/${{ steps.chameleon.outputs.FILENAME }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /*.log 3 | /dist 4 | /dist-zip 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 180, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chameleon 2 | 3 | ![Chameleon version](https://img.shields.io/badge/version-0.22.73-brightgreen.svg) 4 | ![GPL v3 License](https://img.shields.io/badge/license-GPL%20v3-blue.svg) 5 | [![Crowdin](https://d322cqt584bo4o.cloudfront.net/chameleon/localized.svg)](https://crowdin.com/project/chameleon) 6 | 7 | Chameleon is a WebExtension port of the popular Firefox addon [Random Agent Spoofer](https://github.com/dillbyrne/random-agent-spoofer). 8 | 9 | The UI is near identical and contains most of the features found in the original extension. 10 | 11 | ## Features 12 | 13 | ### UI 14 | 15 | - Light/Dark theme 16 | - Notifications 17 | - Quickly toggle Chameleon 18 | 19 | ### Useragents 20 | 21 | - Randomly select from a list of browser profiles 22 | - Choose between different platforms or device types 23 | - Change user agent at specified interval 24 | 25 | ### Headers 26 | 27 | - Enable Do Not Track 28 | - Prevent Etag tracking 29 | - Spoof accept headers 30 | - Spoof X-Forwarded-For/Via IP 31 | - Disable referer 32 | - Modify referer policies 33 | 34 | ### Options 35 | 36 | - Block media devices 37 | - Limit tab history 38 | - Protect keyboard fingerprint 39 | - Protect window.name 40 | - Spoof audio context 41 | - Spoof client rects 42 | - Spoof font fingerprint 43 | - Spoof screen size 44 | - Spoof timezone 45 | - Enable first party isolation 46 | - Enable resist fingerprinting 47 | - Prevent WebRTC leak. 48 | - Enable tracking protection 49 | - Block WebSockets 50 | - Modify cookie policy 51 | 52 | Please note that WebExtensions are unable to modify about:config entries. 53 | 54 | ### Whitelist 55 | 56 | - Use your real or spoofed profile for whitelisted sites 57 | - Supports regular expressions 58 | - Use a custom profile per whitelist rule, multiple sites per rule 59 | 60 | ## Installation 61 | 62 | Chameleon is available on the [Firefox Add-ons website](https://addons.mozilla.org/firefox/addon/chameleon-ext). A developer build is also available on [Github](https://github.com/sereneblue/chameleon/releases). 63 | 64 | ## Contribute 65 | 66 | Want to help improve Chameleon? Send a pull request or open an issue. Keep in mind that some functionality isn't technically possible. 67 | 68 | You can help translate Chameleon by visiting [Crowdin](https://crowdin.com/project/chameleon). 69 | 70 | ## Wiki 71 | 72 | Don't know where to start? Check out the [wiki](https://sereneblue.github.io/chameleon/wiki). If you're having issues with a website, please read the whitelist [guide](https://sereneblue.github.io/chameleon/wiki/whitelist). 73 | 74 | ## Credits 75 | 76 | [dillbyrne](https://github.com/dillbyrne) for creating [Random Agent Spoofer](https://github.com/dillbyrne/random-agent-spoofer) 77 | 78 | [Mike Gualtieri](https://github.com/mlgualtieri) for the [CSS Exfil](https://github.com/mlgualtieri/CSS-Exfil-Protection) code. 79 | 80 | ## Special Thanks 81 | 82 | - giwhub for the Chinese translation 83 | - Kaylier, melegeti, and Tux 528 for the French translation 84 | - Anonymous, Wursttorte, AName, and robinmusch for the German translation 85 | - Shitennouji for the Japanese translation 86 | - gnu-ewm for the Polish translation 87 | - 3ibsand, Alexey, fks7cgsk, Hit Legends, EugeneKh and Дмитрий Кондрашов for the Russian translation 88 | - David P. (Megver83), reii and gallegonovato for the Spanish translation 89 | - xlabx for the Turkish translation 90 | - mezysinc and Lucas Guima for the Portuguese (Brazilian) translation 91 | - Ricky Tigg for the Finnish translation 92 | - LolloGamer_5123 YT for the Italian translation 93 | - azilara for the Portuguese translation 94 | - maiska for the Korean translation 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chameleon-ext", 3 | "version": "0.22.73", 4 | "description": "Spoof your browser profile. Includes a few privacy enhancing options.", 5 | "author": "sereneblue", 6 | "license": "GPLv3", 7 | "scripts": { 8 | "lint": "eslint --ext .js,.vue src", 9 | "prettier": "prettier \"src/**/*.{js,vue}\"", 10 | "prettier:write": "npm run prettier -- --write", 11 | "build": "cross-env NODE_ENV=production NODE_OPTIONS=--openssl-legacy-provider webpack --hide-modules", 12 | "build:dev": "cross-env NODE_ENV=development NODE_OPTIONS=--openssl-legacy-provider webpack --hide-modules", 13 | "build-zip": "node scripts/build-zip.js", 14 | "test": "npm run build && npm run build-zip && jest --detectOpenHandles", 15 | "watch": "npm run build -- --watch", 16 | "watch:dev": "cross-env HMR=true NODE_OPTIONS=--openssl-legacy-provider npm run build:dev -- --watch" 17 | }, 18 | "husky": { 19 | "hooks": { 20 | "pre-commit": "pretty-quick --staged" 21 | } 22 | }, 23 | "jest": { 24 | "globals": { 25 | "__DEV__": true 26 | }, 27 | "globalSetup": "/tests/setup.js", 28 | "globalTeardown": "/tests/teardown.js", 29 | "transform": { 30 | "^.+\\.(ts|tsx)$": "ts-jest" 31 | } 32 | }, 33 | "dependencies": { 34 | "cidr-js": "^2.3.1", 35 | "feather-icons": "^4.29.1", 36 | "moment-timezone": "^0.5.45", 37 | "psl": "^1.8.0", 38 | "tailwindcss": "^1.9.6", 39 | "uuid": "^8.3.2", 40 | "vue": "^2.6.14", 41 | "vue-class-component": "^7.2.6", 42 | "vue-feather": "^1.1.1", 43 | "vue-i18n": "^8.27.2", 44 | "vue-object-merge": "^0.1.8", 45 | "vue-property-decorator": "^8.5.1", 46 | "vue2-perfect-scrollbar": "^1.5.2", 47 | "vuex": "^3.6.2" 48 | }, 49 | "devDependencies": { 50 | "@babel/core": "^7.12.10", 51 | "@babel/plugin-proposal-export-default-from": "^7.12.1", 52 | "@babel/plugin-proposal-optional-chaining": "^7.12.7", 53 | "@babel/preset-env": "^7.24.3", 54 | "@babel/runtime-corejs3": "^7.12.5", 55 | "@fullhuman/postcss-purgecss": "^2.3.0", 56 | "@tailwindcss/custom-forms": "^0.2.1", 57 | "@types/chrome": "^0.0.74", 58 | "@types/node": "^12.19.15", 59 | "archiver": "^3.0.0", 60 | "babel-eslint": "^10.0.1", 61 | "babel-loader": "^8.2.2", 62 | "copy-webpack-plugin": "^5.1.2", 63 | "core-js": "^3.8.3", 64 | "cross-env": "^7.0.3", 65 | "css-loader": "^2.1.1", 66 | "ejs": "^3.1.5", 67 | "eslint": "^5.16.0", 68 | "eslint-config-airbnb-base": "^13.0.0", 69 | "eslint-config-prettier": "^4.3.0", 70 | "eslint-friendly-formatter": "^4.0.1", 71 | "eslint-import-resolver-webpack": "^0.10.1", 72 | "eslint-loader": "^2.1.2", 73 | "eslint-plugin-import": "^2.22.1", 74 | "eslint-plugin-prettier": "^3.3.1", 75 | "eslint-plugin-vue": "^5.2.2", 76 | "express": "^4.17.1", 77 | "express-ws": "^4.0.0", 78 | "file-loader": "^1.1.11", 79 | "geckodriver": "^1.22.1", 80 | "husky": "^2.7.0", 81 | "jest": "^26.6.3", 82 | "mini-css-extract-plugin": "^0.4.4", 83 | "postcss-loader": "^3.0.0", 84 | "prettier": "^1.19.1", 85 | "pretty-quick": "^1.8.0", 86 | "selenium-webdriver": "^4.0.0-alpha.8", 87 | "ts-jest": "^26.5.0", 88 | "ts-loader": "^8.0.0", 89 | "typescript": "^4.1.3", 90 | "vue-loader": "^15.9.6", 91 | "vue-template-compiler": "^2.6.12", 92 | "web-ext": "^7.11.0", 93 | "webpack": "^4.47.0", 94 | "webpack-cli": "^3.3.12", 95 | "webpack-extension-reloader": "^1.1.4" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const purgecss = require('@fullhuman/postcss-purgecss')({ 2 | // Specify the paths to all of the template files in your project 3 | content: ['./src/**/*.html', './src/**/*.vue', './src/**/*.jsx'], 4 | 5 | // Include any special characters you're using in this regular expression 6 | defaultExtractor: content => content.match(/[\w-/:]+(? { 11 | const extPackageJson = require('../package.json'); 12 | 13 | return { 14 | name: extPackageJson.name, 15 | version: extPackageJson.version, 16 | }; 17 | }; 18 | 19 | const makeDestZipDirIfNotExists = () => { 20 | if (!fs.existsSync(DEST_ZIP_DIR)) { 21 | fs.mkdirSync(DEST_ZIP_DIR); 22 | } 23 | }; 24 | 25 | const buildZip = (src, dist, zipFilename) => { 26 | console.info(`Building ${zipFilename}...`); 27 | 28 | const archive_zip = archiver('zip', { zlib: { level: 9 } }); 29 | const archive_xpi = archiver('zip', { zlib: { level: 9 } }); 30 | 31 | const stream = fs.createWriteStream(path.join(dist, zipFilename)); 32 | const xpiStream = fs.createWriteStream(path.join(dist, zipFilename.replace('.zip', '.xpi'))); 33 | 34 | return new Promise((resolve, reject) => { 35 | stream.on('close', () => resolve()); 36 | xpiStream.on('close', () => resolve()); 37 | 38 | archive_zip 39 | .directory(src, false) 40 | .on('error', err => reject(err)) 41 | .pipe(stream); 42 | archive_zip.finalize(); 43 | 44 | archive_xpi 45 | .directory(src, false) 46 | .on('error', err => reject(err)) 47 | .pipe(xpiStream); 48 | archive_xpi.finalize(); 49 | }); 50 | }; 51 | 52 | const main = () => { 53 | const { name, version } = extractExtensionData(); 54 | const zipFilename = `${name}-v${version}.zip`; 55 | 56 | makeDestZipDirIfNotExists(); 57 | 58 | buildZip(DEST_DIR, DEST_ZIP_DIR, zipFilename) 59 | .then(() => console.info('OK')) 60 | .catch(console.err); 61 | }; 62 | 63 | main(); 64 | -------------------------------------------------------------------------------- /src/_locales/index.ts: -------------------------------------------------------------------------------- 1 | const en = require('./en/messages.json'); 2 | const es = require('./es/messages.json'); 3 | const de = require('./de/messages.json'); 4 | const fi = require('./fi/messages.json'); 5 | const fr = require('./fr/messages.json'); 6 | const it = require('./it/messages.json'); 7 | const ja = require('./ja/messages.json'); 8 | const ko = require('./ko/messages.json'); 9 | const pl = require('./pl/messages.json'); 10 | const pt_PT = require('./pt_PT/messages.json'); 11 | const pt_BR = require('./pt_BR/messages.json'); 12 | const ru = require('./ru/messages.json'); 13 | const tr = require('./tr/messages.json'); 14 | const zh_CN = require('./zh_CN/messages.json'); 15 | 16 | export default { 17 | en, 18 | es, 19 | de, 20 | fi, 21 | fr, 22 | it, 23 | ja, 24 | ko, 25 | pl, 26 | pt_PT, 27 | pt_BR, 28 | ru, 29 | tr, 30 | zh_CN, 31 | }; 32 | -------------------------------------------------------------------------------- /src/_locales/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extDescription": { 3 | "message": "ブラウザのプロファイルを偽装します。いくつかのプライバシー強化オプションを含みます。" 4 | }, 5 | "notifications-profileChange": { 6 | "message": "プロフィ―ルが変更されました。" 7 | }, 8 | "notifications-unableToGetIPInfo": { 9 | "message": "IP 情報を取得できません" 10 | }, 11 | "notifications-usingIPInfo": { 12 | "message": "IP 情報を使用して:" 13 | }, 14 | "notifications-usingIPRule": { 15 | "message": "IP ルールを使用して:" 16 | }, 17 | "options-about-issueTracker": { 18 | "message": "Issue Tracker(課題管理)" 19 | }, 20 | "options-about-knownIssues": { 21 | "message": "既知の課題" 22 | }, 23 | "options-about-wiki": { 24 | "message": "Wiki" 25 | }, 26 | "options-about-support": { 27 | "message": "サポート" 28 | }, 29 | "options-about-sourceCode": { 30 | "message": "ソースコード" 31 | }, 32 | "options-about-translate": { 33 | "message": "翻訳を手伝う" 34 | }, 35 | "options-import-couldNotImport": { 36 | "message": "ファイルをインポートできませんでした。" 37 | }, 38 | "options-import-invalid-config": { 39 | "message": "不正な環境設定:設定構成の欠落" 40 | }, 41 | "options-import-invalid-excluded": { 42 | "message": "不正な環境設定:除外されたものが欠落" 43 | }, 44 | "options-import-invalid-excludedProfile": { 45 | "message": "不正な環境設定:除外されたプロファイルが欠落" 46 | }, 47 | "options-import-invalid-headers": { 48 | "message": "不正な環境設定:ヘッダの欠落" 49 | }, 50 | "options-import-invalid-ipRuleId": { 51 | "message": "不正な環境設定:無効な IP ルールの ID" 52 | }, 53 | "options-import-invalid-ipRuleName": { 54 | "message": "不正な環境設定:IP ルール名が欠落" 55 | }, 56 | "options-import-invalid-ipRuleRange": { 57 | "message": "不正な環境設定:無効な IP ルールの範囲" 58 | }, 59 | "options-import-invalid-ipRules": { 60 | "message": "不正な環境設定:IP ルールの欠落" 61 | }, 62 | "options-import-invalid-ipRulesDupe": { 63 | "message": "不正な設定:IP ルール ID の重複が見つかりました" 64 | }, 65 | "options-import-invalid-options": { 66 | "message": "不正な設定:オプションの欠落" 67 | }, 68 | "options-import-invalid-profile": { 69 | "message": "不正な設定:プロファイルの欠落" 70 | }, 71 | "options-import-invalid-setting": { 72 | "message": "不正な設定値:" 73 | }, 74 | "options-import-invalid-settings": { 75 | "message": "不正な環境設定:環境設定が欠落" 76 | }, 77 | "options-import-invalid-spoofIP": { 78 | "message": "不正な設定:偽装したヘッダの IP 範囲に誤りがあります" 79 | }, 80 | "options-import-invalid-version": { 81 | "message": "不正な設定:バージョンが受け付けられません" 82 | }, 83 | "options-import-invalid-whitelist": { 84 | "message": "不正な設定:ホワイトリストの欠落" 85 | }, 86 | "options-import-invalid-whitelistDupe": { 87 | "message": "不正な設定:重複するホワイトリストのルール ID が見つかりました" 88 | }, 89 | "options-import-invalid-whitelistId": { 90 | "message": "不正な設定:無効なホワイトリストのルール ID" 91 | }, 92 | "options-import-invalid-whitelistName": { 93 | "message": "不正な設定:ホワイトリストルール名の欠落" 94 | }, 95 | "options-import-invalid-whitelistOpt": { 96 | "message": "不正な設定:ホワイトリストルールのオプションが不正です" 97 | }, 98 | "options-import-invalid-whitelistSpoofIP": { 99 | "message": "不正な設定:ホワイトリストルールの偽装 IP が不正です" 100 | }, 101 | "options-import-success": { 102 | "message": "環境設定を正常にインポートしました。拡張機能の再読み込み中..." 103 | }, 104 | "options-ipRules-editorTitle": { 105 | "message": "IP ルールエディタ" 106 | }, 107 | "options-ipRules-ipRule": { 108 | "message": "IP ルール" 109 | }, 110 | "options-ipRules-reload": { 111 | "message": "IP 情報の再読み込み" 112 | }, 113 | "options-ipRules-textareaLabel": { 114 | "message": "IP 範囲 / アドレス" 115 | }, 116 | "options-ipRules-textareaPlaceholder": { 117 | "message": "1回線あたり、1つの IP/IP 範囲" 118 | }, 119 | "options-modal-askDelete": { 120 | "message": "このルールを削除してよろしいですか?" 121 | }, 122 | "options-modal-askReset": { 123 | "message": "環境設定をリセットしても大丈夫ですか?" 124 | }, 125 | "options-modal-confirmDelete": { 126 | "message": "はい、削除して下さい!" 127 | }, 128 | "options-modal-confirmReset": { 129 | "message": "はい、私の環境設定をリセットして下さい!" 130 | }, 131 | "options-settings": { 132 | "message": "環境設定" 133 | }, 134 | "options-settings-import": { 135 | "message": "インポート" 136 | }, 137 | "options-settings-importing": { 138 | "message": "環境設定をインポートする" 139 | }, 140 | "options-settings-export": { 141 | "message": "エクスポート" 142 | }, 143 | "options-settings-reset": { 144 | "message": "Default(既定値)にリセット" 145 | }, 146 | "options-settings-permissions": { 147 | "message": "Chameleonは、フィンガープリントへの抵抗やトラッキング保護の有効化など、Firefoxのいくつかの設定を制御できます。これは他の拡張機能と競合する可能性があります。プライバシーの権限を削除することで、Chameleonがこれらの設定を制御することをオプトアウトできます。この権限を削除すると、これらの設定がリセットされることに注意してください。この権限が存在しない場合は、要求することができます。" 148 | }, 149 | "options-settings-permissions-legacy": { 150 | "message": "プライバシー許可を有効にするには、Chameleon の特別なバージョンをインストールする必要があります。これは、クロスプラットフォーム/この拡張機能の新しいバージョンでサポートされている権限の複雑さによるものです。詳細については、以下にリンクされている wiki を参照してください。" 151 | }, 152 | "options-settings-permissions-legacy-wiki": { 153 | "message": "wiki に詳細情報" 154 | }, 155 | "options-settings-permissions-request": { 156 | "message": "プライバシー許可をリクエストする" 157 | }, 158 | "options-settings-permissions-remove": { 159 | "message": "プライバシー許可を削除する" 160 | }, 161 | "options-tab-about": { 162 | "message": "概要" 163 | }, 164 | "options-tab-ipRules": { 165 | "message": "IP ルール" 166 | }, 167 | "options-whitelist-acceptLang": { 168 | "message": "受け入れ言語" 169 | }, 170 | "options-whitelist-editorTitle": { 171 | "message": "ホワイトリストのルールエディタ" 172 | }, 173 | "options-whitelist-headerIPLabel": { 174 | "message": "IP ヘッダ (Via や X-Forwarded のための)" 175 | }, 176 | "options-whitelist-options-audioContext": { 177 | "message": "Enable spoof audio context" 178 | }, 179 | "options-whitelist-options-clientRects": { 180 | "message": "Enable spoof client rects" 181 | }, 182 | "options-whitelist-options-cssExfil": { 183 | "message": "CSS Exfil をブロックする" 184 | }, 185 | "options-whitelist-options-mediaDevices": { 186 | "message": "メディアデバイスをブロックする" 187 | }, 188 | "options-whitelist-options-name": { 189 | "message": "window.name 保護を有効にする" 190 | }, 191 | "options-whitelist-options-tz": { 192 | "message": "タイムゾーンのスプーフィング(偽装化)を有効にします。" 193 | }, 194 | "options-whitelist-options-ws": { 195 | "message": "WebSocket を無効にする" 196 | }, 197 | "options-whitelist-rule": { 198 | "message": "ホワイトリストのルール" 199 | }, 200 | "options-whitelist-sitesTip": { 201 | "message": "1行に、1つのルール:ドメイン@@[任意の正規表現パターン]" 202 | }, 203 | "options-whitelist-textareaLabel": { 204 | "message": "IP 範囲 / アドレス" 205 | }, 206 | "options-whitelist-textareaPlaceholder": { 207 | "message": "1回線あたり、1つの IP/IP 範囲" 208 | }, 209 | "popup-home-change": { 210 | "message": "変更" 211 | }, 212 | "popup-home-currentProfile": { 213 | "message": "現在のプロファイル" 214 | }, 215 | "popup-home-currentProfile-defaultLanguage": { 216 | "message": "既定の言語" 217 | }, 218 | "popup-home-currentProfile-defaultScreen": { 219 | "message": "既定の画面" 220 | }, 221 | "popup-home-currentProfile-defaultTimezone": { 222 | "message": "既定のタイムゾーン" 223 | }, 224 | "popup-home-currentProfile-gettingTimezone": { 225 | "message": "IP 情報を取得する" 226 | }, 227 | "popup-home-currentProfile-screenProfile": { 228 | "message": "画面 (プロファイル)" 229 | }, 230 | "popup-home-disabled": { 231 | "message": "カメレオンは無効化状態です" 232 | }, 233 | "popup-home-enabled": { 234 | "message": "カメレオンは有効化されている" 235 | }, 236 | "popup-home-notification-disabled": { 237 | "message": "通知オフ" 238 | }, 239 | "popup-home-notification-enabled": { 240 | "message": "通知オン" 241 | }, 242 | "popup-home-onThisPage": { 243 | "message": "このページでは" 244 | }, 245 | "popup-home-theme-dark": { 246 | "message": "ダーク(黒基調)" 247 | }, 248 | "popup-home-theme-light": { 249 | "message": "ライト(白基調)" 250 | }, 251 | "popup-profile-changePeriodically": { 252 | "message": "定期的に変更する" 253 | }, 254 | "popup-profile-devicePhone": { 255 | "message": "電話" 256 | }, 257 | "popup-profile-deviceTablet": { 258 | "message": "タブレット端末" 259 | }, 260 | "popup-profile-interval-no": { 261 | "message": "不可" 262 | }, 263 | "popup-profile-interval-custom": { 264 | "message": "カスタム間隔" 265 | }, 266 | "popup-profile-interval-customMax": { 267 | "message": "最大 (分)" 268 | }, 269 | "popup-profile-interval-customMin": { 270 | "message": "最小 (分)" 271 | }, 272 | "popup-profile-interval-minute": { 273 | "message": "1 分ごと" 274 | }, 275 | "popup-profile-interval-5minutes": { 276 | "message": "5 分ごと" 277 | }, 278 | "popup-profile-interval-10minutes": { 279 | "message": "10 分ごと" 280 | }, 281 | "popup-profile-interval-20minutes": { 282 | "message": "20 分ごと" 283 | }, 284 | "popup-profile-interval-30minutes": { 285 | "message": "30 分ごと" 286 | }, 287 | "popup-profile-interval-40minutes": { 288 | "message": "40 分ごと" 289 | }, 290 | "popup-profile-interval-50minutes": { 291 | "message": "50 分ごと" 292 | }, 293 | "popup-profile-interval-hour": { 294 | "message": "1 時間ごと" 295 | }, 296 | "popup-profile-exclude": { 297 | "message": "除外" 298 | }, 299 | "popup-profile-randomAndroid": { 300 | "message": "ランダムな Android Browsers" 301 | }, 302 | "popup-profile-randomIOS": { 303 | "message": "ランダムな iOS Browsers" 304 | }, 305 | "popup-profile-randomMacOS": { 306 | "message": "ランダムな macOS Browsers" 307 | }, 308 | "popup-profile-randomLinux": { 309 | "message": "ランダムな Linux Browsers" 310 | }, 311 | "popup-profile-randomWindows": { 312 | "message": "ランダムな Windows Browsers" 313 | }, 314 | "popup-profile-random": { 315 | "message": "ランダム" 316 | }, 317 | "popup-profile-randomDesktopProfile": { 318 | "message": "ランダムなプロファイル (Desktop)" 319 | }, 320 | "popup-profile-randomMobileProfile": { 321 | "message": "ランダムなプロファイル (Mobile)" 322 | }, 323 | "popup-profile-showProfileOnIcon": { 324 | "message": "Show browser profile on icon" 325 | }, 326 | "popup-headers": { 327 | "message": "ヘッダー" 328 | }, 329 | "popup-headers-enableDNT": { 330 | "message": "DNT (Do Not Track) を有効にする" 331 | }, 332 | "popup-headers-preventEtag": { 333 | "message": "Etag トラッキングを防止する" 334 | }, 335 | "popup-headers-refererWarning": { 336 | "message": "以下のオプションの about:config 設定を変更しないでください。" 337 | }, 338 | "popup-headers-referer-trimming": { 339 | "message": "Trimming Policy のリファラー" 340 | }, 341 | "popup-headers-referer-trimming-sendFullURI": { 342 | "message": "完全な URI を送信する (規定)" 343 | }, 344 | "popup-headers-referer-trimming-schemeHostPortPath": { 345 | "message": "host, port + path のスキーム" 346 | }, 347 | "popup-headers-referer-trimming-schemeHostPort": { 348 | "message": "host + port のスキーム" 349 | }, 350 | "popup-headers-referer-xorigin": { 351 | "message": "X Origin Policy のリファラー" 352 | }, 353 | "popup-headers-referer-xorigin-alwaysSend": { 354 | "message": "常に送る (規定)" 355 | }, 356 | "popup-headers-referer-xorigin-matchBaseDomain": { 357 | "message": "基本ドメインと一致" 358 | }, 359 | "popup-headers-referer-xorigin-matchHost": { 360 | "message": "host と一致" 361 | }, 362 | "popup-headers-spoofAcceptLang": { 363 | "message": "Accept-Language を偽装する" 364 | }, 365 | "popup-headers-spoofIP": { 366 | "message": "X-Forwarded-For/Via IP を偽装する" 367 | }, 368 | "popup-headers-spoofIP-random": { 369 | "message": "ランダムな IP" 370 | }, 371 | "popup-headers-spoofIP-custom": { 372 | "message": "カスタム IP" 373 | }, 374 | "popup-headers-spoofIP-rangeFrom": { 375 | "message": "範囲の始点" 376 | }, 377 | "popup-headers-spoofIP-rangeTo": { 378 | "message": "範囲の終点" 379 | }, 380 | "popup-options": { 381 | "message": "オプション" 382 | }, 383 | "popup-options-grantPermissions": { 384 | "message": "設定を変更する権限を付与する" 385 | }, 386 | "popup-options-injection": { 387 | "message": "インジェクション" 388 | }, 389 | "popup-options-injection-limitTabHistory": { 390 | "message": "タブ履歴を制限する" 391 | }, 392 | "popup-options-injection-protectWinName": { 393 | "message": "関数 window.name(ウィンドウ名)を保護する" 394 | }, 395 | "popup-options-injection-audioContext": { 396 | "message": "音声コンテキストを偽装する" 397 | }, 398 | "popup-options-injection-clientRects": { 399 | "message": "Client Rects を偽装する" 400 | }, 401 | "popup-options-injection-protectKBFingerprint": { 402 | "message": "キーボードの指紋を保護する" 403 | }, 404 | "popup-options-injection-protectKBFingerprintDelay": { 405 | "message": "遅延 (ミリ秒)" 406 | }, 407 | "popup-options-injection-screen": { 408 | "message": "画面サイズ" 409 | }, 410 | "popup-options-injection-spoofFontFingerprint": { 411 | "message": "フォントの指紋を偽装する" 412 | }, 413 | "popup-options-standard": { 414 | "message": "標準" 415 | }, 416 | "popup-options-standard-blockMediaDevices": { 417 | "message": "メディアデバイスをブロックする" 418 | }, 419 | "popup-options-standard-blockCSSExfil": { 420 | "message": "CSS Exfil をブロックする" 421 | }, 422 | "popup-options-standard-disableWebRTC": { 423 | "message": "WebRTC を無効にする" 424 | }, 425 | "popup-options-standard-firstPartyIsolation": { 426 | "message": "ファーストパーティ分離を有効にする" 427 | }, 428 | "popup-options-standard-resistFingerprinting": { 429 | "message": "指紋採取を拒む機能を有効にします。" 430 | }, 431 | "popup-options-standard-spoofMediaDevices": { 432 | "message": "メディアデバイスを偽装する" 433 | }, 434 | "popup-options-standard-trackingProtection": { 435 | "message": "トラッキング保護モード" 436 | }, 437 | "popup-options-standard-trackingProtection-on": { 438 | "message": "On" 439 | }, 440 | "popup-options-standard-trackingProtection-off": { 441 | "message": "Off" 442 | }, 443 | "popup-options-standard-trackingProtection-privateBrowsing": { 444 | "message": "プライベートブラウジングで有効" 445 | }, 446 | "popup-options-standard-webRTCPolicy": { 447 | "message": "WebRTC ポリシー" 448 | }, 449 | "popup-options-standard-webRTCPolicy-nonProxified": { 450 | "message": "プロキシされていない UDP を無効にする" 451 | }, 452 | "popup-options-standard-webRTCPolicy-public": { 453 | "message": "パブリックインターフェースのみを使用する (最善)" 454 | }, 455 | "popup-options-standard-webRTCPolicy-publicPrivate": { 456 | "message": "パブリックおよびプライベートインターフェイスを使用する" 457 | }, 458 | "popup-options-standard-webSockets-blockAll": { 459 | "message": "全てブロック" 460 | }, 461 | "popup-options-standard-webSockets-blockThirdParty": { 462 | "message": "第三者をブロック" 463 | }, 464 | "popup-options-cookie": { 465 | "message": "クッキー" 466 | }, 467 | "popup-options-cookieNotPersistent": { 468 | "message": "ウィンドウを閉じた後、クッキーとサイトデータを削除する" 469 | }, 470 | "popup-options-cookiePolicy": { 471 | "message": "ポリシー" 472 | }, 473 | "popup-options-cookiePolicy-allowVisited": { 474 | "message": "訪問を許可する" 475 | }, 476 | "popup-options-cookiePolicy-rejectAll": { 477 | "message": "すべてを拒絶する" 478 | }, 479 | "popup-options-cookiePolicy-rejectThirdParty": { 480 | "message": "第三者を拒絶する" 481 | }, 482 | "popup-options-cookiePolicy-rejectTrackers": { 483 | "message": "トラッカーを拒絶する" 484 | }, 485 | "popup-options-cookiePolicy-rejectTrackersPartitionForeign": { 486 | "message": "Reject trackers and partition third-party cookies" 487 | }, 488 | "popup-whitelist-contextMenu": { 489 | "message": "ホワイトリストで現在のタブドメインを開くためのコンテキストメニュー項目を追加する" 490 | }, 491 | "popup-whitelist-defaultProfileLabel": { 492 | "message": "既定のプロファイル" 493 | }, 494 | "popup-whitelist-enable": { 495 | "message": "ホワイトリストの有効化" 496 | }, 497 | "popup-whitelist-isNotWhitelisted": { 498 | "message": "ホワイトリストに登録されていません" 499 | }, 500 | "popup-whitelist-isWhitelisted": { 501 | "message": "ホワイトリストに登録済み" 502 | }, 503 | "popup-whitelist-open": { 504 | "message": "ホワイトリストで開く" 505 | }, 506 | "text-addToRule": { 507 | "message": "Add to rule: $RULE_NAME$", 508 | "placeholders": { 509 | "rule_name": { 510 | "content": "$1", 511 | "example": "Firefox whitelist rule profile" 512 | } 513 | } 514 | }, 515 | "text-allowAll": { 516 | "message": "すべて許可する" 517 | }, 518 | "text-cancel": { 519 | "message": "取消" 520 | }, 521 | "text-createNewRule": { 522 | "message": "新しいルールを作成する" 523 | }, 524 | "text-default": { 525 | "message": "Default(規定)" 526 | }, 527 | "text-defaultWhitelistProfile": { 528 | "message": "デフォルトのホワイトリストプロファイル" 529 | }, 530 | "text-disableReferer": { 531 | "message": "リファラーを無効にする" 532 | }, 533 | "text-language": { 534 | "message": "言語" 535 | }, 536 | "text-name": { 537 | "message": "名称" 538 | }, 539 | "text-profile": { 540 | "message": "プロファイル" 541 | }, 542 | "text-realProfile": { 543 | "message": "真正プロファイル" 544 | }, 545 | "text-removeFromRule": { 546 | "message": "Remove from rule: $RULE_NAME$", 547 | "placeholders": { 548 | "rule_name": { 549 | "content": "$1", 550 | "example": "Firefox whitelist rule profile" 551 | } 552 | } 553 | }, 554 | "text-save": { 555 | "message": "保存する" 556 | }, 557 | "text-screen": { 558 | "message": "画面" 559 | }, 560 | "text-searchRules": { 561 | "message": "検索ルール" 562 | }, 563 | "text-startupDelay": { 564 | "message": "Startup delay (sec)" 565 | }, 566 | "text-timezone": { 567 | "message": "タイムゾーン" 568 | }, 569 | "text-whitelist": { 570 | "message": "ホワイトリスト" 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /src/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extDescription": { 3 | "message": "欺骗您的浏览器配置文件。包括一些隐私增强选项。" 4 | }, 5 | "notifications-profileChange": { 6 | "message": "配置文件已更改:" 7 | }, 8 | "notifications-unableToGetIPInfo": { 9 | "message": "无法获取IP信息" 10 | }, 11 | "notifications-usingIPInfo": { 12 | "message": "使用IP信息:" 13 | }, 14 | "notifications-usingIPRule": { 15 | "message": "使用IP规则:" 16 | }, 17 | "options-about-issueTracker": { 18 | "message": "问题跟踪" 19 | }, 20 | "options-about-knownIssues": { 21 | "message": "已知问题" 22 | }, 23 | "options-about-wiki": { 24 | "message": "Wiki" 25 | }, 26 | "options-about-support": { 27 | "message": "支持" 28 | }, 29 | "options-about-sourceCode": { 30 | "message": "源码" 31 | }, 32 | "options-about-translate": { 33 | "message": "帮助翻译" 34 | }, 35 | "options-import-couldNotImport": { 36 | "message": "无法导入文件" 37 | }, 38 | "options-import-invalid-config": { 39 | "message": "设置无效:缺少配置" 40 | }, 41 | "options-import-invalid-excluded": { 42 | "message": "设置无效:缺少排除项" 43 | }, 44 | "options-import-invalid-excludedProfile": { 45 | "message": "无效设置:排除项包含无效的配置文件" 46 | }, 47 | "options-import-invalid-headers": { 48 | "message": "设置无效:缺少头" 49 | }, 50 | "options-import-invalid-ipRuleId": { 51 | "message": "设置无效:IP规则id无效" 52 | }, 53 | "options-import-invalid-ipRuleName": { 54 | "message": "设置无效:缺少IP规则名称" 55 | }, 56 | "options-import-invalid-ipRuleRange": { 57 | "message": "设置无效:IP规则范围无效" 58 | }, 59 | "options-import-invalid-ipRules": { 60 | "message": "设置无效:缺少IP规则" 61 | }, 62 | "options-import-invalid-ipRulesDupe": { 63 | "message": "设置无效:发现重复的IP规则id" 64 | }, 65 | "options-import-invalid-options": { 66 | "message": "设置无效:缺少选项" 67 | }, 68 | "options-import-invalid-profile": { 69 | "message": "设置无效:缺少配置设置" 70 | }, 71 | "options-import-invalid-setting": { 72 | "message": "设置值无效:" 73 | }, 74 | "options-import-invalid-settings": { 75 | "message": "设置无效:缺少设置" 76 | }, 77 | "options-import-invalid-spoofIP": { 78 | "message": "设置无效:欺骗头部IP范围无效" 79 | }, 80 | "options-import-invalid-version": { 81 | "message": "设置无效:版本不被接受" 82 | }, 83 | "options-import-invalid-whitelist": { 84 | "message": "设置无效:缺少白名单" 85 | }, 86 | "options-import-invalid-whitelistDupe": { 87 | "message": "设置无效:发现重复的白名单规则id" 88 | }, 89 | "options-import-invalid-whitelistId": { 90 | "message": "设置无效:白名单规则id无效" 91 | }, 92 | "options-import-invalid-whitelistName": { 93 | "message": "设置无效:缺少白名单规则名称" 94 | }, 95 | "options-import-invalid-whitelistOpt": { 96 | "message": "设置无效:白名单规则选项无效" 97 | }, 98 | "options-import-invalid-whitelistSpoofIP": { 99 | "message": "设置无效:白名单规则的欺骗IP无效" 100 | }, 101 | "options-import-success": { 102 | "message": "导入设置成功。正在重新加载扩展..." 103 | }, 104 | "options-ipRules-editorTitle": { 105 | "message": "IP规则编辑器" 106 | }, 107 | "options-ipRules-ipRule": { 108 | "message": "IP规则" 109 | }, 110 | "options-ipRules-reload": { 111 | "message": "重新加载IP信息" 112 | }, 113 | "options-ipRules-textareaLabel": { 114 | "message": "IP范围/地址" 115 | }, 116 | "options-ipRules-textareaPlaceholder": { 117 | "message": "每行一个IP/IP范围" 118 | }, 119 | "options-modal-askDelete": { 120 | "message": "您确定要删除这条规则吗?" 121 | }, 122 | "options-modal-askReset": { 123 | "message": "您确定要重置您的设置?" 124 | }, 125 | "options-modal-confirmDelete": { 126 | "message": "是的,将其删除!" 127 | }, 128 | "options-modal-confirmReset": { 129 | "message": "是,重置我的设置!" 130 | }, 131 | "options-settings": { 132 | "message": "设置" 133 | }, 134 | "options-settings-import": { 135 | "message": "导入" 136 | }, 137 | "options-settings-importing": { 138 | "message": "正在导入设置" 139 | }, 140 | "options-settings-export": { 141 | "message": "导出" 142 | }, 143 | "options-settings-reset": { 144 | "message": "重置为默认" 145 | }, 146 | "options-settings-permissions": { 147 | "message": "Chameleon可以控制一些Firefox首选项,如抵制指纹识别或启用跟踪保护。这可能会与其他扩展冲突。您可以通过移除隐私权限退出Chameleon对这些首选项的控制。请注意,移除此权限将会重置这些首选项。如果不存在,您可以请求此权限。" 148 | }, 149 | "options-settings-permissions-legacy": { 150 | "message": "您需要安装Chameleon的专用版本以启用隐私权限。这是因为支持跨平台/新版的扩展的权限有些复杂的问题。更多详细信息可在下方链接的wiki找到。" 151 | }, 152 | "options-settings-permissions-legacy-wiki": { 153 | "message": "更多信息请查看wiki" 154 | }, 155 | "options-settings-permissions-request": { 156 | "message": "请求隐私权限" 157 | }, 158 | "options-settings-permissions-remove": { 159 | "message": "移除隐私权限" 160 | }, 161 | "options-tab-about": { 162 | "message": "关于" 163 | }, 164 | "options-tab-ipRules": { 165 | "message": "IP规则" 166 | }, 167 | "options-whitelist-acceptLang": { 168 | "message": "Accept-Language" 169 | }, 170 | "options-whitelist-editorTitle": { 171 | "message": "白名单规则编辑器" 172 | }, 173 | "options-whitelist-headerIPLabel": { 174 | "message": "头部IP(Via & X-Forwarded-For)" 175 | }, 176 | "options-whitelist-options-audioContext": { 177 | "message": "启用欺骗音频上下文" 178 | }, 179 | "options-whitelist-options-clientRects": { 180 | "message": "启用欺骗客户端矩形" 181 | }, 182 | "options-whitelist-options-cssExfil": { 183 | "message": "阻止CSS Exfil" 184 | }, 185 | "options-whitelist-options-mediaDevices": { 186 | "message": "阻止媒体设备" 187 | }, 188 | "options-whitelist-options-name": { 189 | "message": "启用保护窗口名称" 190 | }, 191 | "options-whitelist-options-tz": { 192 | "message": "启用时区欺骗" 193 | }, 194 | "options-whitelist-options-ws": { 195 | "message": "禁用WebSocket" 196 | }, 197 | "options-whitelist-rule": { 198 | "message": "白名单规则" 199 | }, 200 | "options-whitelist-sitesTip": { 201 | "message": "每行一条规则:域名@@[可选正则表达式]" 202 | }, 203 | "options-whitelist-textareaLabel": { 204 | "message": "IP范围/地址" 205 | }, 206 | "options-whitelist-textareaPlaceholder": { 207 | "message": "每行一个IP/IP范围" 208 | }, 209 | "popup-home-change": { 210 | "message": "更改" 211 | }, 212 | "popup-home-currentProfile": { 213 | "message": "当前配置文件" 214 | }, 215 | "popup-home-currentProfile-defaultLanguage": { 216 | "message": "默认语言" 217 | }, 218 | "popup-home-currentProfile-defaultScreen": { 219 | "message": "默认屏幕" 220 | }, 221 | "popup-home-currentProfile-defaultTimezone": { 222 | "message": "默认时区" 223 | }, 224 | "popup-home-currentProfile-gettingTimezone": { 225 | "message": "正在获取IP信息" 226 | }, 227 | "popup-home-currentProfile-screenProfile": { 228 | "message": "屏幕(配置文件)" 229 | }, 230 | "popup-home-disabled": { 231 | "message": "Chameleon已禁用" 232 | }, 233 | "popup-home-enabled": { 234 | "message": "Chameleon已启用" 235 | }, 236 | "popup-home-notification-disabled": { 237 | "message": "通知关" 238 | }, 239 | "popup-home-notification-enabled": { 240 | "message": "通知开" 241 | }, 242 | "popup-home-onThisPage": { 243 | "message": "在本页面" 244 | }, 245 | "popup-home-theme-dark": { 246 | "message": "深色" 247 | }, 248 | "popup-home-theme-light": { 249 | "message": "浅色" 250 | }, 251 | "popup-profile-changePeriodically": { 252 | "message": "定期更改" 253 | }, 254 | "popup-profile-devicePhone": { 255 | "message": "手机" 256 | }, 257 | "popup-profile-deviceTablet": { 258 | "message": "平板" 259 | }, 260 | "popup-profile-interval-no": { 261 | "message": "否" 262 | }, 263 | "popup-profile-interval-custom": { 264 | "message": "自定义间隔" 265 | }, 266 | "popup-profile-interval-customMax": { 267 | "message": "最大值(分钟):" 268 | }, 269 | "popup-profile-interval-customMin": { 270 | "message": "最小值(分钟):" 271 | }, 272 | "popup-profile-interval-minute": { 273 | "message": "每分钟" 274 | }, 275 | "popup-profile-interval-5minutes": { 276 | "message": "每5分钟" 277 | }, 278 | "popup-profile-interval-10minutes": { 279 | "message": "每10分钟" 280 | }, 281 | "popup-profile-interval-20minutes": { 282 | "message": "每20分钟" 283 | }, 284 | "popup-profile-interval-30minutes": { 285 | "message": "每30分钟" 286 | }, 287 | "popup-profile-interval-40minutes": { 288 | "message": "每40分钟" 289 | }, 290 | "popup-profile-interval-50minutes": { 291 | "message": "每50分钟" 292 | }, 293 | "popup-profile-interval-hour": { 294 | "message": "每小时" 295 | }, 296 | "popup-profile-exclude": { 297 | "message": "排除" 298 | }, 299 | "popup-profile-randomAndroid": { 300 | "message": "随机Android浏览器" 301 | }, 302 | "popup-profile-randomIOS": { 303 | "message": "随机的iOS浏览器" 304 | }, 305 | "popup-profile-randomMacOS": { 306 | "message": "随机macOS浏览器" 307 | }, 308 | "popup-profile-randomLinux": { 309 | "message": "随机Linux浏览器" 310 | }, 311 | "popup-profile-randomWindows": { 312 | "message": "随机Windows浏览器" 313 | }, 314 | "popup-profile-random": { 315 | "message": "随机" 316 | }, 317 | "popup-profile-randomDesktopProfile": { 318 | "message": "随机配置文件(桌面)" 319 | }, 320 | "popup-profile-randomMobileProfile": { 321 | "message": "随机配置文件(移动)" 322 | }, 323 | "popup-profile-showProfileOnIcon": { 324 | "message": "在图标上显示浏览器配置文件" 325 | }, 326 | "popup-headers": { 327 | "message": "头信息" 328 | }, 329 | "popup-headers-enableDNT": { 330 | "message": "启用DNT(Do Not Track)" 331 | }, 332 | "popup-headers-preventEtag": { 333 | "message": "阻止Etag跟踪" 334 | }, 335 | "popup-headers-refererWarning": { 336 | "message": "请勿修改以下选项的about:config设置。" 337 | }, 338 | "popup-headers-referer-trimming": { 339 | "message": "Referer微调政策" 340 | }, 341 | "popup-headers-referer-trimming-sendFullURI": { 342 | "message": "发送完整URI(默认)" 343 | }, 344 | "popup-headers-referer-trimming-schemeHostPortPath": { 345 | "message": "方案、主机、端口+路径" 346 | }, 347 | "popup-headers-referer-trimming-schemeHostPort": { 348 | "message": "方案、主机+端口" 349 | }, 350 | "popup-headers-referer-xorigin": { 351 | "message": "Referer X Origin策略" 352 | }, 353 | "popup-headers-referer-xorigin-alwaysSend": { 354 | "message": "总是发送(默认)" 355 | }, 356 | "popup-headers-referer-xorigin-matchBaseDomain": { 357 | "message": "匹配基本域名" 358 | }, 359 | "popup-headers-referer-xorigin-matchHost": { 360 | "message": "匹配主机" 361 | }, 362 | "popup-headers-spoofAcceptLang": { 363 | "message": "欺骗Accept-Language" 364 | }, 365 | "popup-headers-spoofIP": { 366 | "message": "欺骗X-Forwarded-For/Via IP" 367 | }, 368 | "popup-headers-spoofIP-random": { 369 | "message": "随机IP" 370 | }, 371 | "popup-headers-spoofIP-custom": { 372 | "message": "自定义IP" 373 | }, 374 | "popup-headers-spoofIP-rangeFrom": { 375 | "message": "来源范围" 376 | }, 377 | "popup-headers-spoofIP-rangeTo": { 378 | "message": "目标范围" 379 | }, 380 | "popup-options": { 381 | "message": "选项" 382 | }, 383 | "popup-options-grantPermissions": { 384 | "message": "授予修改设置的权限" 385 | }, 386 | "popup-options-injection": { 387 | "message": "注入" 388 | }, 389 | "popup-options-injection-limitTabHistory": { 390 | "message": "限制标签页历史" 391 | }, 392 | "popup-options-injection-protectWinName": { 393 | "message": "保护窗口名称" 394 | }, 395 | "popup-options-injection-audioContext": { 396 | "message": "欺骗音频上下文" 397 | }, 398 | "popup-options-injection-clientRects": { 399 | "message": "欺骗客户端矩形" 400 | }, 401 | "popup-options-injection-protectKBFingerprint": { 402 | "message": "保护键盘指纹" 403 | }, 404 | "popup-options-injection-protectKBFingerprintDelay": { 405 | "message": "延迟(毫秒)" 406 | }, 407 | "popup-options-injection-screen": { 408 | "message": "屏幕大小" 409 | }, 410 | "popup-options-injection-spoofFontFingerprint": { 411 | "message": "欺骗字体指纹" 412 | }, 413 | "popup-options-standard": { 414 | "message": "标准" 415 | }, 416 | "popup-options-standard-blockMediaDevices": { 417 | "message": "阻止媒体设备" 418 | }, 419 | "popup-options-standard-blockCSSExfil": { 420 | "message": "阻止CSS Exfil" 421 | }, 422 | "popup-options-standard-disableWebRTC": { 423 | "message": "禁用WebRTC" 424 | }, 425 | "popup-options-standard-firstPartyIsolation": { 426 | "message": "启用第一方隔离" 427 | }, 428 | "popup-options-standard-resistFingerprinting": { 429 | "message": "抵制指纹识别" 430 | }, 431 | "popup-options-standard-spoofMediaDevices": { 432 | "message": "欺骗媒体设备" 433 | }, 434 | "popup-options-standard-trackingProtection": { 435 | "message": "跟踪保护模式" 436 | }, 437 | "popup-options-standard-trackingProtection-on": { 438 | "message": "开" 439 | }, 440 | "popup-options-standard-trackingProtection-off": { 441 | "message": "关" 442 | }, 443 | "popup-options-standard-trackingProtection-privateBrowsing": { 444 | "message": "在隐私浏览中启用" 445 | }, 446 | "popup-options-standard-webRTCPolicy": { 447 | "message": "WebRTC策略" 448 | }, 449 | "popup-options-standard-webRTCPolicy-nonProxified": { 450 | "message": "禁用非代理的UDP" 451 | }, 452 | "popup-options-standard-webRTCPolicy-public": { 453 | "message": "仅使用公共接口(最佳)" 454 | }, 455 | "popup-options-standard-webRTCPolicy-publicPrivate": { 456 | "message": "使用公共和私有接口" 457 | }, 458 | "popup-options-standard-webSockets-blockAll": { 459 | "message": "全部阻止" 460 | }, 461 | "popup-options-standard-webSockets-blockThirdParty": { 462 | "message": "阻止第三方" 463 | }, 464 | "popup-options-cookie": { 465 | "message": "Cookie选项" 466 | }, 467 | "popup-options-cookieNotPersistent": { 468 | "message": "窗口关闭后删除cookie和站点数据" 469 | }, 470 | "popup-options-cookiePolicy": { 471 | "message": "策略" 472 | }, 473 | "popup-options-cookiePolicy-allowVisited": { 474 | "message": "允许访问过的第三方" 475 | }, 476 | "popup-options-cookiePolicy-rejectAll": { 477 | "message": "全部阻止" 478 | }, 479 | "popup-options-cookiePolicy-rejectThirdParty": { 480 | "message": "阻止第三方" 481 | }, 482 | "popup-options-cookiePolicy-rejectTrackers": { 483 | "message": "拒绝跟踪器" 484 | }, 485 | "popup-options-cookiePolicy-rejectTrackersPartitionForeign": { 486 | "message": "阻隔跟踪器和第三方cookie" 487 | }, 488 | "popup-whitelist-contextMenu": { 489 | "message": "添加右键菜单条目到当前打开标签页白名单中的域名" 490 | }, 491 | "popup-whitelist-defaultProfileLabel": { 492 | "message": "默认配置文件" 493 | }, 494 | "popup-whitelist-enable": { 495 | "message": "启用白名单" 496 | }, 497 | "popup-whitelist-isNotWhitelisted": { 498 | "message": "未加入白名单" 499 | }, 500 | "popup-whitelist-isWhitelisted": { 501 | "message": "已加入白名单" 502 | }, 503 | "popup-whitelist-open": { 504 | "message": "在白名单中打开" 505 | }, 506 | "text-addToRule": { 507 | "message": "添加到规则:$RULE_NAME$", 508 | "placeholders": { 509 | "rule_name": { 510 | "content": "$1", 511 | "example": "Firefox whitelist rule profile" 512 | } 513 | } 514 | }, 515 | "text-allowAll": { 516 | "message": "全部允许" 517 | }, 518 | "text-cancel": { 519 | "message": "取消" 520 | }, 521 | "text-createNewRule": { 522 | "message": "创建新规则" 523 | }, 524 | "text-default": { 525 | "message": "默认" 526 | }, 527 | "text-defaultWhitelistProfile": { 528 | "message": "默认白名单配置文件" 529 | }, 530 | "text-disableReferer": { 531 | "message": "禁用Referer" 532 | }, 533 | "text-language": { 534 | "message": "语言" 535 | }, 536 | "text-name": { 537 | "message": "名称" 538 | }, 539 | "text-profile": { 540 | "message": "配置文件" 541 | }, 542 | "text-realProfile": { 543 | "message": "真实配置文件" 544 | }, 545 | "text-removeFromRule": { 546 | "message": "从规则中移除:$RULE_NAME$", 547 | "placeholders": { 548 | "rule_name": { 549 | "content": "$1", 550 | "example": "Firefox whitelist rule profile" 551 | } 552 | } 553 | }, 554 | "text-save": { 555 | "message": "保存" 556 | }, 557 | "text-screen": { 558 | "message": "屏幕" 559 | }, 560 | "text-searchRules": { 561 | "message": "搜索规则" 562 | }, 563 | "text-startupDelay": { 564 | "message": "启动延迟(秒)" 565 | }, 566 | "text-timezone": { 567 | "message": "时区" 568 | }, 569 | "text-whitelist": { 570 | "message": "已加入白名单" 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import { Chameleon } from './lib/chameleon'; 2 | import store from './store'; 3 | import webext from './lib/webext'; 4 | 5 | webext.firstTimeInstall(); 6 | 7 | store.state.version = browser.runtime.getManifest().version; 8 | 9 | let chameleon = new Chameleon(JSON.parse(JSON.stringify(store.state))); 10 | let messageHandler = (request: any, sender: any, sendResponse: any) => { 11 | if (request.action === 'save') { 12 | if (chameleon.timeout) { 13 | clearTimeout(chameleon.timeout); 14 | } 15 | 16 | chameleon.settings = Object.assign(chameleon.settings, request.data); 17 | chameleon.timeout = setTimeout(() => { 18 | chameleon.saveSettings(request.data); 19 | sendResponse('done'); 20 | }, 200); 21 | } else if (request.action === 'implicitSave') { 22 | if (chameleon.timeout) { 23 | clearTimeout(chameleon.timeout); 24 | } 25 | 26 | chameleon.timeout = setTimeout(() => { 27 | chameleon.saveSettings(chameleon.settings); 28 | sendResponse('done'); 29 | }, 200); 30 | } else if (request.action === 'contextMenu') { 31 | chameleon.toggleContextMenu(request.data); 32 | } else if (request.action === 'toggleBadgeText') { 33 | chameleon.updateBadgeText(request.data); 34 | } else if (request.action === 'getSettings') { 35 | (async () => { 36 | if (!!browser.privacy) { 37 | let cookieSettings = await browser.privacy.websites.cookieConfig.get({}); 38 | chameleon.settings.options.cookieNotPersistent = cookieSettings.value.nonPersistentCookies; 39 | chameleon.settings.options.cookiePolicy = cookieSettings.value.behavior; 40 | 41 | let firstPartyIsolate = await browser.privacy.websites.firstPartyIsolate.get({}); 42 | chameleon.settings.options.firstPartyIsolate = firstPartyIsolate.value; 43 | 44 | let resistFingerprinting = await browser.privacy.websites.resistFingerprinting.get({}); 45 | chameleon.settings.options.resistFingerprinting = resistFingerprinting.value; 46 | 47 | let trackingProtectionMode = await browser.privacy.websites.trackingProtectionMode.get({}); 48 | chameleon.settings.options.trackingProtectionMode = trackingProtectionMode.value; 49 | 50 | let peerConnectionEnabled = await browser.privacy.network.peerConnectionEnabled.get({}); 51 | chameleon.settings.options.disableWebRTC = !peerConnectionEnabled.value; 52 | 53 | let webRTCIPHandlingPolicy = await browser.privacy.network.webRTCIPHandlingPolicy.get({}); 54 | chameleon.settings.options.webRTCPolicy = webRTCIPHandlingPolicy.value; 55 | } 56 | 57 | let settings = Object.assign({}, chameleon.settings); 58 | settings.config.hasPrivacyPermission = !!browser.privacy; 59 | 60 | sendResponse(settings); 61 | })(); 62 | } else if (request.action === 'init') { 63 | browser.runtime.sendMessage( 64 | { 65 | action: 'tempStore', 66 | data: chameleon.tempStore, 67 | }, 68 | response => { 69 | if (browser.runtime.lastError) return; 70 | } 71 | ); 72 | sendResponse('done'); 73 | } else if (request.action === 'reloadInjectionScript') { 74 | chameleon.buildInjectionScript(); 75 | sendResponse('done'); 76 | } else if (request.action === 'reloadIPInfo') { 77 | if (chameleon.settings.options.timeZone === 'ip' || (chameleon.settings.headers.spoofAcceptLang.value === 'ip' && chameleon.settings.headers.spoofAcceptLang.enabled)) { 78 | chameleon.updateIPInfo(); 79 | sendResponse('done'); 80 | } 81 | } else if (request.action === 'reloadProfile') { 82 | chameleon.setTimer(request.data); 83 | chameleon.buildInjectionScript(); 84 | sendResponse('done'); 85 | } else if (request.action === 'reloadSpoofIP') { 86 | if (request.data[0].name === 'headers.spoofIP.enabled') { 87 | chameleon.settings.headers.spoofIP.enabled = request.data[0].value; 88 | } else if (request.data[0].name === 'headers.spoofIP.option') { 89 | chameleon.settings.headers.spoofIP.option = request.data[0].value; 90 | } else if (request.data[0].name === 'headers.spoofIP.rangeFrom') { 91 | chameleon.settings.headers.spoofIP.rangeFrom = request.data[0].value; 92 | chameleon.settings.headers.spoofIP.rangeTo = request.data[1].value; 93 | } 94 | 95 | chameleon.updateSpoofIP(); 96 | sendResponse('done'); 97 | } else if (request.action === 'reset') { 98 | chameleon.reset(); 99 | browser.runtime.reload(); 100 | } else if (request.action === 'updateIPRules') { 101 | chameleon.settings.ipRules = request.data; 102 | 103 | chameleon.timeout = setTimeout(() => { 104 | chameleon.saveSettings(chameleon.settings); 105 | sendResponse('done'); 106 | }, 200); 107 | } else if (request.action === 'updateProfile') { 108 | chameleon.settings.profile.selected = request.data; 109 | 110 | // reset interval timer and send notification 111 | chameleon.setTimer(); 112 | sendResponse('done'); 113 | } else if (request.action === 'updateWhitelist') { 114 | chameleon.settings.whitelist = request.data; 115 | chameleon.updateProfileCache(); 116 | 117 | chameleon.timeout = setTimeout(() => { 118 | chameleon.saveSettings(chameleon.settings); 119 | sendResponse('done'); 120 | }, 200); 121 | } else if (request.action === 'validateSettings') { 122 | let res = chameleon.validateSettings(request.data); 123 | sendResponse(res); 124 | } 125 | 126 | return true; 127 | }; 128 | 129 | browser.alarms.onAlarm.addListener(() => { 130 | chameleon.run(); 131 | }); 132 | 133 | browser.runtime.onMessage.addListener(messageHandler); 134 | 135 | (async () => { 136 | await chameleon.init(await webext.getSettings(null)); 137 | 138 | if (chameleon.settings.options.timeZone === 'ip' || (chameleon.settings.headers.spoofAcceptLang.value === 'ip' && chameleon.settings.headers.spoofAcceptLang.enabled)) { 139 | setTimeout(() => { 140 | chameleon.updateIPInfo(); 141 | }, chameleon.settings.config.reloadIPStartupDelay * 1000); 142 | } 143 | 144 | if (!!browser.privacy) { 145 | await chameleon.changeBrowserSettings(); 146 | } 147 | 148 | chameleon.setupHeaderListeners(); 149 | chameleon.setTimer(); 150 | 151 | webext.enableChameleon(chameleon.settings.config.enabled); 152 | chameleon.toggleContextMenu(chameleon.settings.whitelist.enabledContextMenu); 153 | 154 | /* 155 | Allow Chameleon to be controlled by another extension 156 | 157 | Enabled only in developer builds 158 | */ 159 | if (browser.runtime.getManifest().version_name.includes('-')) { 160 | browser.runtime.onMessageExternal.addListener((request, sender, sendResponse) => { 161 | messageHandler(request, sender, sendResponse); 162 | 163 | return true; 164 | }); 165 | } 166 | 167 | if (chameleon.platform.os != 'android') { 168 | browser.browserAction.setBadgeBackgroundColor({ 169 | color: 'green', 170 | }); 171 | } 172 | })(); 173 | -------------------------------------------------------------------------------- /src/browser.d.ts: -------------------------------------------------------------------------------- 1 | declare var browser: any; 2 | 3 | declare module 'browser' { 4 | export = browser; 5 | } 6 | -------------------------------------------------------------------------------- /src/css/notosans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sereneblue/chameleon/1e620117b5c183f4e7bed50d533190b5e63fc152/src/css/notosans.ttf -------------------------------------------------------------------------------- /src/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | 4 | @font-face { 5 | font-family: 'Noto Sans'; 6 | src: url('notosans.ttf') format('truetype'); 7 | } 8 | 9 | * { 10 | margin: 0; 11 | font-family: 'Noto Sans', sans-serif !important; 12 | } 13 | 14 | .bg-light { 15 | @apply text-dark; 16 | } 17 | 18 | .bg-light .fg { 19 | @apply bg-light-fg; 20 | } 21 | 22 | .bg-dark { 23 | @apply text-light; 24 | } 25 | 26 | .bg-dark .fg { 27 | @apply bg-dark-fg; 28 | } 29 | 30 | .bg-dark .cursor-pointer.fg:hover, 31 | .bg-dark .group.fg:hover { 32 | @apply bg-dark-fg-alt; 33 | } 34 | 35 | .bg-light .cursor-pointer.fg:hover, 36 | .bg-light .group.fg:hover { 37 | @apply bg-light-fg-alt; 38 | } 39 | 40 | .group { 41 | @apply flex-grow items-center px-2 py-1 mb-1; 42 | } 43 | 44 | .group.active { 45 | @apply bg-primary text-light; 46 | } 47 | .group.active.fg:hover { 48 | @apply bg-primary text-light; 49 | } 50 | 51 | .group-options { 52 | @apply flex flex-1 justify-center cursor-pointer; 53 | } 54 | 55 | .group-options.fg:hover { 56 | @apply bg-dark-fg-alt; 57 | } 58 | 59 | .group-options.fg:hover { 60 | @apply bg-light-fg-alt; 61 | } 62 | 63 | .profile-item { 64 | @apply -mx-3 px-3 flex justify-between py-1; 65 | } 66 | 67 | .profile-item.bg-dark-fg:hover { 68 | @apply bg-dark-fg-alt; 69 | } 70 | 71 | .profile-item.bg-light-fg:hover { 72 | @apply bg-light-fg-alt; 73 | } 74 | 75 | .tab { 76 | @apply px-2 pt-2 pb-1 cursor-pointer text-light; 77 | } 78 | 79 | .app.bg-light .tab.active { 80 | @apply text-primary; 81 | } 82 | 83 | .app.bg-dark .tab.active { 84 | @apply text-light; 85 | } 86 | 87 | .options-tab { 88 | @apply px-4 py-1 cursor-pointer text-light; 89 | } 90 | 91 | .bg-light .options-tab.active { 92 | @apply text-primary; 93 | background-color: #fbfbfb; 94 | } 95 | 96 | .bg-dark .options-tab.active { 97 | @apply text-light; 98 | background-color: #33313b; 99 | } 100 | 101 | .transparent-btn { 102 | @apply bg-transparent py-2 px-4 border border-primary-soft rounded block mb-4 mr-4; 103 | } 104 | 105 | .transparent-btn:hover { 106 | @apply bg-primary-soft text-white border-transparent; 107 | } 108 | 109 | input, 110 | select { 111 | @apply text-dark; 112 | } 113 | 114 | .bg-dark input.error, 115 | .bg-dark textarea.error { 116 | @apply bg-red-300; 117 | } 118 | 119 | .bg-light input.error, 120 | .bg-light textarea.error { 121 | @apply bg-red-200; 122 | } 123 | 124 | .bg-dark button.bg-transparent { 125 | @apply text-light; 126 | } 127 | 128 | .bg-light button.bg-transparent { 129 | @apply text-primary; 130 | } 131 | 132 | .bg-light button.bg-transparent:hover { 133 | @apply text-light; 134 | } 135 | 136 | .bg-dark #iprules tbody tr { 137 | @apply border-dark-fg-alt; 138 | } 139 | 140 | .bg-dark #iprules tbody tr:hover { 141 | @apply bg-dark-fg; 142 | } 143 | 144 | .bg-light #iprules tbody tr { 145 | @apply border-light-fg-alt; 146 | } 147 | 148 | .bg-light #iprules tbody tr:hover { 149 | @apply bg-light-fg; 150 | } 151 | 152 | .modal { 153 | @apply m-auto shadow-xl text-dark rounded-lg; 154 | } 155 | 156 | /* cancel button */ 157 | .modal button.bg-transparent { 158 | @apply text-gray-600; 159 | } 160 | 161 | .modal button.bg-transparent:hover { 162 | @apply text-gray-700; 163 | } 164 | 165 | .bg-dark .modal { 166 | @apply bg-gray-400; 167 | } 168 | 169 | .bg-light .modal { 170 | background-color: #fbfbfb; 171 | } 172 | 173 | /* Checkbox switch */ 174 | .form-switch { 175 | @apply relative select-none w-12 mr-1 leading-normal; 176 | } 177 | .form-switch-checkbox { 178 | @apply hidden; 179 | } 180 | .form-switch-label { 181 | @apply block overflow-hidden cursor-pointer bg-gray-300 border rounded-full h-6 shadow-inner; 182 | 183 | transition: background-color 0.2s ease-in; 184 | } 185 | .form-switch-label:before { 186 | @apply absolute block bg-white inset-y-0 w-6 border-2 rounded-full -ml-1; 187 | 188 | right: 50%; 189 | content: ''; 190 | transition: all 0.2s ease-in; 191 | } 192 | .form-switch-checkbox:checked + .form-switch-label, 193 | .form-switch-checkbox:checked + .form-switch-label:before { 194 | } 195 | .form-switch-checkbox:checked + .form-switch-label { 196 | @apply bg-primary shadow-none; 197 | } 198 | .form-switch-checkbox:checked + .form-switch-label:before { 199 | @apply inset-y-0 right-0; 200 | } 201 | 202 | .bg-dark #detected .fp { 203 | @apply bg-dark-fg-alt; 204 | } 205 | 206 | .bg-light #detected .fp { 207 | @apply bg-light-fg-alt text-black; 208 | } 209 | 210 | #detected .fp { 211 | @apply flex items-center justify-center rounded-lg p-1 border-primary w-10 shadow-xs; 212 | } 213 | 214 | #detected .fp.active { 215 | @apply bg-primary text-white; 216 | } 217 | 218 | .fp div { 219 | opacity: 0%; 220 | @apply absolute transition transition-opacity duration-300 ease-in-out flex justify-center -mt-24 px-4 text-black z-20 rounded text-sm bg-yellow-200 border border-yellow-600 p-1 shadow-sm; 221 | } 222 | 223 | .fp:hover div { 224 | opacity: 100%; 225 | } 226 | 227 | @tailwind utilities; 228 | -------------------------------------------------------------------------------- /src/icons/icon.svg: -------------------------------------------------------------------------------- 1 | ChameleonCreated with Sketch. -------------------------------------------------------------------------------- /src/icons/icon_disabled.svg: -------------------------------------------------------------------------------- 1 | ChameleonCreated with Sketch. -------------------------------------------------------------------------------- /src/lib/devices.ts: -------------------------------------------------------------------------------- 1 | interface Device { 2 | name: string; 3 | build: string; 4 | viewport: string; 5 | deviceScaleFactor: number; 6 | memory?: number; 7 | hw?: number; 8 | } 9 | 10 | let devices: any = { 11 | mobile: { 12 | and1: [ 13 | { 14 | name: 'Google Pixel 5', 15 | build: 'Pixel 5', 16 | viewport: '393x851', 17 | deviceScaleFactor: 2.75, 18 | memory: 8, 19 | hw: 8, 20 | }, 21 | { 22 | name: 'Moto G9 Play', 23 | build: 'moto g(9) play', 24 | viewport: '412x915', 25 | deviceScaleFactor: 1.75, 26 | memory: 4, 27 | hw: 8, 28 | }, 29 | { 30 | name: 'OnePlus 8', 31 | build: 'IN2013', 32 | viewport: '412x915', 33 | deviceScaleFactor: 2.625, 34 | memory: 8, 35 | hw: 8, 36 | }, 37 | { 38 | name: 'Samsung Galaxy A52', 39 | build: 'SM-A525F', 40 | viewport: '412x915', 41 | deviceScaleFactor: 2.625, 42 | memory: 4, 43 | hw: 8, 44 | }, 45 | { 46 | name: 'Samsung Galaxy S21', 47 | build: 'SM-G991B', 48 | viewport: '360x800', 49 | deviceScaleFactor: 3, 50 | memory: 8, 51 | hw: 8, 52 | }, 53 | ], 54 | and2: [ 55 | { 56 | name: 'Google Pixel 6', 57 | build: 'Pixel 6', 58 | viewport: '412x915', 59 | deviceScaleFactor: 2.625, 60 | memory: 8, 61 | hw: 8, 62 | }, 63 | { 64 | name: 'Redmi Note 12 Pro', 65 | build: '22101316I', 66 | viewport: '393x873', 67 | deviceScaleFactor: 2.75, 68 | memory: 8, 69 | hw: 8, 70 | }, 71 | { 72 | name: 'Moto G71 5G', 73 | build: 'moto g71 5G', 74 | viewport: '412x915', 75 | deviceScaleFactor: 2.625, 76 | memory: 4, 77 | hw: 8, 78 | }, 79 | { 80 | name: 'OnePlus 9', 81 | build: 'LE2113', 82 | viewport: '384x854', 83 | deviceScaleFactor: 2.8125, 84 | memory: 8, 85 | hw: 8, 86 | }, 87 | { 88 | name: 'Samsung Galaxy S22', 89 | build: 'SM-S901U', 90 | viewport: '360x780', 91 | deviceScaleFactor: 3, 92 | memory: 8, 93 | hw: 8, 94 | }, 95 | ], 96 | and3: [ 97 | { 98 | name: 'Google Pixel 7', 99 | build: 'Pixel 7', 100 | viewport: '412x915', 101 | deviceScaleFactor: 2.625, 102 | memory: 8, 103 | hw: 8, 104 | }, 105 | { 106 | name: 'Google Pixel 7 Pro', 107 | build: 'Pixel 7 Pro', 108 | viewport: '412x892', 109 | deviceScaleFactor: 2.625, 110 | memory: 8, 111 | hw: 8, 112 | }, 113 | { 114 | name: 'Redmi Note 12 4G', 115 | build: '23027RAD4I', 116 | viewport: '393x783', 117 | deviceScaleFactor: 2.75, 118 | memory: 4, 119 | hw: 8, 120 | }, 121 | { 122 | name: 'Samsung Galaxy S23', 123 | build: 'SM-S911B', 124 | viewport: '360x780', 125 | deviceScaleFactor: 3, 126 | memory: 8, 127 | hw: 8, 128 | }, 129 | { 130 | name: 'OnePlus 11', 131 | build: 'CPH2487', 132 | viewport: '355x793', 133 | deviceScaleFactor: 3.5, 134 | memory: 8, 135 | hw: 8, 136 | }, 137 | ], 138 | and4: [ 139 | { 140 | name: 'Google Pixel 8', 141 | build: 'Pixel 8', 142 | viewport: '412x915', 143 | deviceScaleFactor: 2.625, 144 | memory: 8, 145 | hw: 9, 146 | }, 147 | { 148 | name: 'Google Pixel 8 Pro', 149 | build: 'Pixel 8 Pro', 150 | viewport: '448x998', 151 | deviceScaleFactor: 2.25, 152 | memory: 8, 153 | hw: 9, 154 | }, 155 | { 156 | name: 'OnePlus 11', 157 | build: 'CPH2487', 158 | viewport: '355x793', 159 | deviceScaleFactor: 3.5, 160 | memory: 8, 161 | hw: 8, 162 | }, 163 | { 164 | name: 'Samsung Galaxy S23', 165 | build: 'SM-S911B', 166 | viewport: '360x780', 167 | deviceScaleFactor: 3, 168 | memory: 8, 169 | hw: 8, 170 | }, 171 | { 172 | name: 'Samsung Galaxy S23 Ultra', 173 | build: 'SM-S918B', 174 | viewport: '384x824', 175 | deviceScaleFactor: 2.8125, 176 | memory: 8, 177 | hw: 8, 178 | }, 179 | ], 180 | ios1: [ 181 | { 182 | name: 'iPhone 6S', 183 | build: '19H370', 184 | viewport: '375x667', 185 | deviceScaleFactor: 2, 186 | }, 187 | { 188 | name: 'iPhone 6S Plus', 189 | build: '19H370', 190 | viewport: '414x736', 191 | deviceScaleFactor: 3, 192 | }, 193 | { 194 | name: 'iPhone SE (2022)', 195 | build: '19H370', 196 | viewport: '375x548', 197 | deviceScaleFactor: 2, 198 | }, 199 | { 200 | name: 'iPhone 7', 201 | build: '19H370', 202 | viewport: '375x667', 203 | deviceScaleFactor: 2, 204 | }, 205 | { 206 | name: 'iPhone 7 Plus', 207 | build: '19H370', 208 | viewport: '414x736', 209 | deviceScaleFactor: 3, 210 | }, 211 | { 212 | name: 'iPhone 8', 213 | build: '19H370', 214 | viewport: '375x667', 215 | deviceScaleFactor: 2, 216 | }, 217 | { 218 | name: 'iPhone 8 Plus', 219 | build: '19H370', 220 | viewport: '414x736', 221 | deviceScaleFactor: 3, 222 | }, 223 | { 224 | name: 'iPhone X', 225 | build: '19H370', 226 | viewport: '375x812', 227 | deviceScaleFactor: 3, 228 | }, 229 | { 230 | name: 'iPhone XS', 231 | build: '19H370', 232 | viewport: '375x812', 233 | deviceScaleFactor: 3, 234 | }, 235 | { 236 | name: 'iPhone XS Max', 237 | build: '19H370', 238 | viewport: '414x896', 239 | deviceScaleFactor: 3, 240 | }, 241 | { 242 | name: 'iPhone XR', 243 | build: '19H370', 244 | viewport: '414x896', 245 | deviceScaleFactor: 3, 246 | }, 247 | { 248 | name: 'iPhone 11', 249 | build: '19H370', 250 | viewport: '414x896', 251 | deviceScaleFactor: 3, 252 | }, 253 | { 254 | name: 'iPhone 11 Pro', 255 | build: '19H370', 256 | viewport: '375x812', 257 | deviceScaleFactor: 3, 258 | }, 259 | { 260 | name: 'iPhone 11 Pro Max', 261 | build: '19H370', 262 | viewport: '414x896', 263 | deviceScaleFactor: 3, 264 | }, 265 | { 266 | name: 'iPhone 12', 267 | build: '19H370', 268 | viewport: '360x780', 269 | deviceScaleFactor: 3, 270 | }, 271 | { 272 | name: 'iPhone 12 Pro', 273 | build: '19H370', 274 | viewport: '390x844', 275 | deviceScaleFactor: 3, 276 | }, 277 | { 278 | name: 'iPhone 12 Pro Max', 279 | build: '19H370', 280 | viewport: '428x926', 281 | deviceScaleFactor: 3, 282 | }, 283 | { 284 | name: 'iPhone 13', 285 | build: '19H370', 286 | viewport: '390x844', 287 | deviceScaleFactor: 3, 288 | }, 289 | { 290 | name: 'iPhone 13 Pro', 291 | build: '19H370', 292 | viewport: '390x844', 293 | deviceScaleFactor: 3, 294 | }, 295 | { 296 | name: 'iPhone 13 Pro Max', 297 | build: '19H370', 298 | viewport: '428x926', 299 | deviceScaleFactor: 3, 300 | }, 301 | ], 302 | }, 303 | tablet: { 304 | and1: [ 305 | { 306 | name: 'Samsung Galaxy Tab S4 10.5', 307 | build: 'SM-T830', 308 | viewport: '800x1280', 309 | deviceScaleFactor: 2, 310 | memory: 4, 311 | hw: 8, 312 | }, 313 | { 314 | name: 'Samsung Galaxy Tab A 10.5', 315 | build: 'SM-T590', 316 | viewport: '600x960', 317 | deviceScaleFactor: 2, 318 | memory: 2, 319 | hw: 8, 320 | }, 321 | { 322 | name: 'Xiaomi Mi Pad 4', 323 | build: 'MI PAD 4', 324 | viewport: '600x960', 325 | deviceScaleFactor: 2, 326 | memory: 2, 327 | hw: 8, 328 | }, 329 | { 330 | name: 'Xiaomi Mi Pad 4 Plus', 331 | build: 'MI PAD 4 PLUS', 332 | viewport: '600x960', 333 | deviceScaleFactor: 2, 334 | memory: 4, 335 | hw: 8, 336 | }, 337 | ], 338 | and2: [ 339 | { 340 | name: 'Samsung Galaxy Tab S4 10.5', 341 | build: 'SM-T830', 342 | viewport: '800x1280', 343 | deviceScaleFactor: 2, 344 | memory: 4, 345 | hw: 8, 346 | }, 347 | { 348 | name: 'ASUS ZenPad 3S 10', 349 | build: 'P027', 350 | viewport: '748x1024', 351 | deviceScaleFactor: 2, 352 | memory: 4, 353 | hw: 8, 354 | }, 355 | { 356 | name: 'Samsung Galaxy Tab S5e', 357 | build: 'SM-T720', 358 | viewport: '800x1280', 359 | deviceScaleFactor: 2, 360 | memory: 4, 361 | hw: 8, 362 | }, 363 | { 364 | name: 'Samsung Galaxy Tab A 10.1 (2019)', 365 | build: 'SM-T510', 366 | viewport: '600x960', 367 | deviceScaleFactor: 1.5, 368 | memory: 2, 369 | hw: 8, 370 | }, 371 | ], 372 | and3: [ 373 | { 374 | name: 'Samsung Galaxy Tab S4 10.5', 375 | build: 'SM-T830', 376 | viewport: '800x1280', 377 | deviceScaleFactor: 2, 378 | memory: 4, 379 | hw: 8, 380 | }, 381 | { 382 | name: 'Samsung Galaxy Tab S5e', 383 | build: 'SM-T720', 384 | viewport: '800x1280', 385 | deviceScaleFactor: 2, 386 | memory: 4, 387 | hw: 8, 388 | }, 389 | { 390 | name: 'Samsung Galaxy Tab A 10.1 (2019)', 391 | build: 'SM-T510', 392 | viewport: '600x960', 393 | deviceScaleFactor: 1.5, 394 | memory: 2, 395 | hw: 8, 396 | }, 397 | { 398 | name: 'Samsung Galaxy Tab A7 10.4 (2020)', 399 | build: 'SM-T500', 400 | viewport: '600x1000', 401 | deviceScaleFactor: 2, 402 | memory: 2, 403 | hw: 8, 404 | }, 405 | ], 406 | and4: [ 407 | { 408 | name: 'Samsung Galaxy Tab S7+', 409 | build: 'SM-T970', 410 | viewport: '876x1400', 411 | deviceScaleFactor: 2, 412 | memory: 4, 413 | hw: 8, 414 | }, 415 | { 416 | name: 'Samsung Galaxy Tab S5e', 417 | build: 'SM-T720', 418 | viewport: '800x1280', 419 | deviceScaleFactor: 2, 420 | memory: 4, 421 | hw: 8, 422 | }, 423 | { 424 | name: 'Samsung Galaxy Tab A7 10.4 (2020)', 425 | build: 'SM-T500', 426 | viewport: '600x1000', 427 | deviceScaleFactor: 2, 428 | memory: 2, 429 | hw: 8, 430 | }, 431 | ], 432 | ios1: [ 433 | { 434 | device: 'iPad Air 2', 435 | build: '19H370', 436 | viewport: '768x1024', 437 | deviceScaleFactor: 2, 438 | }, 439 | { 440 | device: 'iPad (2017)', 441 | build: '19H370', 442 | viewport: '768x1024', 443 | deviceScaleFactor: 2, 444 | }, 445 | { 446 | device: 'iPad (2018)', 447 | build: '19H370', 448 | viewport: '768x1024', 449 | deviceScaleFactor: 2, 450 | }, 451 | { 452 | device: 'iPad Mini 4', 453 | build: '19H370', 454 | viewport: '768x1024', 455 | deviceScaleFactor: 2, 456 | }, 457 | { 458 | device: 'iPad Pro (9.7in)', 459 | build: '19H370', 460 | viewport: '768x1024', 461 | deviceScaleFactor: 2, 462 | }, 463 | { 464 | device: 'iPad Pro (10.5in)', 465 | build: '19H370', 466 | viewport: '834x1112', 467 | deviceScaleFactor: 2, 468 | }, 469 | { 470 | device: 'iPad Pro (12.9in, 1st gen)', 471 | build: '19H370', 472 | viewport: '1024x1366', 473 | deviceScaleFactor: 2, 474 | }, 475 | ], 476 | }, 477 | }; 478 | 479 | devices.mobile.ios2 = (devices.mobile.ios1.map((d: Device) => { 480 | return { build: '20H115', name: d.name, viewport: d.viewport }; 481 | }) as any).concat([ 482 | { 483 | name: 'iPhone 14', 484 | build: '20H115', 485 | viewport: '390x844', 486 | deviceScaleFactor: 3, 487 | }, 488 | { 489 | name: 'iPhone 14 Pro', 490 | build: '20H115', 491 | viewport: '393x852', 492 | deviceScaleFactor: 3, 493 | }, 494 | { 495 | name: 'iPhone 14 Pro Max', 496 | build: '20H115', 497 | viewport: '430x932', 498 | deviceScaleFactor: 3, 499 | }, 500 | ]); 501 | 502 | devices.mobile.ios3 = (devices.mobile.ios2.map((d: Device) => { 503 | return { build: '21B74', name: d.name, viewport: d.viewport }; 504 | }) as any).concat([ 505 | { 506 | name: 'iPhone 15', 507 | build: '21B80', 508 | viewport: '393x852', 509 | deviceScaleFactor: 3, 510 | }, 511 | { 512 | name: 'iPhone 15 Pro', 513 | build: '21B80', 514 | viewport: '393x852', 515 | deviceScaleFactor: 3, 516 | }, 517 | { 518 | name: 'iPhone 15 Pro Max', 519 | build: '21B74', 520 | viewport: '430x932', 521 | deviceScaleFactor: 3, 522 | }, 523 | ]); 524 | 525 | devices.tablet.ios2 = (devices.tablet.ios1.map((d: Device) => { 526 | return { build: '20H115', name: d.name, viewport: d.viewport, deviceScaleFactor: d.deviceScaleFactor }; 527 | }) as any).concat([ 528 | { 529 | name: 'iPad Air (2019)', 530 | build: '20H115', 531 | viewport: '834x1112', 532 | deviceScaleFactor: 2, 533 | }, 534 | { 535 | name: 'iPad Mini (5th gen)', 536 | build: '20H115', 537 | viewport: '768x1024', 538 | deviceScaleFactor: 2, 539 | }, 540 | { 541 | name: 'iPad Pro (12.9-inch 2nd gen)', 542 | build: '20H115', 543 | viewport: '1024x1366', 544 | deviceScaleFactor: 2, 545 | }, 546 | { 547 | name: 'iPad Pro (12.9-inch 3rd gen)', 548 | build: '20H115', 549 | viewport: '1024x1366', 550 | deviceScaleFactor: 2, 551 | }, 552 | ]); 553 | 554 | devices.tablet.ios3 = devices.tablet.ios2.map((d: Device) => { 555 | return { build: '21B74', name: d.name, viewport: d.viewport, deviceScaleFactor: d.deviceScaleFactor }; 556 | }) as any; 557 | 558 | let getDevice = (hardware: string, id: string): Device => { 559 | return devices[hardware][id][Math.floor(Math.random() * devices[hardware][id].length)]; 560 | }; 561 | 562 | export { getDevice }; 563 | -------------------------------------------------------------------------------- /src/lib/intercept.ts: -------------------------------------------------------------------------------- 1 | // intercepts requests 2 | import * as prof from './profiles'; 3 | import * as lang from './language'; 4 | import util from './util'; 5 | import whitelisted from './whitelisted'; 6 | 7 | enum RefererXOriginOption { 8 | AlwaysSend = 0, 9 | MatchBaseDomain = 1, 10 | MatchHost = 2, 11 | } 12 | 13 | enum RefererTrimOption { 14 | SendFullURI = 0, 15 | SchemeWithPath = 1, 16 | SchemeNoPath = 2, 17 | } 18 | 19 | interface WhitelistOptions { 20 | name: boolean; 21 | ref: boolean; 22 | tz: boolean; 23 | ws: boolean; 24 | } 25 | 26 | interface WhitelistResult { 27 | active: boolean; 28 | opt?: WhitelistOptions; 29 | lang?: string; 30 | pattern?: object; 31 | profile?: string; 32 | spoofIP?: string; 33 | } 34 | 35 | class Interceptor { 36 | private LINK: any; 37 | private profiles: prof.Generator; 38 | private profileCache: any; 39 | private settings: any; 40 | private tempStore: any; 41 | private regex: any; 42 | private olderThanNinety: boolean; 43 | 44 | constructor(settings: any, tempStore: any, profileCache: any, olderThanNinety: boolean) { 45 | this.regex = { 46 | CLOUDFLARE: RegExp(/chk_jschl/), 47 | HTTPS: RegExp(/^https:\/\//), 48 | }; 49 | 50 | this.LINK = document.createElement('a'); 51 | this.profiles = new prof.Generator(); 52 | this.settings = settings; 53 | this.tempStore = tempStore; 54 | this.profileCache = profileCache; 55 | this.olderThanNinety = olderThanNinety; 56 | } 57 | 58 | blockWebsocket(details: any): any { 59 | if (!this.settings.config.enabled) return; 60 | 61 | let wl = this.checkWhitelist(details); 62 | 63 | if (!wl.active && this.settings.options.webSockets === 'allow_all') { 64 | return; 65 | } 66 | 67 | let isWebSocketRequest = false; 68 | if (details.requestHeaders) { 69 | for (let h of details.requestHeaders) { 70 | if (h.name.toLowerCase() == 'x-websocket-extensions') { 71 | isWebSocketRequest = true; 72 | } 73 | } 74 | } 75 | 76 | if (details.type === 'websocket' || details.url.includes('transport=polling') || isWebSocketRequest) { 77 | if (wl.active) { 78 | return { cancel: wl.opt.ws }; 79 | } 80 | 81 | if (this.settings.options.webSockets === 'block_all') { 82 | return { cancel: true }; 83 | } else if (this.settings.options.webSockets === 'block_3rd_party') { 84 | let frame = util.parseURL(details.documentUrl || details.originUrl); 85 | let ws = util.parseURL(details.url); 86 | 87 | if (!frame.error && !ws.error) { 88 | if (frame.domain != ws.domain) { 89 | return { cancel: true }; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | checkWhitelist(request: any): WhitelistResult { 97 | let url: string; 98 | 99 | /* Get document url of request */ 100 | if (request.type === 'main_frame') { 101 | url = request.url; 102 | } else if (request.parentFrameId == -1) { 103 | url = request.documentUrl; 104 | } else { 105 | let root = request.frameAncestors ? request.frameAncestors.find(f => f.frameId === 0) : ''; 106 | if (root) { 107 | url = root.url; 108 | } else { 109 | url = request.documentUrl; 110 | } 111 | } 112 | 113 | if (url) { 114 | this.LINK.href = url; 115 | let rule = util.findWhitelistRule(this.settings.whitelist.rules, this.LINK.host, url); 116 | 117 | if (rule) { 118 | return { 119 | active: true, 120 | lang: rule.lang, 121 | opt: rule.options, 122 | pattern: rule.pattern, 123 | profile: rule.profile, 124 | spoofIP: rule.spoofIP, 125 | }; 126 | } 127 | } 128 | 129 | return { active: false }; 130 | } 131 | 132 | modifyRequest(details: any): any { 133 | if (!this.settings.config.enabled) return; 134 | 135 | // don't modify request for sites below 136 | for (let i = 0; i < whitelisted.length; i++) { 137 | if ( 138 | (details.originUrl && details.originUrl.startsWith(whitelisted[i])) || 139 | (details.documentUrl && details.documentUrl.startsWith(whitelisted[i])) || 140 | (details.url && details.url.startsWith(whitelisted[i])) 141 | ) { 142 | return; 143 | } 144 | } 145 | 146 | this.LINK.href = details.documentUrl || details.url; 147 | if (util.isInternalIP(this.LINK.hostname)) return; 148 | 149 | let wl: WhitelistResult = this.checkWhitelist(details); 150 | 151 | // used to send different accept headers for https requests 152 | let isSecure: boolean = this.regex.HTTPS.test(details.url); 153 | let dntIndex: number = -1; 154 | 155 | let profile: prof.BrowserProfile; 156 | if (wl.active) { 157 | if (wl.profile === 'default') { 158 | if (this.settings.whitelist.defaultProfile != 'none') { 159 | profile = this.profileCache[this.settings.whitelist.defaultProfile]; 160 | } 161 | } else if (wl.profile != 'none') { 162 | profile = this.profileCache[wl.profile]; 163 | } 164 | } else { 165 | if (this.settings.profile.selected != 'none' && !this.settings.excluded.includes(this.settings.profile.selected) && this.tempStore.profile != 'none') { 166 | let profileUsed: string = this.settings.profile.selected.includes('-') ? this.settings.profile.selected : this.tempStore.profile; 167 | profile = this.profileCache[profileUsed]; 168 | } 169 | } 170 | 171 | let isChromeBased: boolean = profile ? profile.navigator.userAgent.includes('Chrome') : false; 172 | 173 | for (let i = 0; i < details.requestHeaders.length; i++) { 174 | let header: string = details.requestHeaders[i].name.toLowerCase(); 175 | 176 | if (header === 'referer') { 177 | if (!wl.active) { 178 | if (this.settings.headers.referer.disabled) { 179 | details.requestHeaders[i].value = ''; 180 | } else { 181 | // check referer policies, preserve referer for cloudflare challenges 182 | if (!this.regex.CLOUDFLARE.test(details.url)) { 183 | if (this.settings.headers.referer.xorigin != RefererXOriginOption.AlwaysSend) { 184 | let dest = util.parseURL(details.url); 185 | let ref = util.parseURL(details.requestHeaders[i].value); 186 | 187 | if (this.settings.headers.referer.xorigin === RefererXOriginOption.MatchBaseDomain) { 188 | if (dest.domain != ref.domain) { 189 | details.requestHeaders[i].value = ''; 190 | } 191 | } else { 192 | if (dest.origin != ref.origin) { 193 | details.requestHeaders[i].value = ''; 194 | } 195 | } 196 | } 197 | 198 | if (this.settings.headers.referer.trimming != RefererTrimOption.SendFullURI) { 199 | if (details.requestHeaders[i].value != '') { 200 | let ref = util.parseURL(details.requestHeaders[i].value); 201 | details.requestHeaders[i].value = this.settings.headers.referer.trimming === RefererTrimOption.SchemeWithPath ? ref.origin + ref.pathname : ref.origin; 202 | } 203 | } 204 | } 205 | } 206 | } else if (wl.opt.ref) { 207 | details.requestHeaders[i].value = ''; 208 | } 209 | } else if (header === 'user-agent') { 210 | if (profile) { 211 | details.requestHeaders[i].value = profile.navigator.userAgent; 212 | } 213 | } else if (header === 'accept') { 214 | if (details.type === 'main_frame' || details.type === 'sub_frame') { 215 | if (profile) { 216 | details.requestHeaders[i].value = profile.accept.header; 217 | } 218 | } 219 | } else if (header === 'accept-encoding') { 220 | if (details.type === 'main_frame' || details.type === 'sub_frame') { 221 | if (profile) { 222 | details.requestHeaders[i].value = isSecure ? profile.accept.encodingHTTPS : profile.accept.encodingHTTP; 223 | } 224 | } 225 | } else if (header === 'accept-language') { 226 | if (wl.active && wl.lang != '') { 227 | details.requestHeaders[i].value = wl.lang; 228 | } else { 229 | if (this.settings.headers.spoofAcceptLang.enabled) { 230 | if (this.settings.headers.spoofAcceptLang.value === 'ip') { 231 | if (this.tempStore.ipInfo.lang) { 232 | details.requestHeaders[i].value = lang.getLanguage(this.tempStore.ipInfo.lang).value; 233 | } 234 | } else if (this.settings.headers.spoofAcceptLang.value !== 'default') { 235 | details.requestHeaders[i].value = lang.getLanguage(this.settings.headers.spoofAcceptLang.value).value; 236 | } 237 | } 238 | } 239 | } else if (header === 'dnt') { 240 | dntIndex = i; 241 | } 242 | } 243 | 244 | if (this.settings.headers.enableDNT) { 245 | if (dntIndex === -1) { 246 | details.requestHeaders.push({ name: 'DNT', value: '1' }); 247 | } 248 | } else { 249 | if (dntIndex > -1) { 250 | details.requestHeaders.splice(dntIndex, 1); 251 | } 252 | } 253 | 254 | if (wl.active) { 255 | if (wl.spoofIP) { 256 | if ( 257 | // don't spoof header IP for cloudflare pages 258 | !details.url.includes('cdn-cgi/challenge-platform/generate/') && 259 | !details.url.includes('__cf_chl_jschl_tk__=') && 260 | !details.url.includes('jschal/js/nocookie/transparent.gif') 261 | ) { 262 | details.requestHeaders.push({ 263 | name: 'Via', 264 | value: '1.1 ' + wl.spoofIP, 265 | }); 266 | details.requestHeaders.push({ 267 | name: 'X-Forwarded-For', 268 | value: wl.spoofIP, 269 | }); 270 | } 271 | } 272 | } else { 273 | if (this.settings.headers.spoofIP.enabled) { 274 | if ( 275 | // don't spoof header IP for cloudflare pages 276 | !details.url.includes('cdn-cgi/challenge-platform/generate/') && 277 | !details.url.includes('__cf_chl_jschl_tk__=') && 278 | !details.url.includes('jschal/js/nocookie/transparent.gif') 279 | ) { 280 | details.requestHeaders.push({ 281 | name: 'Via', 282 | value: '1.1 ' + this.tempStore.spoofIP, 283 | }); 284 | details.requestHeaders.push({ 285 | name: 'X-Forwarded-For', 286 | value: this.tempStore.spoofIP, 287 | }); 288 | } 289 | } 290 | } 291 | 292 | if (isSecure && isChromeBased && this.olderThanNinety) { 293 | // https://www.w3.org/TR/fetch-metadata/#sec-fetch-dest-header 294 | // implementation below is missing some destinations (mostly worker related) 295 | let secDest = 'empty'; 296 | 297 | if (details.type == 'main_frame') { 298 | secDest = 'document'; 299 | } else if (details.type == 'sub_frame') { 300 | secDest = 'iframe'; 301 | } else if (details.type == 'font') { 302 | secDest = 'font'; 303 | } else if (details.type == 'imageset' || details.type == 'image') { 304 | secDest = 'image'; 305 | } else if (details.type == 'media') { 306 | let h = details.requestHeaders.find(r => r.name.toLowerCase() == 'accept'); 307 | if (h.value.charAt(0) == 'a') { 308 | secDest = 'audio'; 309 | } else if (h.value.charAt(0) == 'v') { 310 | secDest = 'video'; 311 | } else if (details.url.includes('.vtt')) { 312 | secDest = 'track'; 313 | } 314 | } else if (details.type == 'xslt') { 315 | secDest = 'xslt'; 316 | } else if (details.type == 'web_manifest') { 317 | secDest = 'manifest'; 318 | } else if (details.type == 'csp_report') { 319 | secDest = 'report'; 320 | } else if (details.type == 'object') { 321 | secDest = 'object'; // object is used for both and 322 | } else if (details.type == 'stylesheet') { 323 | secDest = 'style'; 324 | } else if (details.type == 'script') { 325 | secDest = 'script'; 326 | } 327 | 328 | details.requestHeaders.push({ 329 | name: 'sec-fetch-dest', 330 | value: secDest, 331 | }); 332 | 333 | // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header 334 | // not quite accurate when determining whether a request from a user action 335 | let secSite = util.determineRequestType(details.type == 'main_frame' ? details.documentUrl : details.originUrl, details.url); 336 | details.requestHeaders.push({ 337 | name: 'sec-fetch-site', 338 | value: secSite, 339 | }); 340 | 341 | // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header 342 | // naive implementation 343 | let secMode = 'no-cors'; 344 | let hasOriginHeader = details.requestHeaders.findIndex(r => r.name.toLowerCase() == 'origin') > -1; 345 | if (details.type == 'websocket') { 346 | secMode = 'websocket'; 347 | } else if (details.type == 'main_frame' || details.type == 'sub_frame') { 348 | secMode = 'navigate'; 349 | } else if (hasOriginHeader) { 350 | secMode = 'cors'; 351 | } 352 | 353 | details.requestHeaders.push({ 354 | name: 'sec-fetch-mode', 355 | value: secMode, 356 | }); 357 | 358 | // this is a guesstimate 359 | // can't determine if this is a user request from this method 360 | // iframe navigation won't be included with this request 361 | if (details.type === 'main_frame') { 362 | details.requestHeaders.push({ 363 | name: 'sec-fetch-user', 364 | value: '?1', 365 | }); 366 | } 367 | } 368 | 369 | return { requestHeaders: details.requestHeaders }; 370 | } 371 | 372 | modifyResponse(details: any): any { 373 | if (!this.settings.config.enabled) return; 374 | 375 | let wl = this.checkWhitelist(details); 376 | 377 | if (!wl.active && this.settings.headers.blockEtag) { 378 | for (let i = 0; i < details.responseHeaders.length; i++) { 379 | if (details.responseHeaders[i].name.toLowerCase() === 'etag') { 380 | details.responseHeaders[i].value = ''; 381 | } 382 | } 383 | } 384 | 385 | return { responseHeaders: details.responseHeaders }; 386 | } 387 | } 388 | 389 | export { Interceptor }; 390 | -------------------------------------------------------------------------------- /src/lib/language.ts: -------------------------------------------------------------------------------- 1 | export interface Language { 2 | name: string; 3 | value: string; 4 | code: string; 5 | nav: string[]; 6 | } 7 | 8 | const languages: Language[] = [ 9 | { name: 'Acholi', value: 'ach,en-GB;q=0.8,en-US;q=0.5,en;q=0.3', code: 'ach', nav: ['ach', 'en-GB', 'en-US', 'en'] }, 10 | { name: 'Afrikaans', value: 'af,en-ZA;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2', code: 'af', nav: ['af', 'en-ZA', 'en-GB', 'en'] }, 11 | { name: 'Albanian', value: 'sq,sq-AL;q=0.8,en-US;q=0.5,en;q=0.3', code: 'sq', nav: ['sq', 'sq-AL', 'en-US', 'en'] }, 12 | { name: 'Arabic', value: 'ar,en-US;q=0.7,en;q=0.3', code: 'ar', nav: ['ar', 'en-US', 'en'] }, 13 | { name: 'Aragonese', value: 'an,es-ES;q=0.8,es;q=0.7,ca;q=0.5,en-US;q=0.3,en;q=0.2', code: 'an', nav: ['an', 'es-ES', 'es', 'ca', 'en-US', 'en'] }, 14 | { name: 'Armenian', value: 'hy-AM,hy;q=0.8,en-US;q=0.5,en;q=0.3', code: 'hy-AM', nav: ['hy-AM', 'hy', 'en-US', 'en'] }, 15 | { name: 'Assamese', value: 'as,en-US;q=0.7,en;q=0.3', code: 'as', nav: ['as', 'en-US', 'en'] }, 16 | { name: 'Asturian', value: 'ast,es-ES;q=0.8,es;q=0.6,en-US;q=0.4,en;q=0.2', code: 'ast', nav: ['ast', 'es-ES', 'es', 'en-US', 'en'] }, 17 | { name: 'Azerbaijani', value: 'az-AZ,az;q=0.8,en-US;q=0.5,en;q=0.3', code: 'az-AZ', nav: ['az-AZ', 'az', 'en-US', 'en'] }, 18 | { name: 'Basque', value: 'eu,en-US;q=0.7,en;q=0.3', code: 'eu', nav: ['eu', 'en-US', 'en'] }, 19 | { name: 'Belarusian', value: 'be,en-US;q=0.7,en;q=0.3', code: 'be', nav: ['be', 'en-US', 'en'] }, 20 | { name: 'Bengali (Bangladesh)', value: 'bn-BD,bn;q=0.8,en-US;q=0.5,en;q=0.3', code: 'bn-BD', nav: ['bn-BD', 'bn', 'en-US', 'en'] }, 21 | { name: 'Bengali (India)', value: 'bn-IN,bn;q=0.8,en-US;q=0.5,en;q=0.3', code: 'bn-IN', nav: ['bn-IN', 'bn', 'en-US', 'en'] }, 22 | { name: 'Bosnian', value: 'bs-BA,bs;q=0.8,en-US;q=0.5,en;q=0.3', code: 'bs-BA', nav: ['bs-BA', 'bs', 'en-US', 'en'] }, 23 | { name: 'Breton', value: 'br,fr-FR;q=0.8,fr;q=0.6,en-US;q=0.4,en;q=0.2', code: 'br', nav: ['br', 'fr-FR', 'fr', 'en-US', 'en'] }, 24 | { name: 'Bulgarian', value: 'bg,en-US;q=0.7,en;q=0.3', code: 'bg', nav: ['bg', 'en-US', 'en'] }, 25 | { name: 'Burmese', value: 'my,en-GB;q=0.7,en;q=0.3', code: 'my', nav: ['my', 'en-GB', 'en'] }, 26 | { name: 'Catalan', value: 'ca,en-US;q=0.7,en;q=0.3', code: 'ca', nav: ['ca', 'en-US', 'en'] }, 27 | { name: 'Chinese (Hong Kong)', value: 'zh-HK,zh;q=0.8,en-US;q=0.5,en;q=0.3', code: 'zh-HK', nav: ['zh-HK', 'zh', 'en-US', 'en'] }, 28 | { name: 'Chinese (Simplified)', value: 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', code: 'zh-CN', nav: ['zh-CN', 'zh', 'zh-TW', 'zh-HK', 'en-US', 'en'] }, 29 | { name: 'Chinese (Traditional)', value: 'zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3', code: 'zh-TW', nav: ['zh-TW', 'zh', 'en-US', 'en'] }, 30 | { name: 'Croatian', value: 'hr-HR,hr;q=0.8,en-US;q=0.5,en;q=0.3', code: 'hr-HR', nav: ['hr-HR', 'hr', 'en-US', 'en'] }, 31 | { name: 'Czech', value: 'cs,sk;q=0.8,en-US;q=0.5,en;q=0.3', code: 'cs', nav: ['cs', 'sk', 'en-US', 'en'] }, 32 | { name: 'Danish', value: 'da,en-US;q=0.7,en;q=0.3', code: 'da', nav: ['da', 'en-US', 'en'] }, 33 | { name: 'Dutch', value: 'nl,en-US;q=0.7,en;q=0.3', code: 'nl', nav: ['nl', 'en-US', 'en'] }, 34 | { name: 'English (Australian)', value: 'en-AU,en;q=0.5', code: 'en-AU', nav: ['en-AU', 'en'] }, 35 | { name: 'English (British)', value: 'en-GB,en;q=0.5', code: 'en-GB', nav: ['en-GB', 'en'] }, 36 | { name: 'English (Canadian)', value: 'en-CA,en-US;q=0.7,en;q=0.3', code: 'en-CA', nav: ['en-CA', 'en-US', 'en'] }, 37 | { name: 'English (South African)', value: 'en-ZA,en-GB;q=0.8,en-US;q=0.5,en;q=0.3', code: 'en-ZA', nav: ['en-ZA', 'en-GB', 'en-US', 'en'] }, 38 | { name: 'English (US)', value: 'en-US,en;q=0.5', code: 'en-US', nav: ['en-US', 'en'] }, 39 | { name: 'Esperanto', value: 'eo,en-US;q=0.7,en;q=0.3', code: 'eo', nav: ['eo', 'en-US', 'en'] }, 40 | { name: 'Estonian', value: 'et,et-EE;q=0.8,en-US;q=0.5,en;q=0.3', code: 'et', nav: ['et', 'et-EE', 'en-US', 'en'] }, 41 | { name: 'Finnish', value: 'fi-FI,fi;q=0.8,en-US;q=0.5,en;q=0.3', code: 'fi-FI', nav: ['fi-FI', 'fi', 'en-US', 'en'] }, 42 | { name: 'French', value: 'fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3', code: 'fr', nav: ['fr', 'fr-FR', 'en-US', 'en'] }, 43 | { name: 'Frisian', value: 'fy-NL,fy;q=0.8,nl;q=0.6,en-US;q=0.4,en;q=0.2', code: 'fy-NL', nav: ['fy-NL', 'fy', 'nl', 'en-US', 'en'] }, 44 | { name: 'Fulah', value: 'ff,fr-FR;q=0.8,fr;q=0.7,en-GB;q=0.5,en-US;q=0.3,en;q=0.2', code: 'ff', nav: ['ff', 'fr-FR', 'fr', 'en-GB', 'en-US', 'en'] }, 45 | { name: 'Gaelic (Scotland)', value: 'gd-GB,gd;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2', code: 'gd-GB', nav: ['gd-GB', 'gd', 'en-GB', 'en-US', 'en'] }, 46 | { name: 'Galician', value: 'gl-GL,gl;q=0.8,en-US;q=0.5,en;q=0.3', code: 'gl-GL', nav: ['gl-GL', 'gl', 'en-US', 'en'] }, 47 | { name: 'Georgian', value: 'ka,ka-GE;q=0.7,en;q=0.3', code: 'ka', nav: ['ka', 'ka-GE', 'en'] }, 48 | { name: 'German', value: 'de,en-US;q=0.7,en;q=0.3', code: 'de', nav: ['de', 'en-US', 'en'] }, 49 | { name: 'German (Switzerland)', value: 'de-CH,de;q=0.8,en-US;q=0.5,en;q=0.3', code: 'de-CH', nav: ['de-CH', 'de', 'en-US', 'en'] }, 50 | { name: 'Greek', value: 'el-GR,el;q=0.8,en-US;q=0.5,en;q=0.3', code: 'el-GR', nav: ['el-GR', 'el', 'en-US', 'en'] }, 51 | { name: 'Guarani', value: 'gn,es;q=0.8,en;q=0.5,en-US;q=0.3', code: 'gn', nav: ['gn', 'es', 'en', 'en-US'] }, 52 | { name: 'Gujarati (India)', value: 'gu-IN,gu;q=0.8,en-US;q=0.5,en;q=0.3', code: 'gu-IN', nav: ['gu-IN', 'gu', 'en-US', 'en'] }, 53 | { name: 'Hebrew', value: 'he,he-IL;q=0.8,en-US;q=0.5,en;q=0.3', code: 'he', nav: ['he', 'he-IL', 'en-US', 'en'] }, 54 | { name: 'Hindi (India)', value: 'hi-IN,hi;q=0.8,en-US;q=0.5,en;q=0.3', code: 'hi-IN', nav: ['hi-IN', 'hi', 'en-US', 'en'] }, 55 | { name: 'Hungarian', value: 'hu-HU,hu;q=0.8,en-US;q=0.5,en;q=0.3', code: 'hu-HU', nav: ['hu-HU', 'hu', 'en-US', 'en'] }, 56 | { name: 'Icelandic', value: 'is,en-US;q=0.7,en;q=0.3', code: 'is', nav: ['is', 'en-US', 'en'] }, 57 | { name: 'Indonesian', value: 'id,en-US;q=0.7,en;q=0.3', code: 'id', nav: ['id', 'en-US', 'en'] }, 58 | { name: 'Interlingua', value: 'ia,en-US;q=0.7,en;q=0.3', code: 'ia', nav: ['ia', 'en-US', 'en'] }, 59 | { name: 'Irish', value: 'ga-IE,ga;q=0.8,en-IE;q=0.7,en-GB;q=0.5,en-US;q=0.3,en;q=0.2', code: 'ga-IE', nav: ['ga-IE', 'ga', 'en-IE', 'en-GB', 'en-US', 'en'] }, 60 | { name: 'Italian', value: 'it-IT,it;q=0.8,en-US;q=0.5,en;q=0.3', code: 'it-IT', nav: ['it-IT', 'it', 'en-US', 'en'] }, 61 | { name: 'Japanese', value: 'ja,en-US;q=0.7,en;q=0.3', code: 'ja', nav: ['ja', 'en-US', 'en'] }, 62 | { name: 'Kabyle', value: 'kab-DZ,kab;q=0.8,fr-FR;q=0.7,fr;q=0.5,en-US;q=0.3,en;q=0.2', code: 'kab-DZ', nav: ['kab-DZ', 'kab', 'fr-FR', 'fr', 'en-US', 'en'] }, 63 | { name: 'Kannada', value: 'kn-IN,kn;q=0.8,en-US;q=0.5,en;q=0.3', code: 'kn-IN', nav: ['kn-IN', 'kn', 'en-US', 'en'] }, 64 | { name: 'Kaqchikel', value: 'cak,kaq;q=0.8,es;q=0.6,en-US;q=0.4,en;q=0.2', code: 'cak', nav: ['cak', 'kaq', 'es', 'en-US', 'en'] }, 65 | { name: 'Kazakh', value: 'kk,ru;q=0.8,ru-RU;q=0.6,en-US;q=0.4,en;q=0.2', code: 'kk', nav: ['kk', 'ru', 'ru-RU', 'en-US', 'en'] }, 66 | { name: 'Khmer', value: 'km,en-US;q=0.7,en;q=0.3', code: 'km', nav: ['km', 'en-US', 'en'] }, 67 | { name: 'Korean', value: 'ko-KR,ko;q=0.8,en-US;q=0.5,en;q=0.3', code: 'ko-KR', nav: ['ko-KR', 'ko', 'en-US', 'en'] }, 68 | { name: 'Latvian', value: 'lv,en-US;q=0.7,en;q=0.3', code: 'lv', nav: ['lv', 'en-US', 'en'] }, 69 | { name: 'Ligurian', value: 'lij,it;q=0.8,en-US;q=0.5,en;q=0.3', code: 'lij', nav: ['lij', 'it', 'en-US', 'en'] }, 70 | { name: 'Lithuanian', value: 'lt,en-US;q=0.8,en;q=0.6,ru;q=0.4,pl;q=0.2', code: 'lt', nav: ['lt', 'en-US', 'en', 'ru', 'pl'] }, 71 | { name: 'Lower Sorbian', value: 'dsb,hsb;q=0.8,de;q=0.6,en-US;q=0.4,en;q=0.2', code: 'dsb', nav: ['dsb', 'hsb', 'de', 'en-US', 'en'] }, 72 | { name: 'Macedonian', value: 'mk-MK,mk;q=0.8,en-US;q=0.5,en;q=0.3', code: 'mk-MK', nav: ['mk-MK', 'mk', 'en-US', 'en'] }, 73 | { name: 'Maithili', value: 'mai,hi-IN;q=0.7,en;q=0.3', code: 'mai', nav: ['mai', 'hi-IN', 'en'] }, 74 | { name: 'Malay', value: 'ms,en-US;q=0.7,en;q=0.3', code: 'ms', nav: ['ms', 'en-US', 'en'] }, 75 | { name: 'Malayalam', value: 'ml-IN,ml;q=0.8,en-US;q=0.5,en;q=0.3', code: 'ml-IN', nav: ['ml-IN', 'ml', 'en-US', 'en'] }, 76 | { name: 'Marathi', value: 'mr-IN,mr;q=0.8,en-US;q=0.5,en;q=0.3', code: 'mr-IN', nav: ['mr-IN', 'mr', 'en-US', 'en'] }, 77 | { name: 'Nepali', value: 'ne-NP,ne;q=0.8,en-US;q=0.5,en;q=0.3', code: 'ne-NP', nav: ['ne-NP', 'ne', 'en-US', 'en'] }, 78 | { 79 | name: 'Norwegian (Bokmål)', 80 | value: 'nb-NO,nb;q=0.9,no-NO;q=0.8,no;q=0.6,nn-NO;q=0.5,nn;q=0.4,en-US;q=0.3,en;q=0.1', 81 | code: 'nb-NO', 82 | nav: ['nb-NO', 'nb', 'no-NO', 'no', 'nn-NO', 'nn', 'en-US', 'en'], 83 | }, 84 | { 85 | name: 'Norwegian (Nynorsk)', 86 | value: 'nn-NO,nn;q=0.9,no-NO;q=0.8,no;q=0.6,nb-NO;q=0.5,nb;q=0.4,en-US;q=0.3,en;q=0.1', 87 | code: 'nn-NO', 88 | nav: ['nn-NO', 'nn', 'no-NO', 'no', 'nb-NO', 'nb', 'en-US', 'en'], 89 | }, 90 | { 91 | name: 'Occitan (Lengadocian)', 92 | value: 'oc-OC,oc;q=0.9,ca;q=0.8,fr;q=0.6,es;q=0.5,it;q=0.4,en-US;q=0.3,en;q=0.1', 93 | code: 'oc-OC', 94 | nav: ['oc-OC', 'oc', 'ca', 'fr', 'es', 'it', 'en-US', 'en'], 95 | }, 96 | { name: 'Odia', value: 'or,en-US;q=0.7,en;q=0.3', code: 'or', nav: ['or', 'en-US', 'en'] }, 97 | { name: 'Persian', value: 'fa-IR,fa;q=0.8,en-US;q=0.5,en;q=0.3', code: 'fa-IR', nav: ['fa-IR', 'fa', 'en-US', 'en'] }, 98 | { name: 'Polish', value: 'pl,en-US;q=0.7,en;q=0.3', code: 'pl', nav: ['pl', 'en-US', 'en'] }, 99 | { name: 'Portuguese (Brazilian)', value: 'pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3', code: 'pt-BR', nav: ['pt-BR', 'pt', 'en-US', 'en'] }, 100 | { name: 'Portuguese (Portugal)', value: 'pt-PT,pt;q=0.8,en;q=0.5,en-US;q=0.3', code: 'pt-PT', nav: ['pt-PT', 'pt', 'en-US', 'en'] }, 101 | { name: 'Punjabi (India)', value: 'pa,pa-IN;q=0.8,en-US;q=0.5,en;q=0.3', code: 'pa', nav: ['pa', 'pa-IN', 'en-US', 'en'] }, 102 | { name: 'Romanian', value: 'ro-RO,ro;q=0.8,en-US;q=0.6,en-GB;q=0.4,en;q=0.2', code: 'ro-RO', nav: ['ro-RO', 'ro', 'en-US', 'en-GB', 'en'] }, 103 | { name: 'Romansh', value: 'rm,rm-CH;q=0.8,de-CH;q=0.7,de;q=0.5,en-US;q=0.3,en;q=0.2', code: 'rm', nav: ['rm', 'rm-CH', 'de-CH', 'de', 'en-US', 'en'] }, 104 | { name: 'Russian', value: 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', code: 'ru-RU', nav: ['ru-RU', 'ru', 'en-US', 'en'] }, 105 | { name: 'Serbian', value: 'sr,sr-RS;q=0.8,sr-CS;q=0.6,en-US;q=0.4,en;q=0.2', code: 'sr', nav: ['sr', 'sr-RS', 'sr-CS', 'en-US', 'en'] }, 106 | { name: 'Sinhala', value: 'si,si-LK;q=0.8,en-GB;q=0.5,en;q=0.3', code: 'si', nav: ['si', 'si-LK', 'en-GB', 'en'] }, 107 | { name: 'Slovak', value: 'sk,cs;q=0.8,en-US;q=0.5,en;q=0.3', code: 'sk', nav: ['sk', 'cs', 'en-US', 'en'] }, 108 | { name: 'Slovenian', value: 'sl,en-GB;q=0.7,en;q=0.3', code: 'sl', nav: ['sl', 'en-GB', 'en'] }, 109 | { name: 'Songhai', value: 'son,son-ML;q=0.8,fr;q=0.6,en-US;q=0.4,en;q=0.2', code: 'son', nav: ['son', 'son-ML', 'fr', 'en-US', 'en'] }, 110 | { name: 'Spanish (Argentina)', value: 'es-AR,es;q=0.8,en-US;q=0.5,en;q=0.3', code: 'es-AR', nav: ['es-AR', 'es', 'en-US', 'en'] }, 111 | { name: 'Spanish (Chile)', value: 'es-CL,es;q=0.8,en-US;q=0.5,en;q=0.3', code: 'es-CL', nav: ['es-CL', 'es', 'en-US', 'en'] }, 112 | { name: 'Spanish (Mexico)', value: 'es-MX,es;q=0.8,en-US;q=0.5,en;q=0.3', code: 'es-MX', nav: ['es-MX', 'es', 'en-US', 'en'] }, 113 | { name: 'Spanish (Spain)', value: 'es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3', code: 'es-ES', nav: ['es-ES', 'es', 'en-US', 'en'] }, 114 | { name: 'Swedish', value: 'sv-SE,sv;q=0.8,en-US;q=0.5,en;q=0.3', code: 'sv-SE', nav: ['sv-SE', 'sv', 'en-US', 'en'] }, 115 | { name: 'Tamil', value: 'ta-IN,ta;q=0.8,en-US;q=0.5,en;q=0.3', code: 'ta-IN', nav: ['ta-IN', 'ta', 'en-US', 'en'] }, 116 | { name: 'Telugu', value: 'te-IN,te;q=0.8,en-US;q=0.5,en;q=0.3', code: 'te-IN', nav: ['te-IN', 'te', 'en-US', 'en'] }, 117 | { name: 'Thai', value: 'th,en-US;q=0.7,en;q=0.3', code: 'th', nav: ['th', 'en-US', 'en'] }, 118 | { name: 'Turkish', value: 'tr-TR,tr;q=0.8,en-US;q=0.5,en;q=0.3', code: 'tr-TR', nav: ['tr-TR', 'tr', 'en-US', 'en'] }, 119 | { name: 'Ukranian', value: 'uk,ru;q=0.8,en-US;q=0.5,en;q=0.3', code: 'uk', nav: ['uk', 'ru', 'en-US', 'en'] }, 120 | { name: 'Upper Sorbian', value: 'hsb,dsb;q=0.8,de;q=0.6,en-US;q=0.4,en;q=0.2', code: 'hsb', nav: ['hsb', 'dsb', 'de', 'en-US', 'en'] }, 121 | { name: 'Urdu', value: 'ur-PK,ur;q=0.8,en-US;q=0.5,en;q=0.3', code: 'ur-PK', nav: ['ur-PK', 'ur', 'en-US', 'en'] }, 122 | { name: 'Uzbek', value: 'uz,ru;q=0.8,en;q=0.5,en-US;q=0.3', code: 'uz', nav: ['uz', 'ru', 'en', 'en-US'] }, 123 | { name: 'Vietnamese', value: 'vi-VN,vi;q=0.8,en-US;q=0.5,en;q=0.3', code: 'vi-VN', nav: ['vi-VN', 'vi', 'en-US', 'en'] }, 124 | { name: 'Welsh', value: 'cy-GB,cy;q=0.8,en-US;q=0.5,en;q=0.3', code: 'cy-GB', nav: ['cy-GB', 'cy', 'en-US', 'en'] }, 125 | { name: 'Xhosa', value: 'xh-ZA,xh;q=0.8,en-US;q=0.5,en;q=0.3', code: 'xh-ZA', nav: ['xh-ZA', 'xh', 'en-US', 'en'] }, 126 | ]; 127 | 128 | const languageMap = languages.reduce((m, l) => ((m[l.code] = { name: l.name, nav: l.nav, value: l.value }), m), {}); 129 | 130 | let getAllLanguages = (): Language[] => { 131 | return languages; 132 | }; 133 | 134 | let getLanguage = (langCode: string): Language => { 135 | return languageMap[langCode]; 136 | }; 137 | 138 | export { getAllLanguages, getLanguage }; 139 | -------------------------------------------------------------------------------- /src/lib/spoof/audioContext.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'custom', 3 | data: ` 4 | if ( 5 | spoofContext.AudioContext && 6 | spoofContext.OfflineAudioContext 7 | ) { 8 | let _copyFromChannel = spoofContext.AudioBuffer.prototype.copyFromChannel; 9 | let _getChannelData = spoofContext.AudioBuffer.prototype.getChannelData; 10 | let _getFloatFrequencyData = spoofContext.AnalyserNode.prototype.getFloatFrequencyData; 11 | 12 | let audioContextCache = new WeakMap(); 13 | 14 | spoofContext.AudioBuffer.prototype.getChannelData = function(...args) { 15 | let isEmpty = true; 16 | let c = _getChannelData.apply(this, args); 17 | 18 | if (audioContextCache.has(c)) { 19 | return audioContextCache.get(c); 20 | } 21 | 22 | let tmp = new Float32Array(c); 23 | 24 | for (let i = 0; i < tmp.length; i++) { 25 | if (tmp[i] !== 0){ 26 | isEmpty = false; 27 | tmp[i] = tmp[i] + CHAMELEON_SPOOF.get(spoofContext).audioContextSeed; 28 | } 29 | } 30 | 31 | if (!isEmpty) { 32 | audioContextCache.set(c, tmp); 33 | } 34 | 35 | return tmp; 36 | } 37 | 38 | modifiedAPIs.push([ 39 | spoofContext.AudioBuffer.prototype.getChannelData, "getChannelData" 40 | ]); 41 | 42 | spoofContext.AudioBuffer.prototype.copyFromChannel = function(...args) { 43 | c = this.getChannelData(args[1]); 44 | 45 | if (arguments.length === 3) { 46 | this.copyToChannel(c, args[1], args[2]); 47 | } else { 48 | this.copyToChannel(c, args[1]); 49 | } 50 | 51 | _copyFromChannel.apply(this, args); 52 | } 53 | 54 | modifiedAPIs.push([ 55 | spoofContext.AudioBuffer.prototype.copyFromChannel, "copyFromChannel" 56 | ]); 57 | }`, 58 | }; 59 | -------------------------------------------------------------------------------- /src/lib/spoof/clientRects.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'custom', 3 | data: ` 4 | let _getBoundingClientRect = spoofContext.Element.prototype.getBoundingClientRect; 5 | let _getClientRects = spoofContext.Element.prototype.getClientRects; 6 | let _rgetBoundingClientRect = spoofContext.Range.prototype.getBoundingClientRect; 7 | let _rgetClientRects = spoofContext.Range.prototype.getClientRects; 8 | 9 | let _fuzzer = (val) => { 10 | return Number.isInteger(val) ? val : val + CHAMELEON_SPOOF.get(spoofContext).clientRectsSeed; 11 | }; 12 | 13 | spoofContext.Element.prototype.getBoundingClientRect = function() { 14 | let c = _getBoundingClientRect.apply(this); 15 | 16 | c.x = c.left = _fuzzer(c.x); 17 | c.y = c.top = _fuzzer(c.y); 18 | c.width = _fuzzer(c.width); 19 | c.height = _fuzzer(c.height); 20 | 21 | return c; 22 | } 23 | 24 | modifiedAPIs.push([ 25 | spoofContext.Element.prototype.getBoundingClientRect, "getBoundingClientRect" 26 | ]); 27 | 28 | spoofContext.Element.prototype.getClientRects = function() { 29 | let a = _getClientRects.apply(this); 30 | let b = this.getBoundingClientRect(); 31 | 32 | for (let p in a[0]) { 33 | a[0][p] = b[p]; 34 | } 35 | 36 | return a; 37 | } 38 | 39 | modifiedAPIs.push([ 40 | spoofContext.Element.prototype.getClientRects, "getClientRects" 41 | ]); 42 | 43 | spoofContext.Range.prototype.getBoundingClientRect = function() { 44 | let r = _rgetBoundingClientRect.apply(this); 45 | 46 | r.x = r.left = _fuzzer(r.x); 47 | r.y = r.top = _fuzzer(r.y); 48 | r.width = _fuzzer(r.width); 49 | r.height = _fuzzer(r.height); 50 | 51 | return r; 52 | } 53 | 54 | modifiedAPIs.push([ 55 | spoofContext.Range.prototype.getBoundingClientRect, "getBoundingClientRect" 56 | ]); 57 | 58 | spoofContext.Range.prototype.getClientRects = function() { 59 | let a = _rgetClientRects.apply(this); 60 | let b = this.getBoundingClientRect(); 61 | 62 | for (let p in a[0]) { 63 | a[0][p] = b[p]; 64 | } 65 | 66 | return a; 67 | } 68 | 69 | modifiedAPIs.push([ 70 | spoofContext.Range.prototype.getClientRects, "getClientRects" 71 | ]); 72 | `, 73 | }; 74 | -------------------------------------------------------------------------------- /src/lib/spoof/cssExfil.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/mlgualtieri/CSS-Exfil-Protection/blob/master/firefox/content.js 2 | 3 | // MIT License 4 | 5 | // Copyright (c) 2019 Mike Gualtieri 6 | 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | // SOFTWARE. 24 | 25 | export default { 26 | type: 'script', 27 | data: () => { 28 | // Scan all document stylesheets 29 | function scan_css() { 30 | var sheets = document.styleSheets; 31 | 32 | for (var i = 0; i < sheets.length; i++) { 33 | let selectors = []; 34 | let selectorcss = []; 35 | let rules = getCSSRules(sheets[i]); 36 | 37 | if (rules == null) { 38 | if (sheets[i].href == null) { 39 | // If we reach here it's due to a Firefox CSS load timing error 40 | 41 | let _sheet = sheets[i]; 42 | setTimeout(function checkRulesInit2() { 43 | rules = getCSSRules(_sheet); 44 | if (rules == null) { 45 | setTimeout(checkRulesInit2, 1000); 46 | } else { 47 | incrementSanitize(); 48 | handleImportedCSS(rules); 49 | 50 | // Parse origin stylesheet 51 | let _selectors = parseCSSRules(rules); 52 | filter_css(_selectors[0], _selectors[1]); 53 | 54 | if (checkCSSDisabled(_sheet)) { 55 | enableCSS(_sheet); 56 | } 57 | 58 | decrementSanitize(); 59 | } 60 | }, 1000); 61 | } else { 62 | // Retrieve and parse cross domain stylesheet 63 | incrementSanitize(); 64 | getCrossDomainCSS(sheets[i]); 65 | } 66 | } else { 67 | incrementSanitize(); 68 | handleImportedCSS(rules); 69 | 70 | // Parse origin stylesheet 71 | //console.log("DOM stylesheet..."); 72 | let _selectors = parseCSSRules(rules); 73 | filter_css(_selectors[0], _selectors[1]); 74 | 75 | if (checkCSSDisabled(sheets[i])) { 76 | enableCSS(sheets[i]); 77 | } 78 | 79 | decrementSanitize(); 80 | } 81 | } 82 | } 83 | 84 | function handleImportedCSS(rules) { 85 | // Scan for imported stylesheets 86 | if (rules != null) { 87 | for (var r = 0; r < rules.length; r++) { 88 | if (Object.prototype.toString.call(rules[r]) == '[object CSSImportRule]') { 89 | // Adding new sheet to the list 90 | incrementSanitize(); 91 | 92 | // Found an imported CSS Stylesheet 93 | let _rules = getCSSRules(rules[r].styleSheet); 94 | 95 | if (_rules == null) { 96 | // Parse imported cross domain sheet 97 | getCrossDomainCSS(rules[r].styleSheet); 98 | } else { 99 | // Parse imported DOM sheet 100 | //console.log("Imported DOM CSS..."); 101 | let _selectors = parseCSSRules(_rules); 102 | filter_css(_selectors[0], _selectors[1]); 103 | decrementSanitize(); 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | function getCSSRules(_sheet) { 111 | let rules = null; 112 | 113 | try { 114 | //Loading CSS 115 | rules = _sheet.rules || _sheet.cssRules; 116 | } catch (e) { 117 | if (e.name !== 'SecurityError') { 118 | //console.log("Error loading rules:"); 119 | //throw e; 120 | } 121 | } 122 | 123 | return rules; 124 | } 125 | 126 | function parseCSSRules(rules) { 127 | let selectors = []; 128 | let selectorcss = []; 129 | 130 | if (rules != null) { 131 | // Loop through all selectors and determine if any are looking for the value attribute and calling a remote URL 132 | for (let r = 0; r < rules.length; r++) { 133 | let selectorText = null; 134 | 135 | if (rules[r].selectorText != null) { 136 | selectorText = rules[r].selectorText.toLowerCase(); 137 | } 138 | 139 | let cssText = null; 140 | 141 | if (rules[r].cssText != null) { 142 | cssText = rules[r].cssText.toLowerCase(); 143 | } 144 | 145 | // If CSS selector is parsing text and is loading a remote resource add to our blocking queue 146 | // Flag rules that: 147 | // 1) Match a value attribute selector which appears to be parsing text 148 | // 2) Calls a remote URL (https, http, //) 149 | // 3) The URL is not an xmlns property 150 | if ( 151 | selectorText != null && 152 | cssText != null && 153 | selectorText.indexOf('value') !== -1 && 154 | selectorText.indexOf('=') !== -1 && 155 | cssText.indexOf('url') !== -1 && 156 | (cssText.indexOf('https://') !== -1 || cssText.indexOf('http://') !== -1 || cssText.indexOf('//') !== -1) && 157 | cssText.indexOf("xmlns=\\'http://") === -1 158 | ) { 159 | //console.log("CSS Exfil Protection blocked: "+ rules[r].selectorText); 160 | selectors.push(rules[r].selectorText); 161 | selectorcss.push(cssText); 162 | } 163 | } 164 | } 165 | 166 | // Check if any bad rules were found 167 | // if yes, temporarily disable stylesheet 168 | if (selectors[0] != null) { 169 | //console.log("Found potentially malicious selectors!"); 170 | if (rules[0] != null) { 171 | disableCSS(rules[0].parentStyleSheet); 172 | } 173 | } 174 | 175 | return [selectors, selectorcss]; 176 | } 177 | 178 | function filter_css(selectors: any[], selectorcss: any[]) { 179 | // Loop through found selectors and override CSS 180 | for (let s in selectors) { 181 | if (selectorcss[s].indexOf('background') !== -1) { 182 | filter_sheet.sheet.insertRule(selectors[s] + ' { background:none !important; }', filter_sheet.sheet.cssRules.length); 183 | } 184 | if (selectorcss[s].indexOf('list-style') !== -1) { 185 | filter_sheet.sheet.insertRule(selectors[s] + ' { list-style: inherit !important; }', filter_sheet.sheet.cssRules.length); 186 | } 187 | if (selectorcss[s].indexOf('cursor') !== -1) { 188 | filter_sheet.sheet.insertRule(selectors[s] + ' { cursor: auto !important; }', filter_sheet.sheet.cssRules.length); 189 | } 190 | if (selectorcss[s].indexOf('content') !== -1) { 191 | filter_sheet.sheet.insertRule(selectors[s] + ' { content: normal !important; }', filter_sheet.sheet.cssRules.length); 192 | } 193 | } 194 | } 195 | 196 | function getCrossDomainCSS(orig_sheet) { 197 | let rules; 198 | let url = orig_sheet.href; 199 | 200 | if (url != null) { 201 | if (seen_url.indexOf(url) === -1) { 202 | seen_url.push(url); 203 | } else { 204 | //console.log("Already checked URL"); 205 | decrementSanitize(); 206 | return; 207 | } 208 | } 209 | 210 | let xhr = new XMLHttpRequest(); 211 | xhr.open('GET', url, true); 212 | xhr.onreadystatechange = function() { 213 | if (xhr.readyState == 4) { 214 | // Create stylesheet from remote CSS 215 | let sheet = document.createElement('style'); 216 | sheet.innerText = xhr.responseText; 217 | 218 | // Get all import rules 219 | let matches = xhr.responseText.match(/@import.*?;/g); 220 | let replaced = xhr.responseText; 221 | 222 | // Get URL path of remote stylesheet (url minus the filename) 223 | let _a = document.createElement('a'); 224 | _a.href = url; 225 | let _pathname = _a.pathname.substring(0, _a.pathname.lastIndexOf('/')) + '/'; 226 | let import_url_path = _a.origin + _pathname; 227 | 228 | // Scan through all import rules 229 | // if calling a relative resource, edit to include the original URL path 230 | if (matches != null) { 231 | for (let i = 0; i < matches.length; i++) { 232 | // Only run if import is not calling a remote http:// or https:// resource 233 | if (matches[i].indexOf('://') === -1) { 234 | // Get file/path text from import rule (first text that's between quotes or parentheses) 235 | let import_file = matches[i].match(/['"\(](.*?)['"\)]/g); 236 | 237 | if (import_file != null) { 238 | if (import_file.length > 0) { 239 | let _import_file = import_file[0]; 240 | 241 | // Remove quotes and parentheses 242 | _import_file = _import_file.replace(/['"\(\)]/g, ''); 243 | 244 | // Trim whitespace 245 | _import_file = _import_file.trim(); 246 | 247 | // Remove any URL parameters 248 | _import_file = _import_file.split('?')[0]; 249 | 250 | // Replace filename with full url path 251 | let regex = new RegExp(_import_file); 252 | replaced = replaced.replace(regex, import_url_path + _import_file); 253 | } 254 | } 255 | } 256 | } 257 | } 258 | 259 | // Add CSS to sheet and append to head so we can scan the rules 260 | sheet.innerText = replaced; 261 | document.head.appendChild(sheet); 262 | 263 | // MG: this approach to retrieve the last inserted stylesheet sometimes fails, 264 | // instead get the stylesheet directly from the temporary object (sheet.sheet) 265 | //var sheets = document.styleSheets; 266 | //rules = getCSSRules(sheets[ sheets.length - 1]); 267 | rules = getCSSRules(sheet.sheet); 268 | 269 | // if rules is null is likely means we triggered a firefox 270 | // timing error where the new CSS sheet isn't ready yet 271 | if (rules == null) { 272 | // Keep checking every 10ms until rules have become available 273 | setTimeout(function checkRulesInit() { 274 | //rules = getCSSRules(sheets[ sheets.length - 1]); 275 | rules = getCSSRules(sheet.sheet); 276 | 277 | if (rules == null) { 278 | setTimeout(checkRulesInit, 10); 279 | } else { 280 | handleImportedCSS(rules); 281 | 282 | let _selectors = parseCSSRules(rules); 283 | filter_css(_selectors[0], _selectors[1]); 284 | 285 | // Remove tmp stylesheet 286 | // @ts-ignore 287 | sheet.disabled = true; 288 | sheet.parentNode.removeChild(sheet); 289 | 290 | if (checkCSSDisabled(orig_sheet)) { 291 | enableCSS(orig_sheet); 292 | } 293 | 294 | decrementSanitize(); 295 | return rules; 296 | } 297 | }, 10); 298 | } else { 299 | handleImportedCSS(rules); 300 | 301 | let _selectors = parseCSSRules(rules); 302 | filter_css(_selectors[0], _selectors[1]); 303 | 304 | // Remove tmp stylesheet 305 | // @ts-ignore 306 | sheet.disabled = true; 307 | sheet.parentNode.removeChild(sheet); 308 | 309 | if (checkCSSDisabled(orig_sheet)) { 310 | enableCSS(orig_sheet); 311 | } 312 | 313 | decrementSanitize(); 314 | return rules; 315 | } 316 | } 317 | }; 318 | 319 | xhr.send(); 320 | } 321 | 322 | function disableCSS(_sheet) { 323 | _sheet.disabled = true; 324 | } 325 | 326 | function enableCSS(_sheet) { 327 | // Check to ensure sheet should be enabled before we do 328 | if (!disabled_css_hash[window.btoa(_sheet.href)]) { 329 | _sheet.disabled = false; 330 | 331 | // Some sites like news.google.com require a resize event to properly render all elements after re-enabling CSS 332 | window.dispatchEvent(new Event('resize')); 333 | } 334 | } 335 | 336 | function checkCSSDisabled(_sheet) { 337 | return _sheet.disabled; 338 | } 339 | 340 | function disableAndRemoveCSS(_sheet) { 341 | _sheet.disabled = true; 342 | if (_sheet.parentNode != null) { 343 | _sheet.parentNode.removeChild(_sheet); 344 | } 345 | } 346 | 347 | function incrementSanitize() { 348 | sanitize_inc++; 349 | } 350 | 351 | function decrementSanitize() { 352 | sanitize_inc--; 353 | if (sanitize_inc <= 0) { 354 | disableAndRemoveCSS(css_load_blocker); 355 | } 356 | } 357 | 358 | function buildContentLoadBlockerCSS() { 359 | return 'input,input ~ * { background-image:none !important; list-style: inherit !important; cursor: auto !important; content:normal !important; } input::before,input::after,input ~ *::before, input ~ *::after { content:normal !important; }'; 360 | } 361 | 362 | /* 363 | * Initialize 364 | */ 365 | 366 | let filter_sheet = null; // Create stylesheet which will contain our override styles 367 | let css_load_blocker = null; // Temporary stylesheet to prevent early loading of resources we may block 368 | let sanitize_inc = 0; // Incrementer to keep track when it's safe to unload css_load_blocker 369 | let seen_url = []; // Keep track of scanned cross-domain URL's 370 | let disabled_css_hash = {}; // Keep track if the CSS was disabled before sanitization 371 | 372 | // Run as soon as the DOM has been loaded 373 | window.addEventListener( 374 | 'DOMContentLoaded', 375 | function() { 376 | // Check if the CSS sheet is disabled by default 377 | for (let i = 0; i < document.styleSheets.length; i++) { 378 | disabled_css_hash[window.btoa(document.styleSheets[i].href)] = document.styleSheets[i].disabled; 379 | } 380 | 381 | // Create temporary stylesheet that will block early loading of resources we may want to block 382 | css_load_blocker = document.createElement('style'); 383 | css_load_blocker.innerText = buildContentLoadBlockerCSS(); 384 | css_load_blocker.className = '__tmp_chameleon_load_blocker'; 385 | 386 | // Null check to fix error that triggers when loading PDF's in browser 387 | if (document.head != null) { 388 | document.head.appendChild(css_load_blocker); 389 | } 390 | 391 | // Create stylesheet that will contain our filtering CSS (if any is necessary) 392 | filter_sheet = document.createElement('style'); 393 | filter_sheet.className = '__chameleon_filtered_exfil'; 394 | filter_sheet.innerText = ''; 395 | document.head.appendChild(filter_sheet); 396 | 397 | // Increment once before we scan, just in case decrement is called too quickly 398 | incrementSanitize(); 399 | 400 | scan_css(); 401 | }, 402 | false 403 | ); 404 | 405 | window.addEventListener( 406 | 'load', 407 | function() { 408 | // Unload increment called before scan 409 | decrementSanitize(); 410 | }, 411 | false 412 | ); 413 | }, 414 | }; 415 | -------------------------------------------------------------------------------- /src/lib/spoof/history.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'overwrite', 3 | data: [{ obj: 'window.history', prop: 'length', value: 2 }], 4 | }; 5 | -------------------------------------------------------------------------------- /src/lib/spoof/kbFingerprint.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'custom', 3 | data: ` 4 | let handler = (e) => { 5 | let delay = CHAMELEON_SPOOF.get(spoofContext).kbDelay; 6 | if (e.target && e.target.nodeName == 'INPUT') { 7 | if (Math.floor(Math.random() * 2)) { 8 | let endTime = Date.now() + Math.floor(Math.random() * (30 + (delay || 30))); 9 | while(Date.now() < endTime) {}; 10 | } 11 | } 12 | } 13 | 14 | spoofContext.document.addEventListener('keyup', handler); 15 | spoofContext.document.addEventListener('keydown', handler);`, 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/spoof/language.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'custom', 3 | data: ` 4 | var ORIGINAL_INTL_PR = spoofContext.Intl.PluralRules; 5 | 6 | Object.defineProperty(spoofContext.navigator, 'language', { 7 | configurable: true, 8 | value: CHAMELEON_SPOOF.get(spoofContext).language.code 9 | }); 10 | 11 | Object.defineProperty(spoofContext.navigator, 'languages', { 12 | configurable: true, 13 | value: CHAMELEON_SPOOF.get(spoofContext).language.nav 14 | }); 15 | 16 | spoofContext.Intl.PluralRules = function PluralRules() { 17 | let args = [...arguments]; 18 | 19 | if (arguments.length == 0 || !arguments[0]) { 20 | args[0] = spoofContext.navigator.language || "en-US"; 21 | } 22 | 23 | return new (Function.prototype.bind.apply(ORIGINAL_INTL_PR, [null, ...args])); 24 | }; 25 | 26 | modifiedAPIs.push([ 27 | spoofContext.Intl.PluralRules, "PluralRules" 28 | ]); 29 | 30 | if (spoofContext.Intl.ListFormat) { 31 | var ORIGINAL_INTL_LF = spoofContext.Intl.ListFormat; 32 | 33 | spoofContext.Intl.ListFormat = function ListFormat() { 34 | let args = [...arguments]; 35 | 36 | if (arguments.length == 0 || !arguments[0]) { 37 | args[0] = spoofContext.navigator.language || "en-US"; 38 | } 39 | 40 | return new (Function.prototype.bind.apply(ORIGINAL_INTL_LF, [null, ...args])); 41 | }; 42 | 43 | modifiedAPIs.push([ 44 | spoofContext.Intl.ListFormat, "ListFormat" 45 | ]); 46 | } 47 | 48 | if (spoofContext.Intl.RelativeTimeFormat) { 49 | var ORIGINAL_INTL_RTF = spoofContext.Intl.RelativeTimeFormat; 50 | 51 | spoofContext.Intl.RelativeTimeFormat = function RelativeTimeFormat() { 52 | let args = [...arguments]; 53 | 54 | if (arguments.length == 0 || !arguments[0]) { 55 | args[0] = spoofContext.navigator.language || "en-US"; 56 | } 57 | 58 | return new (Function.prototype.bind.apply(ORIGINAL_INTL_RTF, [null, ...args])); 59 | }; 60 | 61 | modifiedAPIs.push([ 62 | spoofContext.Intl.RelativeTimeFormat, "RelativeTimeFormat" 63 | ]); 64 | } 65 | 66 | if (spoofContext.Intl.NumberFormat) { 67 | var ORIGINAL_INTL_NF = spoofContext.Intl.NumberFormat; 68 | 69 | spoofContext.Intl.NumberFormat = function NumberFormat() { 70 | let args = [...arguments]; 71 | 72 | if (arguments.length == 0 || !arguments[0]) { 73 | args[0] = spoofContext.navigator.language || "en-US"; 74 | } 75 | 76 | return new (Function.prototype.bind.apply(ORIGINAL_INTL_NF, [null, ...args])); 77 | }; 78 | 79 | modifiedAPIs.push([ 80 | spoofContext.Intl.NumberFormat, "NumberFormat" 81 | ]); 82 | } 83 | 84 | if (spoofContext.Intl.Collator) { 85 | var ORIGINAL_INTL_C = spoofContext.Intl.Collator; 86 | 87 | spoofContext.Intl.Collator = function Collator() { 88 | let args = [...arguments]; 89 | 90 | if (arguments.length == 0 || !arguments[0]) { 91 | args[0] = spoofContext.navigator.language || "en-US"; 92 | } 93 | 94 | return new (Function.prototype.bind.apply(ORIGINAL_INTL_C, [null, ...args])); 95 | }; 96 | 97 | modifiedAPIs.push([ 98 | spoofContext.Intl.Collator, "Collator" 99 | ]); 100 | } 101 | ` 102 | .replace( 103 | /ORIGINAL_INTL_PR/g, 104 | String.fromCharCode(65 + Math.floor(Math.random() * 26)) + 105 | Math.random() 106 | .toString(36) 107 | .substring(Math.floor(Math.random() * 5) + 5) 108 | ) 109 | .replace( 110 | /ORIGINAL_INTL_LF/g, 111 | String.fromCharCode(65 + Math.floor(Math.random() * 26)) + 112 | Math.random() 113 | .toString(36) 114 | .substring(Math.floor(Math.random() * 5) + 5) 115 | ) 116 | .replace( 117 | /ORIGINAL_INTL_RTF/g, 118 | String.fromCharCode(65 + Math.floor(Math.random() * 26)) + 119 | Math.random() 120 | .toString(36) 121 | .substring(Math.floor(Math.random() * 5) + 5) 122 | ) 123 | .replace( 124 | /ORIGINAL_INTL_NF/g, 125 | String.fromCharCode(65 + Math.floor(Math.random() * 26)) + 126 | Math.random() 127 | .toString(36) 128 | .substring(Math.floor(Math.random() * 5) + 5) 129 | ) 130 | .replace( 131 | /ORIGINAL_INTL_C/g, 132 | String.fromCharCode(65 + Math.floor(Math.random() * 26)) + 133 | Math.random() 134 | .toString(36) 135 | .substring(Math.floor(Math.random() * 5) + 5) 136 | ), 137 | }; 138 | -------------------------------------------------------------------------------- /src/lib/spoof/media.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'custom', 3 | data: ` 4 | if (spoofContext.navigator.mediaDevices) { 5 | Object.defineProperty(spoofContext.navigator.mediaDevices, 'enumerateDevices', { 6 | configurable: true, 7 | value: async () => { 8 | return Promise.resolve([]); 9 | } 10 | }); 11 | 12 | modifiedAPIs.push([ 13 | spoofContext.navigator.mediaDevices.enumerateDevices, "enumerateDevices" 14 | ]); 15 | 16 | Object.defineProperty(spoofContext.navigator.mediaDevices, 'getUserMedia', { 17 | configurable: true, 18 | value: async () => { 19 | return Promise.resolve([]); 20 | } 21 | }); 22 | 23 | modifiedAPIs.push([ 24 | spoofContext.navigator.mediaDevices.getUserMedia, "getUserMedia" 25 | ]); 26 | } 27 | `, 28 | }; 29 | -------------------------------------------------------------------------------- /src/lib/spoof/mediaSpoof.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'custom', 3 | data: ` 4 | const MEDIA_DEVICES = { 5 | win: [ 6 | { audioinput: 'Microphone (Realtek High Definition Audio)', videoinput: 'USB2.0 HD UVC WebCam' }, 7 | { audioinput: 'Logitech C170', videoinput: 'Logitech C170' } 8 | ], 9 | mac: [ 10 | { audioinput: 'Internal Microphone', videoinput: 'FaceTime HD Camera (Built-in)' }, 11 | { audioinput: 'Logitech Camera', videoinput: 'Logitech Camera' } 12 | ], 13 | lin: [ 14 | { audioinput: 'Built-in Audio Analog Stereo', videoinput: 'USB2.0 HD UVC WebCam: USB2.0 HD' }, 15 | { audioinput: 'HD Pro Webcam C920', videoinput: 'HD Pro Webcam C920' } 16 | ], 17 | ios: [{ 18 | audioinput: 'iPhone Microphone', 19 | videoinput: [ 20 | 'Back Camera', 21 | 'Front Camera' 22 | ] 23 | }, { 24 | audioinput: 'iPad Microphone', 25 | videoinput: [ 26 | 'Back Camera', 27 | 'Front Camera' 28 | ] 29 | }], 30 | and: [{ 31 | audioinput: 'Microphone 1', 32 | videoinput: [ 33 | 'Front facing camera', 34 | 'Back facing camera' 35 | ] 36 | }, { 37 | audioinput: 'Microphone 1', 38 | videoinput: [ 39 | 'camera2 1, facing front', 40 | 'camera2 0, facing back' 41 | ] 42 | }] 43 | }; 44 | 45 | let platform = Object.keys(MEDIA_DEVICES).find(os => CHAMELEON_SPOOF.get(spoofContext).profileOS.includes(os)); 46 | let mediaDevices = MEDIA_DEVICES[platform]; 47 | let isMobile = platform === 'ios' || platform === 'and'; 48 | let deviceIndex = 0; 49 | 50 | if (spoofContext.navigator.mediaDevices && _enumerateDevices) { 51 | if (isMobile) { 52 | if (platform == 'ios') { 53 | if (spoofContext.navigator.userAgent.includes('iPad')) { 54 | deviceIndex = 1; 55 | } 56 | } else { 57 | deviceIndex = Math.random() > 0.5 ? 0 : 1; 58 | } 59 | } 60 | 61 | Object.defineProperty(spoofContext.navigator.mediaDevices, 'enumerateDevices', { 62 | configurable: false, 63 | value: () => { 64 | return new Promise(async (resolve) => { 65 | let devices = await _enumerateDevices(); 66 | let known = {}; 67 | 68 | let videoCount = 0; 69 | let audioCount = 0; 70 | 71 | if (devices.length && devices[0].label == '') { 72 | return resolve(devices); 73 | } 74 | 75 | for (let i = 0; i < devices.length; i++) { 76 | let k = devices[i].groupId + devices[i].kind; 77 | 78 | if (k in known && devices[i].kind == 'audioinput') { 79 | let label = mediaDevices[known[k]][devices[i].kind]; 80 | 81 | let isChrome = /Chrome/.test(navigator.userAgent); 82 | let isFirefox = /Firefox/.test(navigator.userAgent); 83 | 84 | if (platform == 'lin') { 85 | if (isChrome) { 86 | label = 'Default'; 87 | } else if (isFirefox) { 88 | label = 'Monitor of ' + label; 89 | } 90 | } else if (platform == 'win' && isChrome) { 91 | label = 'Default - ' + label; 92 | } 93 | 94 | Object.defineProperty(devices[i], 'label', { 95 | configurable: false, 96 | value: label 97 | }); 98 | } else { 99 | if (devices[i].kind === 'videoinput') { 100 | if (isMobile) { 101 | if (videoCount < 2) { 102 | Object.defineProperty(devices[i], 'label', { 103 | configurable: false, 104 | value: mediaDevices[deviceIndex].videoinput[videoCount] 105 | }); 106 | 107 | videoCount++; 108 | } 109 | } else { 110 | if (videoCount < 2) { 111 | Object.defineProperty(devices[i], 'label', { 112 | configurable: false, 113 | value: mediaDevices[videoCount].videoinput 114 | }); 115 | 116 | known[k] = videoCount; 117 | videoCount++; 118 | } 119 | } 120 | } else if (devices[i].kind === 'audioinput') { 121 | if (isMobile) { 122 | Object.defineProperty(devices[i], 'label', { 123 | configurable: false, 124 | value: mediaDevices[deviceIndex].audioinput 125 | }); 126 | } else { 127 | if (audioCount < 2) { 128 | Object.defineProperty(devices[i], 'label', { 129 | configurable: false, 130 | value: mediaDevices[audioCount].audioinput 131 | }); 132 | 133 | known[k] = audioCount; 134 | audioCount++; 135 | } 136 | } 137 | } 138 | } 139 | }; 140 | 141 | return resolve(devices); 142 | }); 143 | } 144 | }); 145 | 146 | Object.defineProperty(spoofContext.MediaDeviceInfo.prototype, 'toJSON', { 147 | configurable: false, 148 | value: function toJSON() { 149 | return { 150 | label: this.label, 151 | deviceId: this.deviceId, 152 | kind: this.kind, 153 | groupId: this.groupId 154 | }; 155 | } 156 | }); 157 | 158 | modifiedAPIs.push([ 159 | spoofContext.navigator.mediaDevices.enumerateDevices, "enumerateDevices" 160 | ]); 161 | } 162 | `, 163 | }; 164 | -------------------------------------------------------------------------------- /src/lib/spoof/name.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'custom', 3 | data: ` 4 | window.name = ''; 5 | `, 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/spoof/navigator.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'overwrite', 3 | data: [ 4 | { obj: 'window.navigator', prop: 'appMinorVersion', value: '' }, 5 | { obj: 'window.navigator', prop: 'appVersion', value: '' }, 6 | { obj: 'window.navigator', prop: 'buildID', value: '' }, 7 | { obj: 'window.navigator', prop: 'cpuClass', value: '' }, 8 | { obj: 'window.navigator', prop: 'deviceMemory', value: '' }, 9 | { obj: 'window.navigator', prop: 'hardwareConcurrency', value: '' }, 10 | { obj: 'window.navigator', prop: 'maxTouchPoints', value: '' }, 11 | { obj: 'window.navigator', prop: 'mimeTypes', value: '' }, 12 | { obj: 'window.navigator', prop: 'oscpu', value: '' }, 13 | { obj: 'window.navigator', prop: 'platform', value: '' }, 14 | { obj: 'window.navigator', prop: 'plugins', value: '' }, 15 | { obj: 'window.navigator', prop: 'productSub', value: '' }, 16 | { obj: 'window.navigator', prop: 'userAgent', value: '' }, 17 | { obj: 'window.navigator', prop: 'vendor', value: '' }, 18 | { obj: 'window.navigator', prop: 'vendorSub', value: '' }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/spoof/quirks.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'custom', 3 | data: ` 4 | let data = CHAMELEON_SPOOF.get(spoofContext); 5 | let uniqId = Math.floor(Math.random() * (1000) + 1000); 6 | let startTime = new Date().getTime(); 7 | let randLoadTime = Math.floor(Math.random() * 1000); 8 | 9 | if (data.browser !== "firefox") { 10 | delete window.InstallTrigger; 11 | delete window.InstallTriggerImpl; 12 | } 13 | 14 | if (data.browser === "edge" || data.browser === "chrome") { 15 | spoofContext.chrome = { 16 | app: { 17 | InstallState: { 18 | DISABLED: "disabled", 19 | INSTALLED: "installed", 20 | NOT_INSTALLED: "not_installed" 21 | }, 22 | RunningState: { 23 | CANNOT_RUN: "cannot_run", 24 | READY_TO_RUN: "ready_to_run", 25 | RUNNING: "running" 26 | }, 27 | getDetails() { 28 | return null; 29 | }, 30 | getIsInstalled() { 31 | return false; 32 | }, 33 | installState() { 34 | return "not_installed"; 35 | }, 36 | isInstalled: false, 37 | runningState() { 38 | return "cannot_run"; 39 | } 40 | }, 41 | csi() { 42 | return { 43 | onloadT: startTime + randLoadTime, 44 | pageT: new Date().getTime() - startTime, 45 | startE: startTime, 46 | tran: 15 47 | } 48 | }, 49 | loadTimes() { 50 | return { 51 | commitLoadTime: startTime, 52 | connectionInfo: "http/1.0", 53 | finishDocumentLoadTime: 0, 54 | finishLoadTime: 0, 55 | firstPaintAfterLoadTime: 0, 56 | firstPaintTime: 0, 57 | navigationType: "Reload", 58 | npnNegotiatedProtocol: "unknown", 59 | requestTime: startTime, 60 | startLoadTime: startTime, 61 | wasAlternateProtocolAvailable: false, 62 | wasFetchedViaSpdy: false, 63 | wasNpnNegotiated: false 64 | } 65 | }, 66 | runtime: { 67 | OnInstalledReason: { 68 | CHROME_UPDATE: "chrome_update", 69 | INSTALL: "install", 70 | SHARED_MODULE_UPDATE: "shared_module_update", 71 | UPDATE: "update" 72 | }, 73 | OnRestartRequiredReason: { 74 | APP_UPDATE: "app_update", 75 | OS_UPDATE: "os_update", 76 | PERIODIC: "periodic" 77 | }, 78 | PlatformArch: { 79 | ARM: "arm", 80 | ARM64: "arm64", 81 | MIPS: "mips", 82 | MIPS64: "mips64", 83 | X86_32: "x86-32", 84 | X86_64: "x86-64" 85 | }, 86 | PlatformNaclArch: { 87 | ARM: "arm", 88 | ARM64: "arm64", 89 | MIPS: "mips", 90 | MIPS64: "mips64", 91 | X86_32: "x86-32", 92 | X86_64: "x86-64" 93 | }, 94 | PlatformOs: { 95 | ANDROID: "android", 96 | CROS: "cros", 97 | LINUX: "linux", 98 | MAC: "mac", 99 | OPENBSD: "openbsd", 100 | WIN: "win" 101 | }, 102 | RequestUpdateCheckStatus: { 103 | NO_UPDATE: "no_update", 104 | THROTTLED: "throttled", 105 | UPDATE_AVAILABLE: "update_available" 106 | }, 107 | connect() { 108 | // do nothing 109 | }, 110 | id: undefined, 111 | sendMessage() { 112 | // do nothing 113 | } 114 | } 115 | }; 116 | } 117 | 118 | if (data.browser === "safari") { 119 | const SafariRemoteNotification = { 120 | async requestPermission(url, websitePushID, userInfo, callback) { 121 | callback({ 122 | permission: 'denied' 123 | }) 124 | }, 125 | permission() { 126 | return { 127 | permission: 'denied' 128 | } 129 | }, 130 | toString: () => { 131 | return "[object SafariRemoteNotification]"; 132 | } 133 | }; 134 | 135 | spoofContext.safari = { 136 | pushNotification: SafariRemoteNotification 137 | }; 138 | } 139 | 140 | if (data.browser === "ie") { 141 | spoofContext.document.documentMode = 11; 142 | 143 | Object.defineProperty(spoofContext.document, 'uniqueID', { 144 | get() { 145 | ++uniqId; 146 | return 'ms__id' + uniqId; 147 | } 148 | }); 149 | 150 | // stub for MSInputMethodContext 151 | spoofContext.MSInputMethodContext = { 152 | compositionEndOffset: 0, 153 | compositionStartOffset: 0, 154 | target: null, 155 | oncandidatewindowhide: () => {}, 156 | oncandidatewindowhide: () => {}, 157 | oncandidatewindowhide: () => {}, 158 | getCandidateWindowClientRect: () => {}, 159 | getCompositionAlternatives: () => {}, 160 | hasComposition: () => {}, 161 | isCandidateWindowVisible: () => {} 162 | }; 163 | 164 | Object.setPrototypeOf(spoofContext.MSInputMethodContext, EventTarget.prototype); 165 | 166 | spoofContext.ActiveXObject = undefined; 167 | spoofContext.msCrypto = spoofContext.crypto; 168 | } 169 | `, 170 | }; 171 | -------------------------------------------------------------------------------- /src/lib/spoof/referer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'overwrite', 3 | data: [{ obj: 'document', prop: 'referrer', value: '' }], 4 | }; 5 | -------------------------------------------------------------------------------- /src/lib/spoof/screen.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'custom', 3 | data: ` 4 | let screenData = CHAMELEON_SPOOF.get(spoofContext).screen; 5 | 6 | if (screenData.width != null) { 7 | if (screenData.usingProfileRes === false) { 8 | screenData.availHeight = screenData.height - (spoofContext.screen.height - spoofContext.screen.availHeight); 9 | } 10 | 11 | ['top', 'left', 'availTop', 'availLeft'].forEach((k) => { 12 | Object.defineProperty(spoofContext.Screen.prototype, k, { 13 | get: (() => 0).bind(null) 14 | }); 15 | }); 16 | 17 | ['colorDepth', 'pixelDepth'].forEach((k) => { 18 | Object.defineProperty(spoofContext.Screen.prototype, k, { 19 | get: (() => screenData.pixelDepth ).bind(null) 20 | }); 21 | }); 22 | 23 | ['availWidth', 'width'].forEach((k) => { 24 | Object.defineProperty(spoofContext.Screen.prototype, k, { 25 | get: (() => screenData.width).bind(null) 26 | }); 27 | }); 28 | 29 | ['innerWidth', 'outerWidth'].forEach((k) => { 30 | Object.defineProperty(spoofContext, k, { 31 | get: (() => screenData.width).bind(null) 32 | }); 33 | }); 34 | 35 | ['innerHeight'].forEach((k) => { 36 | Object.defineProperty(spoofContext, k, { 37 | get: (() => screenData.availHeight).bind(null) 38 | }); 39 | }); 40 | 41 | ['outerHeight'].forEach((k) => { 42 | Object.defineProperty(spoofContext, k, { 43 | get: (() => screenData.height).bind(null) 44 | }); 45 | }); 46 | 47 | Object.defineProperty(spoofContext.Screen.prototype, 'availHeight', { 48 | get: (() => screenData.availHeight).bind(null) 49 | }); 50 | 51 | Object.defineProperty(spoofContext.Screen.prototype, 'height', { 52 | get: (() => screenData.height).bind(null) 53 | }); 54 | 55 | Object.defineProperty(spoofContext, 'devicePixelRatio', { 56 | get: (() => screenData.deviceScaleFactor || 1).bind(null) 57 | }); 58 | } 59 | `, 60 | }; 61 | -------------------------------------------------------------------------------- /src/lib/spoof/timezone.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | type: 'custom', 3 | data: ` 4 | if (new Date()[spoofContext.CHAMELEON_SPOOF]) { 5 | spoofContext.Date = Date; 6 | return; 7 | } 8 | 9 | let ORIGINAL_DATE = spoofContext.Date; 10 | 11 | const { 12 | getDate, getDay, getFullYear, getHours, getMinutes, getMonth, getTime, getTimezoneOffset, 13 | setDate, setFullYear, setHours, setMinutes, setMilliseconds, setMonth, setSeconds, 14 | setTime, toDateString, toLocaleString, toLocaleDateString, toLocaleTimeString, toTimeString 15 | } = ORIGINAL_DATE.prototype; 16 | 17 | const TZ_LONG_A = new ORIGINAL_DATE(2020, 0, 1).toLocaleDateString(undefined, { timeZoneName: 'long' }).split(', ')[1]; 18 | const TZ_LONG_B = new ORIGINAL_DATE(2020, 6, 1).toLocaleDateString(undefined, { timeZoneName: 'long' }).split(', ')[1]; 19 | const TZ_SHORT_A = new ORIGINAL_DATE(2020, 0, 1).toLocaleDateString(undefined, { timeZoneName: 'short' }).split(', ')[1]; 20 | const TZ_SHORT_B = new ORIGINAL_DATE(2020, 6, 1).toLocaleDateString(undefined, { timeZoneName: 'short' }).split(', ')[1]; 21 | const TZ_INTL = ORIGINAL_INTL('en-us', { timeZone: CHAMELEON_SPOOF.get(spoofContext).timezone.zone.name, timeZoneName: 'long'}); 22 | const TZ_LOCALE_STRING = ORIGINAL_INTL('en-us', { 23 | timeZone: CHAMELEON_SPOOF.get(spoofContext).timezone.zone.name, 24 | year: 'numeric', 25 | month: 'numeric', 26 | day: 'numeric', 27 | hour: 'numeric', 28 | minute: 'numeric', 29 | second: 'numeric' 30 | }); 31 | const TZ_DIFF = 3 * 60 * 60 * 1000; 32 | 33 | const modifyDate = (d) => { 34 | let timestamp = getTime.call(d); 35 | let spoofData = CHAMELEON_SPOOF.get(spoofContext).timezone; 36 | let offsetIndex = spoofData.zone.untils.findIndex(o => o === null || (timestamp < o) ); 37 | let offsetNum = spoofData.zone.offsets[offsetIndex]; 38 | 39 | let offsetStr = \`\${offsetNum < 0 ? '+' : '-' }\${String(Math.abs(offsetNum / 60)).padStart(2, '0')}\${String(offsetNum % 60).padStart(2, '0')}\`; 40 | let tzName = TZ_INTL.format(d).split(', ')[1]; 41 | 42 | let tmp = new ORIGINAL_DATE(TZ_LOCALE_STRING.format(d)); 43 | 44 | d[spoofContext.CHAMELEON_SPOOF] = { 45 | date: tmp, 46 | zoneInfo_offsetNum: offsetNum, 47 | zoneInfo_offsetStr: offsetStr, 48 | zoneInfo_tzAbbr: spoofData.zone.abbrs[offsetIndex], 49 | zoneInfo_tzName: tzName 50 | }; 51 | } 52 | 53 | const replaceName = (d, name) => { 54 | d = d.replace(TZ_LONG_A, name); 55 | d = d.replace(TZ_LONG_B, name); 56 | d = d.replace(TZ_SHORT_A, name); 57 | d = d.replace(TZ_SHORT_B, name); 58 | 59 | return d; 60 | } 61 | 62 | spoofContext.Date = function() { 63 | 'use strict'; 64 | 65 | let tmp = new ORIGINAL_DATE(...arguments); 66 | let timestamp = getTime.call(tmp); 67 | 68 | if (isNaN(timestamp)) { 69 | return tmp; 70 | } 71 | 72 | modifyDate(tmp); 73 | 74 | return (this instanceof Date) ? tmp : tmp.toString(); 75 | }; 76 | 77 | Object.defineProperty(spoofContext.Date, 'length', { 78 | configurable: false, 79 | value: 7 80 | }) 81 | 82 | spoofContext.Date.prototype = ORIGINAL_DATE.prototype; 83 | spoofContext.Date.UTC = ORIGINAL_DATE.UTC; 84 | spoofContext.Date.now = ORIGINAL_DATE.now; 85 | spoofContext.Date.parse = ORIGINAL_DATE.parse; 86 | 87 | spoofContext.Date.prototype.getDate = function() { 88 | if (isNaN(getTime.call(this))) { 89 | return NaN; 90 | } 91 | 92 | if (!this[spoofContext.CHAMELEON_SPOOF]) modifyDate(this); 93 | 94 | return getDate.call(this[spoofContext.CHAMELEON_SPOOF].date); 95 | } 96 | spoofContext.Date.prototype.getDay = function() { 97 | if (isNaN(getTime.call(this))) { 98 | return NaN; 99 | } 100 | 101 | if (!this[spoofContext.CHAMELEON_SPOOF]) modifyDate(this); 102 | 103 | return getDay.call(this[spoofContext.CHAMELEON_SPOOF].date); 104 | } 105 | spoofContext.Date.prototype.getFullYear = function() { 106 | if (isNaN(getTime.call(this))) { 107 | return NaN; 108 | } 109 | 110 | if (!this[spoofContext.CHAMELEON_SPOOF]) modifyDate(this); 111 | 112 | return getFullYear.call(this[spoofContext.CHAMELEON_SPOOF].date); 113 | } 114 | spoofContext.Date.prototype.getHours = function(){ 115 | if (isNaN(getTime.call(this))) { 116 | return NaN; 117 | } 118 | 119 | if (!this[spoofContext.CHAMELEON_SPOOF]) modifyDate(this); 120 | 121 | return getHours.call(this[spoofContext.CHAMELEON_SPOOF].date); 122 | } 123 | spoofContext.Date.prototype.getMinutes = function() { 124 | if (isNaN(getTime.call(this))) { 125 | return NaN; 126 | } 127 | 128 | if (!this[spoofContext.CHAMELEON_SPOOF]) modifyDate(this); 129 | 130 | return getMinutes.call(this[spoofContext.CHAMELEON_SPOOF].date); 131 | } 132 | spoofContext.Date.prototype.getMonth = function() { 133 | if (isNaN(getTime.call(this))) { 134 | return NaN; 135 | } 136 | 137 | if (!this[spoofContext.CHAMELEON_SPOOF]) modifyDate(this); 138 | 139 | return getMonth.call(this[spoofContext.CHAMELEON_SPOOF].date); 140 | } 141 | spoofContext.Date.prototype.getTimezoneOffset = function() { 142 | if (isNaN(getTime.call(this))) { 143 | return NaN; 144 | } 145 | 146 | if (!this[spoofContext.CHAMELEON_SPOOF]) modifyDate(this); 147 | 148 | return this[spoofContext.CHAMELEON_SPOOF].zoneInfo_offsetNum; 149 | } 150 | spoofContext.Date.prototype.setDate = function() { 151 | if (isNaN(getTime.call(this))) { 152 | return NaN; 153 | } 154 | 155 | let nd = setDate.apply(this, arguments); 156 | if (isNaN(nd)) { 157 | return "Invalid Date"; 158 | } 159 | 160 | modifyDate(this); 161 | 162 | return nd; 163 | } 164 | spoofContext.Date.prototype.setFullYear = function() { 165 | if (isNaN(getTime.call(this))) { 166 | return NaN; 167 | } 168 | 169 | let nd = setFullYear.apply(this, arguments); 170 | if (isNaN(nd)) { 171 | return "Invalid Date"; 172 | } 173 | 174 | modifyDate(this); 175 | 176 | return nd; 177 | } 178 | spoofContext.Date.prototype.setHours = function() { 179 | if (isNaN(getTime.call(this))) { 180 | return NaN; 181 | } 182 | 183 | let nd = setHours.apply(this, arguments); 184 | if (isNaN(nd)) { 185 | return "Invalid Date"; 186 | } 187 | 188 | modifyDate(this); 189 | 190 | return nd; 191 | } 192 | spoofContext.Date.prototype.setMilliseconds = function() { 193 | if (isNaN(getTime.call(this))) { 194 | return NaN; 195 | } 196 | 197 | let nd = setMilliseconds.apply(this, arguments); 198 | if (isNaN(nd)) { 199 | return "Invalid Date"; 200 | } 201 | 202 | modifyDate(this); 203 | 204 | return nd; 205 | } 206 | spoofContext.Date.prototype.setMonth = function() { 207 | if (isNaN(getTime.call(this))) { 208 | return NaN; 209 | } 210 | 211 | let nd = setMonth.apply(this, arguments); 212 | if (isNaN(nd)) { 213 | return "Invalid Date"; 214 | } 215 | 216 | modifyDate(this); 217 | 218 | return nd; 219 | } 220 | spoofContext.Date.prototype.setSeconds = function() { 221 | if (isNaN(getTime.call(this))) { 222 | return NaN; 223 | } 224 | 225 | let nd = setSeconds.apply(this, arguments); 226 | if (isNaN(nd)) { 227 | return "Invalid Date"; 228 | } 229 | 230 | modifyDate(this); 231 | 232 | return nd; 233 | } 234 | spoofContext.Date.prototype.setTime = function() { 235 | if (isNaN(getTime.call(this))) { 236 | return NaN; 237 | } 238 | 239 | let nd = setTime.apply(this, arguments); 240 | if (isNaN(nd)) { 241 | return "Invalid Date"; 242 | } 243 | 244 | modifyDate(this); 245 | 246 | return nd; 247 | } 248 | spoofContext.Date.prototype.toDateString = function() { 249 | if (isNaN(getTime.call(this))) { 250 | return "Invalid Date"; 251 | } 252 | 253 | if (!this[spoofContext.CHAMELEON_SPOOF]) modifyDate(this); 254 | 255 | return toDateString.apply(this[spoofContext.CHAMELEON_SPOOF].date); 256 | } 257 | spoofContext.Date.prototype.toString = function() { 258 | if (isNaN(getTime.call(this))) { 259 | return "Invalid Date"; 260 | } 261 | 262 | return this.toDateString() + ' ' + this.toTimeString(); 263 | } 264 | spoofContext.Date.prototype.toTimeString = function() { 265 | if (isNaN(getTime.call(this))) { 266 | return "Invalid Date"; 267 | } 268 | 269 | if (!this[spoofContext.CHAMELEON_SPOOF]) modifyDate(this); 270 | 271 | let parts = toTimeString.apply(this[spoofContext.CHAMELEON_SPOOF].date).split(' ', 1); 272 | 273 | // fix string formatting for negative timestamp 274 | let tzName; 275 | 276 | if (getTime.call(this) >= 0) { 277 | tzName = \`(\${this[spoofContext.CHAMELEON_SPOOF].zoneInfo_tzName})\`; 278 | } else { 279 | tzName = "(" + TZ_LONG_A + ")"; 280 | } 281 | 282 | parts = parts.concat(['GMT' + this[spoofContext.CHAMELEON_SPOOF].zoneInfo_offsetStr, tzName]); 283 | 284 | return parts.join(' '); 285 | } 286 | spoofContext.Date.prototype.toJSON = function() { 287 | if (isNaN(getTime.call(this))) { 288 | return null; 289 | } 290 | return this.toISOString(); 291 | } 292 | spoofContext.Date.prototype.toLocaleString = function() { 293 | if (isNaN(getTime.call(this))) { 294 | return "Invalid Date"; 295 | } 296 | 297 | if (!this[spoofContext.CHAMELEON_SPOOF]) modifyDate(this); 298 | 299 | let tmp = toLocaleString.apply(this[spoofContext.CHAMELEON_SPOOF].date, arguments); 300 | 301 | return replaceName(tmp, this[spoofContext.CHAMELEON_SPOOF].zoneInfo_tzName); 302 | } 303 | spoofContext.Date.prototype.toLocaleDateString = function() { 304 | if (isNaN(getTime.call(this))) { 305 | return "Invalid Date"; 306 | } 307 | 308 | if (!this[spoofContext.CHAMELEON_SPOOF]) modifyDate(this); 309 | 310 | let tmp = toLocaleDateString.apply(this[spoofContext.CHAMELEON_SPOOF].date, arguments); 311 | 312 | return replaceName(tmp, this[spoofContext.CHAMELEON_SPOOF].zoneInfo_tzName); 313 | } 314 | spoofContext.Date.prototype.toLocaleTimeString = function() { 315 | if (isNaN(getTime.call(this))) { 316 | return "Invalid Date"; 317 | } 318 | 319 | if (!this[spoofContext.CHAMELEON_SPOOF]) modifyDate(this); 320 | 321 | let tmp = toLocaleTimeString.apply(this[spoofContext.CHAMELEON_SPOOF].date, arguments); 322 | 323 | return replaceName(tmp, this[spoofContext.CHAMELEON_SPOOF].zoneInfo_tzName); 324 | } 325 | 326 | modifiedAPIs = modifiedAPIs.concat([ 327 | [spoofContext.Date, "Date"], 328 | [spoofContext.Date.prototype.getDate, "getDate"], 329 | [spoofContext.Date.prototype.getDay, "getDay"], 330 | [spoofContext.Date.prototype.getFullYear, "getFullYear"], 331 | [spoofContext.Date.prototype.getHours, "getHours"], 332 | [spoofContext.Date.prototype.getMinutes, "getMinutes"], 333 | [spoofContext.Date.prototype.getMonth, "getMonth"], 334 | [spoofContext.Date.prototype.getTimezoneOffset, "getTimezoneOffset"], 335 | [spoofContext.Date.prototype.setDate, "setDate"], 336 | [spoofContext.Date.prototype.setFullYear, "setFullYear"], 337 | [spoofContext.Date.prototype.setHours, "setHours"], 338 | [spoofContext.Date.prototype.setMilliseconds, "setMilliseconds"], 339 | [spoofContext.Date.prototype.setMonth, "setMonth"], 340 | [spoofContext.Date.prototype.setSeconds, "setSeconds"], 341 | [spoofContext.Date.prototype.setTime, "setTime"], 342 | [spoofContext.Date.prototype.toDateString, "toDateString"], 343 | [spoofContext.Date.prototype.toString, "toString"], 344 | [spoofContext.Date.prototype.toTimeString, "toTimeString"], 345 | [spoofContext.Date.prototype.toJSON, "toJSON"], 346 | [spoofContext.Date.prototype.toLocaleString, "toLocaleString"], 347 | [spoofContext.Date.prototype.toLocaleDateString, "toLocaleDateString"], 348 | [spoofContext.Date.prototype.toLocaleTimeString, "toLocaleTimeString"], 349 | ]); 350 | `.replace( 351 | /ORIGINAL_DATE/g, 352 | String.fromCharCode(65 + Math.floor(Math.random() * 26)) + 353 | Math.random() 354 | .toString(36) 355 | .substring(Math.floor(Math.random() * 5) + 5) 356 | ), 357 | }; 358 | -------------------------------------------------------------------------------- /src/lib/util.ts: -------------------------------------------------------------------------------- 1 | const psl = require('psl'); 2 | const CIDR = require('cidr-js'); 3 | 4 | const cidr = new CIDR(); 5 | const REGEX_HTTP = new RegExp('^https?:', 'i'); 6 | const LINK = document.createElement('a'); 7 | 8 | let deepMerge = (target: object, source: object) => { 9 | Object.entries(source).forEach(([key, value]) => { 10 | if (value && typeof value === 'object') { 11 | deepMerge((target[key] = target[key] || {}), value); 12 | return; 13 | } 14 | target[key] = value; 15 | }); 16 | 17 | return target; 18 | }; 19 | 20 | let determineRequestType = (source: string, destination: string): string => { 21 | if (!source) return 'none'; 22 | 23 | LINK.href = source; 24 | let s = psl.parse(LINK.hostname); 25 | LINK.href = destination; 26 | let d = psl.parse(LINK.hostname); 27 | 28 | if (s.domain != d.domain) { 29 | return 'cross-site'; 30 | } else { 31 | if (s.subdomain === d.subdomain) { 32 | return 'same-origin'; 33 | } 34 | 35 | return 'same-site'; 36 | } 37 | }; 38 | 39 | let findWhitelistRule = (rules: any, host: string, url: string): any => { 40 | for (var i = 0; i < rules.length; i++) { 41 | for (var j = 0; j < rules[i].sites.length; j++) { 42 | let whitelistURL = new URL((REGEX_HTTP.test(rules[i].sites[j].domain) ? '' : 'http://') + rules[i].sites[j].domain); 43 | 44 | if (host.includes(whitelistURL.host.replace(/^(www\.)/, ''))) { 45 | if (!rules[i].sites[j].pattern || (rules[i].sites[j].pattern && new RegExp(rules[i].sites[j].pattern).test(url))) { 46 | return { 47 | id: rules[i].id, 48 | siteIndex: j, 49 | name: rules[i].name, 50 | lang: rules[i].lang, 51 | pattern: rules[i].sites[j], 52 | profile: rules[i].profile, 53 | options: rules[i].options, 54 | spoofIP: rules[i].spoofIP, 55 | }; 56 | } 57 | } 58 | } 59 | } 60 | 61 | return null; 62 | }; 63 | 64 | let generateByte = (): number => { 65 | let octet: number = Math.floor(Math.random() * 256); 66 | return octet === 10 || octet === 172 || octet === 192 ? generateByte() : octet; 67 | }; 68 | 69 | let generateIP = (): string => { 70 | return `${generateByte()}.${generateByte()}.${generateByte()}.${generateByte()}`; 71 | }; 72 | 73 | let getIPRange = (ipRange: string): string => { 74 | let range: any = cidr.range(ipRange); 75 | 76 | if (range === null) { 77 | return ipRange; 78 | } 79 | 80 | return `${range.start}-${range.end}`; 81 | }; 82 | 83 | let ipInRange = (ip: string, range: string): boolean => { 84 | if (range.length === 1) { 85 | return ip === range[0]; 86 | } else { 87 | let ipToCompare: number = ipToInt(ip); 88 | let ipRangeFrom: number = ipToInt(range[0]); 89 | let ipRangeTo: number = ipToInt(range[1]); 90 | 91 | return ipRangeFrom <= ipToCompare && ipToCompare <= ipRangeTo; 92 | } 93 | }; 94 | 95 | let ipToInt = (ip: string): number => { 96 | return ( 97 | ip.split('.').reduce(function(ipInt: number, octet: string) { 98 | return (ipInt << 8) + parseInt(octet, 10); 99 | }, 0) >>> 0 100 | ); 101 | }; 102 | 103 | let ipToString = (ip: number): string => { 104 | return (ip >>> 24) + '.' + ((ip >> 16) & 255) + '.' + ((ip >> 8) & 255) + '.' + (ip & 255); 105 | }; 106 | 107 | let isInternalIP = (host: string): boolean => { 108 | return ( 109 | /^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/.test(host) || 110 | /(^192\.168\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])$)|(^172\.([1][6-9]|[2][0-9]|[3][0-1])\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])$)|(^10\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])$)/.test( 111 | host 112 | ) 113 | ); 114 | }; 115 | 116 | let isValidURL = (url: string): boolean => { 117 | try { 118 | if (!/^https?:\/\//i.test(url)) { 119 | url = 'http://' + url; 120 | } 121 | new URL(url); 122 | return true; 123 | } catch (e) { 124 | return false; 125 | } 126 | }; 127 | 128 | let isValidIP = (ip: string): boolean => { 129 | return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( 130 | ip 131 | ); 132 | }; 133 | 134 | let parseURL = (url: string): any => { 135 | let u = new URL(url); 136 | let uParsed = psl.parse(u.hostname); 137 | 138 | return { 139 | base: u.hostname 140 | .split('.') 141 | .splice(-2) 142 | .join('.'), 143 | domain: uParsed.domain, 144 | hostname: u.hostname, 145 | origin: u.origin, 146 | pathname: u.pathname, 147 | }; 148 | }; 149 | 150 | let validateIPRange = (from: string, to: string): boolean => { 151 | return isValidIP(from) && isValidIP(to) && ipToInt(from) <= ipToInt(to); 152 | }; 153 | 154 | export default { 155 | deepMerge, 156 | determineRequestType, 157 | findWhitelistRule, 158 | generateIP, 159 | getIPRange, 160 | ipInRange, 161 | ipToInt, 162 | ipToString, 163 | isInternalIP, 164 | isValidIP, 165 | isValidURL, 166 | parseURL, 167 | validateIPRange, 168 | }; 169 | -------------------------------------------------------------------------------- /src/lib/webext.ts: -------------------------------------------------------------------------------- 1 | // helpful functions to handle web extension things 2 | let enableChameleon = (enabled: boolean): void => { 3 | browser.runtime.getPlatformInfo().then(plat => { 4 | if (plat.os != 'android') { 5 | if (enabled === false) { 6 | browser.browserAction.setIcon({ 7 | path: '../icons/icon_disabled.svg', 8 | }); 9 | } else { 10 | browser.browserAction.setIcon({ 11 | path: '../icons/icon.svg', 12 | }); 13 | } 14 | } 15 | }); 16 | }; 17 | 18 | let enableContextMenu = (enabled: boolean): void => { 19 | browser.runtime.sendMessage({ 20 | action: 'contextMenu', 21 | data: enabled, 22 | }); 23 | }; 24 | 25 | let firstTimeInstall = (): void => { 26 | browser.runtime.onInstalled.addListener((details: any) => { 27 | if (!details.temporary && details.reason === 'install') { 28 | browser.tabs.create({ 29 | url: 'https://sereneblue.github.io/chameleon/?newinstall', 30 | }); 31 | } 32 | }); 33 | }; 34 | 35 | let getSettings = (key: string | null) => { 36 | return new Promise((resolve: any) => { 37 | browser.storage.local.get(key, (item: any) => { 38 | typeof key == 'string' ? resolve(item[key]) : resolve(item); 39 | }); 40 | }); 41 | }; 42 | 43 | let sendToBackground = (settings: any): void => { 44 | browser.runtime.sendMessage({ 45 | action: 'save', 46 | data: settings, 47 | }); 48 | }; 49 | 50 | let setBrowserConfig = async (setting: string, value: string): Promise => { 51 | if (setting === 'options.cookiePolicy' || setting === 'options.cookieNotPersistent') { 52 | let settings = await browser.privacy.websites.cookieConfig.get({}); 53 | 54 | settings = settings.value; 55 | 56 | if (setting === 'options.cookiePolicy') { 57 | settings.behavior = value; 58 | } else { 59 | settings.nonPersistentCookies = value; 60 | } 61 | 62 | browser.privacy.websites.cookieConfig.set({ 63 | value: settings, 64 | }); 65 | } else if (['options.firstPartyIsolate', 'options.resistFingerprinting', 'options.trackingProtectionMode'].includes(setting)) { 66 | let key: string = setting.split('.')[1]; 67 | browser.privacy.websites[key].set({ 68 | value: value, 69 | }); 70 | } else if (setting === 'options.disableWebRTC') { 71 | browser.privacy.network.peerConnectionEnabled.set({ 72 | value: !value, 73 | }); 74 | } else if (setting === 'options.webRTCPolicy') { 75 | browser.privacy.network.webRTCIPHandlingPolicy.set({ 76 | value: value, 77 | }); 78 | } 79 | }; 80 | 81 | let setSettings = (settings: any) => { 82 | return new Promise((resolve: any) => { 83 | browser.storage.local.set(settings, () => { 84 | resolve(); 85 | }); 86 | }); 87 | }; 88 | 89 | export default { 90 | enableChameleon, 91 | enableContextMenu, 92 | firstTimeInstall, 93 | getSettings, 94 | sendToBackground, 95 | setBrowserConfig, 96 | setSettings, 97 | }; 98 | -------------------------------------------------------------------------------- /src/lib/whitelisted.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'https://www.google.com/recaptcha/api2', 3 | 'https://accounts.google.com', 4 | 'https://docs.google.com', 5 | 'https://drive.google.com', 6 | 'https://accounts.youtube.com', 7 | 'https://disqus.com/embed/comments/', 8 | ]; 9 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chameleon", 3 | "description": "__MSG_extDescription__", 4 | "author": "sereneblue", 5 | "version": "0.22.73.1", 6 | "version_name": "0.22.73", 7 | "manifest_version": 2, 8 | "browser_specific_settings": { 9 | "gecko": { 10 | "id": "{3579f63b-d8ee-424f-bbb6-6d0ce3285e6a}", 11 | "strict_min_version": "68.0a1" 12 | }, 13 | "gecko_android": { 14 | "id": "{3579f63b-d8ee-424f-bbb6-6d0ce3285e6a}", 15 | "strict_min_version": "68.0a1" 16 | } 17 | }, 18 | "default_locale": "en", 19 | "icons": { 20 | "48": "icons/icon.svg", 21 | "128": "icons/icon.svg" 22 | }, 23 | "browser_action": { 24 | "default_icon": "icons/icon.svg", 25 | "default_popup": "popup/popup.html" 26 | }, 27 | "background": { 28 | "scripts": ["background.js"] 29 | }, 30 | "permissions": ["", "alarms", "contextMenus", "notifications", "storage", "tabs", "webRequest", "webRequestBlocking"], 31 | "optional_permissions": ["privacy"] 32 | } 33 | -------------------------------------------------------------------------------- /src/options/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chameleon Dashboard 7 | 8 | <% if (NODE_ENV === 'development') { %> 9 | 10 | <% } %> 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/options/options.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueFeather from 'vue-feather'; 3 | import VueI18n from 'vue-i18n'; 4 | import App from './App'; 5 | import store from '../store'; 6 | import localeMessages from '../_locales'; 7 | import '../css/tailwind.css'; 8 | 9 | import PerfectScrollbar from 'vue2-perfect-scrollbar'; 10 | import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css'; 11 | 12 | Vue.use(PerfectScrollbar); 13 | Vue.use(VueFeather); 14 | Vue.use(VueI18n); 15 | 16 | const i18n = new VueI18n({ 17 | locale: browser.i18n.getUILanguage(), 18 | fallbackLocale: { 19 | 'pt-PT': ['pt_PT'], 20 | 'pt-BR': ['pt_BR'], 21 | 'zh-CN': ['zh_CN'], 22 | default: ['en'], 23 | }, 24 | messages: localeMessages, 25 | }); 26 | 27 | /* eslint-disable no-new */ 28 | new Vue({ 29 | el: '#app', 30 | store, 31 | i18n, 32 | render: h => h(App), 33 | }); 34 | -------------------------------------------------------------------------------- /src/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chameleon 7 | 8 | <% if (NODE_ENV === 'development') { %> 9 | 10 | <% } %> 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/popup/popup.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueFeather from 'vue-feather'; 3 | import VueI18n from 'vue-i18n'; 4 | import App from './App'; 5 | import store from '../store'; 6 | import localeMessages from '../_locales'; 7 | import '../css/tailwind.css'; 8 | 9 | import PerfectScrollbar from 'vue2-perfect-scrollbar'; 10 | import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css'; 11 | 12 | Vue.use(PerfectScrollbar); 13 | Vue.use(VueFeather); 14 | Vue.use(VueI18n); 15 | 16 | const i18n = new VueI18n({ 17 | locale: browser.i18n.getUILanguage(), 18 | fallbackLocale: { 19 | 'pt-PT': ['pt_PT'], 20 | 'pt-BR': ['pt_BR'], 21 | 'zh-CN': ['zh_CN'], 22 | default: ['en'], 23 | }, 24 | messages: localeMessages, 25 | }); 26 | 27 | /* eslint-disable no-new */ 28 | new Vue({ 29 | el: '#app', 30 | store, 31 | i18n, 32 | render: h => h(App), 33 | }); 34 | -------------------------------------------------------------------------------- /src/store/actions.ts: -------------------------------------------------------------------------------- 1 | import * as mtypes from './mutation-types'; 2 | import webext from '../lib/webext'; 3 | 4 | export const changeProfile = ({ commit }, payload) => { 5 | commit(mtypes.CHANGE_PROFILE, payload); 6 | 7 | browser.runtime.sendMessage({ 8 | action: 'updateProfile', 9 | data: payload, 10 | }); 11 | }; 12 | 13 | export const changeSetting = ({ commit }, payload: any) => { 14 | commit(mtypes.CHANGE_SETTING, payload); 15 | 16 | if (payload[0].name === 'whitelist.enabledContextMenu') { 17 | webext.enableContextMenu(payload[0].value); 18 | } else if (payload[0].name === 'profile.interval.option') { 19 | browser.runtime.sendMessage({ 20 | action: 'reloadProfile', 21 | data: payload[0].value, 22 | }); 23 | } else if (payload[0].name === 'profile.showProfileOnIcon') { 24 | browser.runtime.sendMessage({ 25 | action: 'toggleBadgeText', 26 | data: payload[0].value, 27 | }); 28 | } else if ( 29 | [ 30 | 'headers.spoofAcceptLang.enabled', 31 | 'headers.spoofAcceptLang.value', 32 | 'options.blockMediaDevices', 33 | 'options.spoofMediaDevices', 34 | 'options.blockCSSExfil', 35 | 'options.limitHistory', 36 | 'options.protectKBFingerprint.enabled', 37 | 'options.protectKBFingerprint.delay', 38 | 'options.protectWinName', 39 | 'options.spoofAudioContext', 40 | 'options.spoofClientRects', 41 | 'options.spoofFontFingerprint', 42 | 'options.screenSize', 43 | 'options.timeZone', 44 | ].includes(payload[0].name) 45 | ) { 46 | window.setTimeout(async () => { 47 | if (payload[0].name === 'headers.spoofAcceptLang.enabled' || (['headers.spoofAcceptLang.value', 'options.timeZone'].includes(payload[0].name) && payload[0].value === 'ip')) { 48 | await browser.runtime.sendMessage({ 49 | action: 'reloadIPInfo', 50 | data: false, 51 | }); 52 | } else { 53 | await browser.runtime.sendMessage({ 54 | action: 'reloadInjectionScript', 55 | }); 56 | } 57 | }, 350); 58 | } else if (['headers.spoofIP.enabled', 'headers.spoofIP.option', 'headers.spoofIP.rangeFrom'].includes(payload[0].name)) { 59 | browser.runtime.sendMessage({ 60 | action: 'reloadSpoofIP', 61 | data: payload, 62 | }); 63 | } else if ( 64 | [ 65 | 'options.cookiePolicy', 66 | 'options.cookieNotPersistent', 67 | 'options.disableWebRTC', 68 | 'options.firstPartyIsolate', 69 | 'options.resistFingerprinting', 70 | 'options.trackingProtectionMode', 71 | 'options.webRTCPolicy', 72 | ].includes(payload[0].name) 73 | ) { 74 | webext.setBrowserConfig(payload[0].name, payload[0].value); 75 | } 76 | }; 77 | 78 | export const excludeProfile = ({ commit, state }, payload) => { 79 | if (typeof payload === 'string') { 80 | let profileIndex: number = state.excluded.indexOf(payload); 81 | if (profileIndex > -1) { 82 | state.excluded.splice(profileIndex, 1); 83 | } else { 84 | state.excluded.push(payload); 85 | } 86 | } else { 87 | // check if every profile is in excluded list 88 | let indexes = payload.map(p => state.excluded.indexOf(p)); 89 | indexes.sort((a, b) => b - a); 90 | 91 | for (let i = 0; i < indexes.length; i++) { 92 | if (indexes[i] > -1) state.excluded.splice(indexes[i], 1); 93 | } 94 | 95 | if (indexes.includes(-1)) { 96 | state.excluded = state.excluded.concat(payload); 97 | } 98 | } 99 | 100 | commit(mtypes.UPDATE_EXCLUSIONS, state.excluded); 101 | }; 102 | 103 | export const initialize = async ({ commit }) => { 104 | let settings: any = await webext.getSettings(null); 105 | 106 | browser.runtime.sendMessage({ 107 | action: 'init', 108 | }); 109 | 110 | commit(mtypes.INITIALIZE, settings); 111 | }; 112 | 113 | export const toggleChameleon = ({ commit }, payload) => { 114 | commit(mtypes.TOGGLE_CHAMELEON, payload); 115 | webext.enableChameleon(payload); 116 | }; 117 | 118 | export const toggleNotifications = ({ commit }, payload) => { 119 | commit(mtypes.TOGGLE_NOTIFICATIONS); 120 | }; 121 | 122 | export const toggleTheme = ({ commit }, payload) => { 123 | commit(mtypes.TOGGLE_THEME); 124 | }; 125 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import mutations from './mutations'; 5 | import * as actions from './actions'; 6 | 7 | Vue.use(Vuex); 8 | 9 | export default new Vuex.Store({ 10 | state: { 11 | config: { 12 | enabled: true, 13 | notificationsEnabled: false, 14 | theme: 'light', 15 | reloadIPStartupDelay: 0, 16 | }, 17 | excluded: [], 18 | headers: { 19 | blockEtag: false, 20 | enableDNT: false, 21 | referer: { 22 | disabled: false, 23 | xorigin: 0, 24 | trimming: 0, 25 | }, 26 | spoofAcceptLang: { 27 | enabled: false, 28 | value: 'default', 29 | }, 30 | spoofIP: { 31 | enabled: false, 32 | option: 0, 33 | rangeFrom: '', 34 | rangeTo: '', 35 | }, 36 | }, 37 | ipRules: [], 38 | profile: { 39 | selected: 'none', 40 | interval: { 41 | option: 0, 42 | min: 1, 43 | max: 1, 44 | }, 45 | showProfileOnIcon: true, 46 | }, 47 | options: { 48 | cookieNotPersistent: false, 49 | cookiePolicy: 'allow_all', 50 | blockMediaDevices: false, 51 | blockCSSExfil: false, 52 | disableWebRTC: false, 53 | firstPartyIsolate: false, 54 | limitHistory: false, 55 | protectKBFingerprint: { 56 | enabled: false, 57 | delay: 1, 58 | }, 59 | protectWinName: false, 60 | resistFingerprinting: false, 61 | screenSize: 'default', 62 | spoofAudioContext: false, 63 | spoofClientRects: false, 64 | spoofFontFingerprint: false, 65 | spoofMediaDevices: false, 66 | timeZone: 'default', 67 | trackingProtectionMode: 'always', 68 | webRTCPolicy: 'default', 69 | webSockets: 'allow_all', 70 | }, 71 | whitelist: { 72 | enabledContextMenu: false, 73 | defaultProfile: 'none', 74 | rules: [], 75 | }, 76 | }, 77 | mutations, 78 | actions, 79 | }); 80 | -------------------------------------------------------------------------------- /src/store/mutation-types.ts: -------------------------------------------------------------------------------- 1 | export const CHANGE_PROFILE: string = 'CHANGE_PROFILE'; 2 | export const CHANGE_SETTING: string = 'CHANGE_SETTING'; 3 | export const INITIALIZE: string = 'INITIALIZE'; 4 | export const TOGGLE_CHAMELEON: string = 'TOGGLE_CHAMELEON'; 5 | export const TOGGLE_NOTIFICATIONS: string = 'TOGGLE_NOTIFICATIONS'; 6 | export const TOGGLE_THEME: string = 'TOGGLE_THEME'; 7 | export const UPDATE_EXCLUSIONS: string = 'UPDATE_EXCLUSIONS'; 8 | -------------------------------------------------------------------------------- /src/store/mutations.ts: -------------------------------------------------------------------------------- 1 | import * as mtypes from './mutation-types'; 2 | import { stateMerge } from 'vue-object-merge'; 3 | 4 | export default { 5 | [mtypes.CHANGE_PROFILE](state, payload) { 6 | state.profile.selected = payload; 7 | }, 8 | [mtypes.CHANGE_SETTING](state, payload) { 9 | for (let i = 0; i < payload.length; i++) { 10 | let keys = payload[i].name.split('.'); 11 | let beforeLast = keys.slice(0, -1).reduce((o, i) => o[i], state); 12 | 13 | if (typeof payload[i].value != 'boolean') { 14 | if (!isNaN(Number(payload[i].value))) { 15 | payload[i].value = parseInt(payload[i].value, 10); 16 | } 17 | } 18 | 19 | beforeLast[keys.slice(-1).pop()] = payload[i].value; 20 | } 21 | }, 22 | [mtypes.INITIALIZE](state, payload) { 23 | stateMerge(state, payload); 24 | }, 25 | [mtypes.TOGGLE_CHAMELEON](state, payload) { 26 | state.config.enabled = payload; 27 | }, 28 | [mtypes.TOGGLE_NOTIFICATIONS](state, payload) { 29 | state.config.notificationsEnabled = !state.config.notificationsEnabled; 30 | }, 31 | [mtypes.TOGGLE_THEME](state, payload) { 32 | state.config.theme = state.config.theme == 'light' ? 'dark' : 'light'; 33 | }, 34 | [mtypes.UPDATE_EXCLUSIONS](state, payload) { 35 | state.excluded = payload; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | customForms: theme => ({ 4 | default: { 5 | checkbox: { 6 | borderColor: theme('colors.gray.500'), 7 | '&:focus': { 8 | boxShadow: '0 0 0 3px rgba(107, 142, 35, 0.5)', 9 | borderColor: theme('colors.primary'), 10 | }, 11 | }, 12 | input: { 13 | borderColor: theme('colors.gray.500'), 14 | paddingTop: theme('spacing.1'), 15 | paddingRight: theme('spacing.2'), 16 | paddingBottom: theme('spacing.1'), 17 | paddingLeft: theme('spacing.2'), 18 | '&:focus': { 19 | boxShadow: '0 0 0 3px rgba(107, 142, 35, 0.5)', 20 | borderColor: theme('colors.primary'), 21 | }, 22 | }, 23 | radio: { 24 | color: theme('colors.primary'), 25 | borderColor: theme('colors.gray.500'), 26 | '&:focus': { 27 | boxShadow: '0 0 0 3px rgba(107, 142, 35, 0.5)', 28 | borderColor: theme('colors.primary'), 29 | }, 30 | }, 31 | select: { 32 | borderColor: theme('colors.gray.500'), 33 | paddingTop: theme('spacing.1'), 34 | paddingRight: theme('spacing.2'), 35 | paddingBottom: theme('spacing.1'), 36 | paddingLeft: theme('spacing.2'), 37 | '&:focus': { 38 | boxShadow: '0 0 0 3px rgba(107, 142, 35, 0.5)', 39 | borderColor: theme('colors.primary'), 40 | }, 41 | }, 42 | }, 43 | }), 44 | extend: { 45 | colors: { 46 | primary: '#6b8e23', 47 | 'primary-soft': '#7aa329', 48 | dark: '#33313b', 49 | 'dark-fg': '#403E48', 50 | 'dark-fg-alt': '#4D4B55', 51 | 'dark-modal': 'rgba(51, 49, 59, .8)', 52 | light: '#fbfbfb', 53 | 'light-fg': '#e5e4e4', 54 | 'light-fg-alt': '#CECDCD', 55 | }, 56 | spacing: { 57 | '80': '20rem', 58 | }, 59 | fontSize: { 60 | mini: '.84rem', 61 | }, 62 | }, 63 | }, 64 | variants: { 65 | cursor: ['responsive', 'hover'], 66 | }, 67 | plugins: [require('@tailwindcss/custom-forms')], 68 | purge: false, 69 | }; 70 | -------------------------------------------------------------------------------- /tests/options.spec.js: -------------------------------------------------------------------------------- 1 | const { Builder, By, until } = require('selenium-webdriver'); 2 | const firefox = require('selenium-webdriver/firefox'); 3 | const extData = require('../package.json'); 4 | const path = require('path'); 5 | const languages = require('../src/lib/language'); 6 | 7 | const wait = async sec => { 8 | return new Promise(resolve => setTimeout(resolve, sec * 1000)); 9 | }; 10 | 11 | // default wait after changing settings 12 | // gives time for the extension changes to be updated 13 | const defaultExtWait = async () => { 14 | return wait(0.5); 15 | }; 16 | 17 | const MAIN_URL = 'http://chameleon1.test:3000'; 18 | const MAIN_URL2 = 'http://test.chameleon1.test:3000'; 19 | const EXT_URL = 'http://chameleon2.test:3000'; 20 | 21 | let driver, getWithWait; 22 | let POPUP_URL, OPTIONS_URL; 23 | 24 | jest.setTimeout(120000); 25 | 26 | describe('Test options', () => { 27 | beforeAll(async () => { 28 | driver = await new Builder() 29 | .forBrowser('firefox') 30 | .setFirefoxOptions( 31 | new firefox.Options() 32 | .setBinary('firefox-developer-edition') 33 | .setPreference('xpinstall.signatures.required', false) 34 | .setPreference('devtools.jsonview.enabled', false) 35 | .addExtensions(path.join(__dirname, `../dist-zip/${extData.name}-v${extData.version}.xpi`)) 36 | .headless() 37 | ) 38 | .build(); 39 | 40 | await driver.get('about:debugging#/runtime/this-firefox'); 41 | await driver.wait(until.elementLocated(By.css('.card.debug-target-item')), 5 * 1000); 42 | 43 | getWithWait = (driver => { 44 | return async url => { 45 | await driver.get(url); 46 | await defaultExtWait(); 47 | }; 48 | })(driver); 49 | 50 | const EXTENSION_URI = await driver.executeScript(` 51 | return Array 52 | .from(document.querySelectorAll('.card.debug-target-item')) 53 | .filter(e => e.querySelector('span.debug-target-item__name').title == "Chameleon")[0] 54 | .querySelector('.qa-manifest-url').href`); 55 | 56 | POPUP_URL = EXTENSION_URI.replace('manifest.json', 'popup/popup.html'); 57 | OPTIONS_URL = EXTENSION_URI.replace('manifest.json', 'options/options.html'); 58 | }); 59 | 60 | afterAll(async () => { 61 | await driver.quit(); 62 | }); 63 | 64 | beforeEach(async () => { 65 | await getWithWait(POPUP_URL); 66 | 67 | // enable chameleon before each test 68 | let enabled = await driver.executeScript(`return document.querySelector('#chameleonEnabled i').dataset['name'] === 'shield';`); 69 | if (!enabled) await driver.findElement(By.id('chameleonEnabled')).click(); 70 | await defaultExtWait(); 71 | 72 | await driver.findElement(By.id('optionsTab')).click(); 73 | }); 74 | 75 | test('should open about:config checklist', async () => { 76 | let tabs = await driver.getAllWindowHandles(); 77 | 78 | await driver.findElement(By.id('aboutConfigChecklist')).click(); 79 | 80 | let newTabs = await driver.getAllWindowHandles(); 81 | 82 | expect(newTabs.length).toBeGreaterThan(tabs.length); 83 | }); 84 | 85 | test('should enable first party isolation', async () => { 86 | await driver.findElement(By.id('standardTab')).click(); 87 | await driver.findElement(By.id('firstPartyIsolate')).click(); 88 | await defaultExtWait(); 89 | 90 | await driver.navigate().refresh(); 91 | await driver.findElement(By.id('optionsTab')).click(); 92 | await driver.findElement(By.id('standardTab')).click(); 93 | 94 | let selected = await driver.findElement(By.id('firstPartyIsolate')).isSelected(); 95 | 96 | expect(selected).toBe(true); 97 | }); 98 | 99 | test('should enable resist fingerprinting', async () => { 100 | await driver.findElement(By.id('standardTab')).click(); 101 | await driver.findElement(By.id('resistFingerprinting')).click(); 102 | await defaultExtWait(); 103 | 104 | await driver.navigate().refresh(); 105 | await driver.findElement(By.id('optionsTab')).click(); 106 | await driver.findElement(By.id('standardTab')).click(); 107 | 108 | let selected = await driver.findElement(By.css('#resistFingerprinting')).isSelected(); 109 | 110 | expect(selected).toBe(true); 111 | }); 112 | 113 | test('should disable webrtc', async () => { 114 | await getWithWait(MAIN_URL); 115 | 116 | let status = await driver.executeScript('return document.querySelector("#webrtcStatus").innerText;'); 117 | expect(status).toBe('OK'); 118 | 119 | await getWithWait(POPUP_URL); 120 | 121 | await driver.findElement(By.id('optionsTab')).click(); 122 | await driver.findElement(By.id('standardTab')).click(); 123 | await driver.findElement(By.id('disableWebRTC')).click(); 124 | await defaultExtWait(); 125 | 126 | await getWithWait(MAIN_URL); 127 | 128 | status = await driver.executeScript('return document.querySelector("#webrtcStatus").innerText;'); 129 | expect(status).toBe('ERROR'); 130 | 131 | // enable webrtc for next tests 132 | await getWithWait(POPUP_URL); 133 | 134 | await driver.findElement(By.id('optionsTab')).click(); 135 | await driver.findElement(By.id('standardTab')).click(); 136 | await driver.findElement(By.id('disableWebRTC')).click(); 137 | await defaultExtWait(); 138 | }); 139 | 140 | test('should change webrtc policy [default]', async () => { 141 | await driver.findElement(By.id('standardTab')).click(); 142 | await driver.findElement(By.css('#webRTCPolicy > option:nth-child(1)')).click(); 143 | await defaultExtWait(); 144 | 145 | await driver.navigate().refresh(); 146 | await driver.findElement(By.id('optionsTab')).click(); 147 | await driver.findElement(By.id('standardTab')).click(); 148 | 149 | let selected = await driver.findElement(By.css('#webRTCPolicy > option:nth-child(1)')).isSelected(); 150 | 151 | expect(selected).toBe(true); 152 | }); 153 | 154 | test('should change webrtc policy [public and private interface]', async () => { 155 | await driver.findElement(By.id('standardTab')).click(); 156 | await driver.findElement(By.css('#webRTCPolicy > option:nth-child(2)')).click(); 157 | await defaultExtWait(); 158 | 159 | await driver.navigate().refresh(); 160 | await driver.findElement(By.id('optionsTab')).click(); 161 | await driver.findElement(By.id('standardTab')).click(); 162 | 163 | let selected = await driver.findElement(By.css('#webRTCPolicy > option:nth-child(2)')).isSelected(); 164 | 165 | expect(selected).toBe(true); 166 | }); 167 | 168 | test('should change webrtc policy [only public interface]', async () => { 169 | await driver.findElement(By.id('standardTab')).click(); 170 | await driver.findElement(By.css('#webRTCPolicy > option:nth-child(3)')).click(); 171 | await defaultExtWait(); 172 | 173 | await driver.navigate().refresh(); 174 | await driver.findElement(By.id('optionsTab')).click(); 175 | await driver.findElement(By.id('standardTab')).click(); 176 | 177 | let selected = await driver.findElement(By.css('#webRTCPolicy > option:nth-child(3)')).isSelected(); 178 | 179 | expect(selected).toBe(true); 180 | }); 181 | 182 | test('should change webrtc policy [non-proxified UDP]', async () => { 183 | await driver.findElement(By.id('standardTab')).click(); 184 | await driver.findElement(By.css('#webRTCPolicy > option:nth-child(4)')).click(); 185 | await defaultExtWait(); 186 | 187 | await driver.navigate().refresh(); 188 | await driver.findElement(By.id('optionsTab')).click(); 189 | await driver.findElement(By.id('standardTab')).click(); 190 | 191 | let selected = await driver.findElement(By.css('#webRTCPolicy > option:nth-child(4)')).isSelected(); 192 | 193 | expect(selected).toBe(true); 194 | }); 195 | 196 | test('should change tracking protection mode [on]', async () => { 197 | await driver.findElement(By.id('standardTab')).click(); 198 | await driver.findElement(By.css('#trackingProtectionMode > option:nth-child(1)')).click(); 199 | await defaultExtWait(); 200 | 201 | await driver.navigate().refresh(); 202 | await driver.findElement(By.id('optionsTab')).click(); 203 | await driver.findElement(By.id('standardTab')).click(); 204 | 205 | let selected = await driver.findElement(By.css('#trackingProtectionMode > option:nth-child(1)')).isSelected(); 206 | 207 | expect(selected).toBe(true); 208 | }); 209 | 210 | test('should change tracking protection mode [off]', async () => { 211 | await driver.findElement(By.id('standardTab')).click(); 212 | await driver.findElement(By.css('#trackingProtectionMode > option:nth-child(2)')).click(); 213 | await defaultExtWait(); 214 | 215 | await driver.navigate().refresh(); 216 | await driver.findElement(By.id('optionsTab')).click(); 217 | await driver.findElement(By.id('standardTab')).click(); 218 | 219 | let selected = await driver.findElement(By.css('#trackingProtectionMode > option:nth-child(2)')).isSelected(); 220 | 221 | expect(selected).toBe(true); 222 | }); 223 | 224 | test('should change tracking protection mode [enabled in private browsing]', async () => { 225 | await driver.findElement(By.id('standardTab')).click(); 226 | await driver.findElement(By.css('#trackingProtectionMode > option:nth-child(3)')).click(); 227 | await defaultExtWait(); 228 | 229 | await driver.navigate().refresh(); 230 | await driver.findElement(By.id('optionsTab')).click(); 231 | await driver.findElement(By.id('standardTab')).click(); 232 | 233 | let selected = await driver.findElement(By.css('#trackingProtectionMode > option:nth-child(3)')).isSelected(); 234 | 235 | expect(selected).toBe(true); 236 | }); 237 | 238 | test('should change websockets [allow all]', async () => { 239 | await driver.findElement(By.id('standardTab')).click(); 240 | await driver.findElement(By.css('#websockets > option:nth-child(1)')).click(); 241 | await defaultExtWait(); 242 | 243 | await getWithWait(MAIN_URL); 244 | 245 | let wsStatus = await driver.executeScript('return document.querySelector("#wsStatus").innerText'); 246 | expect(wsStatus).toBe('OPEN'); 247 | }); 248 | 249 | test('should change websockets [allow all] [disabled]', async () => { 250 | await driver.findElement(By.id('homeTab')).click(); 251 | await driver.findElement(By.id('chameleonEnabled')).click(); 252 | await defaultExtWait(); 253 | 254 | await getWithWait(MAIN_URL); 255 | 256 | let wsStatus = await driver.executeScript('return document.querySelector("#wsStatus").innerText'); 257 | expect(wsStatus).toBe('OPEN'); 258 | }); 259 | 260 | test('should change websockets [block 3rd party]', async () => { 261 | await driver.findElement(By.id('standardTab')).click(); 262 | await driver.findElement(By.css('#websockets > option:nth-child(2)')).click(); 263 | await defaultExtWait(); 264 | 265 | await getWithWait(MAIN_URL); 266 | 267 | let wsStatus = await driver.executeScript('return document.querySelector("#wsStatus3rdParty").innerText'); 268 | expect(wsStatus).toBe('CLOSED'); 269 | }); 270 | 271 | test('should change websockets [block 3rd party] [disabled]', async () => { 272 | await driver.findElement(By.id('homeTab')).click(); 273 | await driver.findElement(By.id('chameleonEnabled')).click(); 274 | await defaultExtWait(); 275 | 276 | await getWithWait(MAIN_URL); 277 | 278 | let wsStatus = await driver.executeScript('return document.querySelector("#wsStatus3rdParty").innerText'); 279 | expect(wsStatus).toBe('OPEN'); 280 | }); 281 | 282 | test('should change websockets [block all]', async () => { 283 | await driver.findElement(By.id('standardTab')).click(); 284 | await driver.findElement(By.css('#websockets > option:nth-child(3)')).click(); 285 | await defaultExtWait(); 286 | 287 | await getWithWait(MAIN_URL); 288 | 289 | let wsStatus = await driver.executeScript('return document.querySelector("#wsStatus").innerText'); 290 | expect(wsStatus).toBe('CLOSED'); 291 | }); 292 | 293 | test('should change websockets [block all] [disabled]', async () => { 294 | await driver.findElement(By.id('homeTab')).click(); 295 | await driver.findElement(By.id('chameleonEnabled')).click(); 296 | await defaultExtWait(); 297 | 298 | await getWithWait(MAIN_URL); 299 | let wsStatus = await driver.executeScript('return document.querySelector("#wsStatus").innerText'); 300 | expect(wsStatus).toBe('OPEN'); 301 | }); 302 | 303 | test('should change cookie policy [allow all]', async () => { 304 | await driver.findElement(By.id('cookieTab')).click(); 305 | await driver.findElement(By.css('#cookiePolicy > option:nth-child(1)')).click(); 306 | await defaultExtWait(); 307 | 308 | await driver.navigate().refresh(); 309 | await driver.findElement(By.id('optionsTab')).click(); 310 | await driver.findElement(By.id('cookieTab')).click(); 311 | 312 | let selected = await driver.findElement(By.css('#cookiePolicy > option:nth-child(1)')).isSelected(); 313 | 314 | expect(selected).toBe(true); 315 | }); 316 | 317 | test('should change cookie policy [allow visited]', async () => { 318 | await driver.findElement(By.id('cookieTab')).click(); 319 | await driver.findElement(By.css('#cookiePolicy > option:nth-child(2)')).click(); 320 | await defaultExtWait(); 321 | 322 | await driver.navigate().refresh(); 323 | await driver.findElement(By.id('optionsTab')).click(); 324 | await driver.findElement(By.id('cookieTab')).click(); 325 | 326 | let selected = await driver.findElement(By.css('#cookiePolicy > option:nth-child(2)')).isSelected(); 327 | 328 | expect(selected).toBe(true); 329 | }); 330 | 331 | test('should change cookie policy [reject all]', async () => { 332 | await driver.findElement(By.id('cookieTab')).click(); 333 | await driver.findElement(By.css('#cookiePolicy > option:nth-child(3)')).click(); 334 | await defaultExtWait(); 335 | 336 | await driver.navigate().refresh(); 337 | await driver.findElement(By.id('optionsTab')).click(); 338 | await driver.findElement(By.id('cookieTab')).click(); 339 | 340 | let selected = await driver.findElement(By.css('#cookiePolicy > option:nth-child(3)')).isSelected(); 341 | 342 | expect(selected).toBe(true); 343 | }); 344 | 345 | test('should change cookie policy [reject third party]', async () => { 346 | await driver.findElement(By.id('cookieTab')).click(); 347 | await driver.findElement(By.css('#cookiePolicy > option:nth-child(4)')).click(); 348 | await defaultExtWait(); 349 | 350 | await driver.navigate().refresh(); 351 | await driver.findElement(By.id('optionsTab')).click(); 352 | await driver.findElement(By.id('cookieTab')).click(); 353 | 354 | let selected = await driver.findElement(By.css('#cookiePolicy > option:nth-child(4)')).isSelected(); 355 | 356 | expect(selected).toBe(true); 357 | }); 358 | 359 | test('should change cookie policy [reject trackers]', async () => { 360 | await driver.findElement(By.id('cookieTab')).click(); 361 | await driver.findElement(By.css('#cookiePolicy > option:nth-child(5)')).click(); 362 | await defaultExtWait(); 363 | 364 | await driver.navigate().refresh(); 365 | await driver.findElement(By.id('optionsTab')).click(); 366 | await driver.findElement(By.id('cookieTab')).click(); 367 | 368 | let selected = await driver.findElement(By.css('#cookiePolicy > option:nth-child(5)')).isSelected(); 369 | 370 | expect(selected).toBe(true); 371 | }); 372 | }); 373 | -------------------------------------------------------------------------------- /tests/server/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chameleon Test Page 6 | 7 | 8 | 9 |

Chameleon Test Page

10 | 11 |

Referer Test

12 | Test referer 13 | 14 |

WebRTC Test

15 |
WebRTC Connection
16 |

17 | 18 |

WebSocket Test

19 |
WebSocket Connection
20 |

CLOSED

21 |
WebSocket Connection (3rd Party)
22 |

CLOSED

23 | 24 |

Fingerprint using DynamicsCompressor (sum of buffer values):

25 |

26 |

Fingerprint using DynamicsCompressor (hash of full buffer):

27 |

28 |

Fingerprint using OscillatorNode:

29 |

30 |

Fingerprint using hybrid of OscillatorNode/DynamicsCompressor method:

31 |

32 | 33 | 180 | 181 | -------------------------------------------------------------------------------- /tests/server/sha1.js: -------------------------------------------------------------------------------- 1 | /* 2 | CryptoJS v3.1.2 3 | code.google.com/p/crypto-js 4 | (c) 2009-2013 by Jeff Mott. All rights reserved. 5 | code.google.com/p/crypto-js/wiki/License 6 | */ 7 | var CryptoJS = 8 | CryptoJS || 9 | (function(e, m) { 10 | var p = {}, 11 | j = (p.lib = {}), 12 | l = function() {}, 13 | f = (j.Base = { 14 | extend: function(a) { 15 | l.prototype = this; 16 | var c = new l(); 17 | a && c.mixIn(a); 18 | c.hasOwnProperty('init') || 19 | (c.init = function() { 20 | c.$super.init.apply(this, arguments); 21 | }); 22 | c.init.prototype = c; 23 | c.$super = this; 24 | return c; 25 | }, 26 | create: function() { 27 | var a = this.extend(); 28 | a.init.apply(a, arguments); 29 | return a; 30 | }, 31 | init: function() {}, 32 | mixIn: function(a) { 33 | for (var c in a) a.hasOwnProperty(c) && (this[c] = a[c]); 34 | a.hasOwnProperty('toString') && (this.toString = a.toString); 35 | }, 36 | clone: function() { 37 | return this.init.prototype.extend(this); 38 | }, 39 | }), 40 | n = (j.WordArray = f.extend({ 41 | init: function(a, c) { 42 | a = this.words = a || []; 43 | this.sigBytes = c != m ? c : 4 * a.length; 44 | }, 45 | toString: function(a) { 46 | return (a || h).stringify(this); 47 | }, 48 | concat: function(a) { 49 | var c = this.words, 50 | q = a.words, 51 | d = this.sigBytes; 52 | a = a.sigBytes; 53 | this.clamp(); 54 | if (d % 4) for (var b = 0; b < a; b++) c[(d + b) >>> 2] |= ((q[b >>> 2] >>> (24 - 8 * (b % 4))) & 255) << (24 - 8 * ((d + b) % 4)); 55 | else if (65535 < q.length) for (b = 0; b < a; b += 4) c[(d + b) >>> 2] = q[b >>> 2]; 56 | else c.push.apply(c, q); 57 | this.sigBytes += a; 58 | return this; 59 | }, 60 | clamp: function() { 61 | var a = this.words, 62 | c = this.sigBytes; 63 | a[c >>> 2] &= 4294967295 << (32 - 8 * (c % 4)); 64 | a.length = e.ceil(c / 4); 65 | }, 66 | clone: function() { 67 | var a = f.clone.call(this); 68 | a.words = this.words.slice(0); 69 | return a; 70 | }, 71 | random: function(a) { 72 | for (var c = [], b = 0; b < a; b += 4) c.push((4294967296 * e.random()) | 0); 73 | return new n.init(c, a); 74 | }, 75 | })), 76 | b = (p.enc = {}), 77 | h = (b.Hex = { 78 | stringify: function(a) { 79 | var c = a.words; 80 | a = a.sigBytes; 81 | for (var b = [], d = 0; d < a; d++) { 82 | var f = (c[d >>> 2] >>> (24 - 8 * (d % 4))) & 255; 83 | b.push((f >>> 4).toString(16)); 84 | b.push((f & 15).toString(16)); 85 | } 86 | return b.join(''); 87 | }, 88 | parse: function(a) { 89 | for (var c = a.length, b = [], d = 0; d < c; d += 2) b[d >>> 3] |= parseInt(a.substr(d, 2), 16) << (24 - 4 * (d % 8)); 90 | return new n.init(b, c / 2); 91 | }, 92 | }), 93 | g = (b.Latin1 = { 94 | stringify: function(a) { 95 | var c = a.words; 96 | a = a.sigBytes; 97 | for (var b = [], d = 0; d < a; d++) b.push(String.fromCharCode((c[d >>> 2] >>> (24 - 8 * (d % 4))) & 255)); 98 | return b.join(''); 99 | }, 100 | parse: function(a) { 101 | for (var c = a.length, b = [], d = 0; d < c; d++) b[d >>> 2] |= (a.charCodeAt(d) & 255) << (24 - 8 * (d % 4)); 102 | return new n.init(b, c); 103 | }, 104 | }), 105 | r = (b.Utf8 = { 106 | stringify: function(a) { 107 | try { 108 | return decodeURIComponent(escape(g.stringify(a))); 109 | } catch (c) { 110 | throw Error('Malformed UTF-8 data'); 111 | } 112 | }, 113 | parse: function(a) { 114 | return g.parse(unescape(encodeURIComponent(a))); 115 | }, 116 | }), 117 | k = (j.BufferedBlockAlgorithm = f.extend({ 118 | reset: function() { 119 | this._data = new n.init(); 120 | this._nDataBytes = 0; 121 | }, 122 | _append: function(a) { 123 | 'string' == typeof a && (a = r.parse(a)); 124 | this._data.concat(a); 125 | this._nDataBytes += a.sigBytes; 126 | }, 127 | _process: function(a) { 128 | var c = this._data, 129 | b = c.words, 130 | d = c.sigBytes, 131 | f = this.blockSize, 132 | h = d / (4 * f), 133 | h = a ? e.ceil(h) : e.max((h | 0) - this._minBufferSize, 0); 134 | a = h * f; 135 | d = e.min(4 * a, d); 136 | if (a) { 137 | for (var g = 0; g < a; g += f) this._doProcessBlock(b, g); 138 | g = b.splice(0, a); 139 | c.sigBytes -= d; 140 | } 141 | return new n.init(g, d); 142 | }, 143 | clone: function() { 144 | var a = f.clone.call(this); 145 | a._data = this._data.clone(); 146 | return a; 147 | }, 148 | _minBufferSize: 0, 149 | })); 150 | j.Hasher = k.extend({ 151 | cfg: f.extend(), 152 | init: function(a) { 153 | this.cfg = this.cfg.extend(a); 154 | this.reset(); 155 | }, 156 | reset: function() { 157 | k.reset.call(this); 158 | this._doReset(); 159 | }, 160 | update: function(a) { 161 | this._append(a); 162 | this._process(); 163 | return this; 164 | }, 165 | finalize: function(a) { 166 | a && this._append(a); 167 | return this._doFinalize(); 168 | }, 169 | blockSize: 16, 170 | _createHelper: function(a) { 171 | return function(c, b) { 172 | return new a.init(b).finalize(c); 173 | }; 174 | }, 175 | _createHmacHelper: function(a) { 176 | return function(b, f) { 177 | return new s.HMAC.init(a, f).finalize(b); 178 | }; 179 | }, 180 | }); 181 | var s = (p.algo = {}); 182 | return p; 183 | })(Math); 184 | (function() { 185 | var e = CryptoJS, 186 | m = e.lib, 187 | p = m.WordArray, 188 | j = m.Hasher, 189 | l = [], 190 | m = (e.algo.SHA1 = j.extend({ 191 | _doReset: function() { 192 | this._hash = new p.init([1732584193, 4023233417, 2562383102, 271733878, 3285377520]); 193 | }, 194 | _doProcessBlock: function(f, n) { 195 | for (var b = this._hash.words, h = b[0], g = b[1], e = b[2], k = b[3], j = b[4], a = 0; 80 > a; a++) { 196 | if (16 > a) l[a] = f[n + a] | 0; 197 | else { 198 | var c = l[a - 3] ^ l[a - 8] ^ l[a - 14] ^ l[a - 16]; 199 | l[a] = (c << 1) | (c >>> 31); 200 | } 201 | c = ((h << 5) | (h >>> 27)) + j + l[a]; 202 | c = 203 | 20 > a 204 | ? c + (((g & e) | (~g & k)) + 1518500249) 205 | : 40 > a 206 | ? c + ((g ^ e ^ k) + 1859775393) 207 | : 60 > a 208 | ? c + (((g & e) | (g & k) | (e & k)) - 1894007588) 209 | : c + ((g ^ e ^ k) - 899497514); 210 | j = k; 211 | k = e; 212 | e = (g << 30) | (g >>> 2); 213 | g = h; 214 | h = c; 215 | } 216 | b[0] = (b[0] + h) | 0; 217 | b[1] = (b[1] + g) | 0; 218 | b[2] = (b[2] + e) | 0; 219 | b[3] = (b[3] + k) | 0; 220 | b[4] = (b[4] + j) | 0; 221 | }, 222 | _doFinalize: function() { 223 | var f = this._data, 224 | e = f.words, 225 | b = 8 * this._nDataBytes, 226 | h = 8 * f.sigBytes; 227 | e[h >>> 5] |= 128 << (24 - (h % 32)); 228 | e[(((h + 64) >>> 9) << 4) + 14] = Math.floor(b / 4294967296); 229 | e[(((h + 64) >>> 9) << 4) + 15] = b; 230 | f.sigBytes = 4 * e.length; 231 | this._process(); 232 | return this._hash; 233 | }, 234 | clone: function() { 235 | var e = j.clone.call(this); 236 | e._hash = this._hash.clone(); 237 | return e; 238 | }, 239 | })); 240 | e.SHA1 = j._createHelper(m); 241 | e.HmacSHA1 = j._createHmacHelper(m); 242 | })(); 243 | -------------------------------------------------------------------------------- /tests/setup.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const expressWs = require('express-ws'); 4 | 5 | module.exports = async () => { 6 | const app = express(); 7 | expressWs(app); 8 | 9 | app.set('view engine', 'ejs'); 10 | app.set('views', path.join(__dirname, '/server')); 11 | app.use((req, res, next) => { 12 | res.header('Access-Control-Allow-Origin', '*'); 13 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); 14 | next(); 15 | }); 16 | app.use(express.static(path.join(__dirname, '/server'))); 17 | app.get('/', (req, res) => res.render('index')); 18 | app.get('/headers', (req, res) => res.send(req.headers)); 19 | app.get('/referer', (req, res) => res.render('index')); 20 | app.get('/etag', (req, res) => { 21 | res.setHeader('Etag', Math.random().toString(36)); 22 | res.send('OK'); 23 | }); 24 | app.ws('/echo', function(ws, req) { 25 | ws.on('message', function(msg) { 26 | ws.send(msg); 27 | }); 28 | }); 29 | 30 | global.__SERVER__ = app.listen(3000); 31 | }; 32 | -------------------------------------------------------------------------------- /tests/teardown.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | global.__SERVER__.close(); 3 | }; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "esModuleInterop": true, 5 | "sourceMap": true, 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "target": "es2017" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ejs = require('ejs'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | const ExtensionReloader = require('webpack-extension-reloader'); 6 | const { VueLoaderPlugin } = require('vue-loader'); 7 | const { version } = require('./package.json'); 8 | 9 | const config = { 10 | mode: process.env.NODE_ENV, 11 | context: __dirname + '/src', 12 | entry: { 13 | background: './background.ts', 14 | 'popup/popup': './popup/popup.ts', 15 | 'options/options': './options/options.ts', 16 | inject: './lib/inject.ts', 17 | }, 18 | output: { 19 | path: __dirname + '/dist', 20 | filename: '[name].js', 21 | }, 22 | resolve: { 23 | extensions: ['.js', '.ts', '.vue'], 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.vue$/, 29 | loaders: 'vue-loader', 30 | }, 31 | { 32 | test: /\.js$/, 33 | loader: 'babel-loader', 34 | exclude: /node_modules/, 35 | }, 36 | { 37 | test: /\.tsx?$/, 38 | exclude: [/node_modules/], 39 | use: { 40 | loader: 'ts-loader', 41 | options: { 42 | appendTsSuffixTo: [/\.vue$/], 43 | }, 44 | }, 45 | }, 46 | { 47 | test: /\.css$/, 48 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'], 49 | }, 50 | { 51 | test: /\.(png|jpg|jpeg|gif|svg|ico)$/, 52 | loader: 'file-loader', 53 | options: { 54 | name: '[name].[ext]', 55 | outputPath: '/images/', 56 | emitFile: false, 57 | }, 58 | }, 59 | { 60 | test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/, 61 | loader: 'file-loader', 62 | options: { 63 | name: '[name].[ext]', 64 | outputPath: '/fonts/', 65 | emitFile: false, 66 | }, 67 | }, 68 | ], 69 | }, 70 | plugins: [ 71 | new webpack.DefinePlugin({ 72 | global: 'window', 73 | }), 74 | new VueLoaderPlugin(), 75 | new MiniCssExtractPlugin({ 76 | filename: '[name].css', 77 | }), 78 | new CopyWebpackPlugin([ 79 | { from: 'icons', to: 'icons', ignore: ['icon.xcf'] }, 80 | { from: 'popup/popup.html', to: 'popup/popup.html', transform: transformHtml }, 81 | { from: 'options/options.html', to: 'options/options.html', transform: transformHtml }, 82 | { from: '_locales', to: '_locales' }, 83 | { 84 | from: 'manifest.json', 85 | to: 'manifest.json', 86 | transform: content => { 87 | const jsonContent = JSON.parse(content); 88 | 89 | if (config.mode === 'development') { 90 | jsonContent['content_security_policy'] = "script-src 'self' 'unsafe-eval'; object-src 'self'"; 91 | } 92 | 93 | return JSON.stringify(jsonContent, null, 2); 94 | }, 95 | }, 96 | ]), 97 | ], 98 | }; 99 | 100 | if (config.mode === 'production') { 101 | config.plugins = (config.plugins || []).concat([ 102 | new webpack.DefinePlugin({ 103 | 'process.env': { 104 | NODE_ENV: '"production"', 105 | }, 106 | }), 107 | ]); 108 | } 109 | 110 | if (process.env.HMR === 'true') { 111 | config.plugins = (config.plugins || []).concat([ 112 | new ExtensionReloader({ 113 | manifest: __dirname + '/src/manifest.json', 114 | }), 115 | ]); 116 | } 117 | 118 | function transformHtml(content) { 119 | return ejs.render(content.toString(), { 120 | ...process.env, 121 | }); 122 | } 123 | 124 | module.exports = config; 125 | --------------------------------------------------------------------------------