├── images
└── grey_128.png
├── assets
└── xswitch.sketch
├── src
├── window.d.ts
├── bootstrap.ts
├── pages
│ ├── options
│ │ ├── options.less
│ │ ├── options.vx
│ │ └── options.ts
│ └── xswitch
│ │ ├── xswitch.vx
│ │ ├── xswitch.less
│ │ └── xswitch.ts
├── router.ts
├── enums.ts
├── utils.ts
├── editor-config.ts
├── strip-json-comments.ts
├── constants.ts
├── background.ts
├── forward.ts
└── chrome-storage.ts
├── recore.config.js
├── pub.sh
├── .npmignore
├── .vscode
├── settings.json
└── tasks.json
├── .editorconfig
├── .travis.yml
├── options.html
├── .gitignore
├── XSwitch.html
├── index.html
├── tsconfig.json
├── manifest.json
├── tslint.json
├── LICENCE.md
├── lib
└── monaco-editor
│ └── min
│ └── vs
│ ├── language
│ └── json
│ │ ├── monaco.contribution.js
│ │ └── jsonMode.js
│ ├── loader.js
│ └── editor
│ └── editor.main.nls.js
├── package.json
├── __tests__
├── parse.spec.ts
└── index.spec.ts
├── readme.en_US.md
└── readme.md
/images/grey_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tankpt/xswitch/master/images/grey_128.png
--------------------------------------------------------------------------------
/assets/xswitch.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tankpt/xswitch/master/assets/xswitch.sketch
--------------------------------------------------------------------------------
/src/window.d.ts:
--------------------------------------------------------------------------------
1 | interface Window {
2 | require: any;
3 | editor: any;
4 | monaco: any;
5 | _forward: any;
6 | }
7 |
--------------------------------------------------------------------------------
/recore.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extraEntry: {
3 | 'background': './src/background.ts'
4 | },
5 | vendors: false,
6 | }
7 |
--------------------------------------------------------------------------------
/src/bootstrap.ts:
--------------------------------------------------------------------------------
1 | import { runApp } from '@ali/recore';
2 | import { PREFIX } from './constants';
3 |
4 | runApp({
5 | globalHelpers: { prefix: PREFIX },
6 | });
7 |
--------------------------------------------------------------------------------
/src/pages/options/options.less:
--------------------------------------------------------------------------------
1 | .options-container {
2 | width: 500px;
3 | margin: 0 auto;
4 | padding: 50px;
5 | }
6 |
7 | ul,
8 | li {
9 | list-style: none;
10 | }
11 |
--------------------------------------------------------------------------------
/pub.sh:
--------------------------------------------------------------------------------
1 |
2 | rm build/xswitch.css build/xswitch.js build/xswitch.js.map build/background.js.map build/forward.js.map
3 | rm xswitch.zip
4 | zip -r xswitch.zip build
5 | open https://chrome.google.com/webstore/developer/dashboard
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.iml
3 | .idea/
4 | .vscode/
5 | .xdev/
6 | package-lock.json
7 | *~
8 | ~*
9 | *.diff
10 | *.log
11 | *.patch
12 | *.bak
13 | .DS_Store
14 | Thumbs.db
15 | .project
16 | .*proj
17 | .svn/
18 | *.swp
19 |
--------------------------------------------------------------------------------
/src/pages/options/options.vx:
--------------------------------------------------------------------------------
1 |
2 |
3 | - Enable Clear Cache
4 | - Enable CORS
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "./node_modules/typescript/lib",
3 | "json.schemas": [
4 | {
5 | "fileMatch": [
6 | "/manifest.json"
7 | ],
8 | "url": "http://json.schemastore.org/chrome-manifest"
9 | }
10 | ]
11 | }
--------------------------------------------------------------------------------
/src/router.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | baseDir: './pages',
3 | exact: true,
4 | routes: [
5 | {
6 | path: '/options.html',
7 | main: './options',
8 | },
9 | {
10 | path: '/XSwitch.html',
11 | main: './xswitch',
12 | },
13 | {
14 | path: '/',
15 | main: './xswitch',
16 | },
17 | ],
18 | };
19 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Tab indentation
7 | [*]
8 | charset = utf-8
9 | end_of_line = lf
10 | indent_size = 2
11 | indent_style = space
12 | insert_final_newline = true
13 | trim_trailing_whitespace = true
14 |
15 | [*.md]
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '8'
4 | install:
5 | - npm i jest
6 | - npm i ts-jest
7 | - npm i typescript
8 | - npm i @types/jest
9 | - npm i @types/chrome
10 | - npm i @types/node
11 | - npm i @types/react
12 | - npm i coveralls
13 | script:
14 | - NODE_ENV=production npm test
15 | after_script:
16 | - NODE_ENV=production npm run ci
17 |
--------------------------------------------------------------------------------
/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | XSwitch
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/enums.ts:
--------------------------------------------------------------------------------
1 | export enum UrlType {
2 | REG = 'reg',
3 | STRING = 'string',
4 | }
5 |
6 | export enum Enabled {
7 | YES = 'enabled',
8 | NO = 'disabled',
9 | }
10 |
11 | export enum IconBackgroundColor {
12 | ON = '#1890ff',
13 | OFF = '#bfbfbf',
14 | ERROR = '#f5222d',
15 | }
16 |
17 | export enum BadgeText {
18 | ERROR = 'Error',
19 | OFF = 'OFF',
20 | ON = 'ON',
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { stripJsonComments } from './strip-json-comments';
2 | import { REG, EMPTY_STRING } from './constants';
3 |
4 | export function JSONC2JSON(jsonc: string): string {
5 | return stripJsonComments(jsonc)
6 | .replace(REG.WHITESPACE, EMPTY_STRING)
7 | .replace(REG.TRIM_JSON, ($0, $1, $2) => $2);
8 | }
9 |
10 | export function JSON_Parse(json: string, cb: (error: object | boolean, json?: object) => void): void {
11 | try {
12 | cb(false, JSON.parse(json));
13 | } catch (e) {
14 | cb(e);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | coverage/
3 | .idea/
4 | .vscode/
5 | .xdev/
6 | ~*
7 | package-lock.json
8 |
9 | # Packages #
10 | ############
11 | # it's better to unpack these files and commit the raw source
12 | # git has its own built in compression methods
13 | *.7z
14 | *.dmg
15 | *.gz
16 | *.iso
17 | *.jar
18 | *.rar
19 | *.tar
20 | *.zip
21 |
22 | # Logs and databases #
23 | ######################
24 | *.log
25 | *.sql
26 | *.sqlite
27 |
28 | # OS generated files #
29 | ######################
30 | .DS_Store
31 | *.swp
32 | .DS_Store?
33 | ._*
34 | .Spotlight-V100
35 | .Trashes
36 | ehthumbs.db
37 | Thumbs.db
38 |
--------------------------------------------------------------------------------
/XSwitch.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | xswitch
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | xswitch
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/editor-config.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DEFAULT_DATA,
3 | LANGUAGE_JSON,
4 | DEFAULT_FONT_FAMILY,
5 | SHOW_FOLDING_CONTROLS,
6 | } from './constants';
7 |
8 | export function getEditorConfig(value: string): object {
9 | return {
10 | value: value || DEFAULT_DATA,
11 | language: LANGUAGE_JSON,
12 |
13 | minimap: {
14 | enabled: false,
15 | },
16 | fontFamily: DEFAULT_FONT_FAMILY,
17 | fontSize: 13,
18 |
19 | contextmenu: false,
20 | scrollBeyondLastLine: false,
21 | folding: true,
22 | showFoldingControls: SHOW_FOLDING_CONTROLS,
23 |
24 | useTabStops: true,
25 | wordBasedSuggestions: true,
26 | quickSuggestions: true,
27 | suggestOnTriggerCharacters: true,
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "0.1.0",
5 | "command": "npm",
6 | "isShellCommand": true,
7 | "showOutput": "always",
8 | "suppressTaskName": true,
9 | "tasks": [
10 | {
11 | "taskName": "install",
12 | "args": ["install"]
13 | },
14 | {
15 | "taskName": "update",
16 | "args": ["update"]
17 | },
18 | {
19 | "taskName": "test",
20 | "args": ["run", "test"]
21 | },
22 | {
23 | "taskName": "build",
24 | "args": ["run", "watch"]
25 | }
26 | ]
27 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "target": "esnext",
5 | "lib": [
6 | "dom",
7 | "es5",
8 | "es2015",
9 | "es2016",
10 | "es2017"
11 | ],
12 | "sourceMap": true,
13 | "jsx": "react",
14 | "moduleResolution": "node",
15 | "noImplicitReturns": true,
16 | "noImplicitThis": true,
17 | "noImplicitAny": true,
18 | "strictNullChecks": true,
19 | "experimentalDecorators": true,
20 | "forceConsistentCasingInFileNames": true,
21 | "esModuleInterop": true
22 | },
23 | "include": [
24 | "./src/",
25 | "./typings/",
26 | "./test"
27 | ],
28 | "exclude": [
29 | "node_modules",
30 | "build",
31 | "dist"
32 | ],
33 | "types": [
34 | "jest",
35 | "node",
36 | "chrome",
37 | "react"
38 | ],
39 | "awesomeTypescriptLoaderOptions": {
40 | "transpileOnly" : true
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "XSwitch",
3 | "description": "XSwitch tools for proxy web request url, support reg",
4 | "short_name": "xs",
5 | "version": "1.15.9",
6 | "manifest_version": 2,
7 | "browser_action": {
8 | "default_icon": "images/grey_128.png",
9 | "default_title": "XSwitch",
10 | "default_popup": "XSwitch.html"
11 | },
12 | "permissions": [
13 | "webRequest",
14 | "storage",
15 | "webRequestBlocking",
16 | "browsingData",
17 | ""
18 | ],
19 | "icons": {
20 | "48": "images/grey_128.png",
21 | "128": "images/grey_128.png"
22 | },
23 | "commands": {
24 | "_execute_browser_action": {
25 | "suggested_key": {
26 | "windows": "Ctrl+Shift+X",
27 | "mac": "Command+Shift+X",
28 | "default": "Ctrl+Shift+X"
29 | }
30 | }
31 | },
32 | "options_page": "options.html",
33 | "background": {
34 | "scripts": ["background.min.js"]
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "error",
3 | "extends": ["tslint:latest"],
4 | "jsRules": {},
5 | "rules": {
6 | "quotemark": [true, "single"],
7 | "interface-name": false,
8 | "variable-name": false,
9 | "object-literal-sort-keys": false,
10 | "member-access": [true, "no-public"],
11 | "ordered-imports": [true, {
12 | "grouped-imports": true,
13 | "import-sources-order": "any",
14 | "named-imports-order": "any"
15 | }],
16 | "trailing-comma": [
17 | true,
18 | {
19 | "multiline": {
20 | "objects": "always",
21 | "arrays": "always",
22 | "imports": "always",
23 | "exports": "always",
24 | "functions": "never",
25 | "typeLiterals": "ignore"
26 | },
27 | "esSpecCompliant": true
28 | }
29 | ],
30 | "prefer-for-of": false,
31 | "no-namespace": false
32 | },
33 | "rulesDirectory": []
34 | }
35 |
--------------------------------------------------------------------------------
/src/pages/options/options.ts:
--------------------------------------------------------------------------------
1 | import { ViewController, observable, inject } from '@ali/recore';
2 | import { Checkbox } from 'antd';
3 | import { getOptions, setOptions } from '../../chrome-storage';
4 | import { Enabled } from '../../enums';
5 | import './options.less';
6 | @inject({
7 | components: { Checkbox },
8 | })
9 | export default class Options extends ViewController {
10 | @observable
11 | clearCacheEnabled = false;
12 |
13 | @observable
14 | corsEnabled = false;
15 |
16 | setOptionStorage() {
17 | setOptions({
18 | clearCacheEnabled: this.clearCacheEnabled,
19 | corsEnabled: this.corsEnabled,
20 | });
21 | }
22 |
23 | async $init() {
24 | this.clearCacheEnabled = (await getOptions()).clearCacheEnabled !== Enabled.NO;
25 | this.corsEnabled = (await getOptions()).corsEnabled !== Enabled.NO;
26 | }
27 |
28 | setClearCacheEnabled() {
29 | this.clearCacheEnabled = !this.clearCacheEnabled;
30 | this.setOptionStorage();
31 | }
32 |
33 | setCorsEnabled() {
34 | this.corsEnabled = !this.corsEnabled;
35 | this.setOptionStorage();
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LICENCE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 yize
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/pages/xswitch/xswitch.vx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 | {item.name}
6 |
7 |
8 |
9 |
10 |
18 |
19 |
20 |
21 |
22 |
23 |
32 |
--------------------------------------------------------------------------------
/lib/monaco-editor/min/vs/language/json/monaco.contribution.js:
--------------------------------------------------------------------------------
1 | /*!-----------------------------------------------------------------------------
2 | * Copyright (c) Microsoft Corporation. All rights reserved.
3 | * monaco-json version: 2.2.0(370169f666a52e1b91623841799be4eab9204094)
4 | * Released under the MIT license
5 | * https://github.com/Microsoft/monaco-json/blob/master/LICENSE.md
6 | *-----------------------------------------------------------------------------*/
7 | define("vs/language/json/monaco.contribution",["require","exports"],function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var o=monaco.Emitter,n=function(){function e(e,n){this._onDidChange=new o,this._languageId=e,this.setDiagnosticsOptions(n)}return Object.defineProperty(e.prototype,"onDidChange",{get:function(){return this._onDidChange.event},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"languageId",{get:function(){return this._languageId},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"diagnosticsOptions",{get:function(){return this._diagnosticsOptions},enumerable:!0,configurable:!0}),e.prototype.setDiagnosticsOptions=function(e){this._diagnosticsOptions=e||Object.create(null),this._onDidChange.fire(this)},e}(),i=new(e.LanguageServiceDefaultsImpl=n)("json",{validate:!0,allowComments:!0,schemas:[]});monaco.languages.json={jsonDefaults:i},monaco.languages.register({id:"json",extensions:[".json",".bowerrc",".jshintrc",".jscsrc",".eslintrc",".babelrc"],aliases:["JSON","json"],mimetypes:["application/json"]}),monaco.languages.onLanguage("json",function(){monaco.Promise.wrap(new Promise(function(e,n){t(["./jsonMode"],e,n)})).then(function(e){return e.setupMode(i)})})});
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xswitch",
3 | "description": "A proxy tool based on Chrome.extensions",
4 | "author": "yize",
5 | "repository": {
6 | "type": "git",
7 | "url": "git@github.com:yize/xswitch.git"
8 | },
9 | "scripts": {
10 | "start": "nowa2 start",
11 | "build": "nowa2 build && cp -rf images lib XSwitch.html options.html manifest.json build",
12 | "pub": "npm run build && npm t && sh pub.sh",
13 | "test": "jest",
14 | "ci": "jest --coverage && cat ./coverage/lcov.info | coveralls"
15 | },
16 | "keywords": [
17 | "xswitch",
18 | "chrome",
19 | "urlProxy",
20 | "proxy",
21 | "xproxy"
22 | ],
23 | "license": "MIT",
24 | "dependencies": {
25 | "@ali/recore": "^1.0.10",
26 | "antd": "^3.9.2"
27 | },
28 | "devDependencies": {
29 | "@ali/nowa-recore-solution": "^1.0.0",
30 | "@ali/recore-loader": "^1.0.0",
31 | "@nowa/cli": "^0.6.0",
32 | "@types/chrome": "^0.0.73",
33 | "@types/jest": "^23.3.2",
34 | "@types/node": "^10.9.4",
35 | "@types/react": "^16",
36 | "coveralls": "^3.0.2",
37 | "css-loader": "^1.0.0",
38 | "html-webpack-plugin": "^3.2.0",
39 | "jest": "^23.6.0",
40 | "mini-css-extract-plugin": "^0.4.2",
41 | "monaco": "^1.201704190613.0+9ac64297a3b2ace5240299ba54b03f5029378397",
42 | "ts-jest": "^23.1.4",
43 | "ts-loader": "~5.0.0",
44 | "typescript": "~3.0.3",
45 | "webpack": "~4.17.2",
46 | "webpack-cli": "~3.1.0",
47 | "webpack-merge": "~4.1.4"
48 | },
49 | "jest": {
50 | "moduleFileExtensions": [
51 | "ts",
52 | "tsx",
53 | "js"
54 | ],
55 | "transform": {
56 | "^.+\\.(ts|tsx)$": "ts-jest"
57 | },
58 | "globals": {
59 | "ts-jest": {
60 | "tsConfig": "tsconfig.json"
61 | }
62 | },
63 | "testMatch": [
64 | "**/__tests__/*.+(ts|tsx|js)"
65 | ]
66 | },
67 | "nowa": {
68 | "solution": "@ali/nowa-recore-solution"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/pages/xswitch/xswitch.less:
--------------------------------------------------------------------------------
1 | ::-webkit-scrollbar {
2 | display: none;
3 | }
4 |
5 | ::-webkit-scrollbar-thumb {
6 | display: none;
7 | }
8 |
9 | .xswitch-wrapper {
10 | display: flex;
11 | overflow: hidden;
12 | .xswitch-tabs {
13 | padding: 0;
14 | width: 200px;
15 | white-space: nowrap;
16 | overflow: auto;
17 | margin-right: -1px;
18 | z-index: 2;
19 | margin-bottom: 0;
20 | padding-bottom: 44px;
21 | flex: 1;
22 | li {
23 | padding: 5px 10px;
24 | line-height: 40px;
25 | border: 1px solid transparent;
26 | cursor: pointer;
27 | display: flex;
28 | align-items: center;
29 | .delete-icon {
30 | display: none;
31 | opacity: 0;
32 | }
33 | .label {
34 | flex: 1;
35 | overflow: hidden;
36 | }
37 | &:hover {
38 | background: #efefef;
39 | .delete-icon {
40 | display: inline-block;
41 | opacity: 0.5;
42 | &:hover {
43 | opacity: 1;
44 | }
45 | }
46 | }
47 | &:first-of-type {
48 | border-top: 0;
49 | .delete-icon {
50 | display: none !important;
51 | }
52 | }
53 | &.editing {
54 | border-color: #bfbfbf #fff #bfbfbf transparent;
55 | }
56 | }
57 | }
58 | .xswitch-new-item-container {
59 | padding: 10px;
60 | z-index: 3;
61 | background: #fff;
62 | width: 100%;
63 | position: relative;
64 | .confirm-button{
65 | position: absolute;
66 | right: 15px;
67 | top: 15px;
68 | cursor: pointer;
69 | }
70 | }
71 | }
72 |
73 | .xswitch-container {
74 | width: 100%;
75 | min-width: 600px;
76 | min-height: 600px;
77 | height: 100vh;
78 | }
79 |
80 | .xswitch-left-area{
81 | display: flex;
82 | align-items: baseline;
83 | flex-direction: column;
84 | height: 100vh;
85 | border-right: 1px solid #bfbfbf;
86 | }
87 |
88 | .toolbar-area {
89 | width: 120px;
90 | position: fixed;
91 | right: 20px;
92 | top: 13px;
93 | z-index: 1000;
94 | cursor: pointer;
95 | display: flex;
96 | flex-direction: row;
97 | justify-content: space-around;
98 | }
99 |
100 | .switch-control {
101 | width: 50px;
102 | }
103 |
104 | .xswitch-icon {
105 | width: 22px;
106 | opacity: 0.5;
107 | &:hover {
108 | opacity: 1;
109 | }
110 | }
--------------------------------------------------------------------------------
/src/strip-json-comments.ts:
--------------------------------------------------------------------------------
1 | import { EMPTY_STRING } from './constants';
2 |
3 | // https://github.com/sindresorhus/strip-json-comments
4 | const singleComment = 1;
5 | const multiComment = 2;
6 | const stripWithoutWhitespace = (): string => EMPTY_STRING;
7 | const stripWithWhitespace = (str: string, start: number, end: number): string =>
8 | str.slice(start, end).replace(/\S/g, ' ');
9 |
10 | interface IStripOptions {
11 | whitespace?: boolean;
12 | }
13 |
14 | export function stripJsonComments(str: string, opts?: IStripOptions): string {
15 | opts = opts || {};
16 |
17 | const strip =
18 | opts.whitespace === false ? stripWithoutWhitespace : stripWithWhitespace;
19 |
20 | let insideString: boolean = false;
21 | let insideComment: number | boolean = false;
22 | let offset: number = 0;
23 | let ret: string = EMPTY_STRING;
24 |
25 | for (let i: number = 0; i < str.length; i++) {
26 | const currentChar = str[i];
27 | const nextChar = str[i + 1];
28 |
29 | if (!insideComment && currentChar === '"') {
30 | const escaped = str[i - 1] === '\\' && str[i - 2] !== '\\';
31 | if (!escaped) {
32 | insideString = !insideString;
33 | }
34 | }
35 |
36 | if (insideString) {
37 | continue;
38 | }
39 |
40 | if (!insideComment && currentChar + nextChar === '//') {
41 | ret += str.slice(offset, i);
42 | offset = i;
43 | insideComment = singleComment;
44 | i++;
45 | } else if (
46 | insideComment === singleComment &&
47 | currentChar + nextChar === '\r\n'
48 | ) {
49 | i++;
50 | insideComment = false;
51 | ret += strip(str, offset, i);
52 | offset = i;
53 | continue;
54 | } else if (insideComment === singleComment && currentChar === '\n') {
55 | insideComment = false;
56 | ret += strip(str, offset, i);
57 | offset = i;
58 | } else if (!insideComment && currentChar + nextChar === '/*') {
59 | ret += str.slice(offset, i);
60 | offset = i;
61 | insideComment = multiComment;
62 | i++;
63 | continue;
64 | } else if (
65 | insideComment === multiComment &&
66 | currentChar + nextChar === '*/'
67 | ) {
68 | i++;
69 | insideComment = false;
70 | ret += strip(str, offset, i + 1);
71 | offset = i + 1;
72 | continue;
73 | }
74 | }
75 |
76 | return (
77 | ret + (insideComment ? strip(str.substr(offset), 0, 0) : str.substr(offset))
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/__tests__/parse.spec.ts:
--------------------------------------------------------------------------------
1 | import { REG, EMPTY_STRING } from '../src/constants';
2 | import { stripJsonComments } from '../src/strip-json-comments';
3 |
4 | const replace = (jsonc: string): string => {
5 | try {
6 | return JSON.parse(
7 | stripJsonComments(jsonc)
8 | .replace(REG.WHITESPACE, EMPTY_STRING)
9 | .replace(REG.TRIM_JSON, ($0, $1, $2) => $2)
10 | );
11 | } catch (e) {
12 | console.log(
13 | stripJsonComments(jsonc)
14 | .replace(REG.WHITESPACE, EMPTY_STRING)
15 | .replace(REG.TRIM_JSON, ($0, $1, $2) => $2)
16 | );
17 | return 'parsed error';
18 | }
19 | };
20 |
21 | describe('parse', () => {
22 | test('parse pure JSON', () => {
23 | const jsonString = `{
24 | "proxy": [
25 | [
26 | "a.com",
27 | "b.com"
28 | ]
29 | ]
30 | }`;
31 |
32 | expect(replace(jsonString)).not.toEqual('parsed error');
33 | });
34 |
35 | test('parse pure JSON with comma', () => {
36 | const jsonString = `{
37 | "proxy": [
38 | [
39 | "a.com",
40 | "b.com",
41 | ],,,,,
42 | ],
43 | }`;
44 |
45 | expect(replace(jsonString)).toEqual({ proxy: [['a.com', 'b.com']] });
46 | });
47 | test('parse urls with ?? ,', () => {
48 | const jsonString = `{
49 | "proxy": [
50 | [
51 | "a.com??a.js,b.js",
52 | "b.com??a.js,b.js",
53 | ],
54 | ]
55 | }`;
56 |
57 | expect(replace(jsonString)).toEqual({
58 | proxy: [['a.com??a.js,b.js', 'b.com??a.js,b.js']]
59 | });
60 | });
61 |
62 | test('parse urls with ?? with comments,', () => {
63 | const jsonString = `{
64 | "proxy": [
65 | [
66 | "a.com??a.js,b.js",
67 | //
68 | "b.com??a.js,b.js",
69 | ],
70 | ]
71 | }`;
72 |
73 | expect(replace(jsonString)).toEqual({
74 | proxy: [['a.com??a.js,b.js', 'b.com??a.js,b.js']]
75 | });
76 | });
77 |
78 | test('parse urls with ?? with comments,', () => {
79 | const jsonString = `{
80 | "proxy": [
81 | // jQuery
82 | [
83 | // jQuery
84 | "a.com??a.js,b.js",
85 | //
86 | "b.com??a.js,b.js",
87 | // jQuery
88 | ],
89 | // jQuery
90 | [
91 | "jQuery",
92 | // jQuery
93 | ,"jQuery.min.js"
94 | // jQuery
95 | ]
96 | ]
97 | }`;
98 |
99 | expect(replace(jsonString)).toEqual({
100 | proxy: [
101 | ['a.com??a.js,b.js', 'b.com??a.js,b.js'],
102 | ['jQuery', 'jQuery.min.js']
103 | ]
104 | });
105 | });
106 |
107 | test('parse reg rules', () => {
108 | const jsonString = `{
109 | "proxy": [
110 | [
111 | "(.*)a.com??a.js,b.js",
112 | "$1b.com??a.js,b.js",
113 | ],
114 | ]
115 | }`;
116 |
117 | expect(replace(jsonString)).toEqual({
118 | proxy: [['(.*)a.com??a.js,b.js', '$1b.com??a.js,b.js']]
119 | });
120 | });
121 | });
122 |
--------------------------------------------------------------------------------
/readme.en_US.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | [中文版](./readme.md)
8 |
9 | ## XSwitch
10 |
11 | [![Chrome version][badge-cws]][link-cws] [![Chrome version][badge-cws-count]][link-cws] [![Build Status][badge-travis]][link-travis] [![Coverage Status][badge-coverage]][link-coverage] [![license][badge-license]][link-xswitch]
12 |
13 | A [Chrome Extension][link-cws] for redirecting/forwarding request urls.
14 |
15 |
16 |
17 |
18 |
19 | ## Features
20 |
21 | - [x] Redirect `request.url`
22 | - [x] Global switch control
23 | - [x] Disable browser cache
24 | - [x] JSON comments
25 | - [x] Rule suggestions
26 | - [x] CORS
27 | - [x] CORS & browser cache control
28 | - [x] Rules Grouping
29 |
30 | ## Usage
31 |
32 | 更多说明:[https://yuque.com/jiushen/blog/xswitch-readme](https://yuque.com/jiushen/blog/xswitch-readme)
33 |
34 | Rules will be executed in order before all requests are initiated.
35 |
36 | ```js
37 | {
38 | // proxyRules
39 | "proxy": [
40 | [
41 | "//alinw.alicdn.com/platform/daily-test/isDaily.js",
42 | "//alinw.alicdn.com/platform/daily-test/isDaily.json"
43 | ],
44 | // string replace, global mode
45 | [
46 | "alinw",
47 | "g"
48 | ]
49 | // replace all x.min to x
50 | [
51 | ".min",
52 | ""
53 | ],
54 | // use reg
55 | [
56 | "(.*)/platform/daily-test/(.*).js$",
57 | "http://127.0.0.1:3000/daily-test/$1.js"
58 | ],
59 | // replace to inline JavaScript
60 | [
61 | "https://alinw.alicdn.com/platform/daily-test/isDaily.js",
62 | "data:text/javascript,window.__isDaily = true;"
63 | ]
64 | ],
65 | // urls that want CORS
66 | "cors": [
67 | "cors.a.com",
68 | "(.*).b.com"
69 | ]
70 | }
71 | ```
72 |
73 | ## License
74 |
75 | [MIT](https://opensource.org/licenses/MIT) © [yize.shc](https://nsole.co)
76 |
77 | [link-xswitch]: https://github.com/yize/xswitch
78 | [link-cws]: https://chrome.google.com/webstore/detail/xswitch/idkjhjggpffolpidfkikidcokdkdaogg
79 | [link-me]: https://github.com/Microsoft/monaco-editor
80 | [link-travis]: https://travis-ci.org/yize/xswitch
81 | [link-coverage]: https://coveralls.io/github/yize/xswitch?branch=master
82 | [badge-travis]: https://travis-ci.org/yize/xswitch.svg?branch=master
83 | [badge-coverage]: https://coveralls.io/repos/github/yize/xswitch/badge.svg?branch=master
84 | [badge-license]: https://img.shields.io/github/license/yize/xswitch.svg
85 | [badge-cws]: https://img.shields.io/chrome-web-store/v/idkjhjggpffolpidfkikidcokdkdaogg.svg?label=chrome
86 | [badge-cws-count]: https://img.shields.io/chrome-web-store/users/idkjhjggpffolpidfkikidcokdkdaogg.svg
87 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | [English](./readme.en_US.md)
8 |
9 | ## XSwitch
10 |
11 | [![Chrome version][badge-cws]][link-cws] [![Chrome version][badge-cws-count]][link-cws] [![Build Status][badge-travis]][link-travis] [![Coverage Status][badge-coverage]][link-coverage] [![license][badge-license]][link-xswitch]
12 |
13 | 一个用来做请求链接转发的 [Chrome 浏览器插件][link-cws],因为采用的是浏览器原生 `API`,安全性和性能能得到保障。
14 |
15 | [](https://www.youtube.com/watch?v=--gQM3ysCzc)
16 |
17 | [优酷视频介绍](https://v.youku.com/v_show/id_XMzgyNDgwODAwNA==.html)
18 |
19 | ## 功能
20 |
21 | - [x] 请求地址转发
22 | - [x] 全局插件启用开关
23 | - [x] 可禁用浏览器缓存
24 | - [x] 可在 JSON 中写注释
25 | - [x] 自动补全
26 | - [x] 支持 CORS,支持 withCredentials
27 | - [x] 跨域和缓存禁用键
28 | - [x] 分组规则
29 |
30 | ## 用法
31 |
32 | 所有的规则,会按照定义的顺序从前往后执行,即使匹配到了规则,也会继续往下匹配。
33 |
34 | 小提示:把 `HTTPS` 的链接转发到 `http://127.0.0.1` 下,浏览器不会出安全提示。如果之前习惯用 `localhost` 的同学,可以尝试下。
35 |
36 | ```js
37 | {
38 | // 转发规则
39 | "proxy": [
40 | [
41 | "//alinw.alicdn.com/platform/daily-test/isDaily.js", // 匹配 URL
42 | "//alinw.alicdn.com/platform/daily-test/isDaily.json" // 替换成这个 URL
43 | ],
44 | // 字符串替换,会全局匹配
45 | [
46 | "alinw",
47 | "g"
48 | ]
49 | // 把链接里所有的 .min 替换掉
50 | // [
51 | // ".min",
52 | // ""
53 | // ],
54 | // 正则
55 | // [
56 | // "(.*)/platform/daily-test/(.*).js$",
57 | // "http://127.0.0.1:3000/daily-test/$1.js"
58 | // ],
59 | // 直接转换成 inline 模式的 JavaScript
60 | // [
61 | // "https://alinw.alicdn.com/platform/daily-test/isDaily.js",
62 | // "data:text/javascript,window.__isDaily = true;"
63 | // ]
64 | ],
65 | // 希望开启 CORS 跨域的链接
66 | "cors": [
67 | "cors.a.com",
68 | "(.*).b.com"
69 | ]
70 | }
71 | ```
72 |
73 | 更多说明:[https://yuque.com/jiushen/blog/xswitch-readme](https://yuque.com/jiushen/blog/xswitch-readme)
74 |
75 | - 访问 [https://alinw.alicdn.com/platform/daily-test/isDaily.js](https://alinw.alicdn.com/platform/daily-test/isDaily.js)
76 | - 最终, 你的 URL 会被改写成 [https://g.alicdn.com/platform/daily-test/isDaily.json](https://g.alicdn.com/platform/daily-test/isDaily.json)
77 |
78 | ## License
79 |
80 | [MIT](https://opensource.org/licenses/MIT) © [yize.shc](https://nsole.co)
81 |
82 | [link-xswitch]: https://github.com/yize/xswitch
83 | [link-cws]: https://chrome.google.com/webstore/detail/xswitch/idkjhjggpffolpidfkikidcokdkdaogg
84 | [link-me]: https://github.com/Microsoft/monaco-editor
85 | [link-travis]: https://travis-ci.org/yize/xswitch
86 | [link-coverage]: https://coveralls.io/github/yize/xswitch?branch=master
87 | [badge-travis]: https://travis-ci.org/yize/xswitch.svg?branch=master
88 | [badge-coverage]: https://coveralls.io/repos/github/yize/xswitch/badge.svg?branch=master
89 | [badge-license]: https://img.shields.io/github/license/yize/xswitch.svg
90 | [badge-cws]: https://img.shields.io/chrome-web-store/v/idkjhjggpffolpidfkikidcokdkdaogg.svg?label=chrome
91 | [badge-cws-count]: https://img.shields.io/chrome-web-store/users/idkjhjggpffolpidfkikidcokdkdaogg.svg
92 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const REG = {
2 | TRIM_JSON: /(,+)([^a-z0-9["])/gi,
3 | CHROME_EXTENSION: /^chrome-extension:\/\//i,
4 | // support [ ] ( ) \ * ^ $
5 | FORWARD: /\\|\[|]|\(|\)|\*|\$|\^/i,
6 | WHITESPACE: /\s+/g,
7 | X_HEADER: /^x-/,
8 | };
9 |
10 | export const ALL_URLS = '';
11 | export const BLOCKING = 'blocking';
12 | export const REQUEST_HEADERS = 'requestHeaders';
13 | export const RESPONSE_HEADERS = 'responseHeaders';
14 | export const DEFAULT_CREDENTIALS_RESPONSE_HEADERS =
15 | 'Content-Type, access-control-allow-headers, Authorization, X-Requested-With, X-Referer';
16 | export const CORS = {
17 | METHODS: 'access-control-allow-methods',
18 | CREDENTIALS: 'access-control-allow-credentials',
19 | ORIGIN: 'access-control-allow-origin',
20 | HEADERS: 'access-control-allow-headers',
21 | };
22 | export const ACCESS_CONTROL_REQUEST_HEADERS = 'access-control-request-headers';
23 | export const DEFAULT_CORS_ORIGIN = '*';
24 | export const DEFAULT_CORS_METHODS = '*';
25 | export const DEFAULT_CORS_CREDENTIALS = 'true';
26 | export const ORIGIN = 'origin';
27 | /**
28 | * Disabled storage key
29 | */
30 | export const DISABLED = 'disabled';
31 | /**
32 | * pure JSON storage key
33 | */
34 | export const JSON_CONFIG = 'config';
35 | /**
36 | * JSON with comments storage key
37 | */
38 | export const JSONC_CONFIG = 'config_for_shown';
39 |
40 | export const EDITING_CONFIG_KEY = 'config_editing_key';
41 | export const TAB_LIST = 'tab_list';
42 | export const ACTIVE_KEYS = 'active_keys';
43 | export const CLEAR_CACHE_ENABLED = 'clearCacheEnabled';
44 | export const CORS_STORAGE = 'cors';
45 | export const CORS_ENABLED_STORAGE_KEY = 'corsEnabled';
46 | export const PROXY_STORAGE_KEY = 'proxy';
47 | export const MILLISECONDS_PER_WEEK = 1000 * 60 * 60 * 24 * 7;
48 | export const RULE = 'rule';
49 | export const LANGUAGE_JSON = 'json';
50 | export const CHANGE = 'change';
51 | export const DOM_CONTENT_LOADED = 'DOMContentLoaded';
52 | export const SWITCH_DOM_ID = 'J_Switch';
53 | export const SWITCH_INNER_DOM_ID = 'J_SwitchInner';
54 | export const SWITCH_AREA_DOM_ID = 'J_SwitchArea';
55 | export const NEW_TAB_DOM_ID = 'J_OpenInNewTab';
56 | export const OPEN_README_DOM_ID = 'J_OpenReadme';
57 | export const CONTAINER_DOM_ID = 'J_Container';
58 | export const STATUS_DOM_ID = 'J_Status';
59 | export const CLEAR_CACHE_ENABLED_DOM_ID = 'J_ClearCacheEnabled';
60 | export const CORS_ENABLED_DOM_ID = 'J_CorsEnabled';
61 | export const SWITCH_CHECKED_CLASSNAME = 'ant-switch-checked';
62 | export const POPUP_HTML_PATH = 'XSwitch.html';
63 | export const PREFIX = process.env.NODE_ENV !== 'production' ? '/build/' : './';
64 | export const MONACO_VS_PATH = process.env.NODE_ENV !== 'production'
65 | ? '/build/lib/monaco-editor/min/vs'
66 | : './lib/monaco-editor/min/vs';
67 | export const MONACO_CONTRIBUTION_PATH = 'vs/language/json/monaco.contribution';
68 | export const HELP_URL = 'https://yuque.com/jiushen/blog/xswitch-readme';
69 | export const DEFAULT_FONT_FAMILY = 'Menlo, Monaco, "Courier New", monospace';
70 | export const PLATFORM_MAC = 'Mac';
71 | export const OPTIONS_SAVED = 'Options saved.';
72 | export const EMPTY_STRING = '';
73 | export const KEY_DOWN = 'keydown';
74 | export const CLICK = 'click';
75 | export const ANYTHING = 'anyString';
76 | export const FORMAT_DOCUMENT_CMD = 'editor.action.formatDocument';
77 | export const KEY_CODE_S = 83;
78 | export const SHOW_FOLDING_CONTROLS = 'always';
79 | export const OPACITY_VISIBLE = '1';
80 | export const NULL_STRING = 'null';
81 | export const RULE_COMPLETION = `[
82 | "\${1:from}",
83 | "\${1:to}",
84 | ],`;
85 |
86 | export const DEFAULT_DATA = `{
87 | // Use IntelliSense to learn about possible links.
88 | // Type \`rule\` to quick insert rule.
89 | // 输入 rule 来快速插入规则
90 | // For more information, visit: https://github.com/yize/xswitch
91 | "proxy": [
92 | [
93 | "https://unpkg.com/react@16.4.1/umd/react.production.min.js",
94 | "https://unpkg.com/react@16.4.1/umd/react.development.js"
95 | ],
96 | // \`Command/Ctrl + click\` to visit:
97 | // https://unpkg.com/react@16.4.1/umd/react.production.min.js
98 | // [
99 | // "(.*)/path1/path2/(.*)", // https://www.sample.com/path1/path2/index.js
100 | // "http://127.0.0.1:3000/$2", // http://127.0.0.1:3000/index.js
101 | // ],
102 | ],
103 | // urls that want CORS
104 | // "cors": [
105 | // "mocks.a.com",
106 | // "mocks.b.com"
107 | // ]
108 | }
109 | `;
110 |
111 | export const DEFAULT_DUP_DATA = `{
112 | "proxy": [
113 | [
114 | "(.*)/path1/path2/(.*)", // https://www.sample.com/path1/path2/index.js
115 | "http://127.0.0.1:3000/$2", // http://127.0.0.1:3000/index.js
116 | ],
117 | ],
118 | }
119 | `;
--------------------------------------------------------------------------------
/src/pages/xswitch/xswitch.ts:
--------------------------------------------------------------------------------
1 | import { ViewController, observable, inject } from '@ali/recore';
2 | import { Switch, Icon, Checkbox, Input, Popconfirm, Button } from 'antd';
3 |
4 | import './xswitch.less';
5 |
6 | import {
7 | ANYTHING,
8 | FORMAT_DOCUMENT_CMD,
9 | KEY_CODE_S,
10 | KEY_DOWN,
11 | LANGUAGE_JSON,
12 | MONACO_CONTRIBUTION_PATH,
13 | MONACO_VS_PATH,
14 | PLATFORM_MAC,
15 | RULE,
16 | RULE_COMPLETION,
17 | POPUP_HTML_PATH,
18 | HELP_URL,
19 | DEFAULT_DUP_DATA,
20 | } from '../../constants';
21 | import { Enabled } from '../../enums';
22 | import {
23 | getConfig,
24 | saveConfig,
25 | setChecked,
26 | getChecked,
27 | openLink,
28 | getEditingConfigKey,
29 | setEditingConfigKey,
30 | setConfigItems,
31 | getConfigItems,
32 | removeUnusedItems,
33 | } from '../../chrome-storage';
34 | import { getEditorConfig } from '../../editor-config';
35 |
36 | let editor: any;
37 | @inject({
38 | components: { Switch, Icon, Checkbox, Input, Popconfirm, Button },
39 | })
40 | export default class XSwitch extends ViewController {
41 | @observable
42 | checked = true;
43 |
44 | @observable
45 | editingKey = '0';
46 |
47 | @observable
48 | deletingKey = '0';
49 |
50 | @observable
51 | newItem = '';
52 |
53 | @observable
54 | items: any = [];
55 |
56 | async $init() {
57 | this.checked = (await getChecked()) !== Enabled.NO;
58 | }
59 |
60 | async $didMount() {
61 | window.require.config({ paths: { vs: MONACO_VS_PATH } });
62 | const editingConfigKey: string = await getEditingConfigKey();
63 | this.editingKey = editingConfigKey;
64 | const config: any = await getConfig(editingConfigKey);
65 | this.items = Array.from(await getConfigItems());
66 | await removeUnusedItems()
67 |
68 | let monacoReady: boolean = true;
69 |
70 | window.require([MONACO_CONTRIBUTION_PATH], () => {
71 | editor = window.monaco.editor.create(
72 | this.$refs.shell,
73 | getEditorConfig(config)
74 | );
75 |
76 | saveConfig(editor.getValue(), this.editingKey);
77 |
78 | window.monaco.languages.registerCompletionItemProvider(LANGUAGE_JSON, {
79 | provideCompletionItems: () => {
80 | const textArr: any[] = [];
81 | chrome.extension
82 | .getBackgroundPage()!
83 | ._forward.urls.forEach((item: any) => {
84 | if (item) {
85 | textArr.push({
86 | label: item,
87 | kind: window.monaco.languages.CompletionItemKind.Text,
88 | });
89 | }
90 | });
91 |
92 | const extraItems = [
93 | {
94 | label: RULE,
95 | kind: window.monaco.languages.CompletionItemKind.Method,
96 | insertText: {
97 | value: RULE_COMPLETION,
98 | },
99 | },
100 | ];
101 | return [...textArr, ...extraItems];
102 | },
103 | });
104 |
105 | editor.onDidChangeModelContent(() => {
106 | saveConfig(editor.getValue(), this.editingKey);
107 | });
108 |
109 | editor.onDidScrollChange(() => {
110 | if (monacoReady) {
111 | editor.trigger(ANYTHING, FORMAT_DOCUMENT_CMD);
112 | monacoReady = false;
113 | }
114 | });
115 | });
116 |
117 | function preventSave() {
118 | document.addEventListener(
119 | KEY_DOWN,
120 | (e) => {
121 | const controlKeyDown = navigator.platform.match(PLATFORM_MAC)
122 | ? e.metaKey
123 | : e.ctrlKey;
124 | if (e.keyCode === KEY_CODE_S && controlKeyDown) {
125 | e.preventDefault();
126 | }
127 | },
128 | false
129 | );
130 | }
131 | preventSave();
132 | }
133 |
134 | setEditorValue(value: string) {
135 | editor.setValue(value);
136 | }
137 |
138 | toggleButton() {
139 | this.checked = !this.checked;
140 | setChecked(this.checked);
141 | }
142 |
143 | openNewTab() {
144 | openLink(POPUP_HTML_PATH, true);
145 | }
146 | openReadme() {
147 | openLink(HELP_URL);
148 | }
149 |
150 | async setEditingKeyHandler(id: string) {
151 | this.editingKey = id;
152 | const config: any = await getConfig(this.editingKey);
153 | this.setEditorValue(config || DEFAULT_DUP_DATA);
154 | setEditingConfigKey(this.editingKey);
155 | // reset
156 | this.deletingKey = '0';
157 | }
158 |
159 | async setEditingKey(event: EventTarget, ctx: any) {
160 | await this.setEditingKeyHandler(ctx.item.id);
161 | }
162 |
163 | setActive(event: EventTarget, ctx: any) {
164 | ctx.item.active = !ctx.item.active;
165 | setConfigItems(this.items);
166 | }
167 |
168 | async add() {
169 | const id = '' + new Date().getTime();
170 | const self = this;
171 | if (this.newItem) {
172 | this.items.push({
173 | id,
174 | name: this.newItem,
175 | active: true,
176 | });
177 | }
178 | setConfigItems(this.items);
179 | this.editingKey = id;
180 | setEditingConfigKey(this.editingKey);
181 | await this.setEditingKeyHandler(id);
182 | setTimeout(function () {
183 | self.$refs.tabs.scrollTop = self.$refs.tabs.scrollHeight;
184 | }, 0)
185 | this.newItem = '';
186 | }
187 |
188 | async remove(ev: EventTarget, ctx: any) {
189 | ev.stopPropagation();
190 | if(this.deletingKey === ctx.item.id){
191 | const i = this.items.indexOf(ctx.item);
192 | if (i > -1) {
193 | this.items.splice(i, 1);
194 | }
195 | // i will not be 0
196 | if(this.items[i-1].hasOwnProperty('id')){
197 | this.editingKey = this.items[i-1].id;
198 | await this.setEditingKeyHandler(this.editingKey);
199 | }
200 | setConfigItems(this.items);
201 | }else{
202 | this.deletingKey = ctx.item.id;
203 | }
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/src/background.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ALL_URLS,
3 | BLOCKING,
4 | EMPTY_STRING,
5 | MILLISECONDS_PER_WEEK,
6 | REQUEST_HEADERS,
7 | RESPONSE_HEADERS,
8 | JSON_CONFIG,
9 | DISABLED,
10 | CLEAR_CACHE_ENABLED,
11 | CORS_ENABLED_STORAGE_KEY,
12 | PROXY_STORAGE_KEY,
13 | CORS_STORAGE,
14 | ACTIVE_KEYS,
15 | TAB_LIST,
16 | } from './constants';
17 | import {
18 | BadgeText,
19 | Enabled,
20 | IconBackgroundColor,
21 | } from './enums';
22 | import forward from './forward';
23 |
24 | let clearRunning: boolean = false;
25 | let clearCacheEnabled: boolean = true;
26 | let corsEnabled: boolean = true;
27 | let parseError: boolean = false;
28 | let jsonActiveKeys = ['0'];
29 | let conf: StorageJSON = {
30 | 0: {
31 | [PROXY_STORAGE_KEY]: [],
32 | [CORS_STORAGE]: [],
33 | },
34 | };
35 |
36 | interface SingleConfig {
37 | [PROXY_STORAGE_KEY]: Array<[]>;
38 | [CORS_STORAGE]: string[];
39 | }
40 |
41 | interface StorageJSON {
42 | 0: SingleConfig;
43 | [key: string]: any;
44 | }
45 |
46 | chrome.storage.sync.get({
47 | [JSON_CONFIG]: {
48 | 0: {
49 | [PROXY_STORAGE_KEY]: [],
50 | [CORS_STORAGE]: [],
51 | },
52 | },
53 | [ACTIVE_KEYS]: ['0'],
54 | }, (result) => {
55 | jsonActiveKeys = result[ACTIVE_KEYS];
56 | if (result && result[JSON_CONFIG]) {
57 | conf = result[JSON_CONFIG];
58 | const config = getActiveConfig(conf);
59 | forward[JSON_CONFIG] = { ...config };
60 | } else {
61 | forward[JSON_CONFIG] = {
62 | [PROXY_STORAGE_KEY]: [],
63 | [CORS_STORAGE]: [],
64 | };
65 | parseError = false;
66 | }
67 | });
68 |
69 | function getActiveConfig(config: StorageJSON): object {
70 | const activeKeys = [...jsonActiveKeys];
71 | const json = config['0'];
72 | activeKeys.forEach((key: string) => {
73 | if (config[key] && key !== '0') {
74 | if (config[key][PROXY_STORAGE_KEY]) {
75 | if (!json[PROXY_STORAGE_KEY]) {
76 | json[PROXY_STORAGE_KEY] = [];
77 | }
78 | json[PROXY_STORAGE_KEY] = [...json[PROXY_STORAGE_KEY], ...config[key][PROXY_STORAGE_KEY]];
79 | }
80 |
81 | if (config[key][CORS_STORAGE]) {
82 | if (!json[CORS_STORAGE]) {
83 | json[CORS_STORAGE] = [];
84 | }
85 | json[CORS_STORAGE] = [...json[CORS_STORAGE], ...config[key][CORS_STORAGE]];
86 | }
87 | }
88 | });
89 | return json;
90 | }
91 |
92 | chrome.storage.sync.get(
93 | {
94 | [DISABLED]: Enabled.YES,
95 | [CLEAR_CACHE_ENABLED]: Enabled.YES,
96 | [CORS_ENABLED_STORAGE_KEY]: Enabled.YES,
97 | },
98 | (result) => {
99 | forward[DISABLED] = result[DISABLED];
100 | clearCacheEnabled = result[CLEAR_CACHE_ENABLED] === Enabled.YES;
101 | corsEnabled = result[CORS_ENABLED_STORAGE_KEY] === Enabled.YES;
102 | setIcon();
103 | }
104 | );
105 |
106 | chrome.storage.onChanged.addListener((changes) => {
107 | if (changes[ACTIVE_KEYS]) {
108 | jsonActiveKeys = changes[ACTIVE_KEYS].newValue;
109 | }
110 |
111 | if (changes[JSON_CONFIG]) {
112 | const config = getActiveConfig(changes[JSON_CONFIG].newValue);
113 | forward[JSON_CONFIG] = { ...config };
114 | }
115 |
116 | if (changes[DISABLED]) {
117 | forward[DISABLED] = changes[DISABLED].newValue;
118 | }
119 |
120 | if (changes[CLEAR_CACHE_ENABLED]) {
121 | clearCacheEnabled = changes[CLEAR_CACHE_ENABLED].newValue === Enabled.YES;
122 | }
123 |
124 | if (changes[CORS_ENABLED_STORAGE_KEY]) {
125 | corsEnabled = changes[CORS_ENABLED_STORAGE_KEY].newValue === Enabled.YES;
126 | }
127 |
128 | chrome.storage.sync.get({
129 | [JSON_CONFIG]: {
130 | 0: {
131 | [PROXY_STORAGE_KEY]: [],
132 | [CORS_STORAGE]: [],
133 | },
134 | },
135 | }, (result) => {
136 | if (result && result[JSON_CONFIG]) {
137 | conf = result[JSON_CONFIG];
138 | const config = getActiveConfig(conf);
139 | forward[JSON_CONFIG] = { ...config };
140 | }
141 | setIcon();
142 | });
143 | });
144 |
145 | chrome.webRequest.onBeforeRequest.addListener(
146 | (details) => {
147 | if (forward[DISABLED] !== Enabled.NO) {
148 | if (clearCacheEnabled) {
149 | clearCache();
150 | }
151 |
152 | return forward.onBeforeRequestCallback(details);
153 | }
154 | return {};
155 | },
156 | {
157 | urls: [ALL_URLS],
158 | },
159 | [BLOCKING]
160 | );
161 |
162 | // Breaking the CORS Limitation
163 | chrome.webRequest.onHeadersReceived.addListener(
164 | headersReceivedListener,
165 | {
166 | urls: [ALL_URLS],
167 | },
168 | [BLOCKING, RESPONSE_HEADERS]
169 | );
170 |
171 | chrome.webRequest.onBeforeSendHeaders.addListener(
172 | (details) => forward.onBeforeSendHeadersCallback(details),
173 | { urls: [ALL_URLS] },
174 | [BLOCKING, REQUEST_HEADERS]
175 | );
176 |
177 | function setBadgeAndBackgroundColor(
178 | text: string | number,
179 | color: string
180 | ): void {
181 | const { browserAction } = chrome;
182 | browserAction.setBadgeText({
183 | text: EMPTY_STRING + text,
184 | });
185 | browserAction.setBadgeBackgroundColor({
186 | color,
187 | });
188 | }
189 |
190 | function setIcon(): void {
191 | if (parseError) {
192 | setBadgeAndBackgroundColor(BadgeText.ERROR, IconBackgroundColor.ERROR);
193 | return;
194 | }
195 |
196 | if (forward[DISABLED] !== Enabled.NO) {
197 | setBadgeAndBackgroundColor(
198 | forward[JSON_CONFIG][PROXY_STORAGE_KEY].length,
199 | IconBackgroundColor.ON
200 | );
201 | } else {
202 | setBadgeAndBackgroundColor(BadgeText.OFF, IconBackgroundColor.OFF);
203 | return;
204 | }
205 | }
206 |
207 | function headersReceivedListener(
208 | details: chrome.webRequest.WebResponseHeadersDetails): chrome.webRequest.BlockingResponse {
209 | return forward.onHeadersReceivedCallback(details, corsEnabled);
210 | }
211 |
212 | function clearCache(): void {
213 | if (!clearRunning) {
214 | clearRunning = true;
215 | const millisecondsPerWeek = MILLISECONDS_PER_WEEK;
216 | const oneWeekAgo = new Date().getTime() - millisecondsPerWeek;
217 | chrome.browsingData.removeCache(
218 | {
219 | since: oneWeekAgo,
220 | },
221 | () => {
222 | clearRunning = false;
223 | }
224 | );
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/src/forward.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CORS,
3 | DEFAULT_CORS_CREDENTIALS,
4 | DEFAULT_CORS_METHODS,
5 | DEFAULT_CORS_ORIGIN,
6 | ORIGIN,
7 | REG,
8 | EMPTY_STRING,
9 | DEFAULT_CREDENTIALS_RESPONSE_HEADERS,
10 | NULL_STRING,
11 | ACCESS_CONTROL_REQUEST_HEADERS,
12 | } from './constants';
13 | import { Enabled, UrlType } from './enums';
14 |
15 | interface IFowardConfig {
16 | proxy?: string[][];
17 | cors?: string[];
18 | }
19 |
20 | /**
21 | * get url type
22 | * @param url urls
23 | * @param reg rule
24 | */
25 | const matchUrl = (url: string, reg: string): string | boolean => {
26 | if (REG.FORWARD.test(reg)) {
27 | // support ??
28 | const r = new RegExp(reg.replace('??', '\\?\\?'), 'i');
29 | const matched = r.test(url);
30 | if (matched) {
31 | return UrlType.REG;
32 | }
33 | } else {
34 | const matched = url.indexOf(reg) > -1;
35 | if (matched) {
36 | return UrlType.STRING;
37 | }
38 | }
39 | return false;
40 | };
41 |
42 | class Forward {
43 | private _lastRequestId: string | null = null;
44 | private _disabled: Enabled = Enabled.YES;
45 | private _config: IFowardConfig = {};
46 | private _originRequest: Map = new Map();
47 | private _originRequestHeaders: Map = new Map();
48 | private _urls: string[] = new Array(200); // for cache
49 |
50 | get urls(): string[] {
51 | return this._urls;
52 | }
53 |
54 | get disabled(): Enabled {
55 | return this._disabled;
56 | }
57 |
58 | set disabled(newValue: Enabled) {
59 | this._disabled = newValue;
60 | }
61 | get config(): IFowardConfig {
62 | return this._config;
63 | }
64 | set config(newValue: IFowardConfig) {
65 | this._config = { ...newValue };
66 | }
67 |
68 | // Breaking the CORS Limitation
69 | onHeadersReceivedCallback(
70 | details: chrome.webRequest.WebResponseHeadersDetails,
71 | cors: boolean = true
72 | ): chrome.webRequest.BlockingResponse {
73 | // has cors rules
74 | const corsMap: string[] = this.config.cors!;
75 | let corsMatched: boolean = false;
76 |
77 | if (corsMap && corsMap.length) {
78 | corsMap.forEach((rule) => {
79 | if (matchUrl(details.url, rule)) {
80 | corsMatched = true;
81 | }
82 | });
83 | }
84 |
85 | const disabled: boolean =
86 | this.disabled === Enabled.NO || !cors || !corsMatched;
87 |
88 | if (disabled) {
89 | return {};
90 | }
91 |
92 | const originUrl: string = details.url;
93 | let resHeaders: chrome.webRequest.HttpHeader[] = [];
94 | let CORSOrigin: string =
95 | (this._originRequest.get(details.requestId)
96 | ? this._originRequest.get(details.requestId)
97 | : details.initiator) || DEFAULT_CORS_ORIGIN;
98 |
99 | if (details.responseHeaders && details.responseHeaders.filter) {
100 | let hasCredentials: boolean | string = false;
101 | let tempOrigin: string = EMPTY_STRING;
102 | resHeaders = details.responseHeaders.filter((responseHeader) => {
103 | // Already has access-control-allow-origin headers
104 | if (CORS.ORIGIN === responseHeader.name.toLowerCase()) {
105 | tempOrigin = responseHeader.value!;
106 | }
107 |
108 | if (CORS.CREDENTIALS === responseHeader.name.toLowerCase()) {
109 | hasCredentials = responseHeader.value!;
110 | }
111 |
112 | if (
113 | [CORS.ORIGIN, CORS.CREDENTIALS, CORS.METHODS, CORS.HEADERS].indexOf(
114 | responseHeader.name.toLowerCase()
115 | ) < 0
116 | ) {
117 | return true;
118 | }
119 | return false;
120 | });
121 |
122 | // only when hasCredentials
123 | if (hasCredentials) {
124 | CORSOrigin = tempOrigin;
125 | }
126 | }
127 |
128 | // suck point
129 | if (
130 | CORSOrigin === DEFAULT_CORS_ORIGIN &&
131 | this._originRequest.get(details.requestId) === NULL_STRING
132 | ) {
133 | CORSOrigin = DEFAULT_CORS_ORIGIN;
134 | }
135 |
136 | resHeaders.push({
137 | name: CORS.ORIGIN,
138 | value: CORSOrigin,
139 | });
140 | resHeaders.push({
141 | name: CORS.CREDENTIALS,
142 | value: DEFAULT_CORS_CREDENTIALS,
143 | });
144 | resHeaders.push({
145 | name: CORS.METHODS,
146 | value: DEFAULT_CORS_METHODS,
147 | });
148 |
149 | let CORSHeader: string = EMPTY_STRING;
150 |
151 | if (this._originRequestHeaders.get(details.requestId)) {
152 | CORSHeader = ',' + this._originRequestHeaders.get(details.requestId);
153 | }
154 |
155 | resHeaders.push({
156 | name: CORS.HEADERS,
157 | value: DEFAULT_CREDENTIALS_RESPONSE_HEADERS + CORSHeader,
158 | });
159 |
160 | return {
161 | responseHeaders: resHeaders,
162 | };
163 | }
164 |
165 | redirectToMatchingRule(
166 | details: chrome.webRequest.WebRequestHeadersDetails
167 | ): chrome.webRequest.BlockingResponse {
168 | const rules = this.config.proxy;
169 | let redirectUrl: string = details.url;
170 |
171 | // in case of chrome-extension downtime
172 | if (!rules || !rules.length || REG.CHROME_EXTENSION.test(redirectUrl)) {
173 | return {};
174 | }
175 |
176 | if (
177 | /http(s?):\/\/.*\.(js|css|json|jsonp)/.test(redirectUrl) &&
178 | this._urls.indexOf(redirectUrl) < 0
179 | ) {
180 | this._urls.shift();
181 | this._urls.push(redirectUrl);
182 | }
183 |
184 | try {
185 | for (let i: number = 0; i < rules.length; i++) {
186 | const rule = rules[i];
187 | if (rule && rule[0] && typeof rule[1] === 'string') {
188 | const reg = rule[0];
189 | const matched = matchUrl(redirectUrl, reg);
190 |
191 | if (details.requestId !== this._lastRequestId) {
192 | if (matched === UrlType.REG) {
193 | const r = new RegExp(reg.replace('??', '\\?\\?'), 'i');
194 | redirectUrl = redirectUrl.replace(r, rule[1]);
195 | } else if (matched === UrlType.STRING) {
196 | redirectUrl = redirectUrl.split(rule[0]).join(rule[1]);
197 | }
198 | }
199 | }
200 | }
201 | } catch (e) {
202 | console.error('rule match error', e);
203 | }
204 |
205 | this._lastRequestId = details.requestId;
206 | return redirectUrl === details.url ? {} : { redirectUrl };
207 | }
208 |
209 | onBeforeSendHeadersCallback(
210 | details: chrome.webRequest.WebRequestHeadersDetails
211 | ): chrome.webRequest.BlockingResponse {
212 | const headers: string[] = [];
213 | for (let i: number = 0; i < details.requestHeaders!.length; ++i) {
214 | const requestName = details.requestHeaders![i].name.toLowerCase();
215 | if (requestName === ORIGIN) {
216 | this._originRequest.set(
217 | details.requestId,
218 | details.requestHeaders![i].value!
219 | );
220 | } else if (requestName === ACCESS_CONTROL_REQUEST_HEADERS || REG.X_HEADER.test(requestName)) {
221 | headers.push(requestName);
222 | }
223 | }
224 | if (headers.length) {
225 | this._originRequestHeaders.set(details.requestId, headers.join(','));
226 | }
227 | return { requestHeaders: details.requestHeaders };
228 | }
229 |
230 | onBeforeRequestCallback(
231 | details: chrome.webRequest.WebRequestHeadersDetails
232 | ): chrome.webRequest.BlockingResponse {
233 | return this.redirectToMatchingRule(details);
234 | }
235 | }
236 |
237 | if (!window._forward) {
238 | window._forward = new Forward();
239 | }
240 |
241 | export default window._forward;
242 |
--------------------------------------------------------------------------------
/src/chrome-storage.ts:
--------------------------------------------------------------------------------
1 | import {
2 | JSONC_CONFIG,
3 | JSON_CONFIG,
4 | DISABLED,
5 | CLEAR_CACHE_ENABLED,
6 | CORS_ENABLED_STORAGE_KEY,
7 | TAB_LIST,
8 | EDITING_CONFIG_KEY,
9 | ACTIVE_KEYS,
10 | } from './constants';
11 | import { JSONC2JSON, JSON_Parse } from './utils';
12 | import { Enabled } from './enums';
13 |
14 | interface ConfigStorage {
15 | [JSONC_CONFIG]: object;
16 | }
17 | interface OptionsStorage {
18 | [CLEAR_CACHE_ENABLED]: string;
19 | [CORS_ENABLED_STORAGE_KEY]: string;
20 | }
21 |
22 | export function getConfig(editingConfigKey: string): Promise {
23 | return new Promise((resolve) => {
24 | if (process.env.NODE_ENV !== 'production') {
25 | return resolve({
26 | [JSONC_CONFIG]: {
27 | 0: '',
28 | },
29 | });
30 | }
31 | window.chrome.storage.sync.get({
32 | [JSONC_CONFIG]: {
33 | 0: '',
34 | },
35 | }, (result: any) => {
36 | if (typeof result[JSONC_CONFIG] === 'string') {
37 | return resolve(result[JSONC_CONFIG]);
38 | }
39 | resolve(result[JSONC_CONFIG][editingConfigKey]);
40 | });
41 | });
42 | }
43 |
44 | export function getActiveKeys(): Promise {
45 | return new Promise((resolve) => {
46 | if (process.env.NODE_ENV !== 'production') {
47 | return resolve({
48 | [ACTIVE_KEYS]: ['0'],
49 | });
50 | }
51 | window.chrome.storage.sync.get(
52 | {
53 | [ACTIVE_KEYS]: ['0'],
54 | }, (result: any) => {
55 | resolve(result[ACTIVE_KEYS]);
56 | });
57 | });
58 | }
59 |
60 | export function setActiveKeys(keys?: string[]): Promise