├── .eslintignore
├── .eslintrc
├── .github
└── ISSUE_TEMPLATE.md
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGELOG
├── LICENSE
├── README.md
├── babel.config.js
├── bin
├── anyproxy
├── anyproxy-ca
├── rootCACheck.js
└── startServer.js
├── build_scripts
├── build-doc-site.sh
└── prebuild-doc.js
├── docs-src
├── CNAME
├── LANGS.md
├── README.md
├── _layouts
│ └── layout.html
├── assets
│ ├── auto-lang.js
│ ├── favicon.png
│ ├── main.js
│ └── website.css
├── book.json
├── cn
│ ├── README.md
│ ├── SUMMARY.md
│ ├── _layouts
│ │ ├── layout.html
│ │ └── website
│ │ │ └── summary.html
│ └── src_doc.md
└── en
│ ├── README.md
│ ├── SUMMARY.md
│ ├── _layouts
│ ├── layout.html
│ └── website
│ │ └── summary.html
│ ├── book.json
│ └── src_doc.md
├── docs
├── CNAME
├── README.md
├── assets
│ ├── auto-lang.js
│ ├── favicon.png
│ ├── main.js
│ └── website.css
├── cn
│ ├── index.html
│ ├── search_index.json
│ └── src_doc.md
├── en
│ ├── index.html
│ ├── search_index.json
│ └── src_doc.md
├── gitbook
│ ├── fonts
│ │ └── fontawesome
│ │ │ ├── FontAwesome.otf
│ │ │ ├── fontawesome-webfont.eot
│ │ │ ├── fontawesome-webfont.svg
│ │ │ ├── fontawesome-webfont.ttf
│ │ │ ├── fontawesome-webfont.woff
│ │ │ └── fontawesome-webfont.woff2
│ ├── gitbook-plugin-fontsettings
│ │ ├── fontsettings.js
│ │ └── website.css
│ ├── gitbook-plugin-highlight
│ │ ├── ebook.css
│ │ └── website.css
│ ├── gitbook-plugin-lunr
│ │ ├── lunr.min.js
│ │ └── search-lunr.js
│ ├── gitbook-plugin-search
│ │ ├── lunr.min.js
│ │ ├── search-engine.js
│ │ ├── search.css
│ │ └── search.js
│ ├── gitbook-plugin-sharing
│ │ └── buttons.js
│ ├── gitbook.js
│ ├── images
│ │ ├── apple-touch-icon-precomposed-152.png
│ │ └── favicon.ico
│ ├── style.css
│ └── theme.js
├── index.html
└── search_index.json
├── jest.config.js
├── lib
├── certMgr.js
├── configUtil.js
├── httpsServerMgr.js
├── log.js
├── recorder.js
├── requestErrorHandler.js
├── requestHandler.js
├── ruleLoader.js
├── rule_default.js
├── systemProxyMgr.js
├── util.js
├── webInterface.js
├── wsServer.js
└── wsServerMgr.js
├── module_sample
├── core_reload.js
├── https_config.js
├── normal_use.js
└── simple_use.js
├── package.json
├── proxy.js
├── resource
├── 502.pug
├── cert_download.pug
└── cert_error.pug
├── rule_sample
├── sample_modify_request_data.js
├── sample_modify_request_header.js
├── sample_modify_request_path.js
├── sample_modify_request_protocol.js
├── sample_modify_response_data.js
├── sample_modify_response_header.js
├── sample_modify_response_statuscode.js
└── sample_use_local_response.js
├── test
├── __snapshots__
│ └── basic.spec.js.snap
├── basic.spec.js
├── fixtures
│ ├── someRule.js
│ └── upload.txt
├── lib
│ ├── httpsServerMgr.spec.js
│ ├── ruleLoader.spec.js
│ └── util.spec.js
├── rule
│ ├── beforeDealHttpsRequest.spec.js
│ ├── beforeSendRequest.spec.js
│ ├── beforeSendResponse.js
│ └── onError.spec.js
├── util.js
└── web
│ ├── curlUtil.spec.js
│ └── webInterface.spec.js
└── web
├── favico.png
├── index.html
├── postcss.config.js
├── src
├── action
│ ├── globalStatusAction.js
│ └── recordAction.js
├── assets
│ ├── clear.svg
│ ├── download.svg
│ ├── filter.svg
│ ├── https.png
│ ├── play.svg
│ ├── retweet.svg
│ ├── start.svg
│ ├── stop.svg
│ ├── tip.svg
│ ├── touchmeter.svg
│ └── view-eye.svg
├── common
│ ├── ApiUtil.js
│ ├── Constant.js
│ ├── WsUtil.js
│ ├── apiUtil.js
│ ├── commonUtil.js
│ ├── constant.js
│ ├── curlUtil.js
│ ├── promiseUtil.js
│ └── wsUtil.js
├── component
│ ├── download-root-ca.jsx
│ ├── download-root-ca.less
│ ├── header-menu.jsx
│ ├── header-menu.less
│ ├── json-viewer.jsx
│ ├── json-viewer.less
│ ├── left-menu.jsx
│ ├── left-menu.less
│ ├── map-local.jsx
│ ├── map-local.less
│ ├── modal-panel.jsx
│ ├── modal-panel.less
│ ├── record-detail.jsx
│ ├── record-detail.less
│ ├── record-filter.jsx
│ ├── record-filter.less
│ ├── record-list-diff-worker.jsx
│ ├── record-panel.jsx
│ ├── record-panel.less
│ ├── record-request-detail.jsx
│ ├── record-response-detail.jsx
│ ├── record-row.jsx
│ ├── record-row.less
│ ├── record-worker.jsx
│ ├── record-ws-message-detail.jsx
│ ├── record-ws-message-detail.less
│ ├── resizable-panel.jsx
│ ├── resizable-panel.less
│ ├── table-panel.jsx
│ ├── table-panel.less
│ ├── title-bar.jsx
│ ├── title-bar.less
│ └── ws-listener.jsx
├── index.jsx
├── index.less
├── reducer
│ ├── globalStatusReducer.js
│ ├── requestRecordReducer.js
│ └── rootReducer.js
├── saga
│ └── rootSaga.js
└── style
│ ├── animate.less
│ ├── antd-constant.less
│ ├── antd-reset.global.less
│ ├── common.less
│ └── constant.less
└── webpack.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | web
4 | web2
5 | resource
6 | *.sh
7 | docs
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "parser": "babel-eslint",
4 | "env": {
5 | "browser": true,
6 | "node": true,
7 | "es6": true,
8 | "jest": true
9 | },
10 | "globals": {
11 | "React": true,
12 | "ReactDOM": true,
13 | "Zepto": true,
14 | "JsBridgeUtil": true
15 | },
16 | "rules": {
17 | "semi": [0],
18 | "comma-dangle": [0],
19 | "global-require": [0],
20 | "no-alert": [0],
21 | "no-console": [0],
22 | "no-param-reassign": [0],
23 | "max-len": [0],
24 | "func-names": [0],
25 | "no-underscore-dangle": [0],
26 | "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false }],
27 | "object-shorthand": [0],
28 | "arrow-body-style": [0],
29 | "no-new": [0],
30 | "strict": [0],
31 | "no-script-url": [0],
32 | "spaced-comment": [0],
33 | "no-empty": [0],
34 | "no-constant-condition": [0],
35 | "no-else-return": [0],
36 | "no-use-before-define": [0],
37 | "no-unused-expressions": [0],
38 | "no-class-assign": [0],
39 | "new-cap": [0],
40 | "array-callback-return": [0],
41 | "prefer-template": [0],
42 | "no-restricted-syntax": [0],
43 | "no-trailing-spaces": [0],
44 | "import/no-unresolved": [0],
45 | "jsx-a11y/img-has-alt": [0],
46 | "camelcase": [0],
47 | "consistent-return": [0],
48 | "guard-for-in": [0],
49 | "one-var": [0],
50 | "react/wrap-multilines": [0],
51 | "react/no-multi-comp": [0],
52 | "react/jsx-no-bind": [0],
53 | "react/prop-types": [0],
54 | "react/prefer-stateless-function": [0],
55 | "react/jsx-first-prop-new-line": [0],
56 | "react/sort-comp": [0],
57 | "import/no-extraneous-dependencies": [0],
58 | "import/extensions": [0],
59 | "react/forbid-prop-types": [0],
60 | "react/require-default-props": [0],
61 | "class-methods-use-this": [0],
62 | "jsx-a11y/no-static-element-interactions": [0],
63 | "react/no-did-mount-set-state": [0],
64 | "jsx-a11y/alt-text": [0],
65 | "import/no-dynamic-require": [0],
66 | "no-extra-boolean-cast": [0],
67 | "no-lonely-if": [0],
68 | "no-plusplus": [0],
69 | "generator-star-spacing": ["error", {"before": true, "after": false}],
70 | "require-yield": [0],
71 | "arrow-parens": [0],
72 | "no-template-curly-in-string": [0],
73 | "no-mixed-operators": [0]
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 | Plese fill the template when you reporting a new issue, thanks!
3 |
4 |
5 | #### Which platform are you running AnyProxy
6 |
7 |
8 | #### The version of the AnyProxy
9 |
10 |
11 |
12 | #### Your expected behavior of AnyProxy
13 |
14 |
15 | #### The actual behavior of AnyProxy
16 |
17 |
18 | #### The log of the error
19 |
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | cert/**/*.srl
2 | cert/**/*.key
3 | cert/**/*.crt
4 | cert/**/*.csr
5 | web/build/.module-cache/
6 | test/report/*.txt
7 | tmp.txt
8 | .*.swp
9 | ._*
10 | .DS_Store
11 | .vscode
12 | .git
13 | .hg
14 | .lock-wscript
15 | .svn
16 | .wafpickle-*
17 | .vscode
18 | CVS
19 | npm-debug.log
20 | logs
21 | *.log
22 | pids
23 | *.pid
24 | *.seed
25 | lib-cov
26 | coverage
27 | .grunt
28 | build/Release
29 | node_modules
30 | .lock-wscript
31 | temp
32 | dist
33 | test/report
34 | *.tgz
35 | doc_compiled.md
36 | docs-md/cn/doc.md
37 | docs-md/en/doc.md
38 | docs/gitbook/gitbook-plugin-livereload/
39 | node_modules
40 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | cert/**/*.srl
2 | cert/**/*.key
3 | cert/**/*.crt
4 | cert/**/*.csr
5 | web/build/.module-cache/
6 | tmp.txt
7 | .*.swp
8 | ._*
9 | .DS_Store
10 | .git
11 | .hg
12 | .lock-wscript
13 | .svn
14 | .wafpickle-*
15 | CVS
16 | npm-debug.log
17 | logs
18 | *.log
19 | pids
20 | *.pid
21 | *.seed
22 | lib-cov
23 | coverage
24 | .grunt
25 | node_modules
26 | .lock-wscript
27 | temp
28 | releases
29 | rule_sample
30 | test
31 | *.tgz
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 12
4 | before_script:
5 | - node ./bin/anyproxy-ca -g
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | 22 Dec 2016: AnyProxy 4.0.0-beta:
2 |
3 | * to AnyProxy rules: all the rule interfaces are asynchronous now, you can write them in a Promise way
4 | * to the UI, rewrite the code and enhance the user experience
5 |
6 | 26 Feb 2016: AnyProxy 3.10.4:
7 |
8 | * let users assign the port for web socket in AnyProxy cli
9 |
10 | 19 Sep 2016: AnyProxy 3.10.3:
11 |
12 | * fix the cert path issue with Windows
13 | * split out the cert management to an independent module
14 | * add unit tests to AnyProxy
15 |
16 | 29 Apr 2016: AnyProxy 3.10.0:
17 |
18 | * using node-forge to generate HTTPS certificates instead of openssl
19 |
20 | 29 Apr 2016: AnyProxy 3.9.1:
21 |
22 | * update SHA1 to SHA256 for openssl certificates
23 |
24 | 19 Nov 2015: AnyProxy 3.8.1:
25 |
26 | * bugfix for image content in web GUI
27 |
28 | 19 Nov 2015: AnyProxy 3.8.1:
29 |
30 | * bugfix for image content in web GUI
31 |
32 | 16 Nov 2015: AnyProxy 3.8.0:
33 |
34 | * optimize the memory strategy
35 |
36 | 2 Oct 2015: AnyProxy 3.7.7:
37 |
38 | * bugfix for proxy.close() ref #36
39 |
40 | 9 Sep 2015: AnyProxy 3.7.6:
41 |
42 | * optimize detail panel, ref #35
43 |
44 | 3 Sep 2015: AnyProxy 3.7.5:
45 |
46 | * bugfix for intercepting urls like http://a.com?url=http://b.com
47 |
48 | 19 Aug 2015: AnyProxy 3.7.4:
49 |
50 | * bugfix for intercepting urls like http://a.com?url=http://b.com
51 |
52 | 31 July 2015: AnyProxy 3.7.3:
53 |
54 | * show lastest 100 records when visit the web ui
55 | * save map-local config file to local file
56 | * show an indicator when filter or map-local is in use
57 |
58 | 31 July 2015: AnyProxy 3.7.2:
59 |
60 | * bugfix for issue #29
61 |
62 | 28 July 2015: AnyProxy 3.7.1:
63 |
64 | * fix a bug about deflate compression
65 |
66 | 20 July 2015: AnyProxy 3.7.0:
67 |
68 | * add a map-local panel on web ui, now you can easily map some request to your local files
69 |
70 | 1 July 2015: AnyProxy 3.6.0:
71 |
72 | * add a filter on web ui
73 |
74 | 1 July 2015: AnyProxy 3.5.2:
75 |
76 | * optimize the row height on web ui
77 |
78 | 18 June 2015: AnyProxy 3.5.1:
79 |
80 | * print a hint when using SNI features in node <0.12
81 | * Ref : https://github.com/alibaba/anyproxy/issues/25
82 |
83 | 18 June 2015: AnyProxy 3.5.0:
84 |
85 | * it's a formal release of 3.4.0@beta.
86 |
87 | 27 Apr 2015: AnyProxy 3.4.0@beta:
88 |
89 | * optimize web server and web socket interface
90 |
91 | 20 Apr 2015: AnyProxy 3.3.1:
92 |
93 | * now you can assign your own port for web gui
94 |
95 | 31 Mar 2015: AnyProxy 3.3.0:
96 |
97 | * optimize https features in windows
98 | * add switch to mute the console
99 |
100 | 20 Mar 2015: AnyProxy 3.2.5:
101 |
102 | * bugfix for internal https server
103 |
104 | 19 Mar 2015: AnyProxy 3.2.4:
105 |
106 | * bugfix for absolute rule path
107 |
108 | 23 Feb 2015: AnyProxy 3.2.2:
109 |
110 | * [bugfix for relative rule path](https://github.com/alibaba/anyproxy/pull/18)
111 |
112 | 10 Feb 2015: AnyProxy 3.2.1:
113 |
114 | * bugfix for 3.2.0
115 |
116 | 10 Feb 2015: AnyProxy 3.2.0:
117 |
118 | * using SNI when intercepting https requests
119 |
120 | 28 Jan 2015: AnyProxy 3.1.2:
121 |
122 | * thanks to iconv-lite, almost webpage with any charset can be correctly decoded in web interface.
123 |
124 | 28 Jan 2015: AnyProxy 3.1.1:
125 |
126 | * convert GBK to UTF8 in web interface
127 |
128 | 22 Jan 2015: AnyProxy 3.1.0:
129 |
130 | * will NOT intercept https request by default. Use ``anyproxy --intercept`` to turn on this feature.
131 |
132 | 12 Jan 2015: AnyProxy 3.0.4:
133 |
134 | * show anyproxy version by --version
135 |
136 | 12 Jan 2015: AnyProxy 3.0.3:
137 |
138 | * Bugfix: https throttle
139 |
140 | 9 Jan 2015: AnyProxy 3.0.2:
141 |
142 | * UI improvement: add link and qr code to root CA file.
143 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | AnyProxy
2 | ----------------
3 |
4 | [![NPM version][npm-image]][npm-url]
5 | [![node version][node-image]][node-url]
6 | [![npm download][download-image]][download-url]
7 | [](https://travis-ci.org/alibaba/anyproxy)
8 |
9 | [npm-image]: https://img.shields.io/npm/v/anyproxy.svg?style=flat-square
10 | [npm-url]: https://npmjs.org/package/anyproxy
11 | [node-image]: https://img.shields.io/badge/node.js-%3E=_6.0.0-green.svg?style=flat-square
12 | [node-url]: http://nodejs.org/download/
13 | [download-image]: https://img.shields.io/npm/dm/anyproxy.svg?style=flat-square
14 | [download-url]: https://npmjs.org/package/anyproxy
15 |
16 | AnyProxy is A fully configurable HTTP/HTTPS proxy in NodeJS.
17 |
18 | Home page : [AnyProxy.io](http://anyproxy.io)
19 |
20 | Issue: https://github.com/alibaba/anyproxy/issues
21 |
22 | AnyProxy是一个基于NodeJS的,可供插件配置的HTTP/HTTPS代理服务器。
23 |
24 | 主页:[AnyProxy.io](http://anyproxy.io),访问可能需要稳定的国际网络环境
25 |
26 | 
27 |
28 | ----------------
29 |
30 | Legacy doc of version 3.x : https://github.com/alibaba/anyproxy/wiki/3.x-docs
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'test') {
2 | module.exports = {};
3 | } else {
4 | module.exports = {
5 | presets: [
6 | 'es2015',
7 | 'stage-0'
8 | ]
9 | };
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/bin/anyproxy:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict';
4 |
5 | const program = require('commander'),
6 | color = require('colorful'),
7 | co = require('co'),
8 | packageInfo = require('../package.json'),
9 | util = require('../lib/util'),
10 | rootCACheck = require('./rootCACheck'),
11 | startServer = require('./startServer'),
12 | logUtil = require('../lib/log');
13 |
14 | program
15 | .version(packageInfo.version)
16 | .option('-p, --port [value]', 'proxy port, 8001 for default')
17 | .option('-w, --web [value]', 'web GUI port, 8002 for default')
18 | .option('-r, --rule [value]', 'path for rule file,')
19 | .option('-l, --throttle [value]', 'throttle speed in kb/s (kbyte / sec)')
20 | .option('-i, --intercept', 'intercept(decrypt) https requests when root CA exists')
21 | .option('-s, --silent', 'do not print anything into terminal')
22 | .option('-c, --clear', 'clear all the certificates and temp files')
23 | .option('--ws-intercept', 'intercept websocket')
24 | .option('--ignore-unauthorized-ssl', 'ignore all ssl error')
25 | .parse(process.argv);
26 |
27 | if (program.clear) {
28 | require('../lib/certMgr').clearCerts(() => {
29 | util.deleteFolderContentsRecursive(util.getAnyProxyTmpPath());
30 | console.log(color.green('done !'));
31 | process.exit(0);
32 | });
33 | } else if (program.root) {
34 | require('../lib/certMgr').generateRootCA(() => {
35 | process.exit(0);
36 | });
37 | } else {
38 | co(function *() {
39 | if (program.silent) {
40 | logUtil.setPrintStatus(false);
41 | }
42 |
43 | if (program.intercept) {
44 | try {
45 | yield rootCACheck();
46 | } catch (e) {
47 | console.error(e);
48 | }
49 | }
50 |
51 | return startServer(program);
52 | })
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/bin/anyproxy-ca:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict'
4 |
5 | // exist-false, trusted-false : create CA
6 | // exist-true, trusted-false : trust CA
7 | // exist-true, trusted-true : all things done
8 | const program = require('commander');
9 | const color = require('colorful');
10 | const certMgr = require('../lib/certMgr');
11 | const AnyProxy = require('../proxy');
12 | const exec = require('child_process').exec;
13 | const co = require('co');
14 | const path = require('path');
15 | const inquirer = require('inquirer');
16 |
17 | program
18 | .option('-c, --clear', 'clear all the tmp certificates and root CA')
19 | .option('-g, --generate', 'generate a new rootCA')
20 | .parse(process.argv);
21 |
22 | function openFolderOfFile(filePath) {
23 | const platform = process.platform;
24 | if (/^win/.test(platform)) {
25 | exec('start .', { cwd: path.dirname(filePath) });
26 | } else if (/darwin/.test(platform)) {
27 | exec(`open -R ${filePath}`);
28 | }
29 | }
30 |
31 | function guideToGenrateCA() {
32 | AnyProxy.utils.certMgr.generateRootCA((error, keyPath, crtPath) => {
33 | if (!error) {
34 | const certDir = path.dirname(keyPath);
35 | console.log(`The cert is generated at ${certDir}. Please trust the ${color.bold('rootCA.crt')}.`);
36 | openFolderOfFile(crtPath);
37 | } else {
38 | console.error('failed to generate rootCA', error);
39 | }
40 | });
41 | }
42 |
43 | function guideToTrustCA() {
44 | const certPath = AnyProxy.utils.certMgr.getRootCAFilePath();
45 | if (certPath) {
46 | openFolderOfFile(certPath);
47 | } else {
48 | console.error('failed to get cert path');
49 | }
50 | }
51 |
52 | if (program.clear) {
53 | AnyProxy.utils.certMgr.clearCerts(() => {
54 | console.log(color.green('done !'));
55 | });
56 | } else if (program.generate) {
57 | guideToGenrateCA();
58 | } else {
59 | console.log('detecting CA status...');
60 | co(certMgr.getCAStatus)
61 | .then(status => {
62 | if (!status.exist) {
63 | console.log('AnyProxy CA does not exist.');
64 | const questions = [{
65 | type: 'confirm',
66 | name: 'ifCreate',
67 | message: 'Would you like to generate one ?',
68 | default: true
69 | }];
70 | inquirer.prompt(questions).then(answers => {
71 | if (answers.ifCreate) {
72 | guideToGenrateCA();
73 | }
74 | });
75 | } else if (!status.trusted) {
76 | if (/^win/.test(process.platform)) {
77 | console.log('AnyProxy CA exists, make sure it has been trusted');
78 | } else {
79 | console.log('AnyProxy CA exists, but not be trusted');
80 | const questions = [{
81 | type: 'confirm',
82 | name: 'ifGotoTrust',
83 | message: 'Would you like to open the folder and trust it ?',
84 | default: true
85 | }];
86 | inquirer.prompt(questions).then(answers => {
87 | if (answers.ifGotoTrust) {
88 | guideToTrustCA();
89 | }
90 | });
91 | }
92 | // AnyProxy.utils.certMgr.clearCerts()
93 | } else {
94 | console.log(color.green('AnyProxy CA has already been trusted'));
95 | }
96 | })
97 | .catch(e => {
98 | console.log(e);
99 | })
100 | }
101 |
--------------------------------------------------------------------------------
/bin/rootCACheck.js:
--------------------------------------------------------------------------------
1 | /**
2 | * check if root CA exists and installed
3 | * will prompt to generate when needed
4 | */
5 |
6 | const thunkify = require('thunkify');
7 | const AnyProxy = require('../proxy');
8 | const logUtil = require('../lib/log');
9 |
10 | const certMgr = AnyProxy.utils.certMgr;
11 |
12 | function checkRootCAExists() {
13 | return certMgr.isRootCAFileExists();
14 | }
15 |
16 | module.exports = function *() {
17 | try {
18 | if (!checkRootCAExists()) {
19 | logUtil.warn('Missing root CA, generating now');
20 | yield thunkify(certMgr.generateRootCA)();
21 | yield certMgr.trustRootCA();
22 | } else {
23 | const isCATrusted = yield thunkify(certMgr.ifRootCATrusted)();
24 | if (!isCATrusted) {
25 | logUtil.warn('ROOT CA NOT INSTALLED YET');
26 | yield certMgr.trustRootCA();
27 | }
28 | }
29 | } catch (e) {
30 | console.error(e);
31 | }
32 | };
33 |
34 |
--------------------------------------------------------------------------------
/bin/startServer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * start the AnyProxy server
3 | */
4 |
5 | const ruleLoader = require('../lib/ruleLoader');
6 | const logUtil = require('../lib/log');
7 | const AnyProxy = require('../proxy');
8 |
9 | module.exports = function startServer(program) {
10 | let proxyServer;
11 | // load rule module
12 | new Promise((resolve, reject) => {
13 | if (program.rule) {
14 | resolve(ruleLoader.requireModule(program.rule));
15 | } else {
16 | resolve(null);
17 | }
18 | })
19 | .catch(e => {
20 | logUtil.printLog('Failed to load rule file', logUtil.T_ERR);
21 | logUtil.printLog(e, logUtil.T_ERR);
22 | process.exit();
23 | })
24 |
25 | //start proxy
26 | .then(ruleModule => {
27 | proxyServer = new AnyProxy.ProxyServer({
28 | type: 'http',
29 | port: program.port || 8001,
30 | throttle: program.throttle,
31 | rule: ruleModule,
32 | webInterface: {
33 | enable: true,
34 | webPort: program.web,
35 | },
36 | wsIntercept: program.wsIntercept,
37 | forceProxyHttps: program.intercept,
38 | dangerouslyIgnoreUnauthorized: !!program.ignoreUnauthorizedSsl,
39 | silent: program.silent
40 | });
41 | // proxyServer.on('ready', () => {});
42 | proxyServer.start();
43 | })
44 | .catch(e => {
45 | logUtil.printLog(e, logUtil.T_ERR);
46 | if (e && e.code) {
47 | logUtil.printLog('code ' + e.code, logUtil.T_ERR);
48 | }
49 | logUtil.printLog(e.stack, logUtil.T_ERR);
50 | });
51 |
52 |
53 | process.on('exit', (code) => {
54 | if (code > 0) {
55 | logUtil.printLog('AnyProxy is about to exit with code: ' + code, logUtil.T_ERR);
56 | }
57 |
58 | process.exit();
59 | });
60 |
61 | //exit cause ctrl+c
62 | process.on('SIGINT', () => {
63 | try {
64 | proxyServer && proxyServer.close();
65 | } catch (e) {
66 | console.error(e);
67 | }
68 | process.exit();
69 | });
70 |
71 | process.on('uncaughtException', (err) => {
72 | let errorTipText = 'got an uncaught exception, is there anything goes wrong in your rule file ?\n';
73 | try {
74 | if (err && err.stack) {
75 | errorTipText += err.stack;
76 | } else {
77 | errorTipText += err;
78 | }
79 | } catch (e) { }
80 | logUtil.printLog(errorTipText, logUtil.T_ERR);
81 | try {
82 | proxyServer && proxyServer.close();
83 | } catch (e) { }
84 | process.exit();
85 | });
86 | }
87 |
--------------------------------------------------------------------------------
/build_scripts/build-doc-site.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ## get into the "build_scripts" folder regardless of the excution directory
4 | parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
5 | cd "$parent_path/.."
6 |
7 | ## compile the doc
8 | node ./build_scripts/prebuild-doc.js
9 | gitbook build ./docs-src ./docs
10 |
11 | ## push the doc into github
12 | # git add ./docs
13 | # git commit -m 'building docs'
14 | # git push origin
15 |
--------------------------------------------------------------------------------
/build_scripts/prebuild-doc.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | const textTpl = [
5 | '```bash',
6 | 'anyproxy --rule {{url}}',
7 | '```',
8 | '```js',
9 | '{{content}}',
10 | '```'
11 | ].join('\n');
12 |
13 | /**
14 | *
15 | * @param {*} config
16 | * @param {string} config.input input markdown path
17 | * @param {string} config.ouput output markdown path
18 | */
19 | function mergeMdWithRuleFile(config) {
20 | const doc = fs.readFileSync(config.input, { encoding: 'utf8' });
21 | const rules = doc.match(/\{\{sample-rule:([\S]+)\}\}/g).map((rawToReplace) => ({
22 | raw: rawToReplace,
23 | url: rawToReplace.replace(/\{\{sample-rule:([\S]+)\}\}/g, ($0, $1) => {
24 | return $1;
25 | })
26 | }));
27 |
28 | const tasks = rules.map((item) => (
29 | new Promise((resolve, reject) => {
30 | fs.readFile(item.url, 'utf8', (err, data) => {
31 | if (!err) {
32 | const result = Object.assign({}, item);
33 | result.content = data;
34 | resolve(result);
35 | } else {
36 | reject(err);
37 | }
38 | });
39 | })
40 | ));
41 |
42 | // fetch all samples
43 | return Promise.all(tasks)
44 | .then((results) => {
45 | // merge to doc
46 | let resultDoc = doc;
47 | results.forEach((item) => {
48 | const contentToInsert = textTpl.replace('{{url}}', item.url).replace('{{content}}', item.content);
49 | resultDoc = resultDoc.replace(item.raw, contentToInsert);
50 | });
51 | fs.writeFileSync(config.output, resultDoc);
52 | }, (fail) => {
53 | console.log('failed to load resource');
54 | console.log(fail);
55 | process.exit();
56 | })
57 | .catch(e => {
58 | console.log(e);
59 | process.exit();
60 | });
61 | }
62 |
63 | Promise.all([
64 | {
65 | input: path.join(__dirname, '../docs-src/cn/src_doc.md'),
66 | output: path.join(__dirname, '../docs-src/cn/README.md'),
67 | },
68 | {
69 | input: path.join(__dirname, '../docs-src/en/src_doc.md'),
70 | output: path.join(__dirname, '../docs-src/en/README.md'),
71 | }
72 | ].map(mergeMdWithRuleFile)).then(result => {
73 | console.log('done');
74 | }).catch(e => {
75 | console.log('err');
76 | console.log(e);
77 | });
78 |
--------------------------------------------------------------------------------
/docs-src/CNAME:
--------------------------------------------------------------------------------
1 | anyproxy.io
--------------------------------------------------------------------------------
/docs-src/LANGS.md:
--------------------------------------------------------------------------------
1 | # Languages
2 |
3 | * [中文](cn/)
4 | * [English](en/)
5 |
--------------------------------------------------------------------------------
/docs-src/README.md:
--------------------------------------------------------------------------------
1 | # THIS IS AUTO GENERATED FILE, DO NOT EDIT THE HTML DIRECTLY.
2 | # YOU CAN EDIT THE SOURCE IN docs-md FOLDER
3 |
--------------------------------------------------------------------------------
/docs-src/_layouts/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% block title %}{{ config.title|d("GitBook", true) }}{% endblock %}
7 |
8 |
9 |
10 | {% if config.author %}{% endif %}
11 | {% if config.isbn %}{% endif %}
12 | {% block style %}
13 | {% for resource in plugins.resources.css %}
14 | {% if resource.url %}
15 |
16 | {% else %}
17 |
18 | {% endif %}
19 | {% endfor %}
20 | {% endblock %}
21 | {% block head %}{% endblock %}
22 |
23 |
24 |
25 |
26 | {% block body %}{% endblock %}
27 | {% block javascript %}{% endblock %}
28 |
29 |
30 |
--------------------------------------------------------------------------------
/docs-src/assets/auto-lang.js:
--------------------------------------------------------------------------------
1 | /* eslint no-var: off */
2 | /**
3 | * detect if the browser is in UTF-8 zone
4 | * @return boolean
5 | */
6 | function isUTF8Zone() {
7 | return new Date().getTimezoneOffset() === -480;
8 | }
9 |
10 | /**
11 | * detect if the browser is already in a locale view
12 | */
13 | function isInLocaleView() {
14 | return /(cn|en)/i.test(location.href);
15 | }
16 |
17 | function initDefaultLocaleAndStatic() {
18 | if (!isInLocaleView()) {
19 | location.href = isUTF8Zone() ? '/cn' : 'en';
20 | }
21 | }
22 |
23 | initDefaultLocaleAndStatic();
24 |
--------------------------------------------------------------------------------
/docs-src/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/anyproxy/b93f948107b956e07c7b68faeff0c777a1f50486/docs-src/assets/favicon.png
--------------------------------------------------------------------------------
/docs-src/assets/main.js:
--------------------------------------------------------------------------------
1 | /* eslint no-var: off */
2 | function injectBaiduStatic() {
3 | var _hmt = _hmt || [];
4 | var hm = document.createElement('script');
5 | var s = document.getElementsByTagName('script')[0];
6 |
7 | hm.src = '//hm.baidu.com/hm.js?4e51565b7d471fd6623c163a8fd79e07';
8 | s.parentNode.insertBefore(hm, s);
9 | }
10 |
11 |
12 | injectBaiduStatic();
13 |
--------------------------------------------------------------------------------
/docs-src/assets/website.css:
--------------------------------------------------------------------------------
1 | .book .book-summary ul.summary li.active>a, .book .book-summary ul.summary li a:hover {
2 | color: #008cff;
3 | background: transparent;
4 | text-decoration: none !important;
5 | }
6 |
7 | h1 {
8 | color: #2674BA;
9 | }
10 | h2 {
11 | color: #0099CC;
12 | }
13 | h3 {
14 | color: #108ee9;
15 | }
16 | h4 {
17 | color: #662D91;
18 | }
19 | h5 {
20 | color: #444444;
21 | }
22 |
23 | .gitbook-link {
24 | display: none !important;
25 | }
26 |
27 | .summary-title-span {
28 | position: relative !important;
29 | padding: 0 !important;
30 | }
31 | .rule-title:after,
32 | .sample-title:after {
33 | font-size: 12px;
34 | padding: 0 3px;
35 | border-radius: 3px;
36 | color: #fff;
37 | }
38 |
39 | .rule-title:after{
40 | content: 'rule';
41 | background-color: #108ee9;
42 | }
43 |
44 | .sample-title:after {
45 | content: 'sample';
46 | background-color: #00a854;
47 | }
48 |
49 | .page-inner {
50 | max-width: 1000px !important;
51 | }
52 |
--------------------------------------------------------------------------------
/docs-src/book.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "AnyProxy",
3 | "author": "AnyProxy",
4 | "description": "A fully configurable http/https proxy in NodeJS",
5 | "plugins": [
6 |
7 | ],
8 | "pluginsConfig": {
9 |
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/docs-src/cn/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 |
4 | * [简介](README.md)
5 | * [快速开始](README.md#快速开始)
6 | * [安装](README.md#安装)
7 | * [启动](README.md#启动)
8 | * [其他命令](README.md#其他命令)
9 | * [作为npm模块启动](README.md#作为npm模块使用)
10 | * [代理HTTPS](README.md#代理https)
11 | * [代理WebSocket](README.md#代理websocket)
12 | * [rule模块](README.md#rule模块)
13 | * [开发示例](README.md#开发示例)
14 | * [处理流程](README.md#处理流程)
15 | * [如何引用](README.md#如何引用)
16 | * [rule接口文档](README.md#rule接口文档)
17 | * [summary_class=rule-title](README.md#summary)
18 | * [beforeSendRequest_class=rule-title](README.md#beforesendrequest)
19 | * [beforeSendResponse_class=rule-title](README.md#beforesendresponse)
20 | * [beforeDealHttpsRequest_class=rule-title](README.md#beforedealhttpsrequest)
21 | * [onError_class=rule-title](README.md#onerror)
22 | * [onConnectError_class=rule-title](README.md#onconnecterror)
23 | * [rule样例](README.md#rule样例)
24 | * [使用本地数据_class=sample-title](README.md#使用本地数据)
25 | * [修改请求头_class=sample-title](README.md#修改请求头)
26 | * [修改请求数据_class=sample-title](README.md#修改请求数据)
27 | * [修改请求的目标地址_class=sample-title](README.md#修改请求的目标地址)
28 | * [修改请求协议_class=sample-title](README.md#修改请求协议)
29 | * [修改返回状态码_class=sample-title](README.md#修改返回状态码)
30 | * [修改返回头_class=sample-title](README.md#修改返回头)
31 | * [修改返回内容并延迟_class=sample-title](README.md#修改返回内容并延迟)
32 | * [证书配置](README.md#证书配置)
33 | * [OSX系统信任CA证书](README.md#osx系统信任ca证书)
34 | * [Windows系统信任CA证书](README.md#windows系统信任ca证书)
35 | * [配置OSX系统代理](README.md#配置osx系统代理)
36 | * [配置浏览器HTTP代理](README.md#配置浏览器http代理)
37 | * [iOS系统信任CA证书](README.md#ios系统信任ca证书)
38 | * [iOS >= 10.3信任CA证书](README.md#ios--103信任ca证书)
39 | * [安卓系统信任CA证书](README.md#安卓系统信任ca证书)
40 | * [配置iOS/Android系统代理](README.md#配置iosandroid系统代理)
41 | * [FAQ](README.md#faq)
42 |
--------------------------------------------------------------------------------
/docs-src/cn/_layouts/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% block title %}{{ config.title|d("GitBook", true) }}{% endblock %}
7 |
8 |
9 |
10 | {% if config.author %}{% endif %}
11 | {% if config.isbn %}{% endif %}
12 | {% block style %}
13 | {% for resource in plugins.resources.css %}
14 | {% if resource.url %}
15 |
16 | {% else %}
17 |
18 | {% endif %}
19 | {% endfor %}
20 | {% endblock %}
21 |
22 |
23 |
24 |
25 |
26 |
27 | {% set regExp = r/^foo.*/g %}
28 |
29 | {% block body %}{% endblock %}
30 | {% block javascript %}{% endblock %}
31 |
32 |
33 |
--------------------------------------------------------------------------------
/docs-src/cn/_layouts/website/summary.html:
--------------------------------------------------------------------------------
1 | {% macro articles(_articles) %}
2 | {% for article in _articles %}
3 |
4 | {% if article.path and getPageByPath(article.path) %}
5 |
6 | {% elif article.url %}
7 |
8 | {% else %}
9 |
10 | {% endif %}
11 | {% if article.level != "0" and config.pluginsConfig['theme-default'].showLevel %}
12 | {{ article.level }}.
13 | {% endif %}
14 |
15 | {{ article.title | replace(r/_class=.+/, '') }}
16 |
17 |
18 | {% if article.path or article.url %}
19 |
20 | {% else %}
21 |
22 | {% endif %}
23 |
24 | {% if article.articles.length > 0 %}
25 |
26 | {{ articles(article.articles, file, config) }}
27 |
28 | {% endif %}
29 |
30 | {% endfor %}
31 | {% endmacro %}
32 |
33 |
34 | {% set _divider = false %}
35 | {% if config.links.sidebar %}
36 | {% for linkTitle, link in config.links.sidebar %}
37 | {% set _divider = true %}
38 | -
39 | {{ linkTitle }}
40 |
41 | {% endfor %}
42 | {% endif %}
43 |
44 | {% if _divider %}
45 |
46 | {% endif %}
47 |
48 | {% for part in summary.parts %}
49 | {% if part.title %}
50 |
51 | {% elif not loop.first %}
52 |
53 | {% endif %}
54 | {{ articles(part.articles, file, config) }}
55 | {% endfor %}
56 |
57 |
58 |
59 | -
60 |
61 | {{ "GITBOOK_LINK"|t }}
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/docs-src/en/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 | * [Introduction](README.md)
4 | * [Getting-Start](README.md#getting-start)
5 | * [Install](README.md#install)
6 | * [Launch](README.md#launch)
7 | * [Options](README.md#options)
8 | * [As Node Module](README.md#use-anyproxy-as-an-npm-module)
9 | * [Proxy HTTPS](README.md#proxy-https)
10 | * [Proxy WebSocket](README.md#proxy-websocket)
11 | * [Rule Introduction](README.md#rule-introduction)
12 | * [Sample](README.md#sample)
13 | * [How Does It Work](README.md#how-does-it-work)
14 | * [Load A Rule](README.md#how-to-load-rule-module)
15 | * [Rule Module Interfaces](README.md#rule-module-interface)
16 | * [summary_class=rule-title](README.md#summary)
17 | * [beforeSendRequest_class=rule-title](README.md#beforesendrequest)
18 | * [beforeSendResponse_class=rule-title](README.md#beforesendresponse)
19 | * [beforeDealHttpsRequest_class=rule-title](README.md#beforedealhttpsrequest)
20 | * [onError_class=rule-title](README.md#onerror)
21 | * [onConnectError_class=rule-title](README.md#onconnecterror)
22 | * [Rule Samples](README.md#rule-samples)
23 | * [Use local response_class=sample-title](README.md#use-local-response)
24 | * [Modify Request Header_class=sample-title](README.md#modify-request-header)
25 | * [Modify Request Body_class=sample-title](README.md#modify-request-body)
26 | * [Modify The Request Target_class=sample-title](README.md#modify-the-request-target)
27 | * [Modify Request Protocol_class=sample-title](README.md#modify-request-protocol)
28 | * [Modify Response Status Code_class=sample-title](README.md#modify-response-status-code)
29 | * [Modify The Response Header_class=sample-title](README.md#modify-the-response-header)
30 | * [Modify Response Data And Delay_class=sample-title](README.md#modify-response-data-and-delay)
31 | * [Config Certification](README.md#config-certification)
32 | * [Config Root CA In OSX](README.md#config-root-ca-in-osx)
33 | * [Configure Root CA In windows](README.md#config-root-ca-in-windows)
34 | * [Config OSX System Proxy](README.md#config-osx-system-proxy)
35 | * [Config As Http Proxy Server](README.md#config-http-proxy-server)
36 | * [Trust Root CA In IOS](README.md#trust-root-ca-in-ios)
37 | * [Trust Root CA In iOS after 10.3](README.md#trust-root-ca-in-ios-after-103)
38 | * [Trust Root CA In Android](README.md#trust-root-ca-in-android)
39 | * [Config IOS/Android Proxy Server](README.md#config-iosandroid-proxy-server)
40 | * [FAQ](README.md)
41 |
--------------------------------------------------------------------------------
/docs-src/en/_layouts/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% block title %}{{ config.title|d("GitBook", true) }}{% endblock %}
7 |
8 |
9 |
10 | {% if config.author %}{% endif %}
11 | {% if config.isbn %}{% endif %}
12 | {% block style %}
13 | {% for resource in plugins.resources.css %}
14 | {% if resource.url %}
15 |
16 | {% else %}
17 |
18 | {% endif %}
19 | {% endfor %}
20 | {% endblock %}
21 |
22 |
23 |
24 |
25 |
26 | {% block body %}{% endblock %}
27 | {% block javascript %}{% endblock %}
28 |
29 |
30 |
--------------------------------------------------------------------------------
/docs-src/en/_layouts/website/summary.html:
--------------------------------------------------------------------------------
1 | {% macro articles(_articles) %}
2 | {% for article in _articles %}
3 |
4 | {% if article.path and getPageByPath(article.path) %}
5 |
6 | {% elif article.url %}
7 |
8 | {% else %}
9 |
10 | {% endif %}
11 | {% if article.level != "0" and config.pluginsConfig['theme-default'].showLevel %}
12 | {{ article.level }}.
13 | {% endif %}
14 |
15 | {{ article.title | replace(r/_class=.+/, '') }}
16 |
17 |
18 | {% if article.path or article.url %}
19 |
20 | {% else %}
21 |
22 | {% endif %}
23 |
24 | {% if article.articles.length > 0 %}
25 |
26 | {{ articles(article.articles, file, config) }}
27 |
28 | {% endif %}
29 |
30 | {% endfor %}
31 | {% endmacro %}
32 |
33 |
34 | {% set _divider = false %}
35 | {% if config.links.sidebar %}
36 | {% for linkTitle, link in config.links.sidebar %}
37 | {% set _divider = true %}
38 | -
39 | {{ linkTitle }}
40 |
41 | {% endfor %}
42 | {% endif %}
43 |
44 | {% if _divider %}
45 |
46 | {% endif %}
47 |
48 | {% for part in summary.parts %}
49 | {% if part.title %}
50 |
51 | {% elif not loop.first %}
52 |
53 | {% endif %}
54 | {{ articles(part.articles, file, config) }}
55 | {% endfor %}
56 |
57 |
58 |
59 | -
60 |
61 | {{ "GITBOOK_LINK"|t }}
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/docs-src/en/book.json:
--------------------------------------------------------------------------------
1 | {
2 | "styles": {
3 | "website": "styles/website.css"
4 | }
5 | }
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | anyproxy.io
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # THIS IS AUTO GENERATED FILE, DO NOT EDIT THE HTML DIRECTLY.
2 | # YOU CAN EDIT THE SOURCE IN docs-md FOLDER
3 |
--------------------------------------------------------------------------------
/docs/assets/auto-lang.js:
--------------------------------------------------------------------------------
1 | /* eslint no-var: off */
2 | /**
3 | * detect if the browser is in UTF-8 zone
4 | * @return boolean
5 | */
6 | function isUTF8Zone() {
7 | return new Date().getTimezoneOffset() === -480;
8 | }
9 |
10 | /**
11 | * detect if the browser is already in a locale view
12 | */
13 | function isInLocaleView() {
14 | return /(cn|en)/i.test(location.href);
15 | }
16 |
17 | function initDefaultLocaleAndStatic() {
18 | if (!isInLocaleView()) {
19 | location.href = isUTF8Zone() ? '/cn' : 'en';
20 | }
21 | }
22 |
23 | initDefaultLocaleAndStatic();
24 |
--------------------------------------------------------------------------------
/docs/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/anyproxy/b93f948107b956e07c7b68faeff0c777a1f50486/docs/assets/favicon.png
--------------------------------------------------------------------------------
/docs/assets/main.js:
--------------------------------------------------------------------------------
1 | /* eslint no-var: off */
2 | function injectBaiduStatic() {
3 | var _hmt = _hmt || [];
4 | var hm = document.createElement('script');
5 | var s = document.getElementsByTagName('script')[0];
6 |
7 | hm.src = '//hm.baidu.com/hm.js?4e51565b7d471fd6623c163a8fd79e07';
8 | s.parentNode.insertBefore(hm, s);
9 | }
10 |
11 |
12 | injectBaiduStatic();
13 |
--------------------------------------------------------------------------------
/docs/assets/website.css:
--------------------------------------------------------------------------------
1 | .book .book-summary ul.summary li.active>a, .book .book-summary ul.summary li a:hover {
2 | color: #008cff;
3 | background: transparent;
4 | text-decoration: none !important;
5 | }
6 |
7 | h1 {
8 | color: #2674BA;
9 | }
10 | h2 {
11 | color: #0099CC;
12 | }
13 | h3 {
14 | color: #108ee9;
15 | }
16 | h4 {
17 | color: #662D91;
18 | }
19 | h5 {
20 | color: #444444;
21 | }
22 |
23 | .gitbook-link {
24 | display: none !important;
25 | }
26 |
27 | .summary-title-span {
28 | position: relative !important;
29 | padding: 0 !important;
30 | }
31 | .rule-title:after,
32 | .sample-title:after {
33 | font-size: 12px;
34 | padding: 0 3px;
35 | border-radius: 3px;
36 | color: #fff;
37 | }
38 |
39 | .rule-title:after{
40 | content: 'rule';
41 | background-color: #108ee9;
42 | }
43 |
44 | .sample-title:after {
45 | content: 'sample';
46 | background-color: #00a854;
47 | }
48 |
49 | .page-inner {
50 | max-width: 1000px !important;
51 | }
52 |
--------------------------------------------------------------------------------
/docs/gitbook/fonts/fontawesome/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/anyproxy/b93f948107b956e07c7b68faeff0c777a1f50486/docs/gitbook/fonts/fontawesome/FontAwesome.otf
--------------------------------------------------------------------------------
/docs/gitbook/fonts/fontawesome/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/anyproxy/b93f948107b956e07c7b68faeff0c777a1f50486/docs/gitbook/fonts/fontawesome/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/docs/gitbook/fonts/fontawesome/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/anyproxy/b93f948107b956e07c7b68faeff0c777a1f50486/docs/gitbook/fonts/fontawesome/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/anyproxy/b93f948107b956e07c7b68faeff0c777a1f50486/docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/anyproxy/b93f948107b956e07c7b68faeff0c777a1f50486/docs/gitbook/fonts/fontawesome/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/docs/gitbook/gitbook-plugin-highlight/ebook.css:
--------------------------------------------------------------------------------
1 | pre,
2 | code {
3 | /* http://jmblog.github.io/color-themes-for-highlightjs */
4 | /* Tomorrow Comment */
5 | /* Tomorrow Red */
6 | /* Tomorrow Orange */
7 | /* Tomorrow Yellow */
8 | /* Tomorrow Green */
9 | /* Tomorrow Aqua */
10 | /* Tomorrow Blue */
11 | /* Tomorrow Purple */
12 | }
13 | pre .hljs-comment,
14 | code .hljs-comment,
15 | pre .hljs-title,
16 | code .hljs-title {
17 | color: #8e908c;
18 | }
19 | pre .hljs-variable,
20 | code .hljs-variable,
21 | pre .hljs-attribute,
22 | code .hljs-attribute,
23 | pre .hljs-tag,
24 | code .hljs-tag,
25 | pre .hljs-regexp,
26 | code .hljs-regexp,
27 | pre .hljs-deletion,
28 | code .hljs-deletion,
29 | pre .ruby .hljs-constant,
30 | code .ruby .hljs-constant,
31 | pre .xml .hljs-tag .hljs-title,
32 | code .xml .hljs-tag .hljs-title,
33 | pre .xml .hljs-pi,
34 | code .xml .hljs-pi,
35 | pre .xml .hljs-doctype,
36 | code .xml .hljs-doctype,
37 | pre .html .hljs-doctype,
38 | code .html .hljs-doctype,
39 | pre .css .hljs-id,
40 | code .css .hljs-id,
41 | pre .css .hljs-class,
42 | code .css .hljs-class,
43 | pre .css .hljs-pseudo,
44 | code .css .hljs-pseudo {
45 | color: #c82829;
46 | }
47 | pre .hljs-number,
48 | code .hljs-number,
49 | pre .hljs-preprocessor,
50 | code .hljs-preprocessor,
51 | pre .hljs-pragma,
52 | code .hljs-pragma,
53 | pre .hljs-built_in,
54 | code .hljs-built_in,
55 | pre .hljs-literal,
56 | code .hljs-literal,
57 | pre .hljs-params,
58 | code .hljs-params,
59 | pre .hljs-constant,
60 | code .hljs-constant {
61 | color: #f5871f;
62 | }
63 | pre .ruby .hljs-class .hljs-title,
64 | code .ruby .hljs-class .hljs-title,
65 | pre .css .hljs-rules .hljs-attribute,
66 | code .css .hljs-rules .hljs-attribute {
67 | color: #eab700;
68 | }
69 | pre .hljs-string,
70 | code .hljs-string,
71 | pre .hljs-value,
72 | code .hljs-value,
73 | pre .hljs-inheritance,
74 | code .hljs-inheritance,
75 | pre .hljs-header,
76 | code .hljs-header,
77 | pre .hljs-addition,
78 | code .hljs-addition,
79 | pre .ruby .hljs-symbol,
80 | code .ruby .hljs-symbol,
81 | pre .xml .hljs-cdata,
82 | code .xml .hljs-cdata {
83 | color: #718c00;
84 | }
85 | pre .css .hljs-hexcolor,
86 | code .css .hljs-hexcolor {
87 | color: #3e999f;
88 | }
89 | pre .hljs-function,
90 | code .hljs-function,
91 | pre .python .hljs-decorator,
92 | code .python .hljs-decorator,
93 | pre .python .hljs-title,
94 | code .python .hljs-title,
95 | pre .ruby .hljs-function .hljs-title,
96 | code .ruby .hljs-function .hljs-title,
97 | pre .ruby .hljs-title .hljs-keyword,
98 | code .ruby .hljs-title .hljs-keyword,
99 | pre .perl .hljs-sub,
100 | code .perl .hljs-sub,
101 | pre .javascript .hljs-title,
102 | code .javascript .hljs-title,
103 | pre .coffeescript .hljs-title,
104 | code .coffeescript .hljs-title {
105 | color: #4271ae;
106 | }
107 | pre .hljs-keyword,
108 | code .hljs-keyword,
109 | pre .javascript .hljs-function,
110 | code .javascript .hljs-function {
111 | color: #8959a8;
112 | }
113 | pre .hljs,
114 | code .hljs {
115 | display: block;
116 | background: white;
117 | color: #4d4d4c;
118 | padding: 0.5em;
119 | }
120 | pre .coffeescript .javascript,
121 | code .coffeescript .javascript,
122 | pre .javascript .xml,
123 | code .javascript .xml,
124 | pre .tex .hljs-formula,
125 | code .tex .hljs-formula,
126 | pre .xml .javascript,
127 | code .xml .javascript,
128 | pre .xml .vbscript,
129 | code .xml .vbscript,
130 | pre .xml .css,
131 | code .xml .css,
132 | pre .xml .hljs-cdata,
133 | code .xml .hljs-cdata {
134 | opacity: 0.5;
135 | }
136 |
--------------------------------------------------------------------------------
/docs/gitbook/gitbook-plugin-lunr/search-lunr.js:
--------------------------------------------------------------------------------
1 | require([
2 | 'gitbook',
3 | 'jquery'
4 | ], function(gitbook, $) {
5 | // Define global search engine
6 | function LunrSearchEngine() {
7 | this.index = null;
8 | this.store = {};
9 | this.name = 'LunrSearchEngine';
10 | }
11 |
12 | // Initialize lunr by fetching the search index
13 | LunrSearchEngine.prototype.init = function() {
14 | var that = this;
15 | var d = $.Deferred();
16 |
17 | $.getJSON(gitbook.state.basePath+'/search_index.json')
18 | .then(function(data) {
19 | // eslint-disable-next-line no-undef
20 | that.index = lunr.Index.load(data.index);
21 | that.store = data.store;
22 | d.resolve();
23 | });
24 |
25 | return d.promise();
26 | };
27 |
28 | // Search for a term and return results
29 | LunrSearchEngine.prototype.search = function(q, offset, length) {
30 | var that = this;
31 | var results = [];
32 |
33 | if (this.index) {
34 | results = $.map(this.index.search(q), function(result) {
35 | var doc = that.store[result.ref];
36 |
37 | return {
38 | title: doc.title,
39 | url: doc.url,
40 | body: doc.summary || doc.body
41 | };
42 | });
43 | }
44 |
45 | return $.Deferred().resolve({
46 | query: q,
47 | results: results.slice(0, length),
48 | count: results.length
49 | }).promise();
50 | };
51 |
52 | // Set gitbook research
53 | gitbook.events.bind('start', function(e, config) {
54 | var engine = gitbook.search.getEngine();
55 | if (!engine) {
56 | gitbook.search.setEngine(LunrSearchEngine, config);
57 | }
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/docs/gitbook/gitbook-plugin-search/search-engine.js:
--------------------------------------------------------------------------------
1 | require([
2 | 'gitbook',
3 | 'jquery'
4 | ], function(gitbook, $) {
5 | // Global search objects
6 | var engine = null;
7 | var initialized = false;
8 |
9 | // Set a new search engine
10 | function setEngine(Engine, config) {
11 | initialized = false;
12 | engine = new Engine(config);
13 |
14 | init(config);
15 | }
16 |
17 | // Initialize search engine with config
18 | function init(config) {
19 | if (!engine) throw new Error('No engine set for research. Set an engine using gitbook.research.setEngine(Engine).');
20 |
21 | return engine.init(config)
22 | .then(function() {
23 | initialized = true;
24 | gitbook.events.trigger('search.ready');
25 | });
26 | }
27 |
28 | // Launch search for query q
29 | function query(q, offset, length) {
30 | if (!initialized) throw new Error('Search has not been initialized');
31 | return engine.search(q, offset, length);
32 | }
33 |
34 | // Get stats about search
35 | function getEngine() {
36 | return engine? engine.name : null;
37 | }
38 |
39 | function isInitialized() {
40 | return initialized;
41 | }
42 |
43 | // Initialize gitbook.search
44 | gitbook.search = {
45 | setEngine: setEngine,
46 | getEngine: getEngine,
47 | query: query,
48 | isInitialized: isInitialized
49 | };
50 | });
--------------------------------------------------------------------------------
/docs/gitbook/gitbook-plugin-search/search.css:
--------------------------------------------------------------------------------
1 | /*
2 | This CSS only styled the search results section, not the search input
3 | It defines the basic interraction to hide content when displaying results, etc
4 | */
5 | #book-search-results .search-results {
6 | display: none;
7 | }
8 | #book-search-results .search-results ul.search-results-list {
9 | list-style-type: none;
10 | padding-left: 0;
11 | }
12 | #book-search-results .search-results ul.search-results-list li {
13 | margin-bottom: 1.5rem;
14 | padding-bottom: 0.5rem;
15 | /* Highlight results */
16 | }
17 | #book-search-results .search-results ul.search-results-list li p em {
18 | background-color: rgba(255, 220, 0, 0.4);
19 | font-style: normal;
20 | }
21 | #book-search-results .search-results .no-results {
22 | display: none;
23 | }
24 | #book-search-results.open .search-results {
25 | display: block;
26 | }
27 | #book-search-results.open .search-noresults {
28 | display: none;
29 | }
30 | #book-search-results.no-results .search-results .has-results {
31 | display: none;
32 | }
33 | #book-search-results.no-results .search-results .no-results {
34 | display: block;
35 | }
36 |
--------------------------------------------------------------------------------
/docs/gitbook/gitbook-plugin-sharing/buttons.js:
--------------------------------------------------------------------------------
1 | require(['gitbook', 'jquery'], function(gitbook, $) {
2 | var SITES = {
3 | 'facebook': {
4 | 'label': 'Facebook',
5 | 'icon': 'fa fa-facebook',
6 | 'onClick': function(e) {
7 | e.preventDefault();
8 | window.open('http://www.facebook.com/sharer/sharer.php?s=100&p[url]='+encodeURIComponent(location.href));
9 | }
10 | },
11 | 'twitter': {
12 | 'label': 'Twitter',
13 | 'icon': 'fa fa-twitter',
14 | 'onClick': function(e) {
15 | e.preventDefault();
16 | window.open('http://twitter.com/home?status='+encodeURIComponent(document.title+' '+location.href));
17 | }
18 | },
19 | 'google': {
20 | 'label': 'Google+',
21 | 'icon': 'fa fa-google-plus',
22 | 'onClick': function(e) {
23 | e.preventDefault();
24 | window.open('https://plus.google.com/share?url='+encodeURIComponent(location.href));
25 | }
26 | },
27 | 'weibo': {
28 | 'label': 'Weibo',
29 | 'icon': 'fa fa-weibo',
30 | 'onClick': function(e) {
31 | e.preventDefault();
32 | window.open('http://service.weibo.com/share/share.php?content=utf-8&url='+encodeURIComponent(location.href)+'&title='+encodeURIComponent(document.title));
33 | }
34 | },
35 | 'instapaper': {
36 | 'label': 'Instapaper',
37 | 'icon': 'fa fa-instapaper',
38 | 'onClick': function(e) {
39 | e.preventDefault();
40 | window.open('http://www.instapaper.com/text?u='+encodeURIComponent(location.href));
41 | }
42 | },
43 | 'vk': {
44 | 'label': 'VK',
45 | 'icon': 'fa fa-vk',
46 | 'onClick': function(e) {
47 | e.preventDefault();
48 | window.open('http://vkontakte.ru/share.php?url='+encodeURIComponent(location.href));
49 | }
50 | }
51 | };
52 |
53 |
54 |
55 | gitbook.events.bind('start', function(e, config) {
56 | var opts = config.sharing;
57 |
58 | // Create dropdown menu
59 | var menu = $.map(opts.all, function(id) {
60 | var site = SITES[id];
61 |
62 | return {
63 | text: site.label,
64 | onClick: site.onClick
65 | };
66 | });
67 |
68 | // Create main button with dropdown
69 | if (menu.length > 0) {
70 | gitbook.toolbar.createButton({
71 | icon: 'fa fa-share-alt',
72 | label: 'Share',
73 | position: 'right',
74 | dropdown: [menu]
75 | });
76 | }
77 |
78 | // Direct actions to share
79 | $.each(SITES, function(sideId, site) {
80 | if (!opts[sideId]) return;
81 |
82 | gitbook.toolbar.createButton({
83 | icon: site.icon,
84 | label: site.text,
85 | position: 'right',
86 | onClick: site.onClick
87 | });
88 | });
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/docs/gitbook/images/apple-touch-icon-precomposed-152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/anyproxy/b93f948107b956e07c7b68faeff0c777a1f50486/docs/gitbook/images/apple-touch-icon-precomposed-152.png
--------------------------------------------------------------------------------
/docs/gitbook/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/anyproxy/b93f948107b956e07c7b68faeff0c777a1f50486/docs/gitbook/images/favicon.ico
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Choose a language · AnyProxy
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
Choose a language
54 |
55 |
56 |
57 | -
58 | 中文
59 |
60 |
61 | -
62 | English
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/lib/certMgr.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const EasyCert = require('node-easy-cert');
4 | const co = require('co');
5 | const os = require('os');
6 | const inquirer = require('inquirer');
7 |
8 | const util = require('./util');
9 | const logUtil = require('./log');
10 |
11 | const options = {
12 | rootDirPath: util.getAnyProxyPath('certificates'),
13 | inMemory: false,
14 | defaultCertAttrs: [
15 | { name: 'countryName', value: 'CN' },
16 | { name: 'organizationName', value: 'AnyProxy' },
17 | { shortName: 'ST', value: 'SH' },
18 | { shortName: 'OU', value: 'AnyProxy SSL Proxy' }
19 | ]
20 | };
21 |
22 | const easyCert = new EasyCert(options);
23 | const crtMgr = util.merge({}, easyCert);
24 |
25 | // rename function
26 | crtMgr.ifRootCAFileExists = easyCert.isRootCAFileExists;
27 |
28 | crtMgr.generateRootCA = function (cb) {
29 | doGenerate(false);
30 |
31 | // set default common name of the cert
32 | function doGenerate(overwrite) {
33 | const rootOptions = {
34 | commonName: 'AnyProxy',
35 | overwrite: !!overwrite
36 | };
37 |
38 | easyCert.generateRootCA(rootOptions, (error, keyPath, crtPath) => {
39 | cb(error, keyPath, crtPath);
40 | });
41 | }
42 | };
43 |
44 | crtMgr.getCAStatus = function *() {
45 | return co(function *() {
46 | const result = {
47 | exist: false,
48 | };
49 | const ifExist = easyCert.isRootCAFileExists();
50 | if (!ifExist) {
51 | return result;
52 | } else {
53 | result.exist = true;
54 | if (!/^win/.test(process.platform)) {
55 | result.trusted = yield easyCert.ifRootCATrusted;
56 | }
57 | return result;
58 | }
59 | });
60 | }
61 |
62 | /**
63 | * trust the root ca by command
64 | */
65 | crtMgr.trustRootCA = function *() {
66 | const platform = os.platform();
67 | const rootCAPath = crtMgr.getRootCAFilePath();
68 | const trustInquiry = [
69 | {
70 | type: 'list',
71 | name: 'trustCA',
72 | message: 'The rootCA is not trusted yet, install it to the trust store now?',
73 | choices: ['Yes', "No, I'll do it myself"]
74 | }
75 | ];
76 |
77 | if (platform === 'darwin') {
78 | const answer = yield inquirer.prompt(trustInquiry);
79 | if (answer.trustCA === 'Yes') {
80 | logUtil.info('About to trust the root CA, this may requires your password');
81 | // https://ss64.com/osx/security-cert.html
82 | const result = util.execScriptSync(`sudo security add-trusted-cert -d -k /Library/Keychains/System.keychain ${rootCAPath}`);
83 | if (result.status === 0) {
84 | logUtil.info('Root CA install, you are ready to intercept the https now');
85 | } else {
86 | console.error(result);
87 | logUtil.info('Failed to trust the root CA, please trust it manually');
88 | util.guideToHomePage();
89 | }
90 | } else {
91 | logUtil.info('Please trust the root CA manually so https interception works');
92 | util.guideToHomePage();
93 | }
94 | }
95 |
96 |
97 | if (/^win/.test(process.platform)) {
98 | logUtil.info('You can install the root CA manually.');
99 | }
100 | logUtil.info('The root CA file path is: ' + crtMgr.getRootCAFilePath());
101 | }
102 |
103 | module.exports = crtMgr;
104 |
--------------------------------------------------------------------------------
/lib/configUtil.js:
--------------------------------------------------------------------------------
1 | /**
2 | * a util to set and get all configuable constant
3 | *
4 | */
5 | const path = require('path');
6 |
7 | const USER_HOME = process.env.HOME || process.env.USERPROFILE;
8 | const DEFAULT_ANYPROXY_HOME = path.join(USER_HOME, '/.anyproxy/');
9 |
10 | /**
11 | * return AnyProxy's home path
12 | */
13 | module.exports.getAnyProxyHome = function () {
14 | const ENV_ANYPROXY_HOME = process.env.ANYPROXY_HOME || '';
15 | return ENV_ANYPROXY_HOME || DEFAULT_ANYPROXY_HOME;
16 | }
17 |
--------------------------------------------------------------------------------
/lib/log.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const color = require('colorful');
4 | const util = require('./util');
5 |
6 | let ifPrint = true;
7 | let logLevel = 0;
8 | const LogLevelMap = {
9 | tip: 0,
10 | system_error: 1,
11 | rule_error: 2,
12 | warn: 3,
13 | debug: 4,
14 | };
15 |
16 | function setPrintStatus(status) {
17 | ifPrint = !!status;
18 | }
19 |
20 | function setLogLevel(level) {
21 | logLevel = parseInt(level, 10);
22 | }
23 |
24 | function printLog(content, type) {
25 | if (!ifPrint) {
26 | return;
27 | }
28 |
29 | const timeString = util.formatDate(new Date(), 'YYYY-MM-DD hh:mm:ss');
30 | switch (type) {
31 | case LogLevelMap.tip: {
32 | if (logLevel > 0) {
33 | return;
34 | }
35 | console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content));
36 | break;
37 | }
38 |
39 | case LogLevelMap.system_error: {
40 | if (logLevel > 1) {
41 | return;
42 | }
43 | console.error(color.red(`[AnyProxy ERROR][${timeString}]: ` + content));
44 | break;
45 | }
46 |
47 | case LogLevelMap.rule_error: {
48 | if (logLevel > 2) {
49 | return;
50 | }
51 |
52 | console.error(color.red(`[AnyProxy RULE_ERROR][${timeString}]: ` + content));
53 | break;
54 | }
55 |
56 | case LogLevelMap.warn: {
57 | if (logLevel > 3) {
58 | return;
59 | }
60 |
61 | console.error(color.yellow(`[AnyProxy WARN][${timeString}]: ` + content));
62 | break;
63 | }
64 |
65 | case LogLevelMap.debug: {
66 | console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content));
67 | return;
68 | }
69 |
70 | default : {
71 | console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content));
72 | break;
73 | }
74 | }
75 | }
76 |
77 | module.exports.printLog = printLog;
78 |
79 | module.exports.debug = (content) => {
80 | printLog(content, LogLevelMap.debug);
81 | };
82 |
83 | module.exports.info = (content) => {
84 | printLog(content, LogLevelMap.tip);
85 | };
86 |
87 | module.exports.warn = (content) => {
88 | printLog(content, LogLevelMap.warn);
89 | };
90 |
91 | module.exports.error = (content) => {
92 | printLog(content, LogLevelMap.system_error);
93 | };
94 |
95 | module.exports.ruleError = (content) => {
96 | printLog(content, LogLevelMap.rule_error);
97 | };
98 |
99 | module.exports.setPrintStatus = setPrintStatus;
100 | module.exports.setLogLevel = setLogLevel;
101 | module.exports.T_TIP = LogLevelMap.tip;
102 | module.exports.T_ERR = LogLevelMap.system_error;
103 | module.exports.T_RULE_ERROR = LogLevelMap.rule_error;
104 | module.exports.T_WARN = LogLevelMap.warn;
105 | module.exports.T_DEBUG = LogLevelMap.debug;
106 |
--------------------------------------------------------------------------------
/lib/requestErrorHandler.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /*
4 | * handle all request error here,
5 | *
6 | */
7 | const pug = require('pug');
8 | const path = require('path');
9 |
10 | const error502PugFn = pug.compileFile(path.join(__dirname, '../resource/502.pug'));
11 | const certPugFn = pug.compileFile(path.join(__dirname, '../resource/cert_error.pug'));
12 |
13 | /**
14 | * get error content for certification issues
15 | */
16 | function getCertErrorContent(error, fullUrl) {
17 | let content;
18 | const title = 'The connection is not private. ';
19 | let explain = 'There are error with the certfication of the site.';
20 | switch (error.code) {
21 | case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY': {
22 | explain = 'The certfication of the site you are visiting is not issued by a known agency, '
23 | + 'It usually happenes when the cert is a self-signed one.'
24 | + 'If you know and trust the site, you can run AnyProxy with option -ignore-unauthorized-ssl to continue.'
25 |
26 | break;
27 | }
28 | default: {
29 | explain = ''
30 | break;
31 | }
32 | }
33 |
34 | try {
35 | content = certPugFn({
36 | title: title,
37 | explain: explain,
38 | code: error.code
39 | });
40 | } catch (parseErro) {
41 | content = error.stack;
42 | }
43 |
44 | return content;
45 | }
46 |
47 | /*
48 | * get the default error content
49 | */
50 | function getDefaultErrorCotent(error, fullUrl) {
51 | let content;
52 |
53 | try {
54 | content = error502PugFn({
55 | error,
56 | url: fullUrl,
57 | errorStack: error.stack.split(/\n/)
58 | });
59 | } catch (parseErro) {
60 | content = error.stack;
61 | }
62 |
63 | return content;
64 | }
65 |
66 | /*
67 | * get mapped error content for each error
68 | */
69 | module.exports.getErrorContent = function (error, fullUrl) {
70 | let content = '';
71 | error = error || {};
72 | switch (error.code) {
73 | case 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY': {
74 | content = getCertErrorContent(error, fullUrl);
75 | break;
76 | }
77 | default: {
78 | content = getDefaultErrorCotent(error, fullUrl);
79 | break;
80 | }
81 | }
82 |
83 | return content;
84 | }
85 |
--------------------------------------------------------------------------------
/lib/ruleLoader.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const proxyUtil = require('./util');
4 | const path = require('path');
5 | const fs = require('fs');
6 | const request = require('request');
7 |
8 | const cachePath = proxyUtil.getAnyProxyTmpPath();
9 |
10 | /**
11 | * download a file and cache
12 | *
13 | * @param {any} url
14 | * @returns {string} cachePath
15 | */
16 | function cacheRemoteFile(url) {
17 | return new Promise((resolve, reject) => {
18 | request(url, (error, response, body) => {
19 | if (error) {
20 | return reject(error);
21 | } else if (response.statusCode !== 200) {
22 | return reject(`failed to load with a status code ${response.statusCode}`);
23 | } else {
24 | const fileCreatedTime = proxyUtil.formatDate(new Date(), 'YYYY_MM_DD_hh_mm_ss');
25 | const random = Math.ceil(Math.random() * 500);
26 | const fileName = `remote_rule_${fileCreatedTime}_r${random}.js`;
27 | const filePath = path.join(cachePath, fileName);
28 | fs.writeFileSync(filePath, body);
29 | resolve(filePath);
30 | }
31 | });
32 | });
33 | }
34 |
35 |
36 | /**
37 | * load a local npm module
38 | *
39 | * @param {any} filePath
40 | * @returns module
41 | */
42 | function loadLocalPath(filePath) {
43 | return new Promise((resolve, reject) => {
44 | const ruleFilePath = path.resolve(process.cwd(), filePath);
45 | if (fs.existsSync(ruleFilePath)) {
46 | resolve(require(ruleFilePath));
47 | } else {
48 | resolve(require(filePath));
49 | }
50 | });
51 | }
52 |
53 |
54 | /**
55 | * load a module from url or local path
56 | *
57 | * @param {any} urlOrPath
58 | * @returns module
59 | */
60 | function requireModule(urlOrPath) {
61 | return new Promise((resolve, reject) => {
62 | if (/^http/i.test(urlOrPath)) {
63 | resolve(cacheRemoteFile(urlOrPath));
64 | } else {
65 | resolve(urlOrPath);
66 | }
67 | }).then(localPath => loadLocalPath(localPath));
68 | }
69 |
70 | module.exports = {
71 | cacheRemoteFile,
72 | loadLocalPath,
73 | requireModule,
74 | };
75 |
--------------------------------------------------------------------------------
/lib/rule_default.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 |
5 | summary: 'the default rule for AnyProxy',
6 |
7 | /**
8 | *
9 | *
10 | * @param {object} requestDetail
11 | * @param {string} requestDetail.protocol
12 | * @param {object} requestDetail.requestOptions
13 | * @param {object} requestDetail.requestData
14 | * @param {object} requestDetail.response
15 | * @param {number} requestDetail.response.statusCode
16 | * @param {object} requestDetail.response.header
17 | * @param {buffer} requestDetail.response.body
18 | * @returns
19 | */
20 | *beforeSendRequest(requestDetail) {
21 | return null;
22 | },
23 |
24 |
25 | /**
26 | *
27 | *
28 | * @param {object} requestDetail
29 | * @param {object} responseDetail
30 | */
31 | *beforeSendResponse(requestDetail, responseDetail) {
32 | return null;
33 | },
34 |
35 |
36 | /**
37 | * default to return null
38 | * the user MUST return a boolean when they do implement the interface in rule
39 | *
40 | * @param {any} requestDetail
41 | * @returns
42 | */
43 | *beforeDealHttpsRequest(requestDetail) {
44 | return null;
45 | },
46 |
47 | /**
48 | *
49 | *
50 | * @param {any} requestDetail
51 | * @param {any} error
52 | * @returns
53 | */
54 | *onError(requestDetail, error) {
55 | return null;
56 | },
57 |
58 |
59 | /**
60 | *
61 | *
62 | * @param {any} requestDetail
63 | * @param {any} error
64 | * @returns
65 | */
66 | *onConnectError(requestDetail, error) {
67 | return null;
68 | },
69 |
70 |
71 | /**
72 | *
73 | *
74 | * @param {any} requestDetail
75 | * @param {any} error
76 | * @returns
77 | */
78 | *onClientSocketError(requestDetail, error) {
79 | return null;
80 | },
81 | };
82 |
--------------------------------------------------------------------------------
/lib/wsServerMgr.js:
--------------------------------------------------------------------------------
1 | /**
2 | * manage the websocket server
3 | *
4 | */
5 | const ws = require('ws');
6 | const logUtil = require('./log.js');
7 |
8 | const WsServer = ws.Server;
9 |
10 | /**
11 | * get a new websocket server based on the server
12 | * @param @required {object} config
13 | {string} config.server
14 | {handler} config.handler
15 | */
16 | function getWsServer(config) {
17 | const wss = new WsServer({
18 | server: config.server
19 | });
20 |
21 | wss.on('connection', config.connHandler);
22 |
23 | wss.on('headers', (headers) => {
24 | headers.push('x-anyproxy-websocket:true');
25 | });
26 |
27 | wss.on('error', e => {
28 | logUtil.error(`error in websocket proxy: ${e.message},\r\n ${e.stack}`);
29 | console.error('error happened in proxy websocket:', e)
30 | });
31 |
32 | wss.on('close', e => {
33 | console.error('==> closing the ws server');
34 | });
35 |
36 | return wss;
37 | }
38 |
39 | module.exports.getWsServer = getWsServer;
40 |
--------------------------------------------------------------------------------
/module_sample/core_reload.js:
--------------------------------------------------------------------------------
1 | const AnyProxy = require('../proxy');
2 | const exec = require('child_process').exec;
3 |
4 | const AnyProxyRecorder = require('../lib/recorder');
5 | const WebInterfaceLite = require('../lib/webInterface');
6 |
7 | /*
8 | -------------------------------
9 | | ProxyServerA | ProxyServerB |
10 | ------------------------------- ----------------------------
11 | | Common Recorder | -------(by events)------| WebInterfaceLite |
12 | ------------------------------- ----------------------------
13 | */
14 |
15 |
16 | const commonRecorder = new AnyProxyRecorder();
17 |
18 | // web interface依赖recorder
19 | new WebInterfaceLite({ // common web interface
20 | webPort: 8002
21 | }, commonRecorder);
22 |
23 | // proxy core只依赖recorder,与webServer无关
24 | const optionsA = {
25 | port: 8001,
26 | recorder: commonRecorder, // use common recorder
27 | };
28 |
29 | const optionsB = {
30 | port: 8005,
31 | recorder: commonRecorder, // use common recorder
32 | };
33 |
34 | const proxyServerA = new AnyProxy.ProxyCore(optionsA);
35 | const proxyServerB = new AnyProxy.ProxyCore(optionsB);
36 |
37 | proxyServerA.start();
38 | proxyServerB.start();
39 |
40 | // after both ready
41 | setTimeout(() => {
42 | exec('curl http://www.qq.com --proxy http://127.0.0.1:8001');
43 | exec('curl http://www.sina.com.cn --proxy http://127.0.0.1:8005');
44 | }, 1000);
45 |
46 | // visit http://127.0.0.1 , there should be two records
47 |
48 |
--------------------------------------------------------------------------------
/module_sample/https_config.js:
--------------------------------------------------------------------------------
1 | const AnyProxy = require('../proxy');
2 | const exec = require('child_process').exec;
3 |
4 | if (!AnyProxy.utils.certMgr.ifRootCAFileExists()) {
5 | AnyProxy.utils.certMgr.generateRootCA((error, keyPath) => {
6 | // let users to trust this CA before using proxy
7 | if (!error) {
8 | const certDir = require('path').dirname(keyPath);
9 | console.log('The cert is generated at', certDir);
10 | const isWin = /^win/.test(process.platform);
11 | if (isWin) {
12 | exec('start .', { cwd: certDir });
13 | } else {
14 | exec('open .', { cwd: certDir });
15 | }
16 | } else {
17 | console.error('error when generating rootCA', error);
18 | }
19 | });
20 | } else {
21 | // clear all the certificates
22 | // AnyProxy.utils.certMgr.clearCerts()
23 | }
24 |
--------------------------------------------------------------------------------
/module_sample/normal_use.js:
--------------------------------------------------------------------------------
1 | const AnyProxy = require('../proxy');
2 |
3 | const options = {
4 | type: 'http',
5 | port: 8001,
6 | rule: null,
7 | webInterface: {
8 | enable: true,
9 | webPort: 8002
10 | },
11 | throttle: 10000,
12 | forceProxyHttps: true,
13 | silent: false
14 | };
15 | const proxyServer = new AnyProxy.ProxyServer(options);
16 |
17 | proxyServer.on('ready', () => {
18 | console.log('ready');
19 | // set as system proxy
20 | proxyServer.close().then(() => {
21 | const proxyServerB = new AnyProxy.ProxyServer(options);
22 | proxyServerB.start();
23 | });
24 |
25 | console.log('closed');
26 | // setTimeout(() => {
27 |
28 | // }, 2000);
29 |
30 |
31 | // AnyProxy.utils.systemProxyMgr.enableGlobalProxy('127.0.0.1', '8001');
32 | });
33 |
34 | proxyServer.on('error', (e) => {
35 | console.log('proxy error');
36 | console.log(e);
37 | });
38 |
39 | process.on('SIGINT', () => {
40 | // AnyProxy.utils.systemProxyMgr.disableGlobalProxy();
41 | proxyServer.close();
42 | process.exit();
43 | });
44 |
45 |
46 | proxyServer.start();
47 |
48 |
49 | // const WebSocketServer = require('ws').Server;
50 | // const wsServer = new WebSocketServer({ port: 8003 },function(){
51 | // console.log('ready');
52 |
53 | // try {
54 | // const serverB = new WebSocketServer({ port: 8003 }, function (e, result) {
55 | // console.log('---in B---');
56 | // console.log(e);
57 | // console.log(result);
58 | // });
59 | // } catch(e) {
60 | // console.log(e);
61 | // console.log('e');
62 | // }
63 |
64 | // // wsServer.close(function (e, result) {
65 | // // console.log('in close');
66 | // // console.log(e);
67 | // // console.log(result);
68 | // // });
69 | // });
70 |
--------------------------------------------------------------------------------
/module_sample/simple_use.js:
--------------------------------------------------------------------------------
1 | const AnyProxy = require('../proxy');
2 |
3 | const options = {
4 | port: 8001,
5 | webInterface: {
6 | enable: true
7 | }
8 | };
9 | const proxyServer = new AnyProxy.ProxyServer(options);
10 | proxyServer.start();
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "anyproxy",
3 | "version": "4.1.3",
4 | "description": "A fully configurable HTTP/HTTPS proxy in Node.js",
5 | "main": "proxy.js",
6 | "bin": {
7 | "anyproxy-ca": "bin/anyproxy-ca",
8 | "anyproxy": "bin/anyproxy"
9 | },
10 | "dependencies": {
11 | "async": "~0.9.0",
12 | "async-task-mgr": ">=1.1.0",
13 | "body-parser": "^1.13.1",
14 | "brotli": "^1.3.2",
15 | "classnames": "^2.2.5",
16 | "clipboard-js": "^0.3.3",
17 | "co": "^4.6.0",
18 | "colorful": "^2.1.0",
19 | "commander": "~2.11.0",
20 | "component-emitter": "^1.2.1",
21 | "compression": "^1.4.4",
22 | "es6-promise": "^3.3.1",
23 | "express": "^4.8.5",
24 | "fast-json-stringify": "^0.17.0",
25 | "iconv-lite": "^0.4.6",
26 | "inquirer": "^5.2.0",
27 | "ip": "^0.3.2",
28 | "juicer": "^0.6.6-stable",
29 | "mime-types": "2.1.11",
30 | "moment": "^2.15.1",
31 | "nedb": "^1.8.0",
32 | "node-easy-cert": "^1.0.0",
33 | "pug": "^2.0.0-beta6",
34 | "qrcode-npm": "0.0.3",
35 | "request": "^2.74.0",
36 | "stream-throttle": "^0.1.3",
37 | "svg-inline-react": "^1.0.2",
38 | "thunkify": "^2.1.2",
39 | "whatwg-fetch": "^1.0.0",
40 | "ws": "^5.1.0"
41 | },
42 | "devDependencies": {
43 | "@babel/core": "^7.8.3",
44 | "@babel/preset-env": "^7.8.3",
45 | "antd": "^2.5.0",
46 | "autoprefixer": "^6.4.1",
47 | "babel-core": "^6.14.0",
48 | "babel-eslint": "^7.0.0",
49 | "babel-jest": "^24.9.0",
50 | "babel-loader": "^6.2.5",
51 | "babel-plugin-import": "^1.0.0",
52 | "babel-plugin-transform-runtime": "^6.15.0",
53 | "babel-polyfill": "^6.13.0",
54 | "babel-preset-es2015": "^6.13.2",
55 | "babel-preset-react": "^6.11.1",
56 | "babel-preset-stage-0": "^6.5.0",
57 | "babel-register": "^6.11.6",
58 | "babel-runtime": "^6.11.6",
59 | "css-loader": "^0.23.1",
60 | "eslint": ">=4.18.2",
61 | "eslint-config-airbnb": "^15.1.0",
62 | "eslint-plugin-import": "^2.7.0",
63 | "eslint-plugin-jsx-a11y": "^5.1.1",
64 | "eslint-plugin-react": "^7.4.0",
65 | "extract-text-webpack-plugin": "^3.0.2",
66 | "file-loader": "^0.9.0",
67 | "jest": "^24.9.0",
68 | "less": "^2.7.1",
69 | "less-loader": "^2.2.3",
70 | "node-simhash": "^0.1.0",
71 | "nodeunit": "^0.9.1",
72 | "phantom": "^4.0.0",
73 | "postcss-loader": "^0.13.0",
74 | "pre-commit": "^1.2.2",
75 | "react": "^15.3.1",
76 | "react-addons-perf": "^15.4.0",
77 | "react-dom": "^15.3.1",
78 | "react-json-tree": "^0.10.0",
79 | "react-redux": "^4.4.5",
80 | "react-tap-event-plugin": "^1.0.0",
81 | "redux": "^3.6.0",
82 | "redux-saga": "^0.11.1",
83 | "stream-equal": "0.1.8",
84 | "style-loader": "^0.13.1",
85 | "svg-inline-loader": "^0.7.1",
86 | "tunnel": "^0.0.6",
87 | "url-loader": "^0.5.7",
88 | "urllib": "^2.34.2",
89 | "webpack": "^3.10.0",
90 | "worker-loader": "^0.7.1"
91 | },
92 | "scripts": {
93 | "prepublish": "npm run buildweb",
94 | "test": "npx jest",
95 | "lint": "eslint .",
96 | "testserver": "node test/server/startServer.js",
97 | "buildweb": "NODE_ENV=production webpack --config web/webpack.config.js --colors",
98 | "webserver": "NODE_ENV=test webpack --config web/webpack.config.js --colors --watch",
99 | "doc:serve": "node build_scripts/prebuild-doc.js && gitbook serve ./docs-src ./docs --log debug",
100 | "doc:build": "./build_scripts/build-doc-site.sh"
101 | },
102 | "pre-commit": [
103 | "lint"
104 | ],
105 | "repository": {
106 | "type": "git",
107 | "url": "https://github.com/alibaba/anyproxy"
108 | },
109 | "author": "ottomao@gmail.com",
110 | "license": "Apache-2.0",
111 | "engines": {
112 | "node": ">=6.0.0"
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/resource/502.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html(lang="en")
3 | head
4 | title AnyProxy Inner Error
5 | style.
6 | body {
7 | color: #666;
8 | line-height: 1.5;
9 | font-size: 13px;
10 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif;
11 | }
12 |
13 | body * {
14 | box-sizing: border-box;
15 | }
16 |
17 | .stackError {
18 | border-radius: 5px;
19 | padding: 20px;
20 | border: 1px solid #fdc;
21 | background-color: #ffeee6;
22 | color: #666;
23 | }
24 | .stackError li {
25 | list-style-type: none;
26 | }
27 | .infoItem {
28 | position: relative;
29 | overflow: hidden;
30 | border: 1px solid #d5f1fd;
31 | background-color: #eaf8fe;
32 | border-radius: 4px;
33 | margin-bottom: 5px;
34 | padding-left: 70px;
35 | }
36 | .infoItem .label {
37 | position: absolute;
38 | top: 0;
39 | left: 0;
40 | bottom: 0;
41 | display: flex;
42 | justify-content: flex-start;
43 | align-items: center;
44 | width: 70px;
45 | font-weight: 300;
46 | background-color: #76abc1;
47 | color: #fff;
48 | padding: 5px;
49 | }
50 | .infoItem .value {
51 | overflow:hidden;
52 | padding: 5px;
53 | }
54 |
55 | .tipItem .label {
56 | background-color: #ecf6fd;
57 | }
58 | .tip {
59 | color: #808080;
60 | }
61 | body
62 | h1 # AnyProxy Inner Error
63 | h3 Oops! Error happend when AnyProxy handle the request.
64 | p.tip This is an error occurred inside AnyProxy, not from your target website.
65 | .infoItem
66 | .label
67 | | Error:
68 | .value #{error}
69 | .infoItem
70 | .label
71 | | URL:
72 | .value #{url}
73 | if tipMessage
74 | .infoItem
75 | .label
76 | | TIP:
77 | .value!= tipMessage
78 | p
79 | ul.stackError
80 | each item in errorStack
81 | li= item
--------------------------------------------------------------------------------
/resource/cert_download.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html(lang="en")
3 | head
4 | title Download rootCA
5 | meta(name='viewport', content='initial-scale=1, maximum-scale=0.5, minimum-scale=1, user-scalable=no')
6 | style.
7 | body {
8 | color: #666;
9 | line-height: 1.5;
10 | font-size: 16px;
11 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif;
12 | }
13 |
14 | body * {
15 | box-sizing: border-box;
16 | }
17 |
18 | .logo {
19 | font-size: 36px;
20 | margin-bottom: 40px;
21 | text-align: center;
22 | }
23 |
24 | .any {
25 | font-weight: 500;
26 | }
27 |
28 | .proxy {
29 | font-weight: 100;
30 | }
31 |
32 | .title {
33 | font-weight: bold;
34 | margin: 20px 0 6px;
35 | }
36 |
37 | .button {
38 | text-align: center;
39 | padding: 4px 15px 5px 15px;
40 | font-size: 14px;
41 | font-weight: 500;
42 | border-radius: 4px;
43 | height: 32px;
44 | margin-bottom: 10px;
45 | display: block;
46 | text-decoration: none;
47 | border-color: #108ee9;
48 | color: rgba(0, 0, 0, .65);
49 | background-color: #fff;
50 | border-style: solid;
51 | border-width: 1px;
52 | border-style: solid;
53 | border-color: #d9d9d9;
54 | }
55 |
56 | .primary {
57 | color: #fff;
58 | background-color: #108ee9;
59 | border-color: #108ee9;
60 | }
61 |
62 | .more {
63 | text-align: center;
64 | font-size: 14px;
65 | }
66 |
67 | .content {
68 | word-break: break-all;
69 | font-size: 14px;
70 | line-height: 1.2;
71 | margin-bottom: 10px;
72 | }
73 | body
74 | .logo
75 | span.any Any
76 | span.proxy Proxy
77 | .title Download:
78 | .content Select a CA file to download, the .crt file is commonly used.
79 | a(href="/fetchCrtFile?type=crt").button.primary rootCA.crt
80 | a(href="/fetchCrtFile?type=cer").button rootCA.cer
81 | .more More
82 | .buttons(style='display: none')
83 | a(href="/fetchCrtFile?type=pem").button rootCA.pem
84 | a(href="/fetchCrtFile?type=der").button rootCA.der
85 | .title User-Agent:
86 | .content #{ua}
87 | script(type='text/javascript').
88 | window.document.querySelector('.more').addEventListener('click', function (e) {
89 | e.target.style.display = 'none';
90 | window.document.querySelector('.buttons').style.display = 'block';
91 | });
--------------------------------------------------------------------------------
/resource/cert_error.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html(lang="en")
3 | head
4 | title Security Vulnerable
5 | style.
6 | body {
7 | color: #666;
8 | line-height: 1.5;
9 | font-size: 13px;
10 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif;
11 | }
12 |
13 | body * {
14 | box-sizing: border-box;
15 | }
16 |
17 | .container {
18 | max-width: 1200px;
19 | padding: 20px;
20 | padding-top: 150px;
21 | margin: 0 auto;
22 | }
23 |
24 | .title {
25 | font-size: 20px;
26 | margin-bottom: 20px;
27 | }
28 |
29 | .explain {
30 | font-size: 14px;
31 | font-weight: 200;
32 | color: #666;
33 | }
34 |
35 | .explainCode {
36 | color: #999;
37 | margin-bottom: 10px;
38 | }
39 | body
40 | .container
41 | div.title
42 | | #{title}
43 | div.explainCode
44 | | #{code}
45 | div.explain
46 | div!= explain
47 |
--------------------------------------------------------------------------------
/rule_sample/sample_modify_request_data.js:
--------------------------------------------------------------------------------
1 | /*
2 | sample:
3 | modify the post data towards http://httpbin.org/post
4 | test:
5 | curl -H "Content-Type: text/plain" -X POST -d 'original post data' http://httpbin.org/post --proxy http://127.0.0.1:8001
6 | expected response:
7 | { "data": "i-am-anyproxy-modified-post-data" }
8 | */
9 | module.exports = {
10 | summary: 'Rule to modify request data',
11 | *beforeSendRequest(requestDetail) {
12 | if (requestDetail.url.indexOf('http://httpbin.org/post') === 0) {
13 | return {
14 | requestData: 'i-am-anyproxy-modified-post-data'
15 | };
16 | }
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/rule_sample/sample_modify_request_header.js:
--------------------------------------------------------------------------------
1 | /*
2 | sample:
3 | modify the user-agent in requests toward httpbin.org
4 | test:
5 | curl http://httpbin.org/user-agent --proxy http://127.0.0.1:8001
6 | */
7 | module.exports = {
8 | *beforeSendRequest(requestDetail) {
9 | if (requestDetail.url.indexOf('http://httpbin.org') === 0) {
10 | const newRequestOptions = requestDetail.requestOptions;
11 | newRequestOptions.headers['User-Agent'] = 'AnyProxy/0.0.0';
12 | return {
13 | requestOptions: newRequestOptions
14 | };
15 | }
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/rule_sample/sample_modify_request_path.js:
--------------------------------------------------------------------------------
1 | /*
2 | sample:
3 | redirect all https://httpbin.org/user-agent requests to http://localhost:8008/index.html
4 | test:
5 | curl https://httpbin.org/user-agent --proxy http://127.0.0.1:8001
6 | expected response:
7 | 'hello world' from 127.0.0.1:8001/index.html
8 | */
9 | module.exports = {
10 | *beforeSendRequest(requestDetail) {
11 | if (requestDetail.url.indexOf('https://httpbin.org/user-agent') === 0) {
12 | const newRequestOptions = requestDetail.requestOptions;
13 | requestDetail.protocol = 'http';
14 | newRequestOptions.hostname = '127.0.0.1'
15 | newRequestOptions.port = '8008';
16 | newRequestOptions.path = '/index.html';
17 | newRequestOptions.method = 'GET';
18 | return requestDetail;
19 | }
20 | },
21 | *beforeDealHttpsRequest(requestDetail) {
22 | return true;
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/rule_sample/sample_modify_request_protocol.js:
--------------------------------------------------------------------------------
1 | /*
2 | sample:
3 | redirect all http requests of httpbin.org to https
4 | test:
5 | curl 'http://httpbin.org/get?show_env=1' --proxy http://127.0.0.1:8001
6 | expected response:
7 | { "X-Forwarded-Protocol": "https" }
8 | */
9 | module.exports = {
10 | *beforeSendRequest(requestDetail) {
11 | if (requestDetail.url.indexOf('http://httpbin.org') === 0) {
12 | const newOption = requestDetail.requestOptions;
13 | newOption.port = 443;
14 | return {
15 | protocol: 'https',
16 | requestOptions: newOption
17 | };
18 | }
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/rule_sample/sample_modify_response_data.js:
--------------------------------------------------------------------------------
1 | /*
2 | sample:
3 | modify response data of http://httpbin.org/user-agent
4 | test:
5 | curl 'http://httpbin.org/user-agent' --proxy http://127.0.0.1:8001
6 | expected response:
7 | { "user-agent": "curl/7.43.0" } -- AnyProxy Hacked! --
8 | */
9 |
10 | module.exports = {
11 | *beforeSendResponse(requestDetail, responseDetail) {
12 | if (requestDetail.url === 'http://httpbin.org/user-agent') {
13 | const newResponse = responseDetail.response;
14 | newResponse.body += '-- AnyProxy Hacked! --';
15 | return new Promise((resolve, reject) => {
16 | setTimeout(() => { // delay the response for 5s
17 | resolve({ response: newResponse });
18 | }, 5000);
19 | });
20 | }
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/rule_sample/sample_modify_response_header.js:
--------------------------------------------------------------------------------
1 | /*
2 | sample:
3 | modify response header of http://httpbin.org/user-agent
4 | test:
5 | curl -I 'http://httpbin.org/user-agent' --proxy http://127.0.0.1:8001
6 | expected response:
7 | X-Proxy-By: AnyProxy
8 | */
9 | module.exports = {
10 | *beforeSendResponse(requestDetail, responseDetail) {
11 | if (requestDetail.url.indexOf('http://httpbin.org/user-agent') === 0) {
12 | const newResponse = responseDetail.response;
13 | newResponse.header['X-Proxy-By'] = 'AnyProxy';
14 | return {
15 | response: newResponse
16 | };
17 | }
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/rule_sample/sample_modify_response_statuscode.js:
--------------------------------------------------------------------------------
1 | /*
2 | sample:
3 | modify all status code of http://httpbin.org/ to 404
4 | test:
5 | curl -I 'http://httpbin.org/user-agent' --proxy http://127.0.0.1:8001
6 | expected response:
7 | HTTP/1.1 404 Not Found
8 | */
9 | module.exports = {
10 | *beforeSendResponse(requestDetail, responseDetail) {
11 | if (requestDetail.url.indexOf('http://httpbin.org') === 0) {
12 | const newResponse = responseDetail.response;
13 | newResponse.statusCode = 404;
14 | return {
15 | response: newResponse
16 | };
17 | }
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/rule_sample/sample_use_local_response.js:
--------------------------------------------------------------------------------
1 | /*
2 | sample:
3 | intercept all requests toward httpbin.org, use a local response
4 | test:
5 | curl http://httpbin.org/user-agent --proxy http://127.0.0.1:8001
6 | */
7 | module.exports = {
8 | *beforeSendRequest(requestDetail) {
9 | const localResponse = {
10 | statusCode: 200,
11 | header: { 'Content-Type': 'application/json' },
12 | body: '{"hello": "this is local response"}'
13 | };
14 | if (requestDetail.url.indexOf('http://httpbin.org') === 0) {
15 | return {
16 | response: localResponse
17 | };
18 | }
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/test/__snapshots__/basic.spec.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`http - HTTP verbs DELETE: args 1`] = `
4 | Object {
5 | "foo": "bar",
6 | }
7 | `;
8 |
9 | exports[`http - HTTP verbs DELETE: data 1`] = `""`;
10 |
11 | exports[`http - HTTP verbs GET: args 1`] = `
12 | Object {
13 | "param": "param_value",
14 | }
15 | `;
16 |
17 | exports[`http - HTTP verbs GET: data 1`] = `undefined`;
18 |
19 | exports[`http - HTTP verbs PATCH: args 1`] = `Object {}`;
20 |
21 | exports[`http - HTTP verbs PATCH: data 1`] = `""`;
22 |
23 | exports[`http - HTTP verbs POST body and header: args 1`] = `Object {}`;
24 |
25 | exports[`http - HTTP verbs POST body and header: data 1`] = `
26 | "1
27 | "
28 | `;
29 |
30 | exports[`http - HTTP verbs PUT: args 1`] = `Object {}`;
31 |
32 | exports[`http - HTTP verbs PUT: data 1`] = `
33 | "1
34 | "
35 | `;
36 |
37 | exports[`https - HTTP verbs DELETE: args 1`] = `
38 | Object {
39 | "foo": "bar",
40 | }
41 | `;
42 |
43 | exports[`https - HTTP verbs DELETE: data 1`] = `""`;
44 |
45 | exports[`https - HTTP verbs GET: args 1`] = `
46 | Object {
47 | "param": "param_value",
48 | }
49 | `;
50 |
51 | exports[`https - HTTP verbs GET: data 1`] = `undefined`;
52 |
53 | exports[`https - HTTP verbs PATCH: args 1`] = `Object {}`;
54 |
55 | exports[`https - HTTP verbs PATCH: data 1`] = `""`;
56 |
57 | exports[`https - HTTP verbs POST body and header: args 1`] = `Object {}`;
58 |
59 | exports[`https - HTTP verbs POST body and header: data 1`] = `
60 | "1
61 | "
62 | `;
63 |
64 | exports[`https - HTTP verbs PUT: args 1`] = `Object {}`;
65 |
66 | exports[`https - HTTP verbs PUT: data 1`] = `
67 | "1
68 | "
69 | `;
70 |
--------------------------------------------------------------------------------
/test/fixtures/someRule.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | foo: 'bar',
3 | };
4 |
--------------------------------------------------------------------------------
/test/fixtures/upload.txt:
--------------------------------------------------------------------------------
1 | 1
2 |
--------------------------------------------------------------------------------
/test/lib/httpsServerMgr.spec.js:
--------------------------------------------------------------------------------
1 | const tls = require('tls');
2 | const httpsServerMgr = require('../../lib/httpsServerMgr');
3 |
4 | describe('httpsServerMgr', () => {
5 | let serverMgrInstance;
6 |
7 | beforeAll(async () => {
8 | serverMgrInstance = new httpsServerMgr({
9 | hostname: '127.0.0.1',
10 | handler: (req, res) => {
11 | res.end('hello world');
12 | },
13 | wsHandler: () => { },
14 | });
15 | });
16 |
17 | afterAll(async () => {
18 | await serverMgrInstance.close();
19 | });
20 |
21 | it('SNI server should work properly', async () => {
22 | const sniServerA = await serverMgrInstance.getSharedHttpsServer('a.anyproxy.io');
23 | const sniServerB = await serverMgrInstance.getSharedHttpsServer('b.anyproxy.io');
24 |
25 | expect(sniServerA).toEqual(sniServerB); // SNI - common server
26 |
27 | const connectHostname = 'some_new_host.anyproxy.io';
28 | const connectOpt = {
29 | servername: connectHostname, // servername is required for sni server
30 | rejectUnauthorized: false,
31 | }
32 | await new Promise((resolve, reject) => {
33 | const socketToSNIServer = tls.connect(sniServerA.port, '127.0.0.1', connectOpt, (tlsSocket) => {
34 | // console.log('client to SNI server connected, ', socketToSNIServer.authorized ? 'authorized' : 'unauthorized');
35 | const certSubject = socketToSNIServer.getPeerCertificate().subject;
36 | expect(certSubject.CN).toEqual(connectHostname);
37 | socketToSNIServer.end();
38 | resolve();
39 | });
40 |
41 | socketToSNIServer.on('keylog', line => {
42 | console.log(line);
43 | })
44 | });
45 | });
46 |
47 | it('IP server should work properly', async () => {
48 | const ipServerHost = '1.2.3.4';
49 | const anotherSNIServer = await serverMgrInstance.getSharedHttpsServer('c.anyproxy.io');
50 | const ipServerA = await serverMgrInstance.getSharedHttpsServer(ipServerHost);
51 | const ipServerB = await serverMgrInstance.getSharedHttpsServer('5.6.7.8');
52 | expect(ipServerA).not.toEqual(ipServerB);
53 | expect(anotherSNIServer).not.toEqual(ipServerA);
54 |
55 | const connectOpt = {
56 | rejectUnauthorized: false,
57 | }
58 | await new Promise((resolve, reject) => {
59 | const socketToIpServer = tls.connect(ipServerA.port, '127.0.0.1', connectOpt, () => {
60 | const certSubject = socketToIpServer.getPeerCertificate().subject;
61 | expect(certSubject.CN).toEqual(ipServerHost);
62 | socketToIpServer.end();
63 | resolve();
64 | });
65 | });
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/test/lib/ruleLoader.spec.js:
--------------------------------------------------------------------------------
1 | const ruleLoader = require('../../lib/ruleLoader');
2 | const fs = require('fs');
3 | const path = require('path');
4 |
5 | const localModulePath = path.join(__dirname, '../fixtures/someRule.js');
6 | describe('ruleLoader', () => {
7 | it('should successfully cache a remote file', async () => {
8 | await ruleLoader.cacheRemoteFile('https://cdn.bootcss.com/lodash.js/4.16.4/lodash.min.js')
9 | .then(filePath => {
10 | let content;
11 | if (filePath) {
12 | content = fs.readFileSync(filePath, { encoding: 'utf8' });
13 | }
14 | expect(content && content.length > 100).toBe(true);
15 | });
16 | });
17 |
18 | it('should load a local module ../util/CommonUtil', async () => {
19 | await ruleLoader.loadLocalPath(localModulePath)
20 | .then(module => {
21 | expect(module.foo).not.toBeUndefined();
22 | });
23 | });
24 |
25 | it('should smart load a remote module', done => {
26 | ruleLoader.requireModule('https://cdn.bootcss.com/lodash.js/4.16.4/lodash.min.js')
27 | .then(module => {
28 | expect(module.VERSION).toEqual('4.16.4');
29 | done();
30 | })
31 | .catch(done.fail);
32 | });
33 |
34 | it('should smart load a local module', done => {
35 | ruleLoader.requireModule(localModulePath)
36 | .then(module => {
37 | expect(module.foo).not.toBeUndefined();
38 | done();
39 | })
40 | .catch(done.fail);
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/test/lib/util.spec.js:
--------------------------------------------------------------------------------
1 | const util = require('../../lib/util');
2 |
3 | describe('utils', () => {
4 | it('getFreePort', async () => {
5 | const count = 100;
6 | const tasks = [];
7 | for (let i = 1; i <= count; i++) {
8 | tasks.push(util.getFreePort());
9 | }
10 | await Promise.all(tasks)
11 | .then((results) => {
12 | // ensure ports are unique
13 | const portMap = {};
14 | results.map((portNumber) => {
15 | portMap[portNumber] = true;
16 | });
17 | expect(Object.keys(portMap).length).toEqual(count);
18 | });
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/test/rule/beforeDealHttpsRequest.spec.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const { basicProxyRequest, proxyServerWithRule, } = require('../util.js');
4 |
5 | const RULE_PAYLOAD = 'this is something in rule';
6 |
7 | const rule = {
8 | *beforeSendRequest(requestDetail) {
9 | const requestOptions = requestDetail.requestOptions;
10 | return {
11 | requestOptions,
12 | requestData: RULE_PAYLOAD,
13 | };
14 | },
15 |
16 | *beforeDealHttpsRequest(requestDetail) {
17 | return requestDetail.host.indexOf('httpbin.org') >= 0;
18 | }
19 | };
20 |
21 | describe('Rule beforeDealHttpsRequest', () => {
22 | let proxyServer;
23 | let proxyPort;
24 | let proxyHost;
25 |
26 | beforeAll(async () => {
27 | proxyServer = await proxyServerWithRule(rule);
28 | proxyPort = proxyServer.proxyPort;
29 | proxyHost = `http://localhost:${proxyPort}`;
30 | });
31 |
32 | afterAll(() => {
33 | return proxyServer && proxyServer.close();
34 | });
35 | it('Should replace the https request body', async () => {
36 | const url = 'https://httpbin.org/put';
37 | const payloadStream = fs.createReadStream(path.resolve(__dirname, '../fixtures/upload.txt'));
38 | const postHeaders = {
39 | anyproxy_header: 'header_value',
40 | };
41 |
42 | await basicProxyRequest(proxyHost, 'PUT', url, postHeaders, {}, payloadStream).then((result) => {
43 | const proxyRes = result.response;
44 | const body = JSON.parse(result.body);
45 | expect(proxyRes.statusCode).toBe(200);
46 | expect(body.data).toEqual(RULE_PAYLOAD);
47 | expect(body.url.indexOf('/put')).toBeGreaterThan(0);
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/test/rule/beforeSendRequest.spec.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const { basicProxyRequest, proxyServerWithRule, } = require('../util.js');
4 |
5 | const RULE_PAYLOAD = 'this is something in rule';
6 | const RULE_REPLACE_HEADER_KEY = 'rule_replace_header_key';
7 | const RULE_REPLACE_HEADER_VALUE = 'rule_replace_header_value';
8 |
9 | const rule = {
10 | *beforeSendRequest(requestDetail) {
11 | const reqUrl = requestDetail.url;
12 | if (reqUrl.indexOf('/post') >= 0) {
13 | const requestOptions = requestDetail.requestOptions;
14 | requestOptions.path = '/put';
15 | requestOptions.method = 'PUT';
16 | return {
17 | requestOptions,
18 | requestData: RULE_PAYLOAD,
19 | };
20 | } else if (reqUrl.indexOf('/status/302') >= 0) {
21 | return {
22 | response: {
23 | statusCode: 404,
24 | header: {
25 | [RULE_REPLACE_HEADER_KEY]: RULE_REPLACE_HEADER_VALUE,
26 | 'content-type': 'plain/text',
27 | },
28 | body: RULE_PAYLOAD
29 | }
30 | };
31 | } else if (reqUrl.indexOf('/should_be_replaced') >= 0) {
32 | const requestOptions = requestDetail.requestOptions;
33 | requestOptions.hostname = 'httpbin.org';
34 | requestOptions.path = '/status/302';
35 | requestOptions.port = '443';
36 | return {
37 | protocol: 'https',
38 | requestOptions,
39 | };
40 | }
41 | }
42 | };
43 |
44 | describe('Rule replaceRequestData', () => {
45 | let proxyServer;
46 | let proxyPort;
47 | let proxyHost;
48 |
49 | beforeAll(async () => {
50 | proxyServer = await proxyServerWithRule(rule);
51 | proxyPort = proxyServer.proxyPort;
52 | proxyHost = `http://localhost:${proxyPort}`;
53 | });
54 |
55 | afterAll(() => {
56 | return proxyServer && proxyServer.close();
57 | });
58 |
59 | it('should replace the request data in proxy if the assertion is true', async () => {
60 | const url = 'http://httpbin.org/post';
61 | const payloadStream = fs.createReadStream(path.resolve(__dirname, '../fixtures/upload.txt'));
62 | const postHeaders = {
63 | anyproxy_header: 'header_value',
64 | };
65 |
66 | await basicProxyRequest(proxyHost, 'POST', url, postHeaders, {}, payloadStream).then((result) => {
67 | const proxyRes = result.response;
68 | const body = JSON.parse(result.body);
69 | expect(proxyRes.statusCode).toBe(200);
70 | expect(body.data).toEqual(RULE_PAYLOAD);
71 | expect(body.url.indexOf('/put')).toBeGreaterThan(0);
72 | });
73 | });
74 |
75 | it('should respond content specified in rule', async () => {
76 | const url = 'http://httpbin.org/status/302';
77 | await basicProxyRequest(proxyHost, 'GET', url).then((result) => {
78 | const proxyRes = result.response;
79 | const body = result.body;
80 | expect(body).toBe(RULE_PAYLOAD);
81 | expect(proxyRes.statusCode).toBe(404);
82 | expect(proxyRes.headers[RULE_REPLACE_HEADER_KEY]).toBe(RULE_REPLACE_HEADER_VALUE);
83 | });
84 | });
85 |
86 | it('should replace protocol and url', async () => {
87 | const url = 'http://domain_not_exists.anyproxy.io/should_be_replaced';
88 | await basicProxyRequest(proxyHost, 'GET', url).then((result) => {
89 | const proxyRes = result.response;
90 | expect(proxyRes.statusCode).toBe(302);
91 | });
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/test/rule/beforeSendResponse.js:
--------------------------------------------------------------------------------
1 | const { basicProxyRequest, proxyServerWithRule, } = require('../util.js');
2 |
3 | const RULE_REPLACE_HEADER_KEY = 'rule_replace_header_key';
4 | const RULE_REPLACE_HEADER_VALUE = 'rule_replace_header_value';
5 | const RULE_REPLACE_BODY = 'RULE_REPLACE_BODY';
6 | const rule = {
7 | *beforeSendResponse(requestDetail, responseDetail) {
8 | if (requestDetail.url.indexOf('/uuid') >= 0) {
9 | const newResponse = responseDetail.response;
10 | newResponse.header[RULE_REPLACE_HEADER_KEY] = RULE_REPLACE_HEADER_VALUE;
11 | newResponse.body = RULE_REPLACE_BODY;
12 | newResponse.statusCode = 502;
13 | return {
14 | response: newResponse,
15 | };
16 | }
17 | },
18 | };
19 |
20 | describe('Rule replaceResponseData', () => {
21 | let proxyServer;
22 | let proxyPort;
23 | let proxyHost;
24 |
25 | beforeAll(async () => {
26 | proxyServer = await proxyServerWithRule(rule);
27 | proxyPort = proxyServer.proxyPort;
28 | proxyHost = `http://localhost:${proxyPort}`;
29 | });
30 |
31 | afterAll(() => {
32 | return proxyServer && proxyServer.close();
33 | });
34 |
35 | it('Should replace the header and body', async () => {
36 | const url = 'http://httpbin.org/uuid';
37 | await basicProxyRequest(proxyHost, 'GET', url).then((result) => {
38 | const proxyRes = result.response;
39 | const body = result.body;
40 | expect(proxyRes.statusCode).toBe(502);
41 | expect(proxyRes.headers[RULE_REPLACE_HEADER_KEY]).toBe(RULE_REPLACE_HEADER_VALUE);
42 | expect(body).toBe(RULE_REPLACE_BODY);
43 | });
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/test/rule/onError.spec.js:
--------------------------------------------------------------------------------
1 | const { basicProxyRequest, proxyServerWithRule, } = require('../util.js');
2 |
3 | const jestMockErrorFn = jest.fn();
4 | const jestMockConnectErrorFn = jest.fn();
5 |
6 | const ERROR_PAGE_IN_RULE = 'this is my error page';
7 | const rule = {
8 | onConnectError: jestMockConnectErrorFn,
9 | *onError(requestDetail, error) {
10 | jestMockErrorFn(requestDetail, error);
11 | return {
12 | response: {
13 | statusCode: '200',
14 | header: {},
15 | body: ERROR_PAGE_IN_RULE,
16 | }
17 | };
18 | },
19 | *beforeDealHttpsRequest(requestDetail) {
20 | return requestDetail.host.indexOf('intercept') === 0;
21 | },
22 | };
23 |
24 | describe('Rule replaceResponseData', () => {
25 | let proxyServer;
26 | let proxyPort;
27 | let proxyHost;
28 |
29 | beforeAll(async () => {
30 | proxyServer = await proxyServerWithRule(rule);
31 | proxyPort = proxyServer.proxyPort;
32 | proxyHost = `http://localhost:${proxyPort}`;
33 | });
34 |
35 | afterAll(() => {
36 | return proxyServer && proxyServer.close();
37 | });
38 |
39 | it('should get error', async () => {
40 | const url = 'https://intercept.anyproxy_not_exists.io/some_path';
41 | const result = await basicProxyRequest(proxyHost, 'GET', url);
42 | const proxyRes = result.response;
43 | const body = result.body;
44 | expect(proxyRes.statusCode).toBe(200);
45 | expect(body).toBe(ERROR_PAGE_IN_RULE);
46 | expect(jestMockErrorFn.mock.calls.length).toBe(1);
47 | });
48 |
49 | it('should get connec error', async () => {
50 | const url = 'https://anyproxy_not_exists.io/do_not_intercept';
51 | let e;
52 | try {
53 | await basicProxyRequest(proxyHost, 'GET', url);
54 | } catch (err) {
55 | e = err;
56 | }
57 | expect(e).not.toBeUndefined();
58 | expect(jestMockConnectErrorFn.mock.calls.length).toBe(1);
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/test/util.js:
--------------------------------------------------------------------------------
1 | const request = require('request');
2 | const assert = require('assert');
3 | // TODO
4 | const { freshRequire, getFreePort } = require('../lib/util.js');
5 |
6 | function basicProxyRequest(proxyHost, method, url, headers, qs, payload) {
7 | assert(method && url, 'method and url are required');
8 | assert(proxyHost, 'proxyHost is required');
9 | headers = Object.assign({
10 | 'via-anyproxy': 'true',
11 | }, headers || {});
12 |
13 | const requestOpt = {
14 | method,
15 | url,
16 | headers,
17 | followRedirect: false,
18 | rejectUnauthorized: false,
19 | qs,
20 | proxy: proxyHost,
21 | };
22 |
23 | return new Promise((resolve, reject) => {
24 | const callback = (error, response, body) => {
25 | if (error) {
26 | reject(error);
27 | } else {
28 | resolve({
29 | response,
30 | body,
31 | });
32 | }
33 | };
34 | if (payload) {
35 | payload.pipe(request(requestOpt, callback));
36 | } else {
37 | request(requestOpt, callback);
38 | }
39 | });
40 | }
41 |
42 | const DEFAULT_OPTIONS = {
43 | type: 'http',
44 | port: 8001,
45 | webInterface: false,
46 | wsIntercept: true,
47 | // throttle: 10000, // optional, speed limit in kb/s
48 | forceProxyHttps: true, // intercept https as well
49 | dangerouslyIgnoreUnauthorized: true,
50 | silent: false //optional, do not print anything into terminal. do not set it when you are still debugging.
51 | };
52 |
53 | async function proxyServerWithRule(rule, overrideConfig) {
54 | const AnyProxy = freshRequire('../proxy.js');
55 | const freeportA = await getFreePort();
56 | const freeportB = await getFreePort();
57 | const options = Object.assign(DEFAULT_OPTIONS, {
58 | port: freeportA,
59 | webInterface: {
60 | enable: true,
61 | webPort: freeportB,
62 | }
63 | }, overrideConfig || {});
64 | options.rule = rule;
65 |
66 |
67 | return new Promise((resolve, reject) => {
68 | const instance = new AnyProxy.ProxyServer(options);
69 | instance.on('error', reject);
70 | instance.on('ready', () => {
71 | resolve(instance);
72 | });
73 | instance.start();
74 | });
75 | }
76 |
77 | module.exports = {
78 | basicProxyRequest,
79 | proxyServerWithRule,
80 | };
81 |
--------------------------------------------------------------------------------
/test/web/curlUtil.spec.js:
--------------------------------------------------------------------------------
1 | const { curlify } = require('../../web/src/common/curlUtil');
2 |
3 | describe('Test the curlify function', () => {
4 | it('request with headers', () => {
5 | const requestDetail = {
6 | method: 'POST',
7 | url: 'https://localhost:3001/test',
8 | reqHeader: {
9 | 'via-proxy': 'true',
10 | },
11 | };
12 | const result = 'curl \'https://localhost:3001/test\' -X POST -H \'via-proxy: true\'';
13 | expect(curlify(requestDetail)).toBe(result);
14 | });
15 |
16 | it('request with JSON body', () => {
17 | const requestDetail = {
18 | method: 'POST',
19 | url: 'https://localhost:3001/test',
20 | reqHeader: {
21 | 'content-type': 'application/json; charset=utf-8',
22 | },
23 | reqBody: '{"action":1,"method":"test"}',
24 | };
25 | const result = `curl '${requestDetail.url}' -X POST -H 'content-type: application/json; charset=utf-8' -d '${requestDetail.reqBody}'`;
26 | expect(curlify(requestDetail)).toBe(result);
27 | });
28 |
29 | it('accpet gzip encoding with compressed flag', () => {
30 | const requestDetail = {
31 | method: 'GET',
32 | url: 'https://localhost:3001/test',
33 | reqHeader: {
34 | Host: 'localhost',
35 | 'Accept-Encoding': 'gzip',
36 | },
37 | };
38 | const result = 'curl \'https://localhost:3001/test\' -H \'Host: localhost\' -H \'Accept-Encoding: gzip\' --compressed';
39 | expect(curlify(requestDetail)).toBe(result);
40 | });
41 |
42 | it('escape url character', () => {
43 | const requestDetail = {
44 | method: 'GET',
45 | url: 'https://localhost:3001/test?a[]=1',
46 | };
47 | const result = 'curl \'https://localhost:3001/test?a\\[\\]=1\'';
48 | expect(curlify(requestDetail)).toBe(result);
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/test/web/webInterface.spec.js:
--------------------------------------------------------------------------------
1 | const WebInterface = require('../../lib/webInterface.js');
2 | const Recorder = require('../../lib/recorder');
3 | const urllib = require('urllib');
4 |
5 | describe('WebInterface server', () => {
6 | let webServer = null;
7 | const webHost = 'http://127.0.0.1:8002'
8 |
9 | beforeAll(async () => {
10 | const recorder = new Recorder();
11 | webServer = new WebInterface({
12 | webPort: 8002,
13 | }, recorder);
14 | await webServer.start();
15 | });
16 |
17 | afterAll(async () => {
18 | await webServer.close();
19 | });
20 |
21 | it('should response qrcode string in /getQrCode', async () => {
22 | const response = await urllib.request(`${webHost}/api/getQrCode`);
23 | const body = JSON.parse(response.res.data);
24 | expect(body.qrImgDom).toMatch('
6 |
7 |
8 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('autoprefixer')
4 | ]
5 | }
--------------------------------------------------------------------------------
/web/src/action/recordAction.js:
--------------------------------------------------------------------------------
1 | export const FETCH_REQUEST_LOG = 'FETCH_REQUEST_LOG';
2 | export const UPDATE_WHOLE_REQUEST = 'UPDATE_WHOLE_REQUEST';
3 | export const UPDATE_SINGLE_RECORD = 'UPDATE_SINGLE_RECORD';
4 | export const CLEAR_ALL_RECORD = 'CLEAR_ALL_RECORD';
5 | export const CLEAR_ALL_LOCAL_RECORD = 'CLEAR_ALL_LOCAL_RECORD';
6 | export const FETCH_RECORD_DETAIL = 'FETCH_RECORD_DETAIL';
7 | export const SHOW_RECORD_DETAIL = 'SHOW_RECORD_DETAIL';
8 | export const HIDE_RECORD_DETAIL = 'HIDE_RECORD_DETAIL';
9 | export const UPDATE_MULTIPLE_RECORDS = 'UPDATE_MULTIPLE_RECORDS';
10 |
11 | export function fetchRequestLog() {
12 | return {
13 | type: FETCH_REQUEST_LOG
14 | };
15 | }
16 |
17 | export function updateWholeRequest(data) {
18 | return {
19 | type: UPDATE_WHOLE_REQUEST,
20 | data: data
21 | };
22 | }
23 |
24 | export function updateRecord(record) {
25 | return {
26 | type: UPDATE_SINGLE_RECORD,
27 | data: record
28 | };
29 | }
30 |
31 | export function clearAllRecord () {
32 | return {
33 | type: CLEAR_ALL_RECORD
34 | };
35 | }
36 |
37 | export function clearAllLocalRecord () {
38 | return {
39 | type: CLEAR_ALL_LOCAL_RECORD
40 | };
41 | }
42 |
43 | export function fetchRecordDetail (recordId) {
44 | return {
45 | type: FETCH_RECORD_DETAIL,
46 | data: recordId
47 | };
48 | }
49 |
50 | export function showRecordDetail (record) {
51 | return {
52 | type: SHOW_RECORD_DETAIL,
53 | data: record
54 | };
55 | }
56 |
57 | export function hideRecordDetail () {
58 | return {
59 | type: HIDE_RECORD_DETAIL
60 | };
61 | }
62 |
63 | export function updateMultipleRecords (records) {
64 | return {
65 | type: UPDATE_MULTIPLE_RECORDS,
66 | data: records
67 | };
68 | }
69 |
70 |
--------------------------------------------------------------------------------
/web/src/assets/clear.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/download.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/filter.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/https.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alibaba/anyproxy/b93f948107b956e07c7b68faeff0c777a1f50486/web/src/assets/https.png
--------------------------------------------------------------------------------
/web/src/assets/play.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/retweet.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/start.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/stop.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/tip.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/touchmeter.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/assets/view-eye.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/src/common/ApiUtil.js:
--------------------------------------------------------------------------------
1 | /**
2 | * AJAX操作工具类
3 | */
4 | import PromiseUtil from './promiseUtil';
5 | export function getJSON(url, data) {
6 | const d = PromiseUtil.defer();
7 | fetch(url + serializeQuery(data))
8 | .then((data) => {
9 | d.resolve(data.json());
10 | })
11 | .catch((error) => {
12 | console.error(error);
13 | d.reject(error);
14 | });
15 | return d.promise;
16 | }
17 |
18 | export function postJSON(url, data) {
19 | const d = PromiseUtil.defer();
20 | fetch(url, {
21 | method: 'POST',
22 | headers: {
23 | 'Accept': 'application/json',
24 | 'Content-Type': 'application/json'
25 | },
26 | body: JSON.stringify(data)
27 | })
28 | .then((data) => {
29 |
30 | d.resolve(data.json());
31 | })
32 | .catch((error) => {
33 | console.error(error);
34 | d.reject(error);
35 | });
36 | return d.promise;
37 | }
38 |
39 | function serializeQuery (data = {}) {
40 | data['__t'] = Date.now();// disable the cache
41 | const queryArray = [];
42 |
43 | for (let key in data) {
44 | queryArray.push(`${key}=${data[key]}`);
45 | }
46 |
47 | const queryStr = queryArray.join('&');
48 |
49 | return queryStr ? '?' + queryStr : '';
50 | }
51 |
52 | export function isApiSuccess (response) {
53 | return response.status === 'success';
54 | }
55 |
56 | const apiUtil = {
57 | getJSON,
58 | postJSON,
59 | isApiSuccess
60 | };
61 |
62 | export default apiUtil;
63 |
--------------------------------------------------------------------------------
/web/src/common/Constant.js:
--------------------------------------------------------------------------------
1 | /**
2 | * define all constant variables here
3 | */
4 |
5 | module.exports.MenuKeyMap = {
6 | RECORD_FILTER: 'RECORD_FILTER',
7 | MAP_LOCAL: 'MAP_LOCAL',
8 | ROOT_CA: 'ROOT_CA'
9 | };
10 |
--------------------------------------------------------------------------------
/web/src/common/WsUtil.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Utility for websocket
3 | *
4 | */
5 | import { message } from 'antd';
6 |
7 | /**
8 | * Initiate a ws connection.
9 | * The default path `do-not-proxy` means the ws do not need to be proxied.
10 | * This is very important for AnyProxy‘s own server, such as WEB UI,
11 | * and the websocket detail panel in it, to prevent a recursive proxy.
12 | * @param {wsPort} wsPort the port of websocket
13 | * @param {key} path the path of the ws url
14 | *
15 | */
16 | export function initWs(wsPort = location.port, path = 'do-not-proxy') {
17 | if(!WebSocket){
18 | throw (new Error('WebSocket is not supported on this browser'));
19 | }
20 |
21 | const wsClient = new WebSocket(`ws://${location.hostname}:${wsPort}/${path}`);
22 |
23 | wsClient.onerror = (error) => {
24 | console.error(error);
25 | message.error('error happened when setup websocket');
26 | };
27 |
28 | wsClient.onopen = (e) => {
29 | console.info('websocket opened: ', e);
30 | };
31 |
32 | wsClient.onclose = (e) => {
33 | console.info('websocket closed: ', e);
34 | };
35 |
36 | return wsClient;
37 | }
38 |
39 | export default {
40 | initWs: initWs
41 | };
42 |
43 |
--------------------------------------------------------------------------------
/web/src/common/apiUtil.js:
--------------------------------------------------------------------------------
1 | /**
2 | * AJAX操作工具类
3 | */
4 | import PromiseUtil from './promiseUtil';
5 | export function getJSON(url, data) {
6 | const d = PromiseUtil.defer();
7 | fetch(url + serializeQuery(data))
8 | .then((data) => {
9 | d.resolve(data.json());
10 | })
11 | .catch((error) => {
12 | console.error(error);
13 | d.reject(error);
14 | });
15 | return d.promise;
16 | }
17 |
18 | export function postJSON(url, data) {
19 | const d = PromiseUtil.defer();
20 | fetch(url, {
21 | method: 'POST',
22 | headers: {
23 | 'Accept': 'application/json',
24 | 'Content-Type': 'application/json'
25 | },
26 | body: JSON.stringify(data)
27 | })
28 | .then((data) => {
29 |
30 | d.resolve(data.json());
31 | })
32 | .catch((error) => {
33 | console.error(error);
34 | d.reject(error);
35 | });
36 | return d.promise;
37 | }
38 |
39 | function serializeQuery (data = {}) {
40 | data['__t'] = Date.now();// disable the cache
41 | const queryArray = [];
42 |
43 | for (let key in data) {
44 | queryArray.push(`${key}=${data[key]}`);
45 | }
46 |
47 | const queryStr = queryArray.join('&');
48 |
49 | return queryStr ? '?' + queryStr : '';
50 | }
51 |
52 | export function isApiSuccess (response) {
53 | return response.status === 'success';
54 | }
55 |
56 | const apiUtil = {
57 | getJSON,
58 | postJSON,
59 | isApiSuccess
60 | };
61 |
62 | export default apiUtil;
63 |
--------------------------------------------------------------------------------
/web/src/common/commonUtil.js:
--------------------------------------------------------------------------------
1 | /*
2 | * 存放常用工具类
3 | */
4 |
5 | /*
6 | * 格式化日期
7 | * @param date Date or timestamp
8 | * @param formatter yyyyMMddHHmmss
9 | */
10 | export function formatDate(date, formatter) {
11 | if (typeof date !== 'object') {
12 | date = new Date(date);
13 | }
14 |
15 | const transform = function(value) {
16 | return value < 10 ? '0' + value : value;
17 | };
18 | return formatter.replace(/^YYYY|MM|DD|hh|mm|ss|ms/g, function(match) {
19 | switch (match) {
20 | case 'YYYY':
21 | return transform(date.getFullYear());
22 | case 'MM':
23 | return transform(date.getMonth() + 1);
24 | case 'mm':
25 | return transform(date.getMinutes());
26 | case 'DD':
27 | return transform(date.getDate());
28 | case 'hh':
29 | return transform(date.getHours());
30 | case 'ss':
31 | return transform(date.getSeconds());
32 | case 'ms':
33 | return transform(date.getMilliseconds());
34 | }
35 | });
36 | }
37 |
38 | export function selectText(element) {
39 | let range, selection;
40 |
41 | if (window.getSelection) {
42 | selection = window.getSelection();
43 | range = document.createRange();
44 | range.selectNodeContents(element);
45 | selection.removeAllRanges();
46 | selection.addRange(range);
47 | } else if (document.body.createTextRange) {
48 | range = document.body.createTextRange();
49 | range.moveToElementText(element);
50 | range.select();
51 | }
52 | }
53 |
54 | export function getQueryParameter (name) {
55 | var results = new RegExp('[\?&]' + name + '=([^]*)').exec(window.location.href);
56 | if (results == null) {
57 | return '';
58 | } else {
59 | return results[1] || '';
60 | }
61 | }
62 |
63 | const CommonUtil = {
64 | formatDate,
65 | selectText,
66 | getQueryParameter
67 | };
68 |
69 | export default CommonUtil;
70 |
--------------------------------------------------------------------------------
/web/src/common/constant.js:
--------------------------------------------------------------------------------
1 | /**
2 | * define all constant variables here
3 | */
4 |
5 | module.exports.MenuKeyMap = {
6 | RECORD_FILTER: 'RECORD_FILTER',
7 | MAP_LOCAL: 'MAP_LOCAL',
8 | ROOT_CA: 'ROOT_CA'
9 | };
10 |
--------------------------------------------------------------------------------
/web/src/common/curlUtil.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | curlify(recordDetail) {
3 | const headers = { ...recordDetail.reqHeader };
4 | const acceptEncoding = headers['Accept-Encoding'] || headers['accept-encoding'];
5 | // escape reserve character in url
6 | const url = recordDetail.url.replace(/([\[\]])/g, '\\$1');
7 | const curlified = ['curl', `'${url}'`];
8 |
9 | if (recordDetail.method.toUpperCase() !== 'GET') {
10 | curlified.push('-X', recordDetail.method);
11 | }
12 |
13 | Object.keys(headers).forEach((key) => {
14 | curlified.push('-H', `'${key}: ${headers[key]}'`);
15 | });
16 |
17 | if (recordDetail.reqBody) {
18 | curlified.push('-d', `'${recordDetail.reqBody}'`);
19 | }
20 |
21 | if (/deflate|gzip/.test(acceptEncoding)) {
22 | curlified.push('--compressed');
23 | }
24 |
25 | return curlified.join(' ');
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/web/src/common/promiseUtil.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Promise的工具类
3 | */
4 |
5 | export function defer() {
6 | const d = {};
7 | d.promise = new Promise((resolve, reject) => {
8 | d.resolve = resolve;
9 | d.reject = reject;
10 | });
11 |
12 | return d;
13 | }
14 |
15 | export default {
16 | defer
17 | };
--------------------------------------------------------------------------------
/web/src/common/wsUtil.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Utility for websocket
3 | *
4 | */
5 | import { message } from 'antd';
6 |
7 | /**
8 | * Initiate a ws connection.
9 | * The default path `do-not-proxy` means the ws do not need to be proxied.
10 | * This is very important for AnyProxy‘s own server, such as WEB UI,
11 | * and the websocket detail panel in it, to prevent a recursive proxy.
12 | * @param {wsPort} wsPort the port of websocket
13 | * @param {key} path the path of the ws url
14 | *
15 | */
16 | export function initWs(wsPort = location.port, path = 'do-not-proxy') {
17 | if(!WebSocket){
18 | throw (new Error('WebSocket is not supported on this browser'));
19 | }
20 |
21 | const wsClient = new WebSocket(`ws://${location.hostname}:${wsPort}/${path}`);
22 |
23 | wsClient.onerror = (error) => {
24 | console.error(error);
25 | message.error('error happened when setup websocket');
26 | };
27 |
28 | wsClient.onopen = (e) => {
29 | console.info('websocket opened: ', e);
30 | };
31 |
32 | wsClient.onclose = (e) => {
33 | console.info('websocket closed: ', e);
34 | };
35 |
36 | return wsClient;
37 | }
38 |
39 | export default {
40 | initWs: initWs
41 | };
42 |
43 |
--------------------------------------------------------------------------------
/web/src/component/download-root-ca.less:
--------------------------------------------------------------------------------
1 | @import '../style/constant.less';
2 | .wrapper {
3 | position: relative;
4 | height: 100%;
5 | width: 100%;
6 | padding: 10px 13px 15px;
7 | color: @tip-color;
8 | text-align: center;
9 | :global {
10 | .ant-btn {
11 | width: 100%;
12 | font-weight: 400;
13 | }
14 | }
15 | }
16 |
17 | .title {
18 | font-size: @middlepanel-font-size;
19 | text-align: left;
20 | font-weight: 200;
21 | color: @hint-color;
22 | margin-bottom: 12px;
23 | }
24 |
25 | .fullHeightWrapper {
26 | height: 100%;
27 | padding-bottom: 100px;
28 | margin-bottom: -100px;
29 | min-height: 500px;
30 | }
31 |
32 | .arCodeDivWrapper {
33 | margin-top: 63px;
34 | }
35 |
36 | .generateRootCaTip {
37 | padding-top: 100px;
38 | .strongColor {
39 | color: @default-color;
40 | padding-top: 10px;
41 | font-weight: 500;
42 | }
43 | span {
44 | display: block;
45 | }
46 | }
47 |
48 | .generateCAButton {
49 | margin-top: 50px;
50 | }
51 |
52 | .buttons {
53 | width: 100%;
54 | .tipSpan {
55 | margin-top: 18px;
56 | display: block;
57 | }
58 | }
--------------------------------------------------------------------------------
/web/src/component/header-menu.less:
--------------------------------------------------------------------------------
1 | @import '../style/constant.less';
2 |
3 | @svg-default-color: #3A3A3A;
4 | .wrapper {
5 | width: 100%;
6 | }
7 |
8 | .menuList {
9 | overflow: hidden;
10 | }
11 |
12 | .menuItem {
13 | color: @default-color;
14 | font-size: @font-size-xs;
15 | float: left;
16 | cursor: pointer;
17 | padding: 0 5px;
18 | opacity: 0.87;
19 | min-width: 95px;
20 | padding: 0 25px;
21 | text-align: center;
22 | -webkit-app-region: no-drag;
23 | -webkit-user-select: text;
24 | &:focus {
25 | text-decoration: none;
26 | }
27 |
28 | i {
29 | display: block;
30 | }
31 |
32 | .playIcon, .stopIcon {
33 | svg {
34 | width: 18px;
35 | }
36 | }
37 |
38 | .eyeIcon {
39 | svg {
40 | width: 27px;
41 | }
42 | }
43 |
44 | .tipIcon {
45 | svg {
46 | width: 22px;
47 | }
48 | }
49 |
50 | svg {
51 | width: 23px;
52 | height: 21px;
53 | cursor: pointer;
54 | polyline {
55 | fill: @svg-default-color;
56 | stroke: @svg-default-color;
57 | }
58 | g {
59 | fill: @svg-default-color;
60 | opacity: 1;
61 | }
62 |
63 | g > use {
64 | fill: @svg-default-color;
65 | }
66 | }
67 |
68 | span {
69 | display: block;
70 | line-height: 30px;
71 | color: @top-menu-span-color;
72 | }
73 |
74 | &:hover {
75 | color: @primary-color;
76 | span {
77 | color: @primary-color;
78 | }
79 |
80 | svg {
81 | polyline {
82 | fill: @primary-color;
83 | stroke: @primary-color;
84 | }
85 | g {
86 | fill: @primary-color;
87 | }
88 |
89 | use {
90 | fill: @primary-color;
91 | }
92 | }
93 | }
94 |
95 | &.disabled {
96 | color: @tip-color;
97 | }
98 |
99 | &.active {
100 | color: @primary-color;
101 | span {
102 | color: @primary-color;
103 | }
104 |
105 | svg {
106 | polyline {
107 | fill: @primary-color;
108 | stroke: @primary-color;
109 | }
110 |
111 | // fill: @primary-color;
112 | // stroke: @primary-color;
113 | g {
114 | fill: @primary-color;
115 | }
116 |
117 | use {
118 | fill: @primary-color;
119 | }
120 | }
121 | }
122 |
123 | &.rightMenuItem {
124 | float: right;
125 | padding-right: 0;
126 | margin-right: 35px;
127 | }
128 | }
129 |
130 | .menuItemSpliter {
131 | display: block;
132 | float: left;
133 | width: 1px;
134 | background-color: @top-menu-spliter-color;
135 | height: 30px;
136 | margin: 5px 20px 0;
137 | }
138 |
139 | .ruleTip {
140 | color: @tip-color;
141 | line-height: 26px;
142 | i {
143 | padding-right: 5px;
144 | }
145 | }
146 |
147 | .modalInfo {
148 | li {
149 | font-size: @font-size-reg;
150 | overflow: hidden;
151 | strong {
152 | font-weight: normal;
153 | float: left;
154 | }
155 |
156 | span {
157 | display: block;
158 | overflow: hidden;
159 | padding-left: 10px;
160 | color: @tip-color;
161 | }
162 | }
163 |
164 | :global {
165 | .ant-modal-title {
166 | color: @default-color;
167 | font-weight: 400;
168 | }
169 | .ant-btn {
170 | font-weight: normal;
171 | }
172 | }
173 | }
174 |
175 | .runningInfoDivWrapper {
176 | line-height: 1.5;
177 | li {
178 | overflow: hidden;
179 | strong {
180 | float: left;
181 | }
182 |
183 | span {
184 | overflow: hidden;
185 | padding-left: 5px;
186 | display: block;
187 | }
188 | }
189 | :global {
190 | .ant-btn {
191 | margin-top: 10px;
192 | }
193 |
194 | .ant-popover {
195 | font-size: @font-size-reg;
196 | }
197 |
198 | .ant-popover-title {
199 | color: @default-color;
200 | font-weight: bold;
201 | padding-left: 20px;
202 | }
203 |
204 | .ant-popover-inner-content {
205 | padding-bottom: 25px;
206 | padding-left: 20px;
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/web/src/component/json-viewer.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * A copoment to display content in the a modal
3 | */
4 |
5 | import React, { PropTypes } from 'react';
6 | import { Menu } from 'antd';
7 | import ReactDOM from 'react-dom';
8 | import JSONTree from 'react-json-tree';
9 | import Style from './json-viewer.less';
10 |
11 | const PageIndexMap = {
12 | 'JSON_STRING': 'JSON_STRING',
13 | 'JSON_TREE': 'JSON_TREE'
14 | };
15 |
16 | const theme = {
17 | scheme: 'google',
18 | author: 'seth wright (http://sethawright.com)',
19 | base00: '#1d1f21',
20 | base01: '#282a2e',
21 | base02: '#373b41',
22 | base03: '#969896',
23 | base04: '#b4b7b4',
24 | base05: '#c5c8c6',
25 | base06: '#e0e0e0',
26 | base07: '#ffffff',
27 | base08: '#CC342B',
28 | base09: '#F96A38',
29 | base0A: '#FBA922',
30 | base0B: '#198844',
31 | base0C: '#3971ED',
32 | base0D: '#3971ED',
33 | base0E: '#A36AC7',
34 | base0F: '#3971ED'
35 | };
36 |
37 | class JsonViewer extends React.Component {
38 | constructor () {
39 | super();
40 |
41 | this.state = {
42 | pageIndex: PageIndexMap.JSON_STRING
43 | };
44 |
45 | this.getMenuDiv = this.getMenuDiv.bind(this);
46 | this.handleMenuClick = this.handleMenuClick.bind(this);
47 | }
48 |
49 | static propTypes = {
50 | data: PropTypes.string
51 | }
52 |
53 | handleMenuClick(e) {
54 | this.setState({
55 | pageIndex: e.key,
56 | });
57 | }
58 |
59 | getMenuDiv () {
60 | return (
61 |
65 | );
66 | }
67 |
68 | render () {
69 | if (!this.props.data) {
70 | return null;
71 | }
72 |
73 | let jsonTreeDiv = {this.props.data}
;
74 |
75 | try {
76 | // In an invalid JSON string returned, handle the exception
77 | const jsonObj = JSON.parse(this.props.data);
78 | jsonTreeDiv = ;
79 | } catch (e) {
80 | console.warn('Failed to get JSON Tree:', e);
81 | }
82 |
83 | const jsonStringDiv = {this.props.data}
;
84 | return (
85 |
86 | {this.getMenuDiv()}
87 |
88 | {this.state.pageIndex === PageIndexMap.JSON_STRING ? jsonStringDiv : jsonTreeDiv}
89 |
90 |
91 | );
92 | }
93 | }
94 |
95 | export default JsonViewer;
--------------------------------------------------------------------------------
/web/src/component/json-viewer.less:
--------------------------------------------------------------------------------
1 | @import '../style/constant.less';
2 | .wrapper {
3 | border: 1px solid #d9d9d9;
4 | }
5 | .contentDiv {
6 | padding: 20px 25px;
7 | background: #fff;
8 | }
9 |
--------------------------------------------------------------------------------
/web/src/component/left-menu.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * A copoment to for left main menu
3 | */
4 |
5 | import React, { PropTypes } from 'react';
6 | import { connect } from 'react-redux';
7 | import InlineSVG from 'svg-inline-react';
8 | import { getQueryParameter } from 'common/commonUtil';
9 |
10 | import Style from './left-menu.less';
11 | import ClassBind from 'classnames/bind';
12 |
13 | import {
14 | showFilter,
15 | showRootCA
16 | } from 'action/globalStatusAction';
17 |
18 | import { MenuKeyMap } from 'common/constant';
19 |
20 | const StyleBind = ClassBind.bind(Style);
21 | const {
22 | RECORD_FILTER: RECORD_FILTER_MENU_KEY,
23 | ROOT_CA: ROOT_CA_MENU_KEY
24 | } = MenuKeyMap;
25 |
26 | class LeftMenu extends React.Component {
27 | constructor() {
28 | super();
29 |
30 | this.state = {
31 | inAppMode: getQueryParameter('in_app_mode')
32 | };
33 |
34 | // this.showMapLocal = this.showMapLocal.bind(this);
35 | this.showFilter = this.showFilter.bind(this);
36 | this.showRootCA = this.showRootCA.bind(this);
37 | }
38 |
39 | static propTypes = {
40 | dispatch: PropTypes.func,
41 | globalStatus: PropTypes.object
42 | }
43 |
44 | // showMapLocal() {
45 | // this.props.dispatch(showMapLocal());
46 | // }
47 |
48 | showFilter() {
49 | this.props.dispatch(showFilter());
50 | }
51 |
52 | showRootCA() {
53 | this.props.dispatch(showRootCA());
54 | }
55 |
56 | render() {
57 | const { filterStr, activeMenuKey, recording } = this.props.globalStatus;
58 |
59 | const filterMenuStyle = StyleBind('menuItem', {
60 | working: filterStr.length > 0,
61 | active: activeMenuKey === RECORD_FILTER_MENU_KEY
62 | });
63 |
64 | const rootCAMenuStyle = StyleBind('menuItem', {
65 | active: activeMenuKey === ROOT_CA_MENU_KEY
66 | });
67 |
68 | const wrapperStyle = StyleBind('wrapper', { inApp: this.state.inAppMode });
69 | const circleStyle = StyleBind('circles', { active: recording, stop: !recording });
70 |
71 | return (
72 |
73 |
74 |
75 | Any
76 | Proxy
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
113 |
114 |
AnyProxy.io
115 |
116 |
117 |
118 |
119 |
120 |
121 | Version {this.props.globalStatus.appVersion}
122 |
123 |
124 |
125 | );
126 | }
127 | }
128 |
129 | function select(state) {
130 | return {
131 | globalStatus: state.globalStatus
132 | };
133 | }
134 |
135 | export default connect(select)(LeftMenu);
136 |
--------------------------------------------------------------------------------
/web/src/component/map-local.less:
--------------------------------------------------------------------------------
1 | @import '../style/constant.less';
2 | @panel-width: 100%;
3 | .mapLocalWrapper {
4 | padding: 10px 15px 30px;
5 | }
6 |
7 | .title {
8 | font-size: @middlepanel-font-size;
9 | text-align: left;
10 | font-weight: 200;
11 | margin-bottom: 12px;
12 | color: @hint-color;
13 | }
14 |
15 | .treeWrapper {
16 | height: 310px;
17 | width: @panel-width;
18 | overflow: auto;
19 | border: 1px solid @light-border-color;
20 | border-radius: 4px;
21 | }
22 |
23 | .form {
24 | width: @panel-width;
25 | label {
26 | font-weight: 600;
27 | }
28 | }
29 |
30 | .mappedKeyDiv {
31 | position: relative;
32 | strong {
33 | display: block;
34 | width: 230px;
35 | word-wrap: break-word;
36 | }
37 | a {
38 | overflow: hidden;
39 | position: absolute;
40 | top: 1px;
41 | right: 8px;
42 | }
43 | }
44 |
45 | .mappedList {
46 | padding-left: 25px;
47 | padding-bottom: 15px;
48 | li {
49 | list-style-type: disc;
50 | }
51 | }
52 |
53 | .operations {
54 | margin-top: 10px;
55 | button {
56 | width: @panel-width;
57 | }
58 | }
--------------------------------------------------------------------------------
/web/src/component/modal-panel.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * A copoment to display content in the a modal
3 | */
4 |
5 | import React, { PropTypes } from 'react';
6 | import ReactDOM from 'react-dom';
7 | import { Icon } from 'antd';
8 |
9 | import Style from './modal-panel.less';
10 | import ClassBind from 'classnames/bind';
11 |
12 | const StyleBind = ClassBind.bind(Style);
13 |
14 | class ModalPanel extends React.Component {
15 | constructor () {
16 | super();
17 |
18 | this.state = {
19 | dragBarLeft: '',
20 | contentLeft: ''
21 | };
22 | this.onDragbarMoveUp = this.onDragbarMoveUp.bind(this);
23 | this.onDragbarMove = this.onDragbarMove.bind(this);
24 | this.onDragbarMoveDown = this.onDragbarMoveDown.bind(this);
25 | this.onClose = this.onClose.bind(this);
26 | this.doClose = this.doClose.bind(this);
27 | this.onKeyUp = this.onKeyUp.bind(this);
28 | this.addKeyEvent = this.addKeyEvent.bind(this);
29 | this.removeKeyEvent = this.removeKeyEvent.bind(this);
30 | }
31 |
32 | static propTypes = {
33 | children: PropTypes.element,
34 | onClose: PropTypes.func,
35 | visible: PropTypes.bool,
36 | hideBackModal: PropTypes.bool,
37 | left: PropTypes.string
38 | }
39 |
40 | onDragbarMove (event) {
41 | this.setState({
42 | dragBarLeft: event.pageX
43 | });
44 | }
45 |
46 | onKeyUp (e) {
47 | if (e.keyCode == 27) {
48 | this.doClose();
49 | }
50 | }
51 |
52 | addKeyEvent () {
53 | document.addEventListener('keyup', this.onKeyUp);
54 | }
55 |
56 | removeKeyEvent () {
57 | document.removeEventListener('keyup', this.onKeyUp);
58 | }
59 |
60 | onDragbarMoveUp (event) {
61 | this.setState({
62 | contentLeft: event.pageX
63 | });
64 |
65 | document.removeEventListener('mousemove', this.onDragbarMove);
66 | document.removeEventListener('mouseup', this.onDragbarMoveUp);
67 | }
68 |
69 | onDragbarMoveDown (event) {
70 | document.addEventListener('mousemove', this.onDragbarMove);
71 |
72 | document.addEventListener('mouseup', this.onDragbarMoveUp);
73 | }
74 |
75 | onClose (event) {
76 | if (event.target === event.currentTarget) {
77 | this.props.onClose && this.props.onClose();
78 | }
79 | }
80 |
81 | doClose () {
82 | this.props.onClose && this.props.onClose();
83 | }
84 |
85 | render () {
86 | // will not remove the dom but hidden it, so the dom will not be relayouted
87 | let renderLeft = '100%';
88 | if (!this.props.visible) {
89 | this.removeKeyEvent();
90 | // return null;
91 | } else {
92 | const { dragBarLeft, contentLeft } = this.state;
93 | const propsLeft = this.props.left;
94 | renderLeft = dragBarLeft || propsLeft;
95 | this.addKeyEvent();
96 | }
97 |
98 |
99 | // const dragBarStyle = dragBarLeft || propsLeft ? { 'left': dragBarLeft || propsLeft } : null;
100 | // const contentStyle = contentLeft || propsLeft ? { 'left': contentLeft || propsLeft } : null;
101 |
102 | const dragBarStyle = { 'left': renderLeft };
103 | const modalStyle = { 'left': renderLeft };
104 |
105 | // const modalStyle = this.props.hideBackModal ? contentStyle : { 'left': 0 };
106 | return (
107 |
108 |
109 |
110 |
111 |
112 |
117 |
118 |
119 | {this.props.children}
120 |
121 |
122 |
123 |
124 |
125 | );
126 | }
127 | }
128 |
129 | export default ModalPanel;
--------------------------------------------------------------------------------
/web/src/component/modal-panel.less:
--------------------------------------------------------------------------------
1 | @import '../style/constant.less';
2 | .wrapper {
3 | display: block;
4 | position: absolute;
5 | z-index: 1000;
6 | top: 0;
7 | bottom: 0;
8 | left: 60%;
9 | right: 0;
10 | background-color: @opacity-background-color;
11 | overflow: hidden;
12 | -webkit-box-shadow: -3px 0px 6px 0px rgba(128,128,128,0.56);
13 | -moz-box-shadow: -3px 0px 6px 0px rgba(128,128,128,0.56);
14 | box-shadow: -3px 0px 6px 0px rgba(128,128,128,0.56);
15 | will-change: left;
16 | }
17 |
18 | .relativeWrapper {
19 | position: relative;
20 | width: 100%;
21 | height: 100%;
22 | }
23 |
24 | .closeIcon {
25 | position: absolute;
26 | z-index: 2000;
27 | top: 0;
28 | right: 0;
29 | cursor: pointer;
30 | padding: 15px;
31 | i {
32 | font-size: @font-size-big;
33 | }
34 | &:hover {
35 | color: @primary-color;
36 | }
37 | }
38 |
39 | .relativeWrapper {
40 | width: 100%;
41 | height: 100%;
42 | position: relative;
43 | }
44 |
45 | .contentWrapper {
46 | color: @default-color;
47 | position: relative;
48 | width: 100%;
49 | height: 100%;
50 | overflow: hidden;
51 | }
52 |
53 | .dragBar {
54 | position: fixed;
55 | left: 60%;
56 | bottom: 0;
57 | top: 0;
58 | width: 5px;
59 | background-color: @hint-color;
60 | opacity: 0;
61 | z-index: 2000;
62 | cursor: col-resize;
63 | &:hover {
64 | opacity: 0.87;
65 | }
66 | }
67 |
68 | .content {
69 | background: #fff;
70 | overflow: auto;
71 | width: 100%;
72 | height: 100%;
73 | position: relative;
74 | }
--------------------------------------------------------------------------------
/web/src/component/record-detail.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * The panel to display the detial of the record
3 | *
4 | */
5 |
6 | import React, { PropTypes } from 'react';
7 | import ClassBind from 'classnames/bind';
8 | import { Menu, Spin } from 'antd';
9 | import ModalPanel from 'component/modal-panel';
10 | import RecordRequestDetail from 'component/record-request-detail';
11 | import RecordResponseDetail from 'component/record-response-detail';
12 | import RecordWsMessageDetail from 'component/record-ws-message-detail';
13 | import { hideRecordDetail } from 'action/recordAction';
14 |
15 | import Style from './record-detail.less';
16 |
17 | const StyleBind = ClassBind.bind(Style);
18 | const PageIndexMap = {
19 | REQUEST_INDEX: 'REQUEST_INDEX',
20 | RESPONSE_INDEX: 'RESPONSE_INDEX',
21 | WEBSOCKET_INDEX: 'WEBSOCKET_INDEX'
22 | };
23 |
24 | class RecordDetail extends React.Component {
25 | constructor() {
26 | super();
27 | this.onClose = this.onClose.bind(this);
28 | this.state = {
29 | pageIndex: PageIndexMap.REQUEST_INDEX
30 | };
31 |
32 | this.onMenuChange = this.onMenuChange.bind(this);
33 | }
34 |
35 | static propTypes = {
36 | dispatch: PropTypes.func,
37 | globalStatus: PropTypes.object,
38 | requestRecord: PropTypes.object
39 | }
40 |
41 | onClose() {
42 | this.props.dispatch(hideRecordDetail());
43 | }
44 |
45 | onMenuChange(e) {
46 | this.setState({
47 | pageIndex: e.key,
48 | });
49 | }
50 |
51 | hasWebSocket (recordDetail = {}) {
52 | return recordDetail && recordDetail.method && recordDetail.method.toLowerCase() === 'websocket';
53 | }
54 |
55 | getRequestDiv(recordDetail) {
56 | return ;
57 | }
58 |
59 | getResponseDiv(recordDetail) {
60 | return ;
61 | }
62 |
63 | getWsMessageDiv(recordDetail) {
64 | return ;
65 | }
66 |
67 | getRecordContentDiv(recordDetail = {}, fetchingRecord) {
68 | const getMenuBody = () => {
69 | let menuBody = null;
70 | switch (this.state.pageIndex) {
71 | case PageIndexMap.REQUEST_INDEX: {
72 | menuBody = this.getRequestDiv(recordDetail);
73 | break;
74 | }
75 | case PageIndexMap.RESPONSE_INDEX: {
76 | menuBody = this.getResponseDiv(recordDetail);
77 | break;
78 | }
79 | case PageIndexMap.WEBSOCKET_INDEX: {
80 | menuBody = this.getWsMessageDiv(recordDetail);
81 | break;
82 | }
83 | default: {
84 | menuBody = this.getRequestDiv(recordDetail);
85 | break;
86 | }
87 | }
88 | return menuBody;
89 | }
90 |
91 | const websocketMenu = (
92 | WebSocket
93 | );
94 |
95 | return (
96 |
97 |
102 |
103 | {fetchingRecord ? this.getLoaingDiv() : getMenuBody()}
104 |
105 |
106 | );
107 | }
108 |
109 | getLoaingDiv() {
110 | return (
111 |
112 |
113 |
LOADING...
114 |
115 | );
116 | }
117 |
118 | getRecordDetailDiv() {
119 | const { requestRecord, globalStatus } = this.props;
120 | const recordDetail = requestRecord.recordDetail;
121 | const fetchingRecord = globalStatus.fetchingRecord;
122 |
123 | if (!recordDetail && !fetchingRecord) {
124 | return null;
125 | }
126 | return this.getRecordContentDiv(recordDetail, fetchingRecord);
127 | }
128 |
129 | componentWillReceiveProps(nextProps) {
130 | const { requestRecord } = nextProps;
131 | const { pageIndex } = this.state;
132 | // if this is not websocket, reset the index to RESPONSE_INDEX
133 | if (!this.hasWebSocket(requestRecord.recordDetail) && pageIndex === PageIndexMap.WEBSOCKET_INDEX) {
134 | this.setState({
135 | pageIndex: PageIndexMap.RESPONSE_INDEX
136 | });
137 | }
138 | }
139 |
140 | render() {
141 | return (
142 |
148 | {this.getRecordDetailDiv()}
149 |
150 | );
151 | }
152 | }
153 |
154 | export default RecordDetail;
155 |
--------------------------------------------------------------------------------
/web/src/component/record-detail.less:
--------------------------------------------------------------------------------
1 | @import '../style/constant.less';
2 |
3 | .wrapper {
4 | padding: 5px 15px;
5 | height: 100%;
6 | word-wrap: break-word;
7 | }
8 |
9 | .loading {
10 | text-align: center;
11 | padding-top: 100px;
12 | .loadingText {
13 | margin-top: 15px;
14 | color: @primary-color;
15 | font-size: @font-size-big;
16 | }
17 | }
18 |
19 | .detailWrapper {
20 | position: relative;
21 | min-height: 100%;
22 | padding: 5px;
23 | }
24 |
25 | .section {
26 | padding: 10px 0;
27 | font-size: @font-size-xs;
28 | border-bottom: 1px solid @border-color-base;
29 | &.noBorder {
30 | border: none;
31 | }
32 | }
33 |
34 | .okStatus {
35 | color: @success-color;
36 | }
37 |
38 | .reqBody, .resBody {
39 | min-width: 200px;
40 | padding-left: 15px;
41 | }
42 |
43 | .imageBody {
44 | max-width: 100%;
45 | max-height: 400px;
46 | }
47 |
48 | .ulItem {
49 | padding-left: 15px;
50 | overflow: hidden;
51 | }
52 |
53 | .liItem {
54 | overflow: hidden;
55 | strong {
56 | float: left;
57 | // min-width: 125px;
58 | // text-align: right;
59 | opacity: 0.8;
60 | }
61 | span {
62 | display: block;
63 | overflow: hidden;
64 | opacity: 0.87;
65 | padding-left: 15px;
66 | }
67 | }
68 |
69 | .cookieWrapper {
70 | padding-top: 15px;
71 | :global {
72 | .ant-table-middle .ant-table-thead > tr > th,
73 | .ant-table-middle .ant-table-tbody > tr > td {
74 | padding: 5px 8px;
75 | background-color: transparent;
76 | }
77 | }
78 | }
79 | .noCookes {
80 | text-align: center;
81 | color: @tip-color;
82 | padding: 7px 0 8px;
83 | border-bottom: 1px solid @light-border-color;
84 | }
85 | .odd {
86 | background-color: @light-background-color;
87 | }
88 |
89 | .codeWrapper {
90 | overflow: auto;
91 | padding: 15px;
92 | background-color: @info-bkg-color;
93 | border: 1px solid @light-border-color;
94 | }
95 |
--------------------------------------------------------------------------------
/web/src/component/record-filter.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * The panel to edit the filter
3 | *
4 | */
5 |
6 | import React, { PropTypes } from 'react';
7 | import ReactDOM from 'react-dom';
8 | import ClassBind from 'classnames/bind';
9 | import { connect } from 'react-redux';
10 | import { Input, Alert } from 'antd';
11 | import ResizablePanel from 'component/resizable-panel';
12 | import { hideFilter, updateFilter } from 'action/globalStatusAction';
13 | import { MenuKeyMap } from 'common/constant';
14 |
15 | import Style from './record-filter.less';
16 | import CommonStyle from '../style/common.less';
17 |
18 |
19 | class RecordFilter extends React.Component {
20 | constructor () {
21 | super();
22 | this.onChange = this.onChange.bind(this);
23 | this.onClose = this.onClose.bind(this);
24 | this.filterTimeoutId = null;
25 | }
26 |
27 | static propTypes = {
28 | dispatch: PropTypes.func,
29 | globalStatus: PropTypes.object
30 | }
31 |
32 | onChange (event) {
33 | this.props.dispatch(updateFilter(event.target.value));
34 | }
35 |
36 | onClose () {
37 | this.props.dispatch(hideFilter());
38 | }
39 |
40 | render() {
41 | const description = (
42 |
43 | - Multiple filters supported, write them in a single line.
44 | - Each line will be treaded as a Reg expression.
45 | - The result will be an 'OR' of the filters.
46 | - All the filters will be tested against the URL.
47 |
48 | );
49 |
50 | const panelVisible = this.props.globalStatus.activeMenuKey === MenuKeyMap.RECORD_FILTER;
51 |
52 | return (
53 |
54 |
55 |
56 | Filter
57 |
58 |
59 |
60 |
67 |
68 |
76 |
77 |
78 |
79 | );
80 | }
81 | }
82 |
83 | function select (state) {
84 | return {
85 | globalStatus: state.globalStatus
86 | };
87 | }
88 |
89 | export default connect(select)(RecordFilter);
90 |
--------------------------------------------------------------------------------
/web/src/component/record-filter.less:
--------------------------------------------------------------------------------
1 | @import '../style/constant.less';
2 |
3 | .filterWrapper {
4 | padding: 10px 15px 15px;
5 | }
6 |
7 | .title {
8 | font-size: @middlepanel-font-size;
9 | text-align: left;
10 | font-weight: 200;
11 | color: @hint-color;
12 | margin-bottom: 12px;
13 | }
14 |
15 | .filterInput {
16 |
17 | }
18 |
19 | .filterTip {
20 | margin-top: 20px;
21 | }
22 |
23 | .tipList {
24 | color: @tip-color;
25 | li {
26 | list-style-type: decimal;
27 | padding-left: 15px;
28 | }
29 | }
--------------------------------------------------------------------------------
/web/src/component/record-list-diff-worker.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * A webworker to identify whether the component need to be re-rendered
3 | */
4 | const getFilterReg = function (filterStr) {
5 | let filterReg = null;
6 | if (filterStr) {
7 | let regFilterStr = filterStr
8 | .replace(/\r\n/g, '\n')
9 | .replace(/\n\n/g, '\n');
10 |
11 | // remove the last /\n$/ in case an accidential br
12 | regFilterStr = regFilterStr.replace(/\n$/, '');
13 |
14 | if (regFilterStr[0] === '/' && regFilterStr[regFilterStr.length - 1] === '/') {
15 | regFilterStr = regFilterStr.substring(1, regFilterStr.length - 2);
16 | }
17 |
18 | regFilterStr = regFilterStr.replace(/((.+)\n|(.+)$)/g, (matchStr, $1, $2) => {
19 | // if there is '\n' in the string
20 | if ($2) {
21 | return `(${$2})|`;
22 | } else {
23 | return `(${$1})`;
24 | }
25 | });
26 |
27 | try {
28 | filterReg = new RegExp(regFilterStr);
29 | } catch (e) {
30 | console.error(e);
31 | }
32 | }
33 |
34 | return filterReg;
35 | };
36 |
37 | self.addEventListener('message', (e) => {
38 | const data = JSON.parse(e.data);
39 | const { limit, currentData, nextData, filterStr } = data;
40 | const filterReg = getFilterReg(filterStr);
41 | const filterdRecords = [];
42 | const length = nextData.length;
43 |
44 | // mark if the component need to be refreshed
45 | let shouldUpdate = false;
46 |
47 | // filtered out the records
48 | for (let i = 0; i < length; i++) {
49 | const item = nextData[i];
50 | if (filterReg && filterReg.test(item.url)) {
51 | filterdRecords.push(item);
52 | }
53 |
54 | if (!filterReg) {
55 | filterdRecords.push(item);
56 | }
57 |
58 | if (filterdRecords.length >= limit) {
59 | break;
60 | }
61 | }
62 |
63 | const newDataLength = filterdRecords.length;
64 | const currentDataLength = currentData.length;
65 |
66 | if (newDataLength !== currentDataLength) {
67 | shouldUpdate = true;
68 | } else {
69 | // only the two with same index and the `_render` === true then we'll need to render
70 | for (let i = 0; i < currentData.length; i++) {
71 | const item = currentData[i];
72 | const targetItem = filterdRecords[i];
73 | if (item.id !== targetItem.id || targetItem._render === true) {
74 | shouldUpdate = true;
75 | break;
76 | }
77 | }
78 | }
79 |
80 | self.postMessage(JSON.stringify({
81 | shouldUpdate,
82 | data: filterdRecords
83 | }));
84 | });
85 |
--------------------------------------------------------------------------------
/web/src/component/record-panel.less:
--------------------------------------------------------------------------------
1 | @import '../style/constant.less';
2 |
3 | .wrapper {
4 | position: relative;
5 | -webkit-user-select: none;
6 | :global {
7 | .ant-table {
8 | border: none;
9 | border-radius: 0;
10 | border-bottom: 0;
11 | padding-bottom: 50px;
12 | .ant-table-body {
13 | height: calc(100% - 114px);
14 | overflow: auto;
15 | }
16 |
17 | }
18 | th {
19 | padding-top: 15px !important;
20 | padding-bottom: 12px !important;
21 | border: none !important;
22 | border: none !important;
23 | }
24 | }
25 |
26 | .firstRow {
27 | padding-left: 20px;
28 | text-align: center;
29 | }
30 |
31 | .leftRow {
32 | text-align: left;
33 | }
34 |
35 | .centerRow {
36 | text-align: center;
37 | }
38 |
39 | .pathRow {
40 | min-width: 300px;
41 | }
42 | }
43 | .row {
44 | cursor: pointer;
45 | font-size: @font-size-xs;
46 | td {
47 | padding-top: 5px !important;
48 | padding-bottom: 5px !important;
49 | border: none;
50 | }
51 | }
52 |
53 | .lightBackgroundColor {
54 | background: @light-background-color;
55 | }
56 |
57 | .lightColor {
58 | color: @tip-color;
59 | }
60 |
61 | .activeRow {
62 | background: @active-color !important;
63 | color: #fff;
64 | :global {
65 | td {
66 | background-color: transparent !important;
67 | }
68 | }
69 | }
70 |
71 | .okStatus {
72 | color: @ok-color;
73 | }
74 |
75 | tr.loading {
76 | text-align: center;
77 | color: @tip-color;
78 | i {
79 | margin-right: 5px;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/web/src/component/record-response-detail.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * The panel to display the response detial of the record
3 | *
4 | */
5 |
6 | import React, { PropTypes } from 'react';
7 | import ClassBind from 'classnames/bind';
8 | import { Menu, Table, notification, Spin } from 'antd';
9 | import JsonViewer from 'component/json-viewer';
10 | import ModalPanel from 'component/modal-panel';
11 |
12 | import Style from './record-detail.less';
13 | import CommonStyle from '../style/common.less';
14 |
15 | const StyleBind = ClassBind.bind(Style);
16 | const PageIndexMap = {
17 | REQUEST_INDEX: 'REQUEST_INDEX',
18 | RESPONSE_INDEX: 'RESPONSE_INDEX'
19 | };
20 |
21 | // the maximum length of the request body to decide whether to offer a download link for the request body
22 | const MAXIMUM_REQ_BODY_LENGTH = 10000;
23 |
24 | class RecordResponseDetail extends React.Component {
25 | constructor() {
26 | super();
27 | this.state = {
28 |
29 | };
30 |
31 | }
32 |
33 | static propTypes = {
34 | recordDetail: PropTypes.object
35 | }
36 |
37 | onSelectText(e) {
38 | selectText(e.target);
39 | }
40 |
41 | getLiDivs(targetObj) {
42 | const liDom = Object.keys(targetObj).map((key) => {
43 | return (
44 |
45 | {key} :
46 | {targetObj[key]}
47 |
48 | );
49 | });
50 |
51 | return liDom;
52 | }
53 |
54 | getImageBody(recordDetail) {
55 | return
;
56 | }
57 |
58 | getJsonBody(recordDetail) {
59 | return ;
60 | }
61 |
62 | getResBodyDiv() {
63 | const { recordDetail } = this.props;
64 |
65 | const self = this;
66 |
67 | let reqBodyDiv = ;
68 |
69 | switch (recordDetail.type) {
70 | case 'image': {
71 | reqBodyDiv = {self.getImageBody(recordDetail)}
;
72 | break;
73 | }
74 | case 'json': {
75 | reqBodyDiv = self.getJsonBody(recordDetail);
76 | break;
77 | }
78 |
79 | default: {
80 | if (!recordDetail.resBody && recordDetail.ref) {
81 | reqBodyDiv = {recordDetail.fileName};
82 | }
83 | break;
84 | }
85 | }
86 |
87 | return (
88 |
89 | {reqBodyDiv}
90 |
91 | );
92 | }
93 |
94 | getResponseDiv(recordDetail) {
95 | const statusStyle = StyleBind({ okStatus: recordDetail.statusCode === 200 });
96 |
97 | return (
98 |
99 |
100 |
101 | General
102 |
103 |
104 |
105 | -
106 | Status Code:
107 | {recordDetail.statusCode}
108 |
109 |
110 |
111 |
112 |
113 | Header
114 |
115 |
116 |
117 | {this.getLiDivs(recordDetail.resHeader)}
118 |
119 |
120 |
121 |
122 |
123 | Body
124 |
125 |
126 | {this.getResBodyDiv()}
127 |
128 |
129 | );
130 | }
131 |
132 | render() {
133 | return this.getResponseDiv(this.props.recordDetail);
134 | }
135 | }
136 |
137 | export default RecordResponseDetail;
138 |
--------------------------------------------------------------------------------
/web/src/component/record-row.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * A copoment for the request log table
3 | */
4 |
5 | import React, { PropTypes } from 'react';
6 | import { formatDate } from 'common/commonUtil';
7 |
8 | import Style from './record-row.less';
9 | import CommonStyle from '../style/common.less';
10 | import ClassBind from 'classnames/bind';
11 |
12 | const StyleBind = ClassBind.bind(Style);
13 |
14 | class RecordRow extends React.Component {
15 | constructor() {
16 | super();
17 | this.state = {
18 |
19 | };
20 | }
21 |
22 | static propTypes = {
23 | data: PropTypes.object,
24 | detailHanlder: PropTypes.func,
25 | className: PropTypes.string
26 | }
27 |
28 | getMethodDiv(item) {
29 | const httpsIcon = ;
30 | return {item.method}
{item.protocol === 'https' ? httpsIcon : null}
;
31 | }
32 |
33 | getCodeDiv(item) {
34 | const statusCode = parseInt(item.statusCode, 10);
35 | const className = StyleBind({ okStatus: statusCode === 200, errorStatus: statusCode >= 400 });
36 | return {item.statusCode};
37 | }
38 |
39 | shouldComponentUpdate(nextProps) {
40 | if (nextProps.data._render) {
41 | nextProps.data._render = false;
42 | return true;
43 | } else {
44 | return false;
45 | }
46 | }
47 |
48 | render() {
49 | const data = this.props.data;
50 |
51 | if (!data) {
52 | return null;
53 | }
54 |
55 | return (
56 |
57 | {data.id} |
58 | {this.getMethodDiv(data)} |
59 | {this.getCodeDiv(data)} |
60 | {data.host} |
61 | {data.path} |
62 | {data.mime} |
63 | {formatDate(data.startTime, 'hh:mm:ss')} |
64 |
65 | );
66 | }
67 | }
68 |
69 | export default RecordRow;
70 |
--------------------------------------------------------------------------------
/web/src/component/record-row.less:
--------------------------------------------------------------------------------
1 | @import '../style/constant.less';
2 |
3 | .tableRow {
4 | display: block;
5 | cursor: pointer;
6 | font-size: @font-size-sm;
7 | td {
8 | padding-top: 5px !important;
9 | padding-bottom: 5px !important;
10 | }
11 | }
12 |
13 | .lightBackgroundColor {
14 | background: @light-background-color;
15 | }
16 |
17 | .id {
18 | padding-left: 20px;
19 | text-align: center;
20 | }
21 |
22 | .method {
23 | text-align: left;
24 | }
25 |
26 | .code {
27 | text-align: center;
28 | }
29 |
30 | // .host {
31 | // width: 200px;
32 | // }
33 |
34 | .path {
35 | max-width: 0;
36 | min-width: 300px;
37 | overflow: hidden;
38 | text-overflow: ellipsis;
39 | white-space: nowrap;
40 | }
41 |
42 | .mime {
43 | min-width: 160px;
44 | max-width: 0;
45 | overflow: hidden;
46 | text-overflow: ellipsis;
47 | white-space: nowrap;
48 | }
49 |
50 | // .time {
51 | // width: 100px;
52 | // }
53 |
54 | .okStatus {
55 | color: @ok-color;
56 | }
57 |
58 | .errorStatus {
59 | color: @error-color;
60 | }
61 |
62 | .https {
63 | background: url('../assets/https.png');
64 | width: 11px;
65 | height: 11px;
66 | margin-left: 1px;
67 | background-size: contain;
68 | display: inline-block;
69 | }
70 |
--------------------------------------------------------------------------------
/web/src/component/record-ws-message-detail.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * The panel to display the detial of the record
3 | *
4 | */
5 |
6 | import React, { PropTypes } from 'react';
7 | import { message, Button, Icon } from 'antd';
8 | import { formatDate } from 'common/commonUtil';
9 | import { initWs } from 'common/wsUtil';
10 | import ClassBind from 'classnames/bind';
11 |
12 | import Style from './record-ws-message-detail.less';
13 | import CommonStyle from '../style/common.less';
14 |
15 | const ToMessage = (props) => {
16 | const { message: wsMessage } = props;
17 | return (
18 |
19 |
{formatDate(wsMessage.time, 'hh:mm:ss:ms')}
20 |
{wsMessage.message}
21 |
22 | );
23 | }
24 |
25 | const FromMessage = (props) => {
26 | const { message: wsMessage } = props;
27 | return (
28 |
29 |
{formatDate(wsMessage.time, 'hh:mm:ss:ms')}
30 |
{wsMessage.message}
31 |
32 | );
33 | }
34 |
35 | class RecordWsMessageDetail extends React.Component {
36 | constructor() {
37 | super();
38 | this.state = {
39 | stateCheck: false, // a prop only to trigger state check
40 | autoRefresh: true,
41 | socketMessages: [] // the messages from websocket listening
42 | };
43 |
44 | this.updateStateRef = null; // a timeout ref to reduce the calling of update state
45 | this.wsClient = null; // ref to the ws client
46 | this.onMessageHandler = this.onMessageHandler.bind(this);
47 | this.receiveNewMessage = this.receiveNewMessage.bind(this);
48 | this.toggleRefresh = this.toggleRefresh.bind(this);
49 | }
50 |
51 | static propTypes = {
52 | recordDetail: PropTypes.object
53 | }
54 |
55 | toggleRefresh () {
56 | const { autoRefresh } = this.state;
57 | this.state.autoRefresh = !autoRefresh;
58 | this.setState({
59 | stateCheck: true
60 | });
61 | }
62 |
63 | receiveNewMessage (message) {
64 | this.state.socketMessages.push(message);
65 |
66 | this.updateStateRef && clearTimeout(this.updateStateRef);
67 | this.updateStateRef = setTimeout(() => {
68 | this.setState({
69 | stateCheck: true
70 | });
71 | }, 100);
72 | }
73 |
74 | getMessageList () {
75 | const { recordDetail } = this.props;
76 | const { socketMessages } = this.state;
77 | const { wsMessages = [] } = recordDetail;
78 |
79 | const targetMessage = wsMessages.concat(socketMessages);
80 |
81 | return targetMessage.map((messageItem, index) => {
82 | return messageItem.isToServer ?
83 | : ;
84 | });
85 | }
86 |
87 | refreshPage () {
88 | const { autoRefresh } = this.state;
89 | if (autoRefresh && this.messageRef && this.messageContentRef) {
90 | this.messageRef.scrollTop = this.messageContentRef.scrollHeight;
91 | }
92 | }
93 |
94 | onMessageHandler (event) {
95 | const { recordDetail } = this.props;
96 | const data = JSON.parse(event.data);
97 | const content = data.content;
98 | if (data.type === 'updateLatestWsMsg' ) {
99 | if (recordDetail.id === content.id) {
100 | this.receiveNewMessage(content.message);
101 | }
102 | }
103 | }
104 |
105 | componentDidUpdate () {
106 | this.refreshPage();
107 | }
108 |
109 | componentWillUnmount () {
110 | this.wsClient && this.wsClient.removeEventListener('message', this.onMessageHandler);
111 | }
112 |
113 | componentDidMount () {
114 | const { recordDetail } = this.props;
115 |
116 | this.refreshPage();
117 |
118 | this.wsClient = initWs();
119 | this.wsClient.addEventListener('message', this.onMessageHandler);
120 | }
121 |
122 | render() {
123 | const { recordDetail } = this.props;
124 | const { autoRefresh } = this.state;
125 | if (!recordDetail) {
126 | return null;
127 | }
128 |
129 | const playIcon = ;
130 | const pauseIcon = ;
131 | return (
132 | this.messageRef = _ref}>
133 |
this.messageContentRef = _ref}>
134 | {this.getMessageList()}
135 |
136 |
137 | {autoRefresh ? pauseIcon : playIcon}
138 |
139 |
140 | );
141 | }
142 | }
143 |
144 | export default RecordWsMessageDetail;
145 |
--------------------------------------------------------------------------------
/web/src/component/record-ws-message-detail.less:
--------------------------------------------------------------------------------
1 | @import '../style/constant.less';
2 | .wrapper {
3 | position: absolute;
4 | top: 0;
5 | left: 0;
6 | width: 100%;
7 | height: 100%;
8 | overflow: auto;
9 | }
10 |
11 | .contentWrapper {
12 | overflow: hidden;
13 | }
14 |
15 | .toMessage {
16 | float: right;
17 | clear: both;
18 | margin: 5px auto;
19 | .content {
20 | background-color: @primary-color;
21 | }
22 | }
23 |
24 | .fromMessage {
25 | float: left;
26 | clear: both;
27 | max-width: 40%;
28 | margin: 5px auto;
29 | .content {
30 | background: @success-color;
31 | }
32 | }
33 |
34 | .time {
35 | font-size: @font-size-xs;
36 | color: @tip-color;
37 | }
38 |
39 | .content {
40 | clear: both;
41 | border-radius: @border-radius-base;
42 | color: #fff;
43 | padding: 7px 8px;
44 | font-size: @font-size-sm;
45 | word-wrap: break-word;
46 | word-break: break-all;
47 | }
48 |
49 | .refreshBtn {
50 | position: fixed;
51 | right: 20px;
52 | bottom: 5px;
53 | opacity: 0.53;
54 | font-size: @font-size-large;
55 | cursor: pointer;
56 | }
57 |
--------------------------------------------------------------------------------
/web/src/component/resizable-panel.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * A copoment to display content in the a resizable panel
3 | */
4 |
5 | import React, { PropTypes } from 'react';
6 | import ReactDOM from 'react-dom';
7 | import { Icon } from 'antd';
8 |
9 | import Style from './resizable-panel.less';
10 | import ClassBind from 'classnames/bind';
11 |
12 | const StyleBind = ClassBind.bind(Style);
13 |
14 | class ResizablePanel extends React.Component {
15 | constructor () {
16 | super();
17 |
18 | this.state = {
19 | dragBarLeft: '',
20 | contentLeft: ''
21 | };
22 | this.onDragbarMoveUp = this.onDragbarMoveUp.bind(this);
23 | this.onDragbarMove = this.onDragbarMove.bind(this);
24 | this.onDragbarMoveDown = this.onDragbarMoveDown.bind(this);
25 | this.doClose = this.doClose.bind(this);
26 | this.onKeyUp = this.onKeyUp.bind(this);
27 | this.addKeyEvent = this.addKeyEvent.bind(this);
28 | this.removeKeyEvent = this.removeKeyEvent.bind(this);
29 | }
30 |
31 | static propTypes = {
32 | children: PropTypes.element,
33 | onClose: PropTypes.func,
34 | visible: PropTypes.bool
35 | }
36 |
37 | onDragbarMove (event) {
38 | this.setState({
39 | dragBarLeft: event.pageX
40 | });
41 | }
42 |
43 | onKeyUp (e) {
44 | if (e.keyCode == 27) {
45 | this.doClose();
46 | }
47 | }
48 |
49 | addKeyEvent () {
50 | document.addEventListener('keyup', this.onKeyUp);
51 | }
52 |
53 | removeKeyEvent () {
54 | document.removeEventListener('keyup', this.onKeyUp);
55 | }
56 |
57 | onDragbarMoveUp (event) {
58 | this.setState({
59 | contentLeft: event.pageX
60 | });
61 |
62 | document.removeEventListener('mousemove', this.onDragbarMove);
63 | document.removeEventListener('mouseup', this.onDragbarMoveUp);
64 | }
65 |
66 | onDragbarMoveDown (event) {
67 | document.addEventListener('mousemove', this.onDragbarMove);
68 |
69 | document.addEventListener('mouseup', this.onDragbarMoveUp);
70 | }
71 |
72 | doClose () {
73 | this.props.onClose && this.props.onClose();
74 | }
75 |
76 | render () {
77 | if (!this.props.visible) {
78 | this.removeKeyEvent();
79 | return null;
80 | }
81 | this.addKeyEvent();
82 |
83 | const { dragBarLeft, contentLeft } = this.state;
84 | const propsLeft = this.props.left;
85 | const dragBarStyle = dragBarLeft || propsLeft ? { 'left': dragBarLeft || propsLeft } : null;
86 | const contentStyle = contentLeft || propsLeft ? { 'left': contentLeft || propsLeft } : null;
87 |
88 | const modalStyle = this.props.hideBackModal ? contentStyle : { 'left': 0 };
89 | return (
90 |
91 |
92 |
93 | {this.props.children}
94 |
95 |
96 |
101 |
102 |
103 |
104 |
105 | );
106 | }
107 | }
108 |
109 | export default ResizablePanel;
--------------------------------------------------------------------------------
/web/src/component/resizable-panel.less:
--------------------------------------------------------------------------------
1 | @import '../style/constant.less';
2 | .wrapper {
3 | display: block;
4 | background-color: #fff;
5 | position: relative;
6 | width: 360px;
7 | height: 100%;
8 | -webkit-box-shadow: 10px 0px 21px 0px rgba(97,95,97,0.15);
9 | -moz-box-shadow: 10px 0px 21px 0px rgba(97,95,97,0.15);
10 | box-shadow: 10px 0px 21px 0px rgba(97,95,97,0.15);
11 | overflow-y: auto;
12 | overflow-x: hidden;
13 | z-index: 1;
14 | }
15 |
16 | .contentWrapper, .content {
17 | height: 100%;
18 | }
19 |
20 | .dragBar {
21 | width: 1px;
22 | background-color: @hint-color;
23 | z-index: 2000;
24 | cursor: col-resize;
25 | }
26 |
27 | .closeIcon {
28 | position: absolute;
29 | top: 0;
30 | right: 0;
31 | cursor: pointer;
32 | padding: 15px;
33 | i {
34 | font-size: @font-size-large;
35 | }
36 | &:hover {
37 | color: @primary-color;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/web/src/component/table-panel.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * A copoment for the request log table
3 | */
4 |
5 | import React, { PropTypes } from 'react';
6 | import ReactDOM from 'react-dom';
7 | import { Table } from 'antd';
8 | import { formatDate } from 'common/commonUtil';
9 |
10 | import Style from './table-panel.less';
11 | import ClassBind from 'classnames/bind';
12 | import CommonStyle from '../style/common.less';
13 |
14 | const StyleBind = ClassBind.bind(Style);
15 |
16 | class TablePanel extends React.Component {
17 | constructor () {
18 | super();
19 | this.state = {
20 | active: true
21 | };
22 | }
23 | static propTypes = {
24 | data: PropTypes.array
25 | }
26 |
27 | getTr () {
28 |
29 | }
30 | render () {
31 | const httpsIcon = ;
32 | const columns = [
33 | {
34 | title: '#',
35 | width: 50,
36 | dataIndex: 'id'
37 | },
38 | {
39 | title: 'Method',
40 | width:100,
41 | dataIndex: 'method',
42 | render (text, item) {
43 | return {text} {item.protocol === 'https' ? httpsIcon : null};
44 | }
45 | },
46 | {
47 | title: 'Code',
48 | width: 70,
49 | dataIndex: 'statusCode',
50 | render(text) {
51 | const className = StyleBind({ 'okStatus': text === '200' });
52 | return {text};
53 | }
54 | },
55 | {
56 | title: 'Host',
57 | width: 200,
58 | dataIndex: 'host'
59 | },
60 | {
61 | title: 'Path',
62 | dataIndex: 'path'
63 | },
64 | {
65 | title: 'MIME',
66 | width: 150,
67 | dataIndex: 'mime'
68 | },
69 | {
70 | title: 'Start',
71 | width: 100,
72 | dataIndex: 'startTime',
73 | render (text) {
74 | const timeStr = formatDate(text, 'hh:mm:ss');
75 | return {timeStr};
76 | }
77 | }
78 | ];
79 |
80 | function rowClassFunc (record, index) {
81 | return StyleBind('row', { 'lightBackgroundColor': index % 2 === 1 });
82 | }
83 |
84 | return (
85 |
94 | );
95 | }
96 | }
97 |
98 | export default TablePanel;
--------------------------------------------------------------------------------
/web/src/component/table-panel.less:
--------------------------------------------------------------------------------
1 | @import '../style/constant.less';
2 |
3 | .tableWrapper {
4 | clear: both;
5 | margin-top: 30px;
6 | :global {
7 | th {
8 | padding-top: 5px !important;
9 | padding-bottom: 5px !important;
10 | background: @background-color !important;
11 | border-top: 1px solid @hint-color;
12 | border-bottom: 1px solid @hint-color;
13 | }
14 | }
15 | }
16 | .row {
17 | cursor: pointer;
18 | font-size: @font-size-sm;
19 | td {
20 | padding-top: 5px !important;
21 | padding-bottom: 5px !important;
22 | }
23 | }
24 |
25 | .lightBackgroundColor {
26 | background: @light-background-color;
27 | }
28 |
29 | .okStatus {
30 | color: @ok-color;
31 | }
--------------------------------------------------------------------------------
/web/src/component/title-bar.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * The panel to edit the filter
3 | *
4 | */
5 |
6 | import React, { PropTypes } from 'react';
7 | import { Icon } from 'antd';
8 | import { getQueryParameter } from 'common/commonUtil';
9 |
10 | import Style from './title-bar.less';
11 |
12 | class TitleBar extends React.Component {
13 | constructor () {
14 | super();
15 | this.state = {
16 | inMaxWindow: false,
17 | inApp: getQueryParameter('in_app_mode') // will only show the bar when in app
18 | };
19 |
20 | }
21 |
22 | static propTypes = {
23 | }
24 |
25 | render() {
26 |
27 | if (this.state.inApp !== 'true') {
28 | return null;
29 | }
30 |
31 | // the buttons with normal window size
32 | const normalButton = (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | );
45 |
46 | const maxmizeButton = (
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | );
57 |
58 | return this.state.inMaxWindow ? maxmizeButton : normalButton;
59 | }
60 | }
61 |
62 | export default TitleBar;
63 |
--------------------------------------------------------------------------------
/web/src/component/title-bar.less:
--------------------------------------------------------------------------------
1 | @import '../style/constant.less';
2 | .wrapper {
3 | position: relative;
4 | }
5 | .iconButtons {
6 | overflow: hidden;
7 | span {
8 | position: relative;
9 | display: block;
10 | float: left;
11 | width: 12.44px;
12 | height: 12.84px;
13 | border-radius: 50%;
14 | margin: 0 7px;
15 | }
16 |
17 | .close {
18 | background-color: #EB3131;
19 | }
20 |
21 | .disabled {
22 | background-color: #aaa;
23 | }
24 |
25 | .minimize {
26 | background-color: #FFBF11;
27 | }
28 |
29 | .maxmize {
30 | background-color: #09CE26;
31 | }
32 |
33 | i {
34 | display: none;
35 | position: absolute;
36 | left: 0px;
37 | top: 1px;
38 | }
39 |
40 | &:hover {
41 | i {
42 | display: block;
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/web/src/index.less:
--------------------------------------------------------------------------------
1 | @import './style/constant.less';
2 |
3 | .indexWrapper {
4 | display: block;
5 | position: relative;
6 | height: 100%;
7 | }
8 |
9 | .leftPanel {
10 | width: 158px;
11 | height: 100%;
12 | min-height: 300px;
13 | float: left;
14 | -webkit-app-region: drag;
15 | -webkit-user-select: none;
16 | }
17 |
18 | .middlePanel {
19 | float: left;
20 | height: 100%;
21 | }
22 |
23 | .rightPanel {
24 | overflow: hidden;
25 | position: relative;
26 | height: 100%;
27 | z-index: 0;
28 | }
29 |
30 | .headerWrapper {
31 | border-bottom: 1px solid @hint-color;
32 | padding-bottom: 5px;
33 | padding-top: 25px;
34 | -webkit-app-region: drag;
35 | -webkit-user-select: none;
36 | }
37 |
38 | .tableWrapper {
39 | clear: both;
40 | position: absolute;
41 | top: 89px;
42 | left: 0;
43 | right: 0;
44 | bottom: 0;
45 | overflow: auto;
46 | transform: translateZ(0);
47 | }
48 |
49 | .resumeTip {
50 | display: block;
51 | background-color: @primary-color;
52 | color: #fff;
53 | position: absolute;
54 | right: 30px;
55 | bottom: 15px;
56 | padding: 10px 10px;
57 | opacity: 0.9;
58 | border-radius: 4px;
59 | cursor: pointer;
60 | -webkit-box-shadow: 1px 1px 9px 0px rgba(0,0,0,0.37);
61 | -moz-box-shadow: 1px 1px 9px 0px rgba(0,0,0,0.37);
62 | box-shadow: 1px 1px 9px 0px rgba(0,0,0,0.37);
63 | &:hover {
64 | opacity: 1;
65 | }
66 | }
67 |
68 | .arrowDown {
69 | display: block;
70 | position: absolute;
71 | bottom: -15px;
72 | right: 60px;
73 | width: 0;
74 | height: 0;
75 | border-left: 5px solid transparent;
76 | border-right: 5px solid transparent;
77 | border-top: 5px solid @primary-color;
78 | opacity: 0.9;
79 | }
--------------------------------------------------------------------------------
/web/src/reducer/requestRecordReducer.js:
--------------------------------------------------------------------------------
1 | const defaultState = {
2 | recordList: [],
3 | recordDetail: null
4 | };
5 |
6 | import {
7 | UPDATE_WHOLE_REQUEST,
8 | UPDATE_SINGLE_RECORD,
9 | CLEAR_ALL_LOCAL_RECORD,
10 | UPDATE_MULTIPLE_RECORDS,
11 | SHOW_RECORD_DETAIL,
12 | HIDE_RECORD_DETAIL
13 | } from 'action/recordAction';
14 |
15 | const getRecordInList = function (recordId, recordList) {
16 | const newRecordList = recordList.slice();
17 | for (let i = 0; i< newRecordList.length ; i++) {
18 | const record = newRecordList[i];
19 | if (record.id === recordId) {
20 | return record;
21 | }
22 | }
23 | };
24 |
25 | function requestListReducer (state = defaultState, action) {
26 | switch (action.type) {
27 | case UPDATE_WHOLE_REQUEST: {
28 | const newState = Object.assign({}, state);
29 | newState.recordList = action.data.slice();
30 | return newState;
31 | }
32 |
33 | case UPDATE_SINGLE_RECORD: {
34 |
35 | const newState = Object.assign({}, state);
36 |
37 | const list = newState.recordList.slice();
38 |
39 | list.forEach((item) => {
40 | item._render = false;
41 | });
42 |
43 | const record = action.data;
44 |
45 | const index = list.findIndex((item) => {
46 | return item.id === record.id;
47 | });
48 |
49 | if (index >= 0) {
50 | // set the mark to ensure the item get re-rendered
51 | record._render = true;
52 | list[index] = record;
53 | } else {
54 | list.push(record);
55 | }
56 |
57 | newState.recordList = list;
58 | return newState;
59 | }
60 |
61 | case UPDATE_MULTIPLE_RECORDS: {
62 | const newState = Object.assign({}, state);
63 | const list = newState.recordList.slice();
64 |
65 | list.forEach((item) => {
66 | item._render = false;
67 | });
68 |
69 | const records = action.data;
70 | records.forEach((record) => {
71 | const index = list.findIndex((item) => {
72 | return item.id === record.id;
73 | });
74 |
75 | if (index >= 0) {
76 | // set the mark to ensure the item get re-rendered
77 | record._render = true;
78 | list[index] = record;
79 | } else {
80 | list.push(record);
81 | }
82 | });
83 |
84 | newState.recordList = list;
85 | return newState;
86 | }
87 |
88 | case CLEAR_ALL_LOCAL_RECORD: {
89 | const newState = Object.assign({}, state);
90 | newState.recordList = [];
91 | return newState;
92 | }
93 |
94 | case SHOW_RECORD_DETAIL: {
95 | const newState = Object.assign({}, state);
96 | const responseBody = action.data;
97 | const originRecord = getRecordInList(responseBody.id, newState.recordList);
98 | // 只在id存在的时候,才更新, 否则取消
99 | if (originRecord) {
100 | newState.recordDetail = Object.assign(responseBody, originRecord);
101 | } else {
102 | newState.recordDetail = null;
103 | }
104 |
105 | return newState;
106 | }
107 |
108 | case HIDE_RECORD_DETAIL: {
109 | const newState = Object.assign({}, state);
110 | newState.recordDetail = null;
111 | return newState;
112 | }
113 |
114 | default: {
115 | return state;
116 | }
117 | }
118 | }
119 |
120 | export default requestListReducer;
--------------------------------------------------------------------------------
/web/src/reducer/rootReducer.js:
--------------------------------------------------------------------------------
1 | import requestRecordReducer from './requestRecordReducer';
2 | import globalStatusReducer from './globalStatusReducer';
3 |
4 | const defaultState = {
5 |
6 | };
7 |
8 | export default function(state = defaultState, action) {
9 | return {
10 | requestRecord: requestRecordReducer(state.requestRecord, action),
11 | globalStatus: globalStatusReducer(state.globalStatus, action)
12 | };
13 | }
--------------------------------------------------------------------------------
/web/src/saga/rootSaga.js:
--------------------------------------------------------------------------------
1 | import {
2 | take,
3 | put,
4 | call,
5 | fork
6 | } from 'redux-saga/effects';
7 | import { message } from 'antd';
8 |
9 | import {
10 | FETCH_REQUEST_LOG,
11 | CLEAR_ALL_RECORD,
12 | FETCH_RECORD_DETAIL,
13 | clearAllLocalRecord,
14 | updateWholeRequest,
15 | showRecordDetail
16 | } from 'action/recordAction';
17 |
18 | import {
19 | FETCH_DIRECTORY,
20 | FETCH_MAPPED_CONFIG,
21 | UPDATE_REMOTE_MAPPED_CONFIG,
22 | TOGGLE_REMOTE_INTERCEPT_HTTPS,
23 | TOGGLE_REMORE_GLOBAL_PROXY_FLAG,
24 | updateLocalDirectory,
25 | updateLocalMappedConfig,
26 | updateActiveRecordItem,
27 | updateLocalInterceptHttpsFlag,
28 | updateFechingRecordStatus,
29 | updateLocalGlobalProxyFlag
30 | } from 'action/globalStatusAction';
31 |
32 | import { getJSON, postJSON, isApiSuccess } from 'common/apiUtil';
33 |
34 | function* doFetchRequestList() {
35 | const data = yield call(getJSON, '/latestLog');
36 | yield put(updateWholeRequest(data));
37 | }
38 |
39 | function* doFetchDirectory(path = '') {
40 | const sub = yield call(getJSON, '/filetree', { root: path });
41 | yield put(updateLocalDirectory(path, sub));
42 | }
43 |
44 | function* doFetchMappedConfig() {
45 | const config = yield call(getJSON, '/getMapConfig');
46 | yield put(updateLocalMappedConfig(config));
47 | }
48 |
49 | function* doFetchRecordBody(recordId) {
50 | // const recordBody = { id: recordId };
51 | yield put(updateFechingRecordStatus(true));
52 | const recordBody = yield call(getJSON, '/fetchBody', { id: recordId });
53 | if (recordBody.method && recordBody.method.toLowerCase() === 'websocket') {
54 | recordBody.wsMessages = yield call(getJSON, '/fetchWsMessages', { id: recordId});
55 | }
56 | recordBody.id = parseInt(recordBody.id, 10);
57 |
58 | yield put(updateFechingRecordStatus(false));
59 | yield put(updateActiveRecordItem(recordId));
60 | yield put(showRecordDetail(recordBody));
61 | }
62 |
63 | function* doUpdateRemoteMappedConfig(config) {
64 | const newConfig = yield call(postJSON, '/setMapConfig', config);
65 | yield put(updateLocalMappedConfig(newConfig));
66 | }
67 |
68 |
69 | function * doToggleRemoteInterceptHttps(flag) {
70 | yield call(postJSON, '/api/toggleInterceptHttps', { flag: flag });
71 | yield put(updateLocalInterceptHttpsFlag(flag));
72 | }
73 |
74 | function * doToggleRemoteGlobalProxy(flag) {
75 | const result = yield call(postJSON, '/api/toggleGlobalProxy', { flag: flag });
76 | const windowsMessage = 'Successfully turned on, it may take up to 1 min to take effect.';
77 | const linuxMessage = 'Successfully turned on.';
78 | const turnDownMessage = 'Global proxy has been turned down.';
79 | if (isApiSuccess(result)) {
80 | const tipMessage = result.isWindows ? windowsMessage : linuxMessage;
81 | message.success(flag ? tipMessage : turnDownMessage, 3);
82 | yield put(updateLocalGlobalProxyFlag(flag));
83 | } else {
84 | message.error(result.errorMsg, 3);
85 | }
86 | }
87 |
88 | function * fetchRequestSaga() {
89 | while (true) {
90 | yield take(FETCH_REQUEST_LOG);
91 | yield fork(doFetchRequestList);
92 | }
93 | }
94 |
95 | function * clearRequestRecordSaga() {
96 | while (true) {
97 | yield take(CLEAR_ALL_RECORD);
98 | yield put(clearAllLocalRecord());
99 | }
100 | }
101 |
102 | function * fetchDirectorySaga() {
103 | while (true) {
104 | const action = yield take(FETCH_DIRECTORY);
105 | yield fork(doFetchDirectory, action.data);
106 | }
107 | }
108 |
109 | function * fetchMappedConfigSaga() {
110 | while (true) {
111 | yield take(FETCH_MAPPED_CONFIG);
112 | yield fork(doFetchMappedConfig);
113 | }
114 | }
115 |
116 | function * updateRemoteMappedConfigSaga() {
117 | while (true) {
118 | const action = yield take(UPDATE_REMOTE_MAPPED_CONFIG);
119 |
120 | yield fork(doUpdateRemoteMappedConfig, action.data);
121 | }
122 | }
123 |
124 | function * fetchRecordBodySaga() {
125 | while (true) {
126 | const action = yield take(FETCH_RECORD_DETAIL);
127 |
128 | yield fork(doFetchRecordBody, action.data);
129 | }
130 | }
131 |
132 | function * toggleRemoteInterceptHttpsSaga() {
133 | while (true) {
134 | const action = yield take(TOGGLE_REMOTE_INTERCEPT_HTTPS);
135 | yield fork(doToggleRemoteInterceptHttps, action.data);
136 | }
137 | }
138 |
139 | function * toggleRemoteGlobalProxySaga() {
140 | while (true) {
141 | const action = yield take(TOGGLE_REMORE_GLOBAL_PROXY_FLAG);
142 | yield fork(doToggleRemoteGlobalProxy, action.data);
143 | }
144 | }
145 |
146 | export default function* root() {
147 | yield fork(fetchRequestSaga);
148 | yield fork(clearRequestRecordSaga);
149 | yield fork(fetchDirectorySaga);
150 | yield fork(fetchMappedConfigSaga);
151 | yield fork(updateRemoteMappedConfigSaga);
152 | yield fork(fetchRecordBodySaga);
153 | yield fork(toggleRemoteInterceptHttpsSaga);
154 | yield fork(toggleRemoteGlobalProxySaga);
155 | }
156 |
--------------------------------------------------------------------------------
/web/src/style/animate.less:
--------------------------------------------------------------------------------
1 | @keyframes rotation {
2 | 0% {
3 | -webkit-transform: rotate(0deg);
4 | opacity: 0.1;
5 | }
6 |
7 | 20% {
8 | opacity: 0.1;
9 | }
10 |
11 | 40% {
12 | opacity: 0.25;
13 | }
14 |
15 | 80% {
16 | opacity: 0.1;
17 | }
18 |
19 | 100% {
20 | -webkit-transform: rotate(359deg);
21 | opacity: 0.1;
22 | }
23 | }
24 | .rotation {
25 | animation: rotation 1.2s infinite cubic-bezier(.63,.33,.46,.71);
26 | }
27 |
--------------------------------------------------------------------------------
/web/src/style/antd-reset.global.less:
--------------------------------------------------------------------------------
1 | @import "~antd/lib/style/themes/default.less";
2 | @import "./constant.less";
3 | @import "~antd/lib/style/core/index.less";
4 | @import "~antd/lib/style/components.less";
5 |
6 | // .ant-input {
7 | // border-radius: 0;
8 | // height: @form-input-height;
9 | // }
10 |
11 | // .has-error .ant-input:focus {
12 | // box-shadow: 0 0 0 2px rgba(224, 21, 21, 0.2);
13 | // border-color: #e01515;
14 | // }
15 |
16 | // .ant-menu-inline > .ant-menu-item{
17 | // font-size: @font-size-reg;
18 | // line-height: 44px;
19 | // height: 44px;
20 | // }
21 |
22 | // .ant-form-explain {
23 | // margin-top:5px;
24 | // }
25 |
26 | // // 图片上传
27 | // .ant-upload-list-picture-card .ant-upload-list-item {
28 | // width:87px;
29 | // height:87px;
30 | // }
31 |
32 | // // menu
33 | // // .ant-menu-inline > .ant-menu-item {
34 | // // border-bottom:1px solid @light-border-color;
35 | // // }
36 |
37 | // .ant-alert {
38 | // margin-bottom:0;
39 | // }
40 |
41 | // .ant-modal-body {
42 | // padding:15px 30px 20px;
43 | // }
44 |
45 | // .ant-checkbox-wrapper + span, .ant-checkbox + span {
46 | // margin-right:0;
47 | // }
48 |
49 | // .ant-checkbox-wrapper {
50 | // margin-bottom:0;
51 | // }
52 |
53 | // // 分页插件的页码输入
54 | // .ant-pagination-options-quick-jumper input {
55 | // border-radius: 0;
56 | // height: @form-input-height;
57 | // line-height: @form-input-height;
58 | // }
59 |
60 | // // 下拉选择的组件
61 | // .ant-select-selection--single {
62 | // height: @form-input-height;
63 | // }
64 |
65 | // .ant-select-selection {
66 | // border-radius: 0;
67 | // }
68 |
69 | // .ant-select-selection__rendered {
70 | // height: 30px;
71 | // line-height: 30px;
72 | // }
73 |
74 | // .ant-select-dropdown {
75 | // border-radius: 0;
76 | // }
77 |
78 | // .ant-pagination-prev, .ant-pagination-next, .ant-pagination-jump-prev, .ant-pagination-jump-next {
79 | // height: @form-input-height;
80 | // line-height: @form-input-height;
81 | // min-width: @form-input-height;
82 | // }
83 |
84 | // .ant-pagination-prev a:after, .ant-pagination-next a:after {
85 | // height: 28px;
86 | // line-height: 28px;
87 | // }
88 |
89 | // .ant-pagination-item {
90 | // height: @form-input-height;
91 | // line-height: @form-input-height;
92 | // min-width: @form-input-height;
93 | // }
94 |
95 |
--------------------------------------------------------------------------------
/web/src/style/common.less:
--------------------------------------------------------------------------------
1 | @import './constant.less';
2 | @import './animate.less';
3 |
4 | body {
5 | line-height: 1.5;
6 | font-size: @font-size-reg;
7 | position: relative;
8 | font-family: "PingFangSC-Regular", "Helvetica Neue", Helvetica, "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif !important;
9 | word-wrap: break-word;
10 | min-height: 500px;
11 | }
12 |
13 | .sectionTitle {
14 | display: block;
15 | font-size: @font-size-reg;
16 | border-left: 3px solid @primary-color;
17 | line-height: 1;
18 | padding-left: 5px;
19 | }
20 |
21 | .whiteSpace10 {
22 | display: block;
23 | width: 100%;
24 | height: 10px;
25 | }
26 |
27 | .whiteSpace20 {
28 | display: block;
29 | width: 100%;
30 | height: 20px;
31 | }
32 |
33 | .whiteSpace30 {
34 | display: block;
35 | width: 100%;
36 | height: 30px;
37 | }
38 |
39 | .left {
40 | float: left;
41 | }
42 |
43 | .right {
44 | float: right;
45 | }
46 |
47 | .topAlign {
48 | display: flex;
49 | justify-content: flex-start;
50 | align-items: flex-start;
51 | div {
52 | line-height: 1;
53 | }
54 | }
55 |
56 | :global {
57 | // .ant-btn {
58 | // min-width: 100px;
59 | // }
60 | }
61 |
62 | .relativeWrapper {
63 | position: relative;
64 | width: 100%;
65 | height: 100%;
66 | }
67 |
--------------------------------------------------------------------------------
/web/src/style/constant.less:
--------------------------------------------------------------------------------
1 |
2 | @import "./antd-constant";
3 |
4 | @primary-color: #108ee9;
5 | @default-color: #1a1a1a;
6 | @tip-color: #999;
7 | @hint-color: #ddd;
8 | @error-color: #EA2020;
9 | @step-head-color:#666666;
10 | @background-color: #eaeaea;
11 | @light-border-color: #e0e0e0;
12 | @warn-color: #fac450;
13 | @number-color: #FF6600;
14 | @font-size-large: 24px;
15 | @font-size-big: 18px;
16 | @font-size-base: 12px;
17 | @text-color :#1a1a1a;
18 | @light-background-color: #f6f6f6;
19 | @ok-color: #2FD000;
20 | @active-color: #2196f3;
21 | @info-bkg-color: #ecf6fd;
22 |
23 | @success-color: #87d068;
24 | @info-color: #2db7f5;
25 |
26 | @opacity-background-color: rgba(102, 102, 102, 0.37);
27 |
28 | @font-size-reg: 14px;
29 | @font-size-sm: 13px;
30 | @font-size-xs: 12px;
31 | @border-radius-base: 4px;
32 | @border-radius-sm: 4px;
33 | @link-color: #108ee9;
34 | @notice-success-bacolor: #e5f5ff;
35 | @notice-success-borcolor: #cbd7e3;
36 |
37 | @form-input-height: 32px;
38 |
39 |
40 | @left-menu-background-color: #2E303C;
41 | @left-menu-color: rgba(255, 255, 255, 0.5);
42 | @left-menu-font-size: 16px;
43 | @middlepanel-font-size: 48px;
44 | @top-menu-span-color: #A9A9A9;
45 | @top-menu-spliter-color: #DDD;
46 |
47 | @menu-icon-font-size: 19px;
48 |
49 | @logo-font-size: 29.48px;
--------------------------------------------------------------------------------
/web/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const autoprefixer = require('autoprefixer');
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
5 |
6 | const UglifyJsPlugin = webpack.optimize.UglifyJsPlugin;
7 |
8 | const extractCss = new ExtractTextPlugin('[name].css', {
9 | disable: false,
10 | allChunks: true
11 | });
12 |
13 | // a plugin to set the environment
14 | const defineProperty = new webpack.DefinePlugin({
15 | 'process.env': {
16 | NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'test')
17 | }
18 | });
19 |
20 | module.exports = {
21 | entry: ['whatwg-fetch', 'babel-polyfill', path.join(__dirname, './src/index.jsx')],
22 | output: {
23 | path: path.join(__dirname, 'dist'),
24 | filename: 'main.js'
25 | },
26 | resolve: {
27 | modules: [
28 | 'node_modules',
29 | path.join(__dirname, 'src')
30 | ],
31 | extensions: ['.', '.js', '.jsx']
32 | },
33 | module: {
34 | rules: [{
35 | test: /\.js$/,
36 | exclude: /node_modules/,
37 | loader: 'babel-loader',
38 | options: {
39 | presets: ['es2015', 'stage-0']
40 | }
41 | },
42 | {
43 | test: /\.jsx$/,
44 | exclude: /node_modules/,
45 | use: {
46 | loader: 'babel-loader',
47 | options: {
48 | presets: ['es2015', 'stage-0', 'react'],
49 | plugins: [['import', { libraryName: 'antd', style: true }]]
50 | }
51 | }
52 | },
53 | {
54 | test: function (filePath) {
55 | return (/antd\/.*\.less$/.test(filePath) || /\.global\.less$/.test(filePath));
56 | },
57 | use: ExtractTextPlugin.extract({use: 'css-loader!postcss-loader!less-loader'})
58 | },
59 | {
60 | test: function (filePath) {
61 | return (/\.less$/.test(filePath) && !/\.global\.less$/.test(filePath) && !/antd\/.*\.less$/.test(filePath));
62 | },
63 | use: ExtractTextPlugin.extract({use: 'css-loader?modules&localIdentName=[local]___[hash:base64:5]!postcss-loader!less-loader'})
64 | },
65 | {
66 | test: /\.css$/,
67 | use: ExtractTextPlugin.extract({use:'css-loader'})
68 | },
69 | {
70 | test: /\.png(\?v=\d+\.\d+\.\d+)?$/,
71 | use: {
72 | loader: 'url-loader?limit=10000&mimetype=image/png'
73 | }
74 | },
75 | {
76 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
77 | use: {
78 | loader: 'url-loader?limit=10000&mimetype=application/font-woff'
79 | }
80 | },
81 | {
82 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
83 | use: {
84 | loader: 'url-loader?limit=10000&mimetype=application/font-woff'
85 | }
86 | },
87 | {
88 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
89 | use: {
90 | loader: 'url-loader?limit=10000&mimetype=application/octet-stream'
91 | }
92 | },
93 | {
94 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
95 | use: {
96 | loader: 'url-loader?limit=10000&mimetype=application/octet-stream'
97 | }
98 | },
99 | {
100 | test: /font\.svg(\?v=\d+\.\d+\.\d+)?$/,
101 | use: {
102 | loader: 'url-loader?limit=10000&mimetype=image/svg+xml'
103 | }
104 | }]
105 | },
106 | plugins: [
107 | extractCss,
108 | defineProperty,
109 | new UglifyJsPlugin()
110 | ],
111 | stats: {
112 | children: false
113 | }
114 | };
115 |
--------------------------------------------------------------------------------