├── .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 | [![Build Status](https://travis-ci.org/alibaba/anyproxy.svg?branch=master)](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 | ![](https://gw.alipayobjects.com/zos/rmsportal/gUfcjGxLONndTfllxynC.jpg@_90q) 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 | 28 | {% endif %} 29 |
  • 30 | {% endfor %} 31 | {% endmacro %} 32 | 33 | 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 | 28 | {% endif %} 29 |
  • 30 | {% endfor %} 31 | {% endmacro %} 32 | 33 | 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 | 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 | 3 | 4 | clear 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /web/src/assets/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/assets/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | filter 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/assets/https.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/anyproxy/b93f948107b956e07c7b68faeff0c777a1f50486/web/src/assets/https.png -------------------------------------------------------------------------------- /web/src/assets/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | play 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/assets/retweet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /web/src/assets/start.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | start 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/assets/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | stop 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/assets/tip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tip-shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/assets/touchmeter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | touchmeta-shape 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /web/src/assets/view-eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | eye-slash 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /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 | 62 | Source 63 | Preview 64 | 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 |
    89 | 95 | 96 | 97 | 98 | Filter 99 | 100 | 101 | 107 | 108 | 109 | 110 | RootCA 111 | 112 |
    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 | 98 | Request 99 | Response 100 | {this.hasWebSocket(recordDetail) ? websocketMenu : null} 101 | 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 |
    69 | 75 |
    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 =
    {recordDetail.resBody} 
    ; 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 |
    86 | 93 | 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 | --------------------------------------------------------------------------------