├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── client ├── .babelrc ├── dist │ ├── build.js │ ├── icon.png │ ├── sidenav-mock.png │ ├── sidenav-net.png │ └── sidenav-rule.png ├── index.html ├── loading.html ├── package-lock.json ├── package.json ├── renderer.js ├── src │ ├── App.vue │ ├── assets │ │ ├── icon.png │ │ ├── sidenav-mock.png │ │ ├── sidenav-net.png │ │ └── sidenav-rule.png │ ├── components │ │ ├── TreeView.vue │ │ ├── TreeViewItem.vue │ │ ├── filter.vue │ │ ├── menu.vue │ │ ├── mock.vue │ │ ├── network-detail.vue │ │ ├── network.vue │ │ ├── request.vue │ │ └── rule.vue │ ├── lang │ │ ├── en.js │ │ ├── index.js │ │ └── zh-CN.js │ ├── lib │ │ └── util.js │ ├── main.js │ ├── router │ │ └── index.js │ └── store │ │ ├── index.js │ │ └── mutation-types.js └── webpack.config.js ├── icon.icns ├── icon.png ├── icon1.png ├── img ├── 1.PNG ├── 2.PNG └── 3.PNG ├── index.html ├── lib ├── certMgr.js ├── httpsServerMgr.js ├── log.js ├── recorder.js ├── requestHandler.js ├── ruleLoader.js ├── rule_default.js ├── systemProxyMgr.js ├── util.js ├── webInterface.js └── wsServer.js ├── main-api.js ├── main.js ├── menu.js ├── package-lock.json ├── package.json ├── proxy.js ├── resource ├── 502.pug └── rule_default_backup.js ├── 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_unauthorized_access_vulnerability.js └── sample_use_local_response.js └── setting.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | client/node_modules 3 | .vscode 4 | pack 5 | rule_custom 6 | mock_custom 7 | rules.json 8 | mock-project.json 9 | npm-debug.log 10 | yarn.lock 11 | .idea 12 | package-lock.json 13 | pack -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Feng Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PTEye(coding) # 2 | 3 | PTeye(Phantom eye) 是一个代理黑盒漏洞审计工具,使用 NodeJS 结合开源框架(整体框架根据 fwon 的 electron-anyproxy 项目魔改而成 [https://github.com/fwon/electron-anyproxy](https://github.com/fwon/electron-anyproxy "https://github.com/fwon/electron-anyproxy") )完成,主要用于插件式的漏洞审计。 4 | 5 | PTeye 初步设计为使用被动代理+插件的方式重点对相关漏洞进行半自动化/被动化的审计。 6 | 7 | PTeye仅供交流学习使用,请勿用于非法行为。 8 | 9 | 10 | 11 | 12 | ## Features ## 13 | 14 | 1. 沿用原项目的网络抓包以及数据拦截修改功能。 15 | 2. 完成了简单的报文重放功能。 16 | 3. 根据网络抓包可进行重放报文选择。 17 | 4. 基于 AnyProxy Rule 漏洞模块拦截规则编写,Rule 规则编写可以参考已有的插件,详细规则可参考 [http://anyproxy.io/](http://anyproxy.io/ "http://anyproxy.io/")。 18 | 19 | ``` 20 | module.exports = { 21 | // 模块介绍 22 | summary: 'my customized rule for AnyProxy', 23 | // 发送请求前拦截处理 24 | *beforeSendRequest(requestDetail) { /* ... */ }, 25 | // 发送响应前处理 26 | *beforeSendResponse(requestDetail, responseDetail) { /* ... */ }, 27 | // 是否处理https请求 28 | *beforeDealHttpsRequest(requestDetail) { /* ... */ }, 29 | // 请求出错的事件 30 | *onError(requestDetail, error) { /* ... */ }, 31 | // https连接服务器出错 32 | *onConnectError(requestDetail, error) { /* ... */ } 33 | }; 34 | ``` 35 | 36 | 37 | ## Usage ## 38 | 39 | 可参考原项目相关介绍 40 | 41 | ### 开发模式 ### 42 | 43 | - 下载源代码 44 | - 在 client 目录中安装相关模块,启动 element-ui 前端 45 | 46 | ``` 47 | npm install (or yarn) 48 | npm run dev 49 | ``` 50 | 51 | - 在根目录下同时安装相关模块,启动 electron 环境,设置相关环境变量(main.js 中第 29 行)。 52 | 53 | ``` 54 | npm install (or yarn) 55 | npm run start 56 | ``` 57 | 58 | - 开发完成后,对前端代码进行编译,对后端代码进行打包 59 | 60 | ``` 61 | client 目录下: 62 | npm run build 63 | 根目录下: 64 | npm run pack 65 | ``` 66 | 67 | ### 直接使用 ### 68 | 69 | 可下载已经打好的包(建议自己打包,我也不记得是不是在打好的包里放了什么不该放的东西: 70 | 71 | [https://github.com/phantom0301/PTEye/releases](https://github.com/phantom0301/PTEye/releases "https://github.com/phantom0301/PTEye/releases") 72 | 73 | 1. 在主界面右侧工具栏可以配置代理基本信息,下载 https 证书。 74 | 2. 配置完成后即可启动监听,由于实现机制,暂时没有实现 burp 里的 proxy intercept 功能,只能在抓包列表栏观察所有的报文。 75 | 3. 逐行点击相关的报文可以弹出报文详细信息。 76 | 4. 点击重放按钮可以将相应的报文请求包发送到请求重放功能框中,实现类似的 repeater 功能。 77 | 5. 在请求重放功能中,左侧填写任意请求头和请求体信息,右上侧填写发送地址,右侧输出响应返回包。 78 | 6. 漏洞检测插件一次只能加载一个模组,并且加载后需要重启代理(右上的基本配置栏可以有重启按钮,或者点击关闭代理后重新打开) 79 | 80 | 81 | 82 | ## 工具展示 ## 83 | 84 | ### 开启代理 ### 85 | 86 | ![](https://github.com/phantom0301/PTEye/blob/master/img/1.PNG) 87 | 88 | ### 加载漏洞插件 ### 89 | 90 | ![](https://github.com/phantom0301/PTEye/blob/master/img/2.PNG) 91 | 92 | ### 报文重放 ### 93 | 94 | ![](https://github.com/phantom0301/PTEye/blob/master/img/3.PNG) 95 | 96 | 97 | ## Update1.0 ## 98 | 99 | 1. 基本框架完成,部分功能还需优化(intercept 功能,多插件规则合并生效功能) 100 | 101 | 102 | ### Other ### 103 | 104 | Issues submit 105 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015"] 4 | ] 5 | } -------------------------------------------------------------------------------- /client/dist/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/client/dist/icon.png -------------------------------------------------------------------------------- /client/dist/sidenav-mock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/client/dist/sidenav-mock.png -------------------------------------------------------------------------------- /client/dist/sidenav-net.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/client/dist/sidenav-net.png -------------------------------------------------------------------------------- /client/dist/sidenav-rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/client/dist/sidenav-rule.png -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PTEye 6 | 18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 48 | 49 | 50 |
51 | 52 |
53 |

Anyproxy

54 |

proxy and more...

55 |
56 |
57 | 58 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "element-starter", 3 | "scripts": { 4 | "dev": "cross-env NODE_ENV=development webpack-dev-server --inline --hot --port 8099", 5 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" 6 | }, 7 | "dependencies": { 8 | "build": "^0.1.4", 9 | "element-ui": "^1.0.0", 10 | "highlight.js": "^9.10.0", 11 | "lodash": "^4.17.4", 12 | "moment": "^2.17.1", 13 | "vue": "^2.1.6", 14 | "vue-codemirror": "^2.1.8", 15 | "vue-i18n": "^5.0.3", 16 | "vue-router": "^2.3.1", 17 | "vue-template-compiler": "^2.2.6", 18 | "vuex": "^2.2.1" 19 | }, 20 | "devDependencies": { 21 | "babel-core": "^6.0.0", 22 | "babel-loader": "^6.0.0", 23 | "babel-preset-es2015": "^6.13.2", 24 | "babel-preset-stage-3": "^6.22.0", 25 | "cross-env": "^1.0.8", 26 | "css-loader": "^0.23.1", 27 | "extract-text-webpack-plugin": "2.0.0", 28 | "file-loader": "^0.8.5", 29 | "less": "^2.7.2", 30 | "less-loader": "^4.0.2", 31 | "style-loader": "^0.13.1", 32 | "url-loader": "^0.5.8", 33 | "vue-loader": "^10.2.3", 34 | "webpack": "3.3.0", 35 | "webpack-cli": "^3.3.0", 36 | "webpack-dev-server": "2.3.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /client/renderer.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | const electron = require('electron'); 3 | const ipcRenderer = electron.ipcRenderer; 4 | const remote = electron.remote; 5 | const remoteApi = remote.require('./main-api.js'); 6 | const setting = remote.require('./setting.json'); 7 | 8 | //only explose these variable 9 | global.remoteApi = remoteApi; 10 | global.ipcRenderer = ipcRenderer; 11 | global.setting = setting; 12 | })(); 13 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 48 | 49 | -------------------------------------------------------------------------------- /client/src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/client/src/assets/icon.png -------------------------------------------------------------------------------- /client/src/assets/sidenav-mock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/client/src/assets/sidenav-mock.png -------------------------------------------------------------------------------- /client/src/assets/sidenav-net.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/client/src/assets/sidenav-net.png -------------------------------------------------------------------------------- /client/src/assets/sidenav-rule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/client/src/assets/sidenav-rule.png -------------------------------------------------------------------------------- /client/src/components/TreeView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 101 | 102 | 119 | -------------------------------------------------------------------------------- /client/src/components/TreeViewItem.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 101 | 102 | 160 | -------------------------------------------------------------------------------- /client/src/components/filter.vue: -------------------------------------------------------------------------------- 1 | 11 | 38 | -------------------------------------------------------------------------------- /client/src/components/menu.vue: -------------------------------------------------------------------------------- 1 | 81 | 262 | -------------------------------------------------------------------------------- /client/src/components/mock.vue: -------------------------------------------------------------------------------- 1 | 90 | 264 | -------------------------------------------------------------------------------- /client/src/components/network-detail.vue: -------------------------------------------------------------------------------- 1 | 47 | 133 | -------------------------------------------------------------------------------- /client/src/components/network.vue: -------------------------------------------------------------------------------- 1 | 71 | 155 | -------------------------------------------------------------------------------- /client/src/components/request.vue: -------------------------------------------------------------------------------- 1 | 19 | 221 | -------------------------------------------------------------------------------- /client/src/components/rule.vue: -------------------------------------------------------------------------------- 1 | 81 | 242 | -------------------------------------------------------------------------------- /client/src/lang/en.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ap: { 3 | bigtab: { 4 | a: 'HTTP history', 5 | b: 'Vul-Module', 6 | c: 'API Mock', 7 | d: 'Repeater', 8 | }, 9 | menubtn: { 10 | starttip: 'proxy On', 11 | stoptip: 'proxy Off', 12 | cleartip: 'clear records', 13 | showlist: 'show records', 14 | catip: 'download CA', 15 | cabtn: 'CA' 16 | }, 17 | menuset: { 18 | port: 'port', 19 | global: 'global', 20 | throttle: 'throttle(kb/s)', 21 | save: 'save', 22 | restart: 'restart' 23 | }, 24 | menumsg: { 25 | proxy: 'proxy message', 26 | notopen: 'proxy closed', 27 | tips: 'Tips' 28 | }, 29 | 30 | rulelist: { 31 | addbtn: 'Add Module', 32 | title1: 'status', 33 | title2: 'rule', 34 | title3: 'operating', 35 | editbtn: 'edit', 36 | delbtn: 'delete', 37 | cancelbtn: 'cancel', 38 | usebtn: 'apply', 39 | tip: 'Notice: must restart proxy after choose' 40 | }, 41 | rulepop: { 42 | title: 'Rule Editor', 43 | sample: 'Import Module', 44 | sample1: 'Modify Request Data', 45 | sample2: 'Modify Request Headers', 46 | sample3: 'Modify Request Destination', 47 | sample4: 'Modify Request Protocal', 48 | sample5: 'Modify Response Data', 49 | sample6: 'Modify Response Headers', 50 | sample7: 'Modify Response Code', 51 | sample8: 'Response local Data', 52 | sample9: 'Unauthorized Access Vulnerability', 53 | name: 'Name(custom)', 54 | content: 'Rule Code', 55 | cancelbtn: 'cancel', 56 | savebtn: 'save' 57 | }, 58 | 59 | mocklist: { 60 | addbtn: 'New Project', 61 | currentpro: 'Current Project:', 62 | currenttip: 'Notice: You need to restart proxy after switch project or api', 63 | delprotip: 'Confirm to delete this project?', 64 | addapibtn: 'New Api', 65 | title1: 'Url', 66 | title2: 'Operating', 67 | editbtn: 'edit', 68 | delbtn: 'delete', 69 | delapitip: 'Confirm to delete this api?' 70 | }, 71 | request: { 72 | addbtn: 'send', 73 | clear: 'clear', 74 | currentpro: 'Current Project:', 75 | currenttip: 'otice: You need to restart proxy after switch project or api', 76 | delprotip: 'Confirm to delete this project?', 77 | addapibtn: 'New Api', 78 | title1: 'Url', 79 | title2: 'Operating', 80 | editbtn: 'edit', 81 | delbtn: 'delete', 82 | delapitip: 'Confirm to delete this api?', 83 | savebtn: 'save', 84 | cancelbtn: 'cancel' 85 | }, 86 | mockpop: { 87 | title: 'API setting', 88 | cancelbtn: 'cancel', 89 | savebtn: 'save' 90 | }, 91 | 92 | message: { 93 | MSG_HAD_OPEN_PROXY: 'Proxy Opend!', 94 | MSG_OPEN_PROXY_SUCCESS: 'Started', 95 | MSG_OPEN_PROXY_ERROR: 'Failed', 96 | MSG_HASNOT_OPEN_PROXY: 'Proxy is disable!', 97 | MSG_CLOSE_PROXY_SUCCESS: 'Closed!', 98 | 99 | MSG_RULE_GET_FAIL: 'Get sample fail', 100 | MSG_RULE_FORMAT_FAIL: 'Please Fill Correctly', 101 | MSG_RULE_NAME_REPEAT: 'Cannot repeate same name', 102 | 103 | MSG_MOCK_PRO_EXIST: 'Project has exist', 104 | MSG_MOCK_NAME_EMPTY: 'Name cannot be empty', 105 | MSG_MOCK_CONFIRM_DEL: 'Confirm to delete?', 106 | MSG_MOCK_SAVE_SUCCESS: 'Save Success', 107 | MSG_MOCK_DEL_SUCCESS: 'Delete Success', 108 | 109 | 110 | MSG_CA_DOWN_SUCCESS: 'CA download, dbclick to install', 111 | MSG_CA_EXIST: 'CA had existed', 112 | MSG_CA_DOWN_FAIL: 'CA download failed' 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /client/src/lang/index.js: -------------------------------------------------------------------------------- 1 | //Local lang 2 | import en from './en.js'; 3 | import zh from './zh-CN.js'; 4 | 5 | //Element lang 6 | import Een from 'element-ui/lib/locale/lang/en' 7 | import Ezh from 'element-ui/lib/locale/lang/zh-CN' 8 | 9 | module.exports = { 10 | 'zh-CN': Object.assign(zh, Ezh), 11 | 'en': Object.assign(en, Een) 12 | } -------------------------------------------------------------------------------- /client/src/lang/zh-CN.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ap: { 3 | bigtab: { 4 | a: '抓包列表', 5 | b: '漏洞检测', 6 | d: '请求重放', 7 | c: '接口Mock' 8 | }, 9 | menubtn: { 10 | starttip: '启动代理服务器', 11 | stoptip: '关闭代理服务器', 12 | cleartip: '清空列表', 13 | showlist: '抓包列表', 14 | catip: '下载证书', 15 | cabtn: '证书' 16 | }, 17 | menuset: { 18 | port: '端口', 19 | global: '全局代理', 20 | throttle: '限速(kb/s)', 21 | save: '保存', 22 | restart: '重启' 23 | }, 24 | menumsg: { 25 | proxy: '代理地址', 26 | notopen: '未开启代理', 27 | tips: '提示' 28 | }, 29 | 30 | rulelist: { 31 | addbtn: '添加漏洞模块', 32 | title1: '状态', 33 | title2: '规则', 34 | title3: '操作', 35 | editbtn: '编辑', 36 | delbtn: '删除', 37 | cancelbtn: '取消', 38 | usebtn: '应用', 39 | tip: '注意:应用规则后要重新启动代理' 40 | }, 41 | rulepop: { 42 | title: '规则编辑', 43 | sample: '导入漏洞规则', 44 | sample1: '修改请求数据', 45 | sample2: '修改请求头', 46 | sample3: '修改请求目标地址', 47 | sample4: '修改请求协议', 48 | sample5: '修改返回内容并延迟', 49 | sample6: '修改返回头', 50 | sample7: '修改返回状态码', 51 | sample8: '使用本地数据', 52 | sample9: '越权漏洞测试', 53 | name: '名称(自定义)', 54 | content: '规则代码', 55 | cancelbtn: '取消', 56 | savebtn: '保存' 57 | }, 58 | 59 | mocklist: { 60 | addbtn: '添加项目', 61 | currentpro: '当前项目:', 62 | currenttip: '注意:切换项目或勾选接口后要重新启动代理', 63 | delprotip: '确定要删除该项目吗?', 64 | addapibtn: '添加接口', 65 | title1: '接口', 66 | title2: '操作', 67 | editbtn: '编辑', 68 | delbtn: '删除', 69 | delapitip: '确定要删除这个接口吗?' 70 | }, 71 | request: { 72 | addbtn: '发送', 73 | clear: '清空输入', 74 | currentpro: '当前项目:', 75 | currenttip: '注意:切换项目或勾选接口后要重新启动代理', 76 | delprotip: '确定要删除该项目吗?', 77 | addapibtn: '添加接口', 78 | title1: '接口', 79 | title2: '操作', 80 | editbtn: '编辑', 81 | delbtn: '删除', 82 | delapitip: '确定要删除这个接口吗?', 83 | savebtn: '选择', 84 | cancelbtn: '取消' 85 | }, 86 | mockpop: { 87 | title: '接口设置', 88 | cancelbtn: '取消', 89 | savebtn: '保存' 90 | }, 91 | 92 | message: { 93 | MSG_HAD_OPEN_PROXY: '代理开启成功!', 94 | MSG_OPEN_PROXY_SUCCESS: '代理已开启', 95 | MSG_OPEN_PROXY_ERROR: '开启失败', 96 | MSG_HASNOT_OPEN_PROXY: '代理未开启!', 97 | MSG_CLOSE_PROXY_SUCCESS: '关闭成功', 98 | 99 | MSG_RULE_GET_FAIL: '样例获取失败', 100 | MSG_RULE_FORMAT_FAIL: '请正确填写', 101 | MSG_RULE_NAME_REPEAT: '规则名已存在', 102 | 103 | MSG_MOCK_PRO_EXIST: '项目已存在', 104 | MSG_MOCK_NAME_EMPTY: '请选择所需重放的报文', 105 | MSG_MOCK_CONFIRM_DEL: '确定要删除吗?', 106 | MSG_MOCK_SAVE_SUCCESS: '保存成功', 107 | MSG_MOCK_DEL_SUCCESS: '删除成功', 108 | 109 | 110 | MSG_CA_DOWN_SUCCESS: '证书下载成功,请双击安装', 111 | MSG_CA_EXIST: '证书已经存在', 112 | MSG_CA_DOWN_FAIL: '证书下载失败' 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /client/src/lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | generateUUIDv4() { 5 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 6 | var r = Math.random() * 16|0, v = c == 'x' ? r : (r&0x3|0x8); 7 | return v.toString(16); 8 | }); 9 | } 10 | } -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import ElementUI from 'element-ui' 3 | import App from './App.vue' 4 | import store from './store' 5 | import router from './router' 6 | import * as types from './store/mutation-types' 7 | import _ from 'lodash' 8 | import 'element-ui/lib/theme-default/index.css' 9 | import VueI18n from 'vue-i18n' 10 | import locales from './lang' 11 | 12 | 13 | //全局插件 14 | Vue.use(VueI18n); 15 | Vue.use(ElementUI); 16 | Vue.use({ 17 | install (Vue, options) { 18 | //添加实例方法 19 | Vue.prototype.$ipc = global.ipcRenderer || {}; 20 | Vue.prototype.$remoteApi = global.remoteApi; 21 | } 22 | }); 23 | 24 | //language setting 25 | console.log(global.setting) 26 | Vue.config.lang = global.setting.lang || 'en'; 27 | 28 | Object.keys(locales).forEach((lang) => { 29 | Vue.locale(lang, locales[lang]); 30 | }); 31 | 32 | new Vue({ 33 | el: '#app', 34 | store, 35 | router, 36 | render: h => h(App) 37 | }) -------------------------------------------------------------------------------- /client/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import proxyNetwork from '../components/network.vue' 4 | import proxyRule from '../components/rule.vue' 5 | import proxyMock from '../components/mock.vue' 6 | import proxyRequest from '../components/request.vue' 7 | 8 | Vue.use(Router); 9 | 10 | const routes = [ 11 | {path: '/', component: proxyNetwork}, 12 | {path: '/network', component: proxyNetwork}, 13 | {path: '/rule', component: proxyRule}, 14 | {path: '/mock', component: proxyMock}, 15 | {path: '/request', component: proxyRequest,name:"request"} 16 | ]; 17 | 18 | export default new Router({ 19 | routes 20 | }); -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import * as types from './mutation-types' 4 | import _ from 'lodash'; 5 | 6 | Vue.use(Vuex) 7 | 8 | const debug = process.env.NODE_ENV !== 'production' 9 | 10 | export default new Vuex.Store({ 11 | state: { 12 | ipc: 'abc', 13 | proxy_is_open: false, 14 | recorder_len: 0, 15 | recorder: [], //存储用于显示列表的请求头 16 | // recorder_detail: [], //存储请求头详情 17 | recorder_filter: '', 18 | proxy_ip: '', 19 | proxy_port: '', 20 | proxy_rules: [], //代理配置规则 21 | current_rule: {}, //当前运用规则 22 | mock_paths: [] //mock接口 23 | }, 24 | mutations: { 25 | [types.INCREMENT] (state) { 26 | console.log(state.ipc); 27 | }, 28 | [types.TOGGLE_PROXY] (state) { 29 | state.proxy_is_open = !state.proxy_is_open; 30 | }, 31 | [types.UPDATE_RECORDER] (state, newrecorder) { 32 | //渲染列表不存储所有值,提高渲染速度 33 | let filterRecorder = newrecorder.slice( 34 | state.recorder_len, 35 | newrecorder.length 36 | ).map((item) => { 37 | return _.pick(item, ['id', 'method', 'statusCode', 'host', 'path', 'mine', 'startTime']) 38 | }) 39 | state.recorder = state.recorder.concat(filterRecorder); 40 | // state.recorder_detail = newrecorder.slice( 41 | // newrecorder.length - state.recorder.length, 42 | // newrecorder.length 43 | // ); 44 | console.log('len:' + state.recorder_len); 45 | console.log('newlen:' + newrecorder.length); 46 | state.recorder_len += (newrecorder.length - state.recorder_len); 47 | }, 48 | [types.CLEAR_RECORDER] (state) { 49 | console.log('clear') 50 | state.recorder = []; 51 | }, 52 | [types.CHANGE_RECORDER_FILTER] (state, filter) { 53 | state.recorder_filter = filter; 54 | }, 55 | [types.CLEAR_RECORDER_FILTER] (state) { 56 | state.recorder_filter = ''; 57 | }, 58 | [types.SET_PROXY_IP] (state, ip) { 59 | state.proxy_ip = ip; 60 | }, 61 | [types.SET_PROXY_PORT] (state, port) { 62 | state.proxy_port = port; 63 | }, 64 | [types.INIT_PROXY_RULE] (state, rules) { 65 | state.proxy_rules = rules; 66 | }, 67 | [types.STORE_PROXY_RULE] (state, rule) { 68 | console.log('store') 69 | state.proxy_rules.push(rule); 70 | }, 71 | [types.MODIFY_PROXY_RULE] (state, payload) { 72 | console.log('modify') 73 | state.proxy_rules.splice(payload.index, 1); 74 | state.proxy_rules.push(payload.rule); 75 | }, 76 | [types.DELETE_RULE] (state, id) { 77 | state.proxy_rules = state.proxy_rules.filter((item) => { 78 | return item.id !== id; 79 | }); 80 | }, 81 | [types.TOGGLE_CURRENT_RULE] (state, rule) { 82 | state.current_rule = rule; 83 | }, 84 | [types.SET_SELECTED_MOCKPATH] (state, paths) { 85 | state.mock_paths = paths; 86 | } 87 | }, 88 | actions: { 89 | [types.FETCH_BODY] (context) { 90 | } 91 | }, 92 | getters: { 93 | filterTableDate: state => { 94 | console.log('filter') 95 | if (state.recorder_filter) { 96 | return state.recorder.filter((item) => { 97 | return (item.host + item.path).match(state.recorder_filter); 98 | }); 99 | } else { 100 | return state.recorder; 101 | } 102 | } 103 | }, 104 | modules: {}, 105 | strict: debug 106 | }) -------------------------------------------------------------------------------- /client/src/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mutations 3 | */ 4 | export const INCREMENT = 'INCREMENT';//default 5 | export const TOGGLE_PROXY = 'TOGGLE_PROXY';//开关代理 6 | 7 | export const UPDATE_RECORDER = 'UPDATE_RECORDER';//更新列表数据 8 | export const CLEAR_RECORDER = 'CLEAR_RECORDER';//清除请求列表 9 | 10 | export const CHANGE_RECORDER_FILTER = 'CHANGE_RECORDER_FILTER';//过滤器修改 11 | export const CLEAR_RECORDER_FILTER = 'CLEAR_RECORDER_FILTER';//清除过滤器 12 | 13 | export const SET_PROXY_IP = 'SET_PROXY_IP';//存储代理IP 14 | export const SET_PROXY_PORT = 'SET_PROXY_PORT';//存储代理端口 15 | 16 | export const INIT_PROXY_RULE = 'INIT_PROXY_RULE';//从本地初始化规则 17 | export const STORE_PROXY_RULE = 'STORE_PROXY_RULE';//添加代理规则 18 | export const MODIFY_PROXY_RULE = 'MODIFY_PROXY_RULE';//添加代理规则 19 | export const DELETE_RULE = 'DELETE_RULE'//删除规则 20 | export const TOGGLE_CURRENT_RULE = 'TOGGLE_CURRENT_RULE'; //切换当前运用规则 21 | 22 | export const SET_SELECTED_MOCKPATH = 'SET_SELECTED_MOCKPATH';//设置mock接口 23 | 24 | /** 25 | * Actions 26 | */ 27 | export const FETCH_BODY = 'FETCH_BODY'//获取请求内容 -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | var isPro = process.env.NODE_ENV === 'production'; 5 | 6 | module.exports = { 7 | entry: './src/main.js', 8 | output: { 9 | path: path.resolve(__dirname, './dist'), //真实存放路径 10 | publicPath: isPro ? 'D:/projects/github/PTEye/client/dist':'/dist/' ,//发布引用路径 11 | // , //开发引用路径 12 | filename: 'build.js', 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.vue$/, 18 | loader: 'vue-loader', 19 | options: { 20 | loaders: { 21 | css: 'style-loader!css-loader!less-loader' 22 | // ExtractTextPlugin.extract({ 23 | // use: ['css-loader', 'less-loader'], 24 | // fallback: 'vue-style-loader' 25 | // }) 26 | } 27 | } 28 | }, 29 | { 30 | test: /\.js$/, 31 | loader: 'babel-loader', 32 | exclude: /node_modules/ 33 | }, 34 | { 35 | test: /\.css$/, 36 | loader: 'style-loader!css-loader' 37 | // use: ExtractTextPlugin.extract({ 38 | // fallback: 'style-loader', 39 | // //resolve-url-loader may be chained before sass-loader if necessary 40 | // use: ['css-loader', 'less-loader'] 41 | // }) 42 | }, 43 | { 44 | test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/, 45 | loader: 'url-loader', 46 | query: { 47 | name: '[name].[ext]' 48 | } 49 | }, 50 | { 51 | test: /\.(png|jpe?g|gif|svg)(\?\S*)?$/, 52 | loader: 'file-loader', 53 | query: { 54 | name: '/[name].[ext]', 55 | } 56 | } 57 | ] 58 | }, 59 | // plugins: [ 60 | // new ExtractTextPlugin('style.css') 61 | // ], 62 | devServer: { 63 | historyApiFallback: true, 64 | noInfo: true 65 | }, 66 | devtool: '#eval-source-map' 67 | } 68 | 69 | if (process.env.NODE_ENV === 'production') { 70 | console.log(path.resolve(__dirname, './dist')); 71 | module.exports.devtool = '#source-map' 72 | // http://vue-loader.vuejs.org/en/workflow/production.html 73 | module.exports.plugins = (module.exports.plugins || []).concat([ 74 | new webpack.DefinePlugin({ 75 | 'process.env': { 76 | NODE_ENV: '"production"' 77 | } 78 | }), 79 | new webpack.optimize.UglifyJsPlugin({ 80 | compress: { 81 | warnings: false 82 | } 83 | }) 84 | 85 | ]) 86 | } -------------------------------------------------------------------------------- /icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/icon.icns -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/icon.png -------------------------------------------------------------------------------- /icon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/icon1.png -------------------------------------------------------------------------------- /img/1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/img/1.PNG -------------------------------------------------------------------------------- /img/2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/img/2.PNG -------------------------------------------------------------------------------- /img/3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phantom0301/PTEye/9d6b7987ad51ca14956855a2082914dd48847f0c/img/3.PNG -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World! 6 | 7 | 8 | 9 | 10 | 11 | 25 | 26 | 27 |

Hello World!

28 | We are using node , 29 | Chrome , 30 | and Electron . 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /lib/certMgr.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const util = require('./util'); 4 | const EasyCert = require('node-easy-cert'); 5 | const co = require('co'); 6 | 7 | const options = { 8 | rootDirPath: util.getAnyProxyPath('certificates'), 9 | defaultCertAttrs: [ 10 | { name: 'countryName', value: 'CN' }, 11 | { name: 'organizationName', value: 'AnyProxy' }, 12 | { shortName: 'ST', value: 'SH' }, 13 | { shortName: 'OU', value: 'AnyProxy SSL Proxy' } 14 | ] 15 | }; 16 | 17 | const easyCert = new EasyCert(options); 18 | const crtMgr = util.merge({}, easyCert); 19 | 20 | // rename function 21 | crtMgr.ifRootCAFileExists = easyCert.isRootCAFileExists; 22 | 23 | crtMgr.generateRootCA = function (cb) { 24 | doGenerate(false); 25 | 26 | // set default common name of the cert 27 | function doGenerate(overwrite) { 28 | const rootOptions = { 29 | commonName: 'AnyProxy', 30 | overwrite: !!overwrite 31 | }; 32 | 33 | easyCert.generateRootCA(rootOptions, (error, keyPath, crtPath) => { 34 | cb(error, keyPath, crtPath); 35 | }); 36 | } 37 | }; 38 | 39 | crtMgr.getCAStatus = function *() { 40 | return co(function *() { 41 | const result = { 42 | exist: false, 43 | }; 44 | const ifExist = easyCert.isRootCAFileExists(); 45 | if (!ifExist) { 46 | return result; 47 | } else { 48 | result.exist = true; 49 | if (!/^win/.test(process.platform)) { 50 | result.trusted = yield easyCert.ifRootCATrusted; 51 | } 52 | return result; 53 | } 54 | }); 55 | } 56 | 57 | module.exports = crtMgr; 58 | -------------------------------------------------------------------------------- /lib/httpsServerMgr.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | //manage https servers 4 | const async = require('async'), 5 | https = require('https'), 6 | tls = require('tls'), 7 | crypto = require('crypto'), 8 | color = require('colorful'), 9 | certMgr = require('./certMgr'), 10 | logUtil = require('./log'), 11 | util = require('./util'), 12 | co = require('co'), 13 | asyncTask = require('async-task-mgr'); 14 | 15 | const createSecureContext = tls.createSecureContext || crypto.createSecureContext; 16 | 17 | //using sni to avoid multiple ports 18 | function SNIPrepareCert(serverName, SNICallback) { 19 | let keyContent, 20 | crtContent, 21 | ctx; 22 | 23 | async.series([ 24 | (callback) => { 25 | certMgr.getCertificate(serverName, (err, key, crt) => { 26 | if (err) { 27 | callback(err); 28 | } else { 29 | keyContent = key; 30 | crtContent = crt; 31 | callback(); 32 | } 33 | }); 34 | }, 35 | (callback) => { 36 | try { 37 | ctx = createSecureContext({ 38 | key: keyContent, 39 | cert: crtContent 40 | }); 41 | callback(); 42 | } catch (e) { 43 | callback(e); 44 | } 45 | } 46 | ], (err) => { 47 | if (!err) { 48 | const tipText = 'proxy server for __NAME established'.replace('__NAME', serverName); 49 | logUtil.printLog(color.yellow(color.bold('[internal https]')) + color.yellow(tipText)); 50 | SNICallback(null, ctx); 51 | } else { 52 | logUtil.printLog('err occurred when prepare certs for SNI - ' + err, logUtil.T_ERR); 53 | logUtil.printLog('err occurred when prepare certs for SNI - ' + err.stack, logUtil.T_ERR); 54 | logUtil.printLog('you may upgrade your Node.js to >= v0.12', logUtil.T_ERR); 55 | } 56 | }); 57 | } 58 | 59 | //config.port - port to start https server 60 | //config.handler - request handler 61 | 62 | 63 | /** 64 | * Create an https server 65 | * 66 | * @param {object} config 67 | * @param {number} config.port 68 | * @param {function} config.handler 69 | */ 70 | function createHttpsServer(config) { 71 | if (!config || !config.port || !config.handler) { 72 | throw (new Error('please assign a port')); 73 | } 74 | 75 | return new Promise((resolve) => { 76 | certMgr.getCertificate('anyproxy_internal_https_server', (err, keyContent, crtContent) => { 77 | const server = https.createServer({ 78 | SNICallback: SNIPrepareCert, 79 | key: keyContent, 80 | cert: crtContent 81 | }, config.handler).listen(config.port); 82 | 83 | resolve(server); 84 | }); 85 | }); 86 | } 87 | 88 | /** 89 | * 90 | * 91 | * @class httpsServerMgr 92 | * @param {object} config 93 | * @param {function} config.handler handler to deal https request 94 | * 95 | */ 96 | class httpsServerMgr { 97 | constructor(config) { 98 | if (!config || !config.handler) { 99 | throw new Error('handler is required'); 100 | } 101 | this.instanceHost = '127.0.0.1'; 102 | this.httpsAsyncTask = new asyncTask(); 103 | this.handler = config.handler; 104 | } 105 | 106 | getSharedHttpsServer() { 107 | const self = this; 108 | function prepareServer(callback) { 109 | let instancePort; 110 | co(util.getFreePort) 111 | .then(co.wrap(function* (port) { 112 | instancePort = port; 113 | yield createHttpsServer({ 114 | port, 115 | handler: self.handler 116 | }); 117 | const result = { 118 | host: self.instanceHost, 119 | port: instancePort, 120 | }; 121 | callback(null, result); 122 | return result; 123 | })) 124 | .catch(e => { 125 | callback(e); 126 | }); 127 | } 128 | 129 | return new Promise((resolve, reject) => { 130 | self.httpsAsyncTask.addTask('createHttpsServer', prepareServer, (error, serverInfo) => { 131 | if (error) { 132 | reject(error); 133 | } else { 134 | resolve(serverInfo); 135 | } 136 | }); 137 | }); 138 | } 139 | } 140 | 141 | module.exports = httpsServerMgr; 142 | -------------------------------------------------------------------------------- /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 | }; 13 | 14 | function setPrintStatus(status) { 15 | ifPrint = !!status; 16 | } 17 | 18 | function setLogLevel(level) { 19 | logLevel = parseInt(level, 10); 20 | } 21 | 22 | function printLog(content, type) { 23 | if (!ifPrint) { 24 | return; 25 | } 26 | const timeString = util.formatDate(new Date(), 'YYYY-MM-DD hh:mm:ss'); 27 | switch (type) { 28 | case LogLevelMap.tip: { 29 | if (logLevel > 0) { 30 | return; 31 | } 32 | console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content)); 33 | break; 34 | } 35 | 36 | case LogLevelMap.system_error: { 37 | if (logLevel > 1) { 38 | return; 39 | } 40 | console.error(color.red(`[AnyProxy ERROR][${timeString}]: ` + content)); 41 | break; 42 | } 43 | 44 | case LogLevelMap.rule_error: { 45 | if (logLevel > 2) { 46 | return; 47 | } 48 | 49 | console.error(color.red(`[AnyProxy RULE_ERROR] [${timeString}]: ` + content)); 50 | break; 51 | } 52 | 53 | default : { 54 | console.log(color.cyan(`[AnyProxy Log][${timeString}]: ` + content)); 55 | break; 56 | } 57 | } 58 | } 59 | 60 | module.exports.printLog = printLog; 61 | module.exports.setPrintStatus = setPrintStatus; 62 | module.exports.setLogLevel = setLogLevel; 63 | module.exports.T_TIP = LogLevelMap.tip; 64 | module.exports.T_ERR = LogLevelMap.system_error; 65 | module.exports.T_RULE_ERROR = LogLevelMap.rule_error; 66 | -------------------------------------------------------------------------------- /lib/recorder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | //start recording and share a list when required 4 | const Datastore = require('nedb'), 5 | util = require('util'), 6 | path = require('path'), 7 | fs = require('fs'), 8 | events = require('events'), 9 | iconv = require('iconv-lite'), 10 | proxyUtil = require('./util'), 11 | logUtil = require('./log'); 12 | 13 | const CACHE_DIR_PREFIX = 'cache_r'; 14 | const DB_FILE_NAME = 'anyproxy_db'; 15 | function getCacheDir() { 16 | const rand = Math.floor(Math.random() * 1000000), 17 | cachePath = path.join(proxyUtil.getAnyProxyPath('cache'), './' + CACHE_DIR_PREFIX + rand); 18 | 19 | fs.mkdirSync(cachePath); 20 | return cachePath; 21 | } 22 | 23 | //option.filename 24 | function Recorder() { 25 | const self = this, 26 | cachePath = getCacheDir(); 27 | let globalId = 1; 28 | let db; 29 | 30 | try { 31 | const dbFilePath = path.join(cachePath, DB_FILE_NAME); 32 | fs.writeFileSync(dbFilePath, ''); 33 | 34 | db = new Datastore({ 35 | filename: dbFilePath, 36 | autoload: true 37 | }); 38 | db.persistence.setAutocompactionInterval(5001); 39 | } catch (e) { 40 | logUtil.printLog(e, logUtil.T_ERR); 41 | logUtil.printLog('Failed to load on-disk db file. Will use in-meomory db instead.', logUtil.T_ERR); 42 | db = new Datastore(); 43 | } 44 | 45 | self.recordBodyMap = []; // id - body 46 | 47 | self.emitUpdate = function (id, info) { 48 | if (info) { 49 | self.emit('update', info); 50 | } else { 51 | self.getSingleRecord(id, (err, doc) => { 52 | if (!err && !!doc && !!doc[0]) { 53 | self.emit('update', doc[0]); 54 | } 55 | }); 56 | } 57 | }; 58 | 59 | self.updateRecord = function (id, info) { 60 | if (id < 0) return; 61 | 62 | const finalInfo = normalizeInfo(id, info); 63 | 64 | db.update({ _id: id }, finalInfo); 65 | self.updateRecordBody(id, info); 66 | 67 | self.emitUpdate(id, finalInfo); 68 | }; 69 | 70 | self.updateExtInfo = function (id, extInfo) { 71 | db.update({ _id: id }, { $set: { ext: extInfo } }, {}, (err, nums) => { 72 | if (!err) { 73 | self.emitUpdate(id); 74 | } 75 | }); 76 | } 77 | 78 | self.appendRecord = function (info) { 79 | if (info.req.headers.anyproxy_web_req) { //request from web interface 80 | return -1; 81 | } 82 | 83 | const thisId = globalId++; 84 | const finalInfo = normalizeInfo(thisId, info); 85 | db.insert(finalInfo); 86 | self.updateRecordBody(thisId, info); 87 | 88 | self.emitUpdate(thisId, finalInfo); 89 | return thisId; 90 | }; 91 | 92 | //update recordBody if exits 93 | 94 | //TODO : trigger update callback 95 | const BODY_FILE_PRFIX = 'res_body_'; 96 | self.updateRecordBody = function (id, info) { 97 | if (id === -1) return; 98 | 99 | if (!id || !info.resBody) return; 100 | //add to body map 101 | //ignore image data 102 | const bodyFile = path.join(cachePath, BODY_FILE_PRFIX + id); 103 | fs.writeFile(bodyFile, info.resBody); 104 | }; 105 | 106 | self.getBody = function (id, cb) { 107 | if (id < 0) { 108 | cb && cb(''); 109 | } 110 | 111 | const bodyFile = path.join(cachePath, BODY_FILE_PRFIX + id); 112 | fs.access(bodyFile, fs.F_OK | fs.R_OK, (err) => { 113 | if (err) { 114 | cb && cb(err); 115 | } else { 116 | fs.readFile(bodyFile, cb); 117 | } 118 | }); 119 | }; 120 | 121 | self.getDecodedBody = function (id, cb) { 122 | const result = { 123 | type: 'unknown', 124 | mime: '', 125 | content: '' 126 | }; 127 | global.recorder.getSingleRecord(id, (err, doc) => { 128 | //check whether this record exists 129 | if (!doc || !doc[0]) { 130 | cb(new Error('failed to find record for this id')); 131 | return; 132 | } 133 | 134 | self.getBody(id, (error, bodyContent) => { 135 | if (error) { 136 | cb(error); 137 | } else if (!bodyContent) { 138 | cb(null, result); 139 | } else { 140 | const record = doc[0], 141 | resHeader = record.resHeader || {}; 142 | try { 143 | const headerStr = JSON.stringify(resHeader), 144 | charsetMatch = headerStr.match(/charset='?([a-zA-Z0-9-]+)'?/), 145 | contentType = resHeader && (resHeader['content-type'] || resHeader['Content-Type']); 146 | 147 | if (charsetMatch && charsetMatch.length) { 148 | const currentCharset = charsetMatch[1].toLowerCase(); 149 | if (currentCharset !== 'utf-8' && iconv.encodingExists(currentCharset)) { 150 | bodyContent = iconv.decode(bodyContent, currentCharset); 151 | } 152 | } 153 | 154 | if (contentType && /application\/json/i.test(contentType)) { 155 | result.type = 'json'; 156 | result.mime = contentType; 157 | result.content = bodyContent.toString(); 158 | } else if (contentType && /image/i.test(contentType)) { 159 | result.type = 'image'; 160 | result.mime = contentType; 161 | result.content = bodyContent; 162 | } else { 163 | result.type = contentType; 164 | result.content = bodyContent.toString(); 165 | } 166 | } catch (e) {} 167 | cb(null, result); 168 | } 169 | }); 170 | }); 171 | }; 172 | 173 | self.getSingleRecord = function (id, cb) { 174 | db.find({ _id: parseInt(id, 10) }, cb); 175 | }; 176 | 177 | self.getSummaryList = function (cb) { 178 | db.find({}, cb); 179 | }; 180 | 181 | self.getRecords = function (idStart, limit, cb) { 182 | limit = limit || 10; 183 | idStart = typeof idStart === 'number' ? idStart : (globalId - limit); 184 | db.find({ _id: { $gte: parseInt(idStart, 10) } }) 185 | .sort({ _id: 1 }) 186 | .limit(limit) 187 | .exec(cb); 188 | }; 189 | 190 | self.clear = function () { 191 | proxyUtil.deleteFolderContentsRecursive(cachePath, true); 192 | } 193 | 194 | self.db = db; 195 | } 196 | 197 | util.inherits(Recorder, events.EventEmitter); 198 | 199 | function normalizeInfo(id, info) { 200 | const singleRecord = {}; 201 | 202 | //general 203 | singleRecord._id = id; 204 | singleRecord.id = id; 205 | singleRecord.url = info.url; 206 | singleRecord.host = info.host; 207 | singleRecord.path = info.path; 208 | singleRecord.method = info.method; 209 | 210 | //req 211 | singleRecord.reqHeader = info.req.headers; 212 | singleRecord.startTime = info.startTime; 213 | singleRecord.reqBody = info.reqBody || ''; 214 | singleRecord.protocol = info.protocol || ''; 215 | 216 | //res 217 | if (info.endTime) { 218 | singleRecord.statusCode = info.statusCode; 219 | singleRecord.endTime = info.endTime; 220 | singleRecord.resHeader = info.resHeader; 221 | singleRecord.length = info.length; 222 | const contentType = info.resHeader['content-type'] || info.resHeader['Content-Type']; 223 | if (contentType) { 224 | singleRecord.mime = contentType.split(';')[0]; 225 | } else { 226 | singleRecord.mime = ''; 227 | } 228 | 229 | singleRecord.duration = info.endTime - info.startTime; 230 | } else { 231 | singleRecord.statusCode = ''; 232 | singleRecord.endTime = ''; 233 | singleRecord.resHeader = ''; 234 | singleRecord.length = ''; 235 | singleRecord.mime = ''; 236 | singleRecord.duration = ''; 237 | } 238 | 239 | return singleRecord; 240 | } 241 | 242 | module.exports = Recorder; 243 | -------------------------------------------------------------------------------- /lib/requestHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const http = require('http'), 4 | https = require('https'), 5 | net = require('net'), 6 | url = require('url'), 7 | zlib = require('zlib'), 8 | color = require('colorful'), 9 | Buffer = require('buffer').Buffer, 10 | util = require('./util'), 11 | Stream = require('stream'), 12 | logUtil = require('./log'), 13 | co = require('co'), 14 | pug = require('pug'), 15 | HttpsServerMgr = require('./httpsServerMgr'); 16 | 17 | // to fix issue with TLS cache, refer to: https://github.com/nodejs/node/issues/8368 18 | https.globalAgent.maxCachedSessions = 0; 19 | 20 | const error502PugFn = pug.compileFile(require('path').join(__dirname, '../resource/502.pug')); 21 | 22 | /** 23 | * fetch remote response 24 | * 25 | * @param {string} protocol 26 | * @param {object} options options of http.request 27 | * @param {buffer} reqData request body 28 | * @param {object} config 29 | * @param {boolean} config.dangerouslyIgnoreUnauthorized 30 | * @returns 31 | */ 32 | function fetchRemoteResponse(protocol, options, reqData, config) { 33 | reqData = reqData || ''; 34 | return new Promise((resolve, reject) => { 35 | delete options.headers['content-length']; // will reset the content-length after rule 36 | delete options.headers['Content-Length']; 37 | options.headers['Content-Length'] = reqData.length; //rewrite content length info 38 | 39 | if (config.dangerouslyIgnoreUnauthorized) { 40 | options.rejectUnauthorized = false; 41 | } 42 | //send request 43 | const proxyReq = (/https/i.test(protocol) ? https : http).request(options, (res) => { 44 | res.headers = util.getHeaderFromRawHeaders(res.rawHeaders); 45 | //deal response header 46 | const statusCode = res.statusCode; 47 | const resHeader = res.headers; 48 | 49 | // remove gzip related header, and ungzip the content 50 | // note there are other compression types like deflate 51 | const contentEncoding = resHeader['content-encoding'] || resHeader['Content-Encoding']; 52 | const ifServerGzipped = /gzip/i.test(contentEncoding); 53 | if (ifServerGzipped) { 54 | delete resHeader['content-encoding']; 55 | delete resHeader['Content-Encoding']; 56 | } 57 | delete resHeader['content-length']; 58 | delete resHeader['Content-Length']; 59 | 60 | //deal response data 61 | const resData = []; 62 | 63 | res.on('data', (chunk) => { 64 | resData.push(chunk); 65 | }); 66 | 67 | res.on('end', () => { 68 | new Promise((fulfill, rejectParsing) => { 69 | const serverResData = Buffer.concat(resData); 70 | if (ifServerGzipped) { 71 | zlib.gunzip(serverResData, (err, buff) => { 72 | if (err) { 73 | rejectParsing(err); 74 | } else { 75 | fulfill(buff); 76 | } 77 | }); 78 | } else { 79 | fulfill(serverResData); 80 | } 81 | }).then((serverResData) => { 82 | resolve({ 83 | statusCode, 84 | header: resHeader, 85 | body: serverResData, 86 | _res: res, 87 | }); 88 | }); 89 | }); 90 | res.on('error', (error) => { 91 | logUtil.printLog('error happend in response:' + error, logUtil.T_ERR); 92 | reject(error); 93 | }); 94 | }); 95 | 96 | proxyReq.on('error', reject); 97 | proxyReq.end(reqData); 98 | }); 99 | } 100 | 101 | /** 102 | * get a request handler for http/https server 103 | * 104 | * @param {RequestHandler} reqHandlerCtx 105 | * @param {object} userRule 106 | * @param {Recorder} recorder 107 | * @returns 108 | */ 109 | function getUserReqHandler(reqHandlerCtx, userRule, recorder) { 110 | return function (req, userRes) { 111 | /* 112 | note 113 | req.url is wired 114 | in http server: http://www.example.com/a/b/c 115 | in https server: /a/b/c 116 | */ 117 | 118 | const host = req.headers.host; 119 | const protocol = (!!req.connection.encrypted && !/^http:/.test(req.url)) ? 'https' : 'http'; 120 | const fullUrl = protocol === 'http' ? req.url : (protocol + '://' + host + req.url); 121 | 122 | const urlPattern = url.parse(fullUrl); 123 | const path = urlPattern.path; 124 | 125 | let resourceInfo = null; 126 | let resourceInfoId = -1; 127 | let reqData; 128 | let requestDetail; 129 | 130 | // refer to https://github.com/alibaba/anyproxy/issues/103 131 | // construct the original headers as the reqheaders 132 | req.headers = util.getHeaderFromRawHeaders(req.rawHeaders); 133 | 134 | logUtil.printLog(color.green(`received request to: ${req.method} ${host}${path}`)); 135 | 136 | /** 137 | * fetch complete req data 138 | */ 139 | const fetchReqData = () => new Promise((resolve) => { 140 | const postData = []; 141 | req.on('data', (chunk) => { 142 | postData.push(chunk); 143 | }); 144 | req.on('end', () => { 145 | reqData = Buffer.concat(postData); 146 | resolve(); 147 | }); 148 | }); 149 | 150 | 151 | /** 152 | * prepare detailed request info 153 | */ 154 | const prepareRequestDetail = () => { 155 | const options = { 156 | hostname: urlPattern.hostname || req.headers.host, 157 | port: urlPattern.port || req.port || (/https/.test(protocol) ? 443 : 80), 158 | path, 159 | method: req.method, 160 | headers: req.headers 161 | }; 162 | 163 | requestDetail = { 164 | requestOptions: options, 165 | protocol, 166 | url: fullUrl, 167 | requestData: reqData, 168 | _req: req 169 | }; 170 | 171 | return Promise.resolve(); 172 | }; 173 | 174 | /** 175 | * send response to client 176 | * 177 | * @param {object} finalResponseData 178 | * @param {number} finalResponseData.statusCode 179 | * @param {object} finalResponseData.header 180 | * @param {buffer|string} finalResponseData.body 181 | */ 182 | const sendFinalResponse = (finalResponseData) => { 183 | const responseInfo = finalResponseData.response; 184 | if (!responseInfo) { 185 | throw new Error('failed to get response info'); 186 | } else if (!responseInfo.statusCode) { 187 | throw new Error('failed to get response status code') 188 | } else if (!responseInfo.header) { 189 | throw new Error('filed to get response header'); 190 | } 191 | 192 | userRes.writeHead(responseInfo.statusCode, responseInfo.header); 193 | const responseBody = responseInfo.body || ''; 194 | 195 | if (global._throttle) { 196 | const thrStream = new Stream(); 197 | 198 | thrStream.pipe(global._throttle.throttle()).pipe(userRes); 199 | thrStream.emit('data', responseBody); 200 | thrStream.emit('end'); 201 | } else { 202 | userRes.end(responseBody); 203 | } 204 | 205 | return responseInfo; 206 | } 207 | 208 | // fetch complete request data 209 | co(fetchReqData) 210 | .then(prepareRequestDetail) 211 | 212 | .then(() => { 213 | // record request info 214 | if (recorder) { 215 | resourceInfo = { 216 | host, 217 | method: req.method, 218 | path, 219 | protocol, 220 | url: protocol + '://' + host + path, 221 | req, 222 | reqBody: reqData.toString(), 223 | startTime: new Date().getTime() 224 | }; 225 | resourceInfoId = recorder.appendRecord(resourceInfo); 226 | } 227 | 228 | // resourceInfo.reqBody = reqData.toString(); 229 | // recorder && recorder.updateRecord(resourceInfoId, resourceInfo); 230 | }) 231 | 232 | // invoke rule before sending request 233 | .then(co.wrap(function*() { 234 | const userModifiedInfo = (yield userRule.beforeSendRequest(Object.assign({}, requestDetail))) || {}; 235 | const finalReqDetail = {}; 236 | ['protocol', 'requestOptions', 'requestData', 'response'].map((key) => { 237 | finalReqDetail[key] = userModifiedInfo[key] || requestDetail[key] 238 | }); 239 | return finalReqDetail; 240 | })) 241 | 242 | // route user config 243 | .then(co.wrap(function*(userConfig) { 244 | if (userConfig.response) { 245 | // user-assigned local response 246 | userConfig._directlyPassToRespond = true; 247 | return userConfig; 248 | } else if (userConfig.requestOptions) { 249 | const remoteResponse = yield fetchRemoteResponse(userConfig.protocol, userConfig.requestOptions, userConfig.requestData, { 250 | dangerouslyIgnoreUnauthorized: reqHandlerCtx.dangerouslyIgnoreUnauthorized, 251 | }); 252 | return { 253 | response: { 254 | statusCode: remoteResponse.statusCode, 255 | header: remoteResponse.header, 256 | body: remoteResponse.body 257 | }, 258 | _res: remoteResponse._res, 259 | }; 260 | } else { 261 | throw new Error('lost response or requestOptions, failed to continue'); 262 | } 263 | })) 264 | 265 | // invoke rule before responding to client 266 | .then(co.wrap(function*(responseData) { 267 | if (responseData._directlyPassToRespond) { 268 | return responseData; 269 | } else { 270 | // TODO: err etimeout 271 | return (yield userRule.beforeSendResponse(Object.assign({}, requestDetail), Object.assign({}, responseData))) || responseData; 272 | } 273 | })) 274 | 275 | .catch(co.wrap(function*(error) { 276 | logUtil.printLog('An error occurred when dealing with request', logUtil.T_ERR); 277 | logUtil.printLog(error && error.stack ? error.stack : error, logUtil.T_ERR); 278 | 279 | let content; 280 | try { 281 | content = error502PugFn({ 282 | error, 283 | url: fullUrl, 284 | errorStack: error.stack.split(/\n/) 285 | }); 286 | } catch (parseErro) { 287 | content = error.stack; 288 | } 289 | 290 | // default error response 291 | let errorResponse = { 292 | statusCode: 500, 293 | header: { 294 | 'Content-Type': 'text/html; charset=utf-8', 295 | 'Proxy-Error': true, 296 | 'Proxy-Error-Message': error || 'null' 297 | }, 298 | body: content 299 | }; 300 | 301 | // call user rule 302 | try { 303 | const userResponse = yield userRule.onError(Object.assign({}, requestDetail), error); 304 | if (userResponse && userResponse.response && userResponse.response.header) { 305 | errorResponse = userResponse.response; 306 | } 307 | } catch (e) {} 308 | 309 | return { 310 | response: errorResponse 311 | }; 312 | })) 313 | .then(sendFinalResponse) 314 | 315 | //update record info 316 | .then((responseInfo) => { 317 | resourceInfo.endTime = new Date().getTime(); 318 | resourceInfo.res = { //construct a self-defined res object 319 | statusCode: responseInfo.statusCode, 320 | headers: responseInfo.header, 321 | }; 322 | 323 | resourceInfo.statusCode = responseInfo.statusCode; 324 | resourceInfo.resHeader = responseInfo.header; 325 | resourceInfo.resBody = responseInfo.body; 326 | resourceInfo.length = resourceInfo.resBody.length; 327 | 328 | recorder && recorder.updateRecord(resourceInfoId, resourceInfo); 329 | }); 330 | } 331 | } 332 | 333 | /** 334 | * get a handler for CONNECT request 335 | * 336 | * @param {RequestHandler} reqHandlerCtx 337 | * @param {object} userRule 338 | * @param {Recorder} recorder 339 | * @param {object} httpsServerMgr 340 | * @returns 341 | */ 342 | function getConnectReqHandler(reqHandlerCtx, userRule, recorder, httpsServerMgr) { 343 | return function (req, socket) { 344 | const host = req.url.split(':')[0], 345 | targetPort = req.url.split(':')[1]; 346 | let resourceInfo; 347 | let resourceInfoId; 348 | 349 | function _sendFailedSocket(error) { 350 | let errorHeader = 'Proxy-Error: true\r\n'; 351 | errorHeader += 'Proxy-Error-Message: ' + (error || 'null') + '\r\n'; 352 | errorHeader += 'Content-Type: text/html\r\n'; 353 | socket.write('HTTP/' + req.httpVersion + ' 502 Proxy Inner Error\r\n' + errorHeader + '\r\n\r\n'); 354 | } 355 | 356 | let shouldIntercept; 357 | let requestDetail; 358 | co(function *() { 359 | // determine whether to use the man-in-the-middle server 360 | logUtil.printLog(color.green('received https CONNECT request ' + host)); 361 | if (reqHandlerCtx.forceProxyHttps) { 362 | shouldIntercept = true; 363 | } else if (targetPort === 8003) { //bypass webSocket on webinterface 364 | shouldIntercept = false; 365 | } else { 366 | requestDetail = { 367 | host: req.url, 368 | _req: req 369 | }; 370 | shouldIntercept = yield userRule.beforeDealHttpsRequest(requestDetail); 371 | } 372 | }).then(() => { 373 | // log and recorder 374 | if (shouldIntercept) { 375 | logUtil.printLog('will forward to local https server'); 376 | } else { 377 | logUtil.printLog('will bypass the man-in-the-middle proxy'); 378 | } 379 | 380 | //record 381 | resourceInfo = { 382 | host, 383 | method: req.method, 384 | path: '', 385 | url: 'https://' + host, 386 | req, 387 | reqBody: reqData.toString(), 388 | startTime: new Date().getTime() 389 | }; 390 | resourceInfoId = recorder.appendRecord(resourceInfo); 391 | }).then(() => { 392 | // determine the request target 393 | if (!shouldIntercept) { 394 | return { 395 | host, 396 | port: (targetPort === 80) ? 443 : targetPort, 397 | } 398 | } else { 399 | return httpsServerMgr.getSharedHttpsServer() 400 | .then((serverInfo) => { 401 | return { 402 | host: serverInfo.host, 403 | port: serverInfo.port, 404 | } 405 | }); 406 | } 407 | }) 408 | .then((serverInfo) => { 409 | if (!serverInfo.port || !serverInfo.host) { 410 | throw new Error('failed to get https server info'); 411 | } 412 | return new Promise((resolve, reject) => { 413 | const conn = net.connect(serverInfo.port, serverInfo.host, () => { 414 | socket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'UTF-8', () => { 415 | //throttle for direct-foward https 416 | if (global._throttle && !shouldIntercept) { 417 | conn.pipe(global._throttle.throttle()).pipe(socket); 418 | socket.pipe(conn); 419 | } else { 420 | conn.pipe(socket); 421 | socket.pipe(conn); 422 | } 423 | 424 | resolve(); 425 | }); 426 | }); 427 | conn.on('error', (e) => { 428 | reject(e); 429 | }); 430 | }); 431 | }) 432 | .then(() => { 433 | // resourceInfo.endTime = new Date().getTime(); 434 | // resourceInfo.statusCode = '200'; 435 | // resourceInfo.resHeader = {}; 436 | // resourceInfo.resBody = ''; 437 | // resourceInfo.length = 0; 438 | 439 | // recorder && recorder.updateRecord(resourceInfoId, resourceInfo); 440 | }) 441 | .catch(co.wrap(function *(error) { 442 | logUtil.printLog('error happend when dealing https req:' + error + ' ' + host, logUtil.T_ERR); 443 | logUtil.printLog(error.stack, logUtil.T_ERR); 444 | 445 | try { 446 | yield userRule.onConnectError(requestDetail, error); 447 | } catch (e) { } 448 | 449 | try { 450 | _sendFailedSocket(error); 451 | } catch (e) { 452 | console.e('error', error); 453 | } 454 | })); 455 | } 456 | } 457 | 458 | class RequestHandler { 459 | 460 | /** 461 | * Creates an instance of RequestHandler. 462 | * 463 | * @param {object} config 464 | * @param {boolean} config.forceProxyHttps proxy all https requests 465 | * @param {boolean} config.dangerouslyIgnoreUnauthorized 466 | * @param {object} rule 467 | * @param {Recorder} recorder 468 | * 469 | * @memberOf RequestHandler 470 | */ 471 | constructor(config, rule, recorder) { 472 | const reqHandlerCtx = this; 473 | if (config.forceProxyHttps) { 474 | this.forceProxyHttps = true; 475 | } 476 | if (config.dangerouslyIgnoreUnauthorized) { 477 | this.dangerouslyIgnoreUnauthorized = true; 478 | } 479 | const default_rule = util.freshRequire('./rule_default'); 480 | const userRule = util.merge(default_rule, rule); 481 | 482 | const userRequestHandler = getUserReqHandler(reqHandlerCtx, userRule, recorder); 483 | const httpsServerMgr = new HttpsServerMgr({ 484 | handler: userRequestHandler 485 | }); 486 | 487 | this.userRequestHandler = userRequestHandler; 488 | this.connectReqHandler = getConnectReqHandler(reqHandlerCtx, userRule, recorder, httpsServerMgr); 489 | } 490 | } 491 | 492 | module.exports = RequestHandler; 493 | -------------------------------------------------------------------------------- /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 | const cachePath = proxyUtil.getAnyProxyPath('cache'); 8 | 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() { 6 | return 'the default rule for AnyProxy'; 7 | }, 8 | 9 | /** 10 | * 11 | * 12 | * @param {object} requestDetail 13 | * @param {string} requestDetail.protocol 14 | * @param {object} requestDetail.requestOptions 15 | * @param {object} requestDetail.requestData 16 | * @param {object} requestDetail.response 17 | * @param {number} requestDetail.response.statusCode 18 | * @param {object} requestDetail.response.header 19 | * @param {buffer} requestDetail.response.body 20 | * @returns 21 | */ 22 | *beforeSendRequest(requestDetail) { 23 | return null; 24 | }, 25 | 26 | 27 | /** 28 | * 29 | * 30 | * @param {object} requestDetail 31 | * @param {object} responseDetail 32 | */ 33 | *beforeSendResponse(requestDetail, responseDetail) { 34 | return null; 35 | }, 36 | 37 | 38 | /** 39 | * 40 | * 41 | * @param {any} requestDetail 42 | * @returns 43 | */ 44 | *beforeDealHttpsRequest(requestDetail) { 45 | return false; 46 | }, 47 | 48 | /** 49 | * 50 | * 51 | * @param {any} requestDetail 52 | * @param {any} error 53 | * @returns 54 | */ 55 | *onError(requestDetail, error) { 56 | return null; 57 | }, 58 | 59 | 60 | /** 61 | * 62 | * 63 | * @param {any} requestDetail 64 | * @param {any} error 65 | * @returns 66 | */ 67 | *onConnectError(requestDetail, error) { 68 | return null; 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /lib/systemProxyMgr.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const child_process = require('child_process'); 4 | 5 | const networkTypes = ['Ethernet', 'Thunderbolt Ethernet', 'Wi-Fi']; 6 | 7 | function execSync(cmd) { 8 | let stdout, 9 | status = 0; 10 | try { 11 | stdout = child_process.execSync(cmd); 12 | } catch (err) { 13 | stdout = err.stdout; 14 | status = err.status; 15 | } 16 | 17 | return { 18 | stdout: stdout.toString(), 19 | status 20 | }; 21 | } 22 | 23 | /** 24 | * proxy for CentOs 25 | * ------------------------------------------------------------------------ 26 | * 27 | * file: ~/.bash_profile 28 | * 29 | * http_proxy=http://proxy_server_address:port 30 | * export no_proxy=localhost,127.0.0.1,192.168.0.34 31 | * export http_proxy 32 | * ------------------------------------------------------------------------ 33 | */ 34 | 35 | /** 36 | * proxy for Ubuntu 37 | * ------------------------------------------------------------------------ 38 | * 39 | * file: /etc/environment 40 | * more info: http://askubuntu.com/questions/150210/how-do-i-set-systemwide-proxy-servers-in-xubuntu-lubuntu-or-ubuntu-studio 41 | * 42 | * http_proxy=http://proxy_server_address:port 43 | * export no_proxy=localhost,127.0.0.1,192.168.0.34 44 | * export http_proxy 45 | * ------------------------------------------------------------------------ 46 | */ 47 | 48 | /** 49 | * ------------------------------------------------------------------------ 50 | * mac proxy manager 51 | * ------------------------------------------------------------------------ 52 | */ 53 | 54 | const macProxyManager = {}; 55 | 56 | macProxyManager.getNetworkType = () => { 57 | for (let i = 0; i < networkTypes.length; i++) { 58 | const type = networkTypes[i], 59 | result = execSync('networksetup -getwebproxy ' + type); 60 | 61 | if (result.status === 0) { 62 | macProxyManager.networkType = type; 63 | return type; 64 | } 65 | } 66 | 67 | throw new Error('Unknown network type'); 68 | }; 69 | 70 | 71 | macProxyManager.enableGlobalProxy = (ip, port, proxyType) => { 72 | if (!ip || !port) { 73 | console.log('failed to set global proxy server.\n ip and port are required.'); 74 | return; 75 | } 76 | 77 | proxyType = proxyType || 'http'; 78 | 79 | const networkType = macProxyManager.networkType || macProxyManager.getNetworkType(); 80 | 81 | return /^http$/i.test(proxyType) ? 82 | 83 | // set http proxy 84 | execSync( 85 | 'networksetup -setwebproxy ${networkType} ${ip} ${port} && networksetup -setproxybypassdomains ${networkType} 127.0.0.1 localhost' 86 | .replace(/\${networkType}/g, networkType) 87 | .replace('${ip}', ip) 88 | .replace('${port}', port)) : 89 | 90 | // set https proxy 91 | execSync('networksetup -setsecurewebproxy ${networkType} ${ip} ${port} && networksetup -setproxybypassdomains ${networkType} 127.0.0.1 localhost' 92 | .replace(/\${networkType}/g, networkType) 93 | .replace('${ip}', ip) 94 | .replace('${port}', port)); 95 | }; 96 | 97 | macProxyManager.disableGlobalProxy = (proxyType) => { 98 | proxyType = proxyType || 'http'; 99 | const networkType = macProxyManager.networkType || macProxyManager.getNetworkType(); 100 | return /^http$/i.test(proxyType) ? 101 | 102 | // set http proxy 103 | execSync( 104 | 'networksetup -setwebproxystate ${networkType} off' 105 | .replace('${networkType}', networkType)) : 106 | 107 | // set https proxy 108 | execSync( 109 | 'networksetup -setsecurewebproxystate ${networkType} off' 110 | .replace('${networkType}', networkType)); 111 | }; 112 | 113 | macProxyManager.getProxyState = () => { 114 | const networkType = macProxyManager.networkType || macProxyManager.getNetworkType(); 115 | const result = execSync('networksetup -getwebproxy ${networkType}'.replace('${networkType}', networkType)); 116 | 117 | return result; 118 | }; 119 | 120 | /** 121 | * ------------------------------------------------------------------------ 122 | * windows proxy manager 123 | * 124 | * netsh does not alter the settings for IE 125 | * ------------------------------------------------------------------------ 126 | */ 127 | 128 | const winProxyManager = {}; 129 | 130 | winProxyManager.enableGlobalProxy = (ip, port) => { 131 | if (!ip && !port) { 132 | console.log('failed to set global proxy server.\n ip and port are required.'); 133 | return; 134 | } 135 | 136 | return execSync( 137 | // set proxy 138 | 'reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyServer /t REG_SZ /d ${ip}:${port} /f & ' 139 | .replace('${ip}', ip) 140 | .replace('${port}', port) + 141 | 142 | // enable proxy 143 | 'reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyEnable /t REG_DWORD /d 1 /f'); 144 | }; 145 | 146 | winProxyManager.disableGlobalProxy = () => execSync('reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyEnable /t REG_DWORD /d 0 /f'); 147 | 148 | winProxyManager.getProxyState = () => '' 149 | 150 | winProxyManager.getNetworkType = () => '' 151 | 152 | module.exports = /^win/.test(process.platform) ? winProxyManager : macProxyManager; 153 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const fs = require('fs'), 3 | path = require('path'), 4 | mime = require('mime-types'), 5 | color = require('colorful'), 6 | logUtil = require('./log'); 7 | const networkInterfaces = require('os').networkInterfaces(); 8 | 9 | // {"Content-Encoding":"gzip"} --> {"content-encoding":"gzip"} 10 | module.exports.lower_keys = (obj) => { 11 | for (const key in obj) { 12 | const val = obj[key]; 13 | delete obj[key]; 14 | 15 | obj[key.toLowerCase()] = val; 16 | } 17 | 18 | return obj; 19 | }; 20 | 21 | module.exports.merge = function (baseObj, extendObj) { 22 | for (const key in extendObj) { 23 | baseObj[key] = extendObj[key]; 24 | } 25 | 26 | return baseObj; 27 | }; 28 | 29 | function getUserHome() { 30 | return process.env.HOME || process.env.USERPROFILE; 31 | } 32 | module.exports.getUserHome = getUserHome; 33 | 34 | function getAnyProxyHome() { 35 | const home = path.join(getUserHome(), '/.anyproxy/'); 36 | if (!fs.existsSync(home)) { 37 | fs.mkdirSync(home); 38 | } 39 | return home; 40 | } 41 | module.exports.getAnyProxyHome = getAnyProxyHome; 42 | 43 | module.exports.getAnyProxyPath = function (pathName) { 44 | const home = getAnyProxyHome(); 45 | const targetPath = path.join(home, pathName); 46 | if (!fs.existsSync(targetPath)) { 47 | fs.mkdirSync(targetPath); 48 | } 49 | return targetPath; 50 | } 51 | 52 | module.exports.simpleRender = function (str, object, regexp) { 53 | return String(str).replace(regexp || (/\{\{([^{}]+)\}\}/g), (match, name) => { 54 | if (match.charAt(0) === '\\') { 55 | return match.slice(1); 56 | } 57 | return (object[name] != null) ? object[name] : ''; 58 | }); 59 | }; 60 | 61 | module.exports.filewalker = function (root, cb) { 62 | root = root || process.cwd(); 63 | 64 | const ret = { 65 | directory: [], 66 | file: [] 67 | }; 68 | 69 | fs.readdir(root, (err, list) => { 70 | if (list && list.length) { 71 | list.map((item) => { 72 | const fullPath = path.join(root, item), 73 | stat = fs.lstatSync(fullPath); 74 | 75 | if (stat.isFile()) { 76 | ret.file.push({ 77 | name: item, 78 | fullPath 79 | }); 80 | } else if (stat.isDirectory()) { 81 | ret.directory.push({ 82 | name: item, 83 | fullPath 84 | }); 85 | } 86 | }); 87 | } 88 | 89 | cb && cb.apply(null, [null, ret]); 90 | }); 91 | }; 92 | 93 | /* 94 | * 获取文件所对应的content-type以及content-length等信息 95 | * 比如在useLocalResponse的时候会使用到 96 | */ 97 | module.exports.contentType = function (filepath) { 98 | return mime.contentType(path.extname(filepath)); 99 | }; 100 | 101 | /* 102 | * 读取file的大小,以byte为单位 103 | */ 104 | module.exports.contentLength = function (filepath) { 105 | try { 106 | const stat = fs.statSync(filepath); 107 | return stat.size; 108 | } catch (e) { 109 | logUtil.printLog(color.red('\nfailed to ready local file : ' + filepath)); 110 | logUtil.printLog(color.red(e)); 111 | return 0; 112 | } 113 | }; 114 | 115 | /* 116 | * remove the cache before requiring, the path SHOULD BE RELATIVE TO UTIL.JS 117 | */ 118 | module.exports.freshRequire = function (modulePath) { 119 | delete require.cache[require.resolve(modulePath)]; 120 | return require(modulePath); 121 | }; 122 | 123 | /* 124 | * format the date string 125 | * @param date Date or timestamp 126 | * @param formatter YYYYMMDDHHmmss 127 | */ 128 | module.exports.formatDate = function (date, formatter) { 129 | if (typeof date !== 'object') { 130 | date = new Date(date); 131 | } 132 | const transform = function (value) { 133 | return value < 10 ? '0' + value : value; 134 | }; 135 | return formatter.replace(/^YYYY|MM|DD|hh|mm|ss/g, (match) => { 136 | switch (match) { 137 | case 'YYYY': 138 | return transform(date.getFullYear()); 139 | case 'MM': 140 | return transform(date.getMonth() + 1); 141 | case 'mm': 142 | return transform(date.getMinutes()); 143 | case 'DD': 144 | return transform(date.getDate()); 145 | case 'hh': 146 | return transform(date.getHours()); 147 | case 'ss': 148 | return transform(date.getSeconds()); 149 | default: 150 | return '' 151 | } 152 | }); 153 | }; 154 | 155 | 156 | /** 157 | * get headers(Object) from rawHeaders(Array) 158 | * @param rawHeaders [key, value, key2, value2, ...] 159 | 160 | */ 161 | 162 | module.exports.getHeaderFromRawHeaders = function (rawHeaders) { 163 | const headerObj = {}; 164 | if (!!rawHeaders) { 165 | for (let i = 0; i < rawHeaders.length; i += 2) { 166 | const key = rawHeaders[i]; 167 | const value = rawHeaders[i + 1]; 168 | headerObj[key] = value; 169 | } 170 | } 171 | 172 | return headerObj; 173 | }; 174 | 175 | module.exports.getAllIpAddress = function () { 176 | const allIp = []; 177 | 178 | Object.keys(networkInterfaces).map((nic) => { 179 | networkInterfaces[nic].filter((detail) => { 180 | if (detail.family.toLowerCase() === 'ipv4') { 181 | allIp.push(detail.address); 182 | } 183 | }); 184 | }); 185 | 186 | return allIp.length ? allIp : ['127.0.0.1']; 187 | }; 188 | 189 | function deleteFolderContentsRecursive(dirPath, ifClearFolderItself) { 190 | if (!dirPath.trim() || dirPath === '/') { 191 | throw new Error('can_not_delete_this_dir'); 192 | } 193 | 194 | if (fs.existsSync(dirPath)) { 195 | fs.readdirSync(dirPath).forEach((file) => { 196 | const curPath = path.join(dirPath, file); 197 | if (fs.lstatSync(curPath).isDirectory()) { 198 | deleteFolderContentsRecursive(curPath, true); 199 | } else { // delete all files 200 | fs.unlinkSync(curPath); 201 | } 202 | }); 203 | 204 | if (ifClearFolderItself) { 205 | try { 206 | // ref: https://github.com/shelljs/shelljs/issues/49 207 | const start = Date.now(); 208 | while (true) { 209 | try { 210 | fs.rmdirSync(dirPath); 211 | break; 212 | } catch (er) { 213 | if (process.platform === 'win32' && (er.code === 'ENOTEMPTY' || er.code === 'EBUSY' || er.code === 'EPERM')) { 214 | // Retry on windows, sometimes it takes a little time before all the files in the directory are gone 215 | if (Date.now() - start > 1000) throw er; 216 | } else if (er.code === 'ENOENT') { 217 | break; 218 | } else { 219 | throw er; 220 | } 221 | } 222 | } 223 | } catch (e) { 224 | throw new Error('could not remove directory (code ' + e.code + '): ' + dirPath); 225 | } 226 | } 227 | } 228 | } 229 | 230 | module.exports.deleteFolderContentsRecursive = deleteFolderContentsRecursive; 231 | 232 | function getFreePort() { 233 | return new Promise((resolve, reject) => { 234 | const server = require('net').createServer(); 235 | server.unref(); 236 | server.on('error', reject); 237 | server.listen(0, () => { 238 | const port = server.address().port; 239 | server.close(() => { 240 | resolve(port); 241 | }); 242 | }); 243 | }); 244 | } 245 | 246 | module.exports.getFreePort = getFreePort; 247 | -------------------------------------------------------------------------------- /lib/webInterface.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const DEFAULT_WEB_PORT = 8002; // port for web interface 4 | const DEFAULT_WEBSOCKET_PORT = 8003; // internal web socket for web interface, not for end users 5 | 6 | const express = require('express'), 7 | url = require('url'), 8 | bodyParser = require('body-parser'), 9 | fs = require('fs'), 10 | path = require('path'), 11 | events = require('events'), 12 | inherits = require('util').inherits, 13 | qrCode = require('qrcode-npm'), 14 | util = require('./util'), 15 | certMgr = require('./certMgr'), 16 | wsServer = require('./wsServer'), 17 | juicer = require('juicer'), 18 | ip = require('ip'), 19 | compress = require('compression'); 20 | 21 | const packageJson = require('../package.json'); 22 | 23 | /** 24 | * 25 | * 26 | * @class webInterface 27 | * @extends {events.EventEmitter} 28 | */ 29 | class webInterface extends events.EventEmitter { 30 | 31 | /** 32 | * Creates an instance of webInterface. 33 | * 34 | * @param {object} config 35 | * @param {number} config.webPort 36 | * @param {number} config.proxyWsPort 37 | * @param {object} proxyInstance 38 | * 39 | * @memberOf webInterface 40 | */ 41 | constructor(config, proxyInstance) { 42 | super(); 43 | const self = this; 44 | self.webPort = config.webPort || DEFAULT_WEB_PORT; 45 | self.wsPort = config.proxyWsPort || DEFAULT_WEBSOCKET_PORT; 46 | self.proxyInstance = proxyInstance; 47 | 48 | self.app = null; 49 | self.server = null; 50 | self.wsServer = null; 51 | 52 | self.start(); 53 | } 54 | 55 | start() { 56 | const self = this; 57 | const proxyInstance = self.proxyInstance; 58 | 59 | const ipAddress = ip.address(), 60 | userRule = proxyInstance.proxyRule, 61 | webBasePath = 'web'; 62 | let ruleSummary = ''; 63 | let customMenu = []; 64 | let server; 65 | 66 | try { 67 | ruleSummary = userRule.summary(); 68 | customMenu = userRule._getCustomMenu(); 69 | } catch (e) {} 70 | 71 | const myAbsAddress = 'http://' + ipAddress + ':' + self.webPort + '/', 72 | staticDir = path.join(__dirname, '../', webBasePath); 73 | 74 | const app = express(); 75 | app.use(compress()); //invoke gzip 76 | app.use((req, res, next) => { 77 | res.setHeader('note', 'THIS IS A REQUEST FROM ANYPROXY WEB INTERFACE'); 78 | return next(); 79 | }); 80 | app.use(bodyParser.json()); 81 | 82 | app.get('/latestLog', (req, res) => { 83 | proxyInstance.recorder.getRecords(null, 10000, (err, docs) => { 84 | if (err) { 85 | res.end(err.toString()); 86 | } else { 87 | res.json(docs); 88 | } 89 | }); 90 | }); 91 | 92 | app.get('/fetchBody', (req, res) => { 93 | const query = req.query; 94 | if (query && query.id) { 95 | proxyInstance.recorder.getDecodedBody(query.id, (err, result) => { 96 | if (err || !result || !result.content) { 97 | res.json({}); 98 | } else if (result.type && result.type === 'image' && result.mime) { 99 | if (query.raw) { 100 | //TODO : cache query result 101 | res.type(result.mime).end(result.content); 102 | } else { 103 | res.json({ 104 | id: query.id, 105 | type: result.type, 106 | ref: '/fetchBody?id=' + query.id + '&raw=true' 107 | }); 108 | } 109 | } else { 110 | res.json({ 111 | id: query.id, 112 | type: result.type, 113 | content: result.content 114 | }); 115 | } 116 | }); 117 | } else { 118 | res.end({}); 119 | } 120 | }); 121 | 122 | app.get('/fetchCrtFile', (req, res) => { 123 | const _crtFilePath = certMgr.getRootCAFilePath(); 124 | if (_crtFilePath) { 125 | res.setHeader('Content-Type', 'application/x-x509-ca-cert'); 126 | res.setHeader('Content-Disposition', 'attachment; filename="rootCA.crt"'); 127 | res.end(fs.readFileSync(_crtFilePath, { encoding: null })); 128 | } else { 129 | res.setHeader('Content-Type', 'text/html'); 130 | res.end('can not file rootCA ,plase use anyproxy --root to generate one'); 131 | } 132 | }); 133 | 134 | //make qr code 135 | app.get('/qr', (req, res) => { 136 | const qr = qrCode.qrcode(4, 'M'), 137 | targetUrl = myAbsAddress; 138 | 139 | qr.addData(targetUrl); 140 | qr.make(); 141 | const qrImageTag = qr.createImgTag(4); 142 | const resDom = ' __img
click or scan qr code to start client
'.replace(/__url/, targetUrl).replace(/__img/, qrImageTag); 143 | res.setHeader('Content-Type', 'text/html'); 144 | res.end(resDom); 145 | }); 146 | 147 | app.get('/api/getQrCode', (req, res) => { 148 | const qr = qrCode.qrcode(4, 'M'), 149 | targetUrl = myAbsAddress + 'fetchCrtFile'; 150 | 151 | qr.addData(targetUrl); 152 | qr.make(); 153 | const qrImageTag = qr.createImgTag(4); 154 | 155 | // resDom = ' __img
click or scan qr code to download rootCA.crt
'.replace(/__url/,targetUrl).replace(/__img/,qrImageTag); 156 | // res.setHeader("Content-Type", "text/html"); 157 | // res.end(resDom); 158 | 159 | const isRootCAFileExists = certMgr.isRootCAFileExists(); 160 | res.json({ 161 | status: 'success', 162 | url: targetUrl, 163 | isRootCAFileExists, 164 | qrImgDom: qrImageTag 165 | }); 166 | }); 167 | 168 | // response init data 169 | app.get('/api/getInitData', (req, res) => { 170 | const rootCAExists = certMgr.isRootCAFileExists(); 171 | const rootDirPath = certMgr.getRootDirPath(); 172 | const interceptFlag = false; //proxyInstance.getInterceptFlag(); TODO 173 | const globalProxyFlag = false; // TODO: proxyInstance.getGlobalProxyFlag(); 174 | 175 | res.json({ 176 | status: 'success', 177 | rootCAExists, 178 | rootCADirPath: rootDirPath, 179 | currentInterceptFlag: interceptFlag, 180 | currentGlobalProxyFlag: globalProxyFlag, 181 | ruleSummary: ruleSummary || '', 182 | ipAddress: util.getAllIpAddress(), 183 | port: proxyInstance.proxyPort, // TODO 184 | appVersion: packageJson.version 185 | }); 186 | }); 187 | 188 | app.post('/api/generateRootCA', (req, res) => { 189 | const rootExists = certMgr.isRootCAFileExists(); 190 | if (!rootExists) { 191 | certMgr.generateRootCA(() => { 192 | res.json({ 193 | status: 'success', 194 | code: 'done' 195 | }); 196 | }); 197 | } else { 198 | res.json({ 199 | status: 'success', 200 | code: 'root_ca_exists' 201 | }); 202 | } 203 | }); 204 | 205 | app.post('/api/toggleInterceptHttps', (req, res) => { 206 | const rootExists = certMgr.isRootCAFileExists(); 207 | if (!rootExists) { 208 | certMgr.generateRootCA(() => { 209 | proxyInstance.setIntercept(req.body.flag); 210 | // Also inform the web if RootCa exists 211 | res.json({ 212 | status: 'success', 213 | rootExists 214 | }); 215 | }); 216 | } else { 217 | proxyInstance.setIntercept(req.body.flag); 218 | res.json({ 219 | status: 'success', 220 | rootExists 221 | }); 222 | } 223 | }); 224 | 225 | app.post('/api/toggleGlobalProxy', (req, res) => { 226 | const flag = req.body.flag; 227 | let result = {}; 228 | result = flag ? proxyInstance.enableGlobalProxy() : proxyInstance.disableGlobalProxy(); 229 | 230 | if (result.status) { 231 | res.json({ 232 | status: 'failed', 233 | errorMsg: result.stdout 234 | }); 235 | } else { 236 | res.json({ 237 | status: 'success', 238 | isWindows: /^win/.test(process.platform) 239 | }); 240 | } 241 | }); 242 | 243 | app.use((req, res, next) => { 244 | const indexTpl = fs.readFileSync(path.join(staticDir, '/index.html'), { encoding: 'utf8' }), 245 | opt = { 246 | rule: ruleSummary || '', 247 | customMenu: customMenu || [], 248 | wsPort: self.wsPort, 249 | ipAddress: ipAddress || '127.0.0.1' 250 | }; 251 | 252 | if (url.parse(req.url).pathname === '/') { 253 | res.setHeader('Content-Type', 'text/html'); 254 | res.end(juicer(indexTpl, opt)); 255 | } else { 256 | next(); 257 | } 258 | }); 259 | 260 | app.use(express.static(staticDir)); 261 | 262 | //plugin from rule file 263 | if (typeof userRule._plugIntoWebinterface === 'function') { 264 | userRule._plugIntoWebinterface(app, () => { 265 | server = app.listen(self.webPort); 266 | }); 267 | } else { 268 | server = app.listen(self.webPort); 269 | } 270 | 271 | // start ws server 272 | self.wsServer = new wsServer({ 273 | port: self.wsPort 274 | }, proxyInstance.recorder); 275 | 276 | self.app = app; 277 | self.server = server; 278 | } 279 | 280 | close() { 281 | this.server && this.server.close(); 282 | this.wsServer && this.wsServer.closeAll(); 283 | 284 | this.server = null; 285 | this.wsServer = null; 286 | this.proxyInstance = null; 287 | } 288 | } 289 | 290 | module.exports = webInterface; 291 | -------------------------------------------------------------------------------- /lib/wsServer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | //websocket server manager 3 | 4 | const WebSocketServer = require('ws').Server; 5 | const logUtil = require('./log'); 6 | 7 | function resToMsg(msg, recorder, cb) { 8 | let result = {}, 9 | jsonData; 10 | 11 | try { 12 | jsonData = JSON.parse(msg); 13 | } catch (e) { 14 | result = { 15 | type: 'error', 16 | error: 'failed to parse your request : ' + e.toString() 17 | }; 18 | cb && cb(result); 19 | return; 20 | } 21 | 22 | if (jsonData.reqRef) { 23 | result.reqRef = jsonData.reqRef; 24 | } 25 | 26 | if (jsonData.type === 'reqBody' && jsonData.id) { 27 | result.type = 'body'; 28 | recorder.getBody(jsonData.id, (err, data) => { 29 | if (err) { 30 | result.content = { 31 | id: null, 32 | body: null, 33 | error: err.toString() 34 | }; 35 | } else { 36 | result.content = { 37 | id: jsonData.id, 38 | body: data.toString() 39 | }; 40 | } 41 | cb && cb(result); 42 | }); 43 | } else { // more req handler here 44 | return null; 45 | } 46 | } 47 | 48 | //config.port 49 | function wsServer(config, recorder) { 50 | if (!recorder) { 51 | throw new Error('proxy recorder is required'); 52 | } 53 | 54 | //web socket interface 55 | const self = this; 56 | const wss = new WebSocketServer({ port: config.port }); 57 | 58 | // the queue of the messages to be delivered 59 | let messageQueue = []; 60 | // the flat to indicate wheter to broadcast the record 61 | let broadcastFlag = true; 62 | 63 | setInterval(() => { 64 | broadcastFlag = true; 65 | sendMultipleMessage(); 66 | }, 50); 67 | 68 | function sendMultipleMessage(data) { 69 | // if the flag goes to be true, and there are records to send 70 | if (broadcastFlag && messageQueue.length > 0) { 71 | wss && wss.broadcast({ 72 | type: 'updateMultiple', 73 | content: messageQueue 74 | }); 75 | messageQueue = []; 76 | broadcastFlag = false; 77 | } else { 78 | data && messageQueue.push(data); 79 | } 80 | } 81 | 82 | wss.broadcast = function (data) { 83 | if (typeof data === 'object') { 84 | data = JSON.stringify(data); 85 | } 86 | 87 | for (const i in this.clients) { 88 | try { 89 | this.clients[i].send(data); 90 | } catch (e) { 91 | logUtil.printLog('websocket failed to send data, ' + e, logUtil.T_ERR); 92 | } 93 | } 94 | }; 95 | 96 | wss.on('connection', (ws) => { 97 | ws.on('message', (msg) => { 98 | resToMsg(msg, recorder, (res) => { 99 | res && ws.send(JSON.stringify(res)); 100 | }); 101 | }); 102 | 103 | ws.on('error', (e) => { 104 | console.error('error in ws:', e); 105 | }); 106 | }); 107 | 108 | wss.on('error', (e) => { 109 | logUtil.printLog('websocket error, ' + e, logUtil.T_ERR); 110 | }); 111 | 112 | wss.on('close', () => {}); 113 | 114 | recorder.on('update', (data) => { 115 | try { 116 | sendMultipleMessage(data); 117 | } catch (e) { 118 | console.log('ws error'); 119 | console.log(e); 120 | } 121 | }); 122 | 123 | self.wss = wss; 124 | } 125 | 126 | wsServer.prototype.closeAll = function () { 127 | const self = this; 128 | self.wss.close(); 129 | }; 130 | 131 | module.exports = wsServer; 132 | -------------------------------------------------------------------------------- /main-api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * proxy 的接口封装,用于主进程与渲染进程之间数据通信 4 | */ 5 | const util = require('./lib/util'); 6 | const ip = require('ip'); 7 | const packageInfo = require('./package.json'); 8 | const ProxyServer = require('./proxy.js').ProxyServer; 9 | const path = require('path'); 10 | const fs = require('fs'); 11 | const mockjs = require('mockjs'); 12 | const http = require('http'); 13 | const qs = require('querystring'); 14 | const zlib = require('zlib'); 15 | let ruleModule; 16 | let mainProxy; 17 | 18 | const ruleFile = __dirname + '/rules.json'; 19 | const ruleCustomPath = __dirname + '/rule_custom'; 20 | const ruleSamplePath = __dirname + '/rule_sample'; 21 | 22 | const mockProjectsFile = __dirname + '/mock-project.json'; 23 | const mockCustomPath = __dirname + '/mock_custom'; 24 | 25 | const certMgr = require('./proxy.js').utils.certMgr; 26 | const exec = require('child_process').exec; 27 | 28 | const MSG_HAD_OPEN_PROXY = 'MSG_HAD_OPEN_PROXY'; 29 | const MSG_OPEN_PROXY_SUCCESS = 'MSG_OPEN_PROXY_SUCCESS'; 30 | const MSG_OPEN_PROXY_ERROR = 'MSG_OPEN_PROXY_ERROR'; 31 | const MSG_HASNOT_OPEN_PROXY = 'MSG_HASNOT_OPEN_PROXY'; 32 | const MSG_CLOSE_PROXY_SUCCESS = 'MSG_CLOSE_PROXY_SUCCESS'; 33 | 34 | //获取rule文件 35 | function getRuleModule(id) { 36 | if (!id) return null; 37 | const pathname = './rule_custom'; 38 | const filename = 'custom_' + id + '.js'; 39 | const filepath = path.resolve(pathname, filename); 40 | if (fs.existsSync(filepath)) { 41 | return require(filepath); 42 | } else { 43 | return null; 44 | } 45 | } 46 | 47 | //合并rule和mock(mock也是一种rule) 48 | function combineRuleAndMock(ruleid, mocks) { 49 | let rules = getRuleModule(ruleid) || {}; 50 | if (!mocks) return rules; 51 | //覆盖rules的beforeRequest 52 | return Object.assign(rules, { 53 | *beforeSendRequest(requestDetail) { 54 | //先调用rules的beforeRequest 55 | if (rules.beforeSendRequest) { 56 | rules.beforeSendRequest(requestDetail); 57 | } 58 | //再调用mock 59 | const reqOptions = requestDetail.requestOptions; 60 | let localResponse = null; 61 | mocks.forEach((item) => { 62 | let req = item.request; 63 | let res = item.response; 64 | let resHeader = res.headers.split(/[:;]/g); 65 | resHeader.pop(); 66 | if (requestDetail.url.indexOf(req.url) === 0 && 67 | reqOptions.method.toLowerCase() === req.method.toLowerCase()) { 68 | localResponse = { 69 | statusCode: res.status, 70 | header: util.getHeaderFromRawHeaders(resHeader), 71 | body: JSON.stringify(mockjs.mock(JSON.parse(res.body))) 72 | } 73 | } 74 | }); 75 | if (localResponse) { 76 | return { 77 | response: localResponse 78 | }; 79 | } 80 | } 81 | }); 82 | 83 | } 84 | 85 | //proxy工厂 86 | function createProxy(options) { 87 | return mainProxy || new ProxyServer(Object.assign({ 88 | rule: combineRuleAndMock(options.ruleid, options.mock), 89 | webInterface: { 90 | enable: false, 91 | }, 92 | port: 8001, 93 | forceProxyHttps: true 94 | }, options)); 95 | } 96 | 97 | //proxy回调 98 | function proxyCbManager(action, options) { 99 | if (action === 'start') { 100 | return function(resolve, reject) { 101 | if (mainProxy) { 102 | resolve({ 103 | msg: MSG_HAD_OPEN_PROXY, 104 | open: true, 105 | ip: options.ip || ip.address(), 106 | port: options.port 107 | }); 108 | } else { 109 | console.log('create proxy') 110 | mainProxy = createProxy(options); 111 | 112 | mainProxy.on('ready', () => { 113 | resolve({ 114 | msg: MSG_OPEN_PROXY_SUCCESS, 115 | open: true, 116 | ip: options.ip || ip.address(), 117 | port: options.port 118 | }); 119 | }); 120 | 121 | mainProxy.on('error', () => { 122 | mainProxy = null; 123 | reject({ 124 | msg: MSG_OPEN_PROXY_ERROR 125 | }) 126 | }); 127 | 128 | mainProxy.start(); 129 | } 130 | } 131 | } else if (action === 'stop') { 132 | return function(resolve, reject) { 133 | if (!mainProxy) { 134 | reject({ 135 | msg: MSG_HASNOT_OPEN_PROXY 136 | }); 137 | } else { 138 | mainProxy.close(); 139 | mainProxy = null; 140 | resolve({ 141 | msg: MSG_CLOSE_PROXY_SUCCESS 142 | }); 143 | } 144 | } 145 | } 146 | } 147 | 148 | module.exports = { 149 | /** 150 | * recorder 相关接口 151 | */ 152 | getSingleLog(id) { 153 | return new Promise((resolve, reject) => { 154 | if (global.recorder) { 155 | global.recorder.getSingleRecord(id, (err, data) => { 156 | if (err) { 157 | reject(err.toString()); 158 | } else { 159 | resolve(data[0]); 160 | } 161 | }) 162 | } else { 163 | reject(); 164 | } 165 | }); 166 | }, 167 | getlatestLog() { 168 | let self = this; 169 | return new Promise((resolve, reject) => { 170 | if (global.recorder) { 171 | global.recorder.getRecords(null, 10000, (err, docs) => { 172 | if (err) { 173 | reject(err.toString()); 174 | } else { 175 | resolve(docs); 176 | } 177 | }); 178 | } else { 179 | reject(); 180 | } 181 | }); 182 | 183 | }, 184 | fetchBody(id) { 185 | let self = this; 186 | return new Promise((resolve, reject) => { 187 | global.recorder.getDecodedBody(id, (err, result) => { 188 | if (err || !result || !result.content) { 189 | reject(); 190 | } else if (result.type && result.type === 'image' && result.mime) { 191 | resolve({ 192 | raw: true, 193 | type: result.mime, 194 | content: result.content 195 | }) 196 | } else { 197 | resolve({ 198 | id: id, 199 | type: result.type, 200 | content: result.content 201 | }) 202 | } 203 | }) 204 | }); 205 | }, 206 | clearRecorder() { 207 | // global.recorder && global.recorder.clear(); 208 | //there is bug in anyproxy clear 209 | }, 210 | offUpdate() { 211 | // global.recorder.off('update'); 212 | }, 213 | onUpdate(callback) { 214 | console.log('onUpdate'); 215 | global.recorder.on('update', (data) => { 216 | callback(data); 217 | }); 218 | }, 219 | sendRequest(host,data,callback1){ 220 | // var result = qs.parse(data,'\n',':'); 221 | var method = data.split('\n')[0].split(' ')[0]; 222 | var hostname = host.split(':')[0]; 223 | var port = host.split(':')[1]; 224 | var path = data.split('\n')[0].split(' ')[1]; 225 | var contents = ''; 226 | 227 | var header_arr = data.split('\n\n')[0]; 228 | var header_arr2 = header_arr.split('\n'); 229 | var header_arr3 = header_arr2.slice(0); 230 | header_arr3.shift(header_arr2[0]); 231 | var header_arr4 = header_arr3.join('\n'); 232 | var headers = qs.parse(header_arr4,'\n',':'); 233 | // if (method==='POST'){ 234 | var arr = data.split('\n\n'); 235 | var content = arr.slice(0); 236 | content.shift(); 237 | contents = content.join('\n\n'); 238 | // } 239 | 240 | var options = { 241 | hostname: hostname, 242 | port: port, 243 | path: path, 244 | headers:headers, 245 | method: method 246 | }; 247 | var req_func = function (options, contents ,callback) { 248 | var req = http.request(options, function (res) { 249 | var response = 'HTTP/' + res.httpVersion + ' ' + res.statusCode + ' ' + res.statusMessage + '\n'; 250 | for (var key in res.headers) { 251 | response += key +':' + res.headers[key] + '\n'; 252 | } 253 | var chunks = [],data,encoding = res.headers['content-encoding']; 254 | if(encoding === 'undefined'){ 255 | res.setEncoding('utf8'); 256 | } 257 | res.on('data', function (chunk) { 258 | chunks.push(chunk); 259 | }); 260 | res.on('end',function () { 261 | var buffer = Buffer.concat(chunks); 262 | if(encoding=='gzip'){ 263 | zlib.gunzip(buffer,function (err, decoded) { 264 | data = decoded.toString(); 265 | callback(err, response,data); 266 | }); 267 | }else if (encoding=='deflate'){ 268 | zlib.inflate(buffer,function (err, decoded) { 269 | data = decoded.toString(); 270 | callback(err, response,data); 271 | }); 272 | }else{ 273 | data = buffer.toString(); 274 | callback(null,response,data); 275 | } 276 | }); 277 | }); 278 | req.write(contents); 279 | req.end(); 280 | }; 281 | req_func(options, contents, function (err, response, data) { 282 | callback1(response + '\n\n' + data); 283 | }) 284 | }, 285 | /** 286 | * 证书相关接口 287 | */ 288 | generateRootCA(successCb, errorCb) { 289 | const isWin = /^win/.test(process.platform); 290 | if (!certMgr.ifRootCAFileExists()) { 291 | certMgr.generateRootCA((error, keyPath) => { 292 | if (!error) { 293 | const certDir = path.dirname(keyPath); 294 | console.log('The cert is generated at ', certDir); 295 | if (isWin) { 296 | exec('start .', {cwd: certDir}); 297 | } else { 298 | exec('open .', {cwd: certDir}); 299 | } 300 | successCb && successCb('success'); 301 | } else { 302 | errorCb && errorCb('error'); 303 | console.error('error when generating rootCA', error); 304 | } 305 | }); 306 | } else { 307 | console.log('c'); 308 | successCb && successCb('exist'); 309 | const rootPath = util.getAnyProxyPath('certificates'); 310 | if (!rootPath) return; 311 | if (isWin) { 312 | exec('start .', {cwd: rootPath}); 313 | } else { 314 | exec('open .', {cwd: rootPath}); 315 | } 316 | } 317 | }, 318 | /** 319 | * 代理相关API 320 | */ 321 | startProxy(options) { 322 | const startcb = proxyCbManager('start', options); 323 | return new Promise(startcb); 324 | }, 325 | stopProxy(options) { 326 | const stopcb = proxyCbManager('stop'); 327 | return new Promise(stopcb); 328 | }, 329 | /** 330 | * 规则相关API 331 | */ 332 | readRulesFromFile() { 333 | if (fs.existsSync(ruleFile)) { 334 | return fs.readFileSync(ruleFile, 'utf8'); 335 | } else { 336 | return '[]'; 337 | } 338 | }, 339 | saveRulesIntoFile(rules) { 340 | return new Promise((resolve, reject) => { 341 | fs.writeFile(ruleFile, JSON.stringify(rules), 'utf8', (err) => { 342 | if (err) { 343 | reject(); 344 | } else { 345 | resolve(); 346 | } 347 | }); 348 | }); 349 | }, 350 | deleteCustomRuleFile(id) { 351 | const filename = 'custom_' + id + '.js'; 352 | const rulepath = path.resolve(ruleCustomPath, filename); 353 | if (fs.existsSync(rulepath)) { 354 | fs.unlink(rulepath, (err) => { 355 | if (err) throw err; 356 | }); 357 | } 358 | }, 359 | saveCustomRuleToFile(id, rule) { 360 | const filename = 'custom_' + id + '.js'; 361 | if (!fs.existsSync(ruleCustomPath)) { 362 | fs.mkdir(ruleCustomPath); 363 | } 364 | 365 | const rulepath = path.resolve(ruleCustomPath, filename); 366 | 367 | fs.writeFile(rulepath, rule, 'utf8', (err) => { 368 | if (err) throw err; 369 | }); 370 | }, 371 | fetchCustomRule(id) { 372 | const filename = 'custom_' + id + '.js'; 373 | const rulepath = path.resolve(ruleCustomPath, filename); 374 | return new Promise((resolve, reject) => { 375 | if (fs.existsSync(rulepath)) { 376 | fs.readFile(rulepath, (err, data) => { 377 | if (err) { 378 | reject(''); 379 | } else { 380 | resolve(data.toString()); 381 | } 382 | }); 383 | } else { 384 | reject(''); 385 | } 386 | }); 387 | }, 388 | fetchSampleRule(rulename) { 389 | const filename = 'sample_' + rulename + '.js'; 390 | const rulePath = path.resolve(ruleSamplePath, filename); 391 | return new Promise((resolve, reject) => { 392 | if (fs.existsSync(rulePath)) { 393 | fs.readFile(rulePath, 'utf8', (err, data) => { 394 | if (err) { 395 | reject(''); 396 | } else { 397 | resolve(data.toString()); 398 | } 399 | }); 400 | } else { 401 | reject(''); 402 | } 403 | }); 404 | }, 405 | /** 406 | * Mock相关接口 407 | */ 408 | getMockProjects() { 409 | if (fs.existsSync(mockProjectsFile)) { 410 | return fs.readFileSync(mockProjectsFile, 'utf8'); 411 | } else { 412 | return '[]'; 413 | } 414 | }, 415 | saveMockProject(projects) { 416 | return new Promise((resolve, reject) => { 417 | fs.writeFile(mockProjectsFile, JSON.stringify(projects), 'utf8', (err) => { 418 | if (err) { 419 | reject() 420 | } else { 421 | resolve(); 422 | } 423 | }); 424 | }); 425 | }, 426 | getProjectPaths(id) { 427 | const filename = 'mock_' + id + '.js'; 428 | const mockpath = path.resolve(mockCustomPath, filename); 429 | return new Promise((resolve, reject) => { 430 | if (fs.existsSync(mockpath)) { 431 | fs.readFile(mockpath, (err, data) => { 432 | if (err) { 433 | reject(''); 434 | } else { 435 | resolve(data.toString()); 436 | } 437 | }); 438 | } else { 439 | reject(''); 440 | } 441 | }); 442 | }, 443 | saveProjectPaths(id, paths) { 444 | const filename = 'mock_' + id + '.js'; 445 | return new Promise((resolve, reject) => { 446 | if (!fs.existsSync(mockCustomPath)) { 447 | fs.mkdir(mockCustomPath); 448 | } 449 | 450 | const mockpath = path.resolve(mockCustomPath, filename); 451 | 452 | fs.writeFile(mockpath, JSON.stringify(paths), 'utf8', (err) => { 453 | if (err) { 454 | reject(); 455 | } else { 456 | resolve(); 457 | } 458 | }); 459 | }); 460 | }, 461 | } 462 | 463 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const menuTemplate = require('./menu.js'); 3 | const ipcMain = electron.ipcMain; 4 | const app = electron.app; 5 | const Menu = electron.Menu; 6 | const BrowserWindow = electron.BrowserWindow; 7 | let loadingParams = { 8 | width: 580, 9 | height: 200, 10 | frame: false, 11 | show: false 12 | }; 13 | let mainParams = { 14 | width: 1300, 15 | height: 780, 16 | icon: __dirname + '/icon1.png', 17 | // titleBarStyle: 'hidden-inset', 18 | backgroundColor: '#fff', 19 | show: false 20 | }; 21 | 22 | let mainWindow; 23 | 24 | function createWindow() { 25 | mainWindow = new BrowserWindow(mainParams); 26 | mainWindow.setTitle(require('./package.json').name); 27 | 28 | //setting in .vscode/launch.json 29 | if (process.env.NODE_ENV === 'development') { 30 | console.log('develop'); 31 | mainWindow.loadURL('http://localhost:8099'); 32 | mainWindow.webContents.openDevTools(); 33 | } else { 34 | mainWindow.loadURL(`file://${__dirname}/client/index.html`); 35 | } 36 | 37 | mainWindow.webContents.on('did-finish-load', () => { 38 | mainWindow.show(); 39 | if (loadingScreen) { 40 | loadingScreen.close(); 41 | } 42 | }); 43 | 44 | mainWindow.on('closed', () => { 45 | mainWindow = null; 46 | }); 47 | } 48 | 49 | function createLoadingScreen() { 50 | loadingScreen = new BrowserWindow(Object.assign(loadingParams, {parent: mainWindow})); 51 | 52 | if (process.env.NODE_ENV === 'development') { 53 | loadingScreen.loadURL('http://localhost:4000/loading.html'); 54 | } else { 55 | loadingScreen.loadURL(`file://${__dirname}/client/loading.html`); 56 | } 57 | 58 | loadingScreen.on('closed', () => loadingScreen = null); 59 | loadingScreen.webContents.on('did-finish-load', () => { 60 | loadingScreen.show(); 61 | }); 62 | } 63 | 64 | function createMenu() { 65 | const menu = Menu.buildFromTemplate(menuTemplate); 66 | Menu.setApplicationMenu(menu); 67 | } 68 | 69 | app.on('ready', () => { 70 | createLoadingScreen(); 71 | createWindow(); 72 | createMenu(); 73 | }); 74 | 75 | app.on('window-all-closed', () => { 76 | if (process.platform !== 'darwin') { 77 | app.quit(); 78 | } 79 | }); 80 | 81 | app.on('activate', () => { 82 | if (mainWindow === null) { 83 | createWindow(); 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /menu.js: -------------------------------------------------------------------------------- 1 | const {app} = require('electron'); 2 | const fs = require('fs'); 3 | const defaultSetting = require('./setting.json'); 4 | 5 | const settingPath = __dirname + '/setting.json'; 6 | 7 | function settingLang(label, win) { 8 | let setting = require('./setting.json'); 9 | const lang = setting.lang; 10 | if (label !== lang) { 11 | setting.lang = label; 12 | fs.writeFile(settingPath, JSON.stringify(setting), 'utf8', (err) => { 13 | if (err) { 14 | return; 15 | } else { 16 | win.reload(); 17 | } 18 | }); 19 | } 20 | } 21 | 22 | const template = [ 23 | { 24 | label: 'Edit', 25 | submenu: [ 26 | { 27 | role: 'undo' 28 | }, 29 | { 30 | role: 'redo' 31 | }, 32 | { 33 | type: 'separator' 34 | }, 35 | { 36 | role: 'cut' 37 | }, 38 | { 39 | role: 'copy' 40 | }, 41 | { 42 | role: 'paste' 43 | }, 44 | { 45 | role: 'pasteandmatchstyle' 46 | }, 47 | { 48 | role: 'delete' 49 | }, 50 | { 51 | role: 'selectall' 52 | } 53 | ] 54 | }, 55 | { 56 | label: 'View', 57 | submenu: [ 58 | { 59 | role: 'reload' 60 | }, 61 | { 62 | role: 'forcereload' 63 | }, 64 | // { 65 | // role: 'toggledevtools' 66 | // }, 67 | { 68 | type: 'separator' 69 | }, 70 | { 71 | role: 'resetzoom' 72 | }, 73 | { 74 | type: 'separator' 75 | }, 76 | { 77 | role: 'togglefullscreen' 78 | }, 79 | { 80 | label: 'language', 81 | submenu: [ 82 | { 83 | label: 'en', 84 | type: 'radio', 85 | checked: defaultSetting.lang == 'en', 86 | click(menuItem, browserWindow, event) { 87 | settingLang(menuItem.label, browserWindow); 88 | } 89 | }, 90 | { 91 | label: 'zh-CN', 92 | type: 'radio', 93 | checked: defaultSetting.lang == 'zh-CN', 94 | click(menuItem, browserWindow, event) { 95 | settingLang(menuItem.label, browserWindow); 96 | } 97 | } 98 | ] 99 | } 100 | ] 101 | }, 102 | { 103 | role: 'window', 104 | submenu: [ 105 | { 106 | role: 'minimize' 107 | }, 108 | { 109 | role: 'close' 110 | } 111 | ] 112 | }, 113 | { 114 | role: 'help', 115 | submenu: [ 116 | { 117 | label: 'Learn More', 118 | click () { require('electron').shell.openExternal('https://github.com/phantom0301/PTEye') } 119 | } 120 | ] 121 | } 122 | ] 123 | 124 | if (process.platform === 'darwin') { 125 | template.unshift({ 126 | label: app.getName(), 127 | submenu: [ 128 | { 129 | role: 'about' 130 | }, 131 | { 132 | type: 'separator' 133 | }, 134 | { 135 | role: 'services', 136 | submenu: [] 137 | }, 138 | { 139 | type: 'separator' 140 | }, 141 | { 142 | role: 'hide' 143 | }, 144 | { 145 | role: 'hideothers' 146 | }, 147 | { 148 | role: 'unhide' 149 | }, 150 | { 151 | type: 'separator' 152 | }, 153 | { 154 | role: 'quit' 155 | } 156 | ] 157 | }) 158 | // Edit menu. 159 | template[1].submenu.push( 160 | { 161 | type: 'separator' 162 | }, 163 | { 164 | label: 'Speech', 165 | submenu: [ 166 | { 167 | role: 'startspeaking' 168 | }, 169 | { 170 | role: 'stopspeaking' 171 | } 172 | ] 173 | } 174 | ) 175 | // Window menu. 176 | template[3].submenu = [ 177 | { 178 | label: 'Close', 179 | accelerator: 'CmdOrCtrl+W', 180 | role: 'close' 181 | }, 182 | { 183 | label: 'Minimize', 184 | accelerator: 'CmdOrCtrl+M', 185 | role: 'minimize' 186 | }, 187 | { 188 | label: 'Zoom', 189 | role: 'zoom' 190 | }, 191 | { 192 | type: 'separator' 193 | }, 194 | { 195 | label: 'Bring All to Front', 196 | role: 'front' 197 | } 198 | ] 199 | } 200 | 201 | module.exports = template; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-anyproxy", 3 | "version": "0.2.1", 4 | "description": "A Electron client for Anyproxy", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "electron .", 8 | "pack": "electron-packager . PTeye --out=pack --overwrite --ignore=client/node_modules --icon=icon.icns" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/fwon/electron-anyproxy.git" 13 | }, 14 | "keywords": [ 15 | "Electron", 16 | "Anyproxy", 17 | "proxy", 18 | "electron-vue" 19 | ], 20 | "author": "fwon", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/fwon/electron-anyproxy/issues" 24 | }, 25 | "homepage": "https://github.com/fwon/electron-anyproxy/blob/master/README.md", 26 | "dependencies": { 27 | "async": "~0.9.0", 28 | "async-task-mgr": ">=1.1.0", 29 | "body-parser": "^1.13.1", 30 | "co": "^4.6.0", 31 | "colorful": "^2.1.0", 32 | "commander": "~2.3.0", 33 | "compression": "^1.4.4", 34 | "express": "^4.8.5", 35 | "iconv-lite": "^0.4.6", 36 | "ip": "^0.3.2", 37 | "juicer": "^0.6.6-stable", 38 | "mime-types": "2.1.11", 39 | "mockjs": "^1.0.1-beta3", 40 | "nedb": "^0.11.0", 41 | "node-easy-cert": "^1.0.0", 42 | "node-forge": "^0.6.39", 43 | "npm": "^2.7.0", 44 | "pug": "^2.0.0-beta6", 45 | "qrcode-npm": "0.0.3", 46 | "stream-throttle": "^0.1.3", 47 | "ws": "^1.1.0" 48 | }, 49 | "devDependencies": { 50 | "cross-env": "^5.2.0", 51 | "devtron": "^1.4.0", 52 | "electron": "^1.6.2", 53 | "electron-packager": "^8.6.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const http = require('http'), 4 | https = require('https'), 5 | async = require('async'), 6 | color = require('colorful'), 7 | certMgr = require('./lib/certMgr'), 8 | Recorder = require('./lib/recorder'), 9 | logUtil = require('./lib/log'), 10 | util = require('./lib/util'), 11 | events = require('events'), 12 | ThrottleGroup = require('stream-throttle').ThrottleGroup; 13 | 14 | const T_TYPE_HTTP = 'http', 15 | T_TYPE_HTTPS = 'https', 16 | DEFAULT_CONFIG_PORT = 8088, 17 | DEFAULT_TYPE = T_TYPE_HTTP; 18 | 19 | const PROXY_STATUS_INIT = 'INIT'; 20 | const PROXY_STATUS_READY = 'READY'; 21 | const PROXY_STATUS_CLOSED = 'CLOSED'; 22 | 23 | /** 24 | * 25 | * @class ProxyServer 26 | * @extends {events.EventEmitter} 27 | */ 28 | class ProxyServer extends events.EventEmitter { 29 | 30 | /** 31 | * Creates an instance of ProxyServer. 32 | * 33 | * @param {object} config - configs 34 | * @param {number} config.port - port of the proxy server 35 | * @param {object} [config.rule=null] - rule module to use 36 | * @param {string} [config.type=http] - type of the proxy server, could be 'http' or 'https' 37 | * @param {strign} [config.hostname=localhost] - host name of the proxy server, required when this is an https proxy 38 | * @param {object} [config.webInterface] - config of the web interface 39 | * @param {boolean} [config.webInterface.enable=false] - if web interface is enabled 40 | * @param {number} [config.webInterface.webPort=8002] - http port of the web interface 41 | * @param {number} [config.webInterface.wsPort] - web socket port of the web interface 42 | * @param {number} [config.throttle] - speed limit in kb/s 43 | * @param {boolean} [config.forceProxyHttps=false] - if proxy all https requests 44 | * @param {boolean} [config.silent=false] - if keep the console silent 45 | * @param {boolean} [config.dangerouslyIgnoreUnauthorized=false] - if ignore unauthorized server response 46 | * 47 | * @memberOf ProxyServer 48 | */ 49 | constructor(config) { 50 | super(); 51 | config = config || {}; 52 | 53 | this.status = PROXY_STATUS_INIT; 54 | this.proxyPort = config.port; 55 | this.proxyType = /https/i.test(config.type || DEFAULT_TYPE) ? T_TYPE_HTTPS : T_TYPE_HTTP; 56 | this.proxyHostName = config.hostname || 'localhost'; 57 | this.proxyWebinterfaceConfig = config.webInterface; 58 | this.proxyConfigPort = config.webConfigPort || DEFAULT_CONFIG_PORT; //TODO : port to ui config server 59 | 60 | if (config.forceProxyHttps && !certMgr.ifRootCAFileExists()) { 61 | throw new Error('root CA not found. can not intercept https'); // TODO : give a reference to user 62 | } else if (this.proxyType === T_TYPE_HTTPS && !config.hostname) { 63 | throw new Error('hostname is required in https proxy'); 64 | } else if (!this.proxyPort) { 65 | throw new Error('proxy port is required'); 66 | } 67 | 68 | // ?? 69 | // currentRule.setInterceptFlag(true); 70 | // logUtil.printLog(color.blue("The WebSocket will not work properly in the https intercept mode :("), logUtil.T_TIP); 71 | 72 | this.httpProxyServer = null; 73 | this.requestHandler = null; 74 | 75 | // copy the rule to keep the original proxyRule independent 76 | this.proxyRule = config.rule || {}; 77 | 78 | if (config.silent) { 79 | logUtil.setPrintStatus(false); 80 | } 81 | 82 | if (config.throttle) { 83 | logUtil.printLog('throttle :' + config.throttle + 'kb/s'); 84 | const rate = parseInt(config.throttle, 10); 85 | if (rate < 1) { 86 | throw new Error('Invalid throttle rate value, should be positive integer'); 87 | } 88 | global._throttle = new ThrottleGroup({ rate: 1024 * rate }); // rate - byte/sec 89 | } 90 | 91 | // init recorder 92 | this.recorder = new Recorder(); 93 | global.recorder = this.recorder; // TODO 消灭这个global 94 | 95 | // init request handler 96 | const RequestHandler = util.freshRequire('./requestHandler'); 97 | this.requestHandler = new RequestHandler({ 98 | forceProxyHttps: !!config.forceProxyHttps, 99 | dangerouslyIgnoreUnauthorized: !!config.dangerouslyIgnoreUnauthorized 100 | }, this.proxyRule, this.recorder); 101 | } 102 | 103 | /** 104 | * start the proxy server 105 | * 106 | * @returns ProxyServer 107 | * 108 | * @memberOf ProxyServer 109 | */ 110 | start() { 111 | const self = this; 112 | if (self.status !== PROXY_STATUS_INIT) { 113 | throw new Error('server status is not PROXY_STATUS_INIT, can not run start()'); 114 | } 115 | async.series( 116 | [ 117 | //creat proxy server 118 | function (callback) { 119 | if (self.proxyType === T_TYPE_HTTPS) { 120 | certMgr.getCertificate(self.proxyHostName, (err, keyContent, crtContent) => { 121 | if (err) { 122 | callback(err); 123 | } else { 124 | self.httpProxyServer = https.createServer({ 125 | key: keyContent, 126 | cert: crtContent 127 | }, self.requestHandler.userRequestHandler); 128 | callback(null); 129 | } 130 | }); 131 | } else { 132 | self.httpProxyServer = http.createServer(self.requestHandler.userRequestHandler); 133 | callback(null); 134 | } 135 | }, 136 | 137 | //handle CONNECT request for https over http 138 | function (callback) { 139 | self.httpProxyServer.on('connect', self.requestHandler.connectReqHandler); 140 | callback(null); 141 | }, 142 | 143 | //start proxy server 144 | function (callback) { 145 | self.httpProxyServer.listen(self.proxyPort); 146 | callback(null); 147 | }, 148 | 149 | //start web socket service 150 | // function(callback){ 151 | // self.ws = new wsServer({ port : self.proxyWsPort }, self.recorder); 152 | // callback(null); 153 | // }, 154 | 155 | //set proxy rule 156 | // function(callback){ 157 | // if (self.interceptHttps) { 158 | // self.proxyRule.setInterceptFlag(true); 159 | // } 160 | // callback(null); 161 | // }, 162 | 163 | //start web interface 164 | function (callback) { 165 | if (self.proxyWebinterfaceConfig && self.proxyWebinterfaceConfig.enable) { 166 | const webInterface = require('./lib/webInterface'); 167 | self.webServerInstance = new webInterface(self.proxyWebinterfaceConfig, self); 168 | } 169 | callback(null); 170 | }, 171 | ], 172 | 173 | //final callback 174 | (err, result) => { 175 | if (!err) { 176 | const tipText = (self.proxyType === T_TYPE_HTTP ? 'Http' : 'Https') + ' proxy started on port ' + self.proxyPort; 177 | logUtil.printLog(color.green(tipText)); 178 | 179 | if (self.webServerInstance) { 180 | const webTip = 'web interface started on port ' + self.webServerInstance.webPort; 181 | logUtil.printLog(color.green(webTip)); 182 | } 183 | 184 | self.status = PROXY_STATUS_READY; 185 | self.emit('ready'); 186 | } else { 187 | const tipText = 'err when start proxy server :('; 188 | logUtil.printLog(color.red(tipText), logUtil.T_ERR); 189 | logUtil.printLog(err, logUtil.T_ERR); 190 | self.emit('error', { 191 | error: err 192 | }); 193 | } 194 | } 195 | ); 196 | 197 | return self; 198 | } 199 | 200 | 201 | /** 202 | * close the proxy server 203 | * 204 | * @returns ProxyServer 205 | * 206 | * @memberOf ProxyServer 207 | */ 208 | close() { 209 | // clear recorder cache 210 | this.recorder && this.recorder.clear(); 211 | 212 | this.httpProxyServer && this.httpProxyServer.close(); 213 | this.webServerInstance && this.webServerInstance.close(); 214 | 215 | this.recorder = null; 216 | this.httpProxyServer = null; 217 | this.webServerInstance = null; 218 | 219 | this.status = PROXY_STATUS_CLOSED; 220 | logUtil.printLog('server closed ' + this.proxyHostName + ':' + this.proxyPort); 221 | 222 | return this 223 | } 224 | } 225 | 226 | module.exports.ProxyServer = ProxyServer; 227 | module.exports.utils = { 228 | systemProxyMgr: require('./lib/systemProxyMgr'), 229 | certMgr, 230 | }; 231 | 232 | -------------------------------------------------------------------------------- /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 | .stackError { 13 | border-radius: 5px; 14 | padding: 20px; 15 | border: 1px solid #fdc; 16 | background-color: #ffeee6; 17 | color: #666; 18 | } 19 | .stackError li { 20 | list-style-type: none; 21 | } 22 | .infoItem { 23 | overflow: hidden; 24 | border: 1px solid #d5f1fd; 25 | background-color: #eaf8fe; 26 | border-radius: 4px; 27 | margin-bottom: 5px; 28 | } 29 | .infoItem .label { 30 | min-width: 70px; 31 | float: left; 32 | font-weight: 600; 33 | background-color: #76abc1; 34 | color: #fff; 35 | padding: 5px; 36 | } 37 | .infoItem .value { 38 | overflow:hidden; 39 | padding: 5px; 40 | } 41 | .tip { 42 | color: #808080; 43 | } 44 | body 45 | h1 # AnyProxy Inner Error 46 | h3 Oops! Error happend when AnyProxy handle the request. 47 | p.tip This is an error occurred inside AnyProxy, not from your target website. 48 | .infoItem 49 | .label 50 | | Error: 51 | .value #{error} 52 | .infoItem 53 | .label 54 | | URL: 55 | .value #{url} 56 | p 57 | ul.stackError 58 | each item in errorStack 59 | li= item -------------------------------------------------------------------------------- /resource/rule_default_backup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('./util'), 4 | bodyParser = require('body-parser'), 5 | path = require('path'), 6 | fs = require('fs'), 7 | logUtil = require('./log'), 8 | Q = require('q'); 9 | 10 | //e.g. [ { keyword: 'aaa', local: '/Users/Stella/061739.pdf' } ] 11 | let mapConfig = [], 12 | configFile = 'mapConfig.json'; 13 | function saveMapConfig(content,cb){ 14 | 15 | const d = Q.defer(); 16 | Q.fcall(function() { 17 | const anyproxyHome = utils.getAnyProxyHome(), 18 | mapCfgPath = path.join(anyproxyHome,configFile); 19 | 20 | if(typeof content == 'object'){ 21 | content = JSON.stringify(content); 22 | } 23 | return { 24 | path :mapCfgPath, 25 | content :content 26 | }; 27 | }) 28 | .then(function(config){ 29 | const d = Q.defer(); 30 | fs.writeFile(config.path, config.content, function(e){ 31 | if(e){ 32 | d.reject(e); 33 | }else{ 34 | d.resolve(); 35 | } 36 | }); 37 | return d.promise; 38 | }) 39 | .catch(function(e){ 40 | cb && cb(e); 41 | }) 42 | .done(function(){ 43 | cb && cb(); 44 | }); 45 | } 46 | function getMapConfig(cb){ 47 | const read = Q.denodeify(fs.readFile); 48 | 49 | Q.Promise(function(resolve,reject){ 50 | var anyproxyHome = utils.getAnyProxyHome(), 51 | mapCfgPath = path.join(anyproxyHome,configFile); 52 | 53 | resolve(mapCfgPath); 54 | }) 55 | .then(read) 56 | .then(function(content){ 57 | return JSON.parse(content); 58 | }) 59 | .catch(function(e){ 60 | cb && cb(e); 61 | }) 62 | .done(function(obj){ 63 | cb && cb(null,obj); 64 | }); 65 | } 66 | 67 | setTimeout(function(){ 68 | //load saved config file 69 | getMapConfig(function(err,result){ 70 | if(result){ 71 | mapConfig = result; 72 | } 73 | }); 74 | },1000); 75 | 76 | 77 | module.exports = { 78 | summary:function(){ 79 | var tip = 'the default rule for AnyProxy.'; 80 | return tip; 81 | }, 82 | 83 | shouldUseLocalResponse : function(req,reqBody){ 84 | const d = Q.defer(); 85 | //intercept all options request 86 | var simpleUrl = (req.headers.host || '') + (req.url || ''); 87 | mapConfig.map(function(item){ 88 | var key = item.keyword; 89 | if(simpleUrl.indexOf(key) >= 0){ 90 | req.anyproxy_map_local = item.local; 91 | return false; 92 | } 93 | }); 94 | d.resolve(!!req.anyproxy_map_local); 95 | return d.promise; 96 | }, 97 | 98 | dealLocalResponse : function(req,reqBody,callback){ 99 | const d = Q.defer(); 100 | if(req.anyproxy_map_local){ 101 | fs.readFile(req.anyproxy_map_local,function(err,buffer){ 102 | if(err){ 103 | d.resolve({ 104 | code: 200, 105 | header: {}, 106 | body: '[AnyProxy failed to load local file] ' + err 107 | }); 108 | }else{ 109 | var header = { 110 | 'Content-Type': utils.contentType(req.anyproxy_map_local) 111 | }; 112 | d.resolve({ 113 | code: 200, 114 | header: header, 115 | body: buffer 116 | }); 117 | } 118 | }); 119 | } 120 | 121 | return d.promise; 122 | }, 123 | 124 | replaceRequestProtocol:function *(req,protocol){ 125 | return protocol; 126 | }, 127 | 128 | replaceRequestOption : function *(req,option){ 129 | return option; 130 | }, 131 | 132 | replaceRequestData: function *(req,data){ 133 | return data; 134 | }, 135 | 136 | replaceResponseStatusCode: function *(req,res,statusCode){ 137 | return statusCode; 138 | }, 139 | 140 | replaceResponseHeader: function *(req,res,header){ 141 | return header; 142 | }, 143 | 144 | replaceServerResData: function *(req,res,serverResData){ 145 | return serverResData; 146 | }, 147 | 148 | shouldInterceptHttpsReq:function *(req){ 149 | return false; 150 | }, 151 | 152 | _plugIntoWebinterface: function(app,cb){ 153 | 154 | app.get('/filetree',function(req,res){ 155 | try{ 156 | var root = req.query.root || utils.getUserHome() || '/'; 157 | utils.filewalker(root,function(err, info){ 158 | res.json(info); 159 | }); 160 | }catch(e){ 161 | res.end(e); 162 | } 163 | }); 164 | 165 | app.use(bodyParser.json()); 166 | app.get('/getMapConfig',function(req,res){ 167 | res.json(mapConfig); 168 | }); 169 | app.post('/setMapConfig',function(req,res){ 170 | mapConfig = req.body; 171 | res.json(mapConfig); 172 | 173 | saveMapConfig(mapConfig); 174 | }); 175 | 176 | cb(); 177 | }, 178 | 179 | _getCustomMenu : function(){ 180 | return [ 181 | // { 182 | // name:'test', 183 | // icon:'uk-icon-lemon-o', 184 | // url :'http://anyproxy.io' 185 | // } 186 | ]; 187 | } 188 | }; -------------------------------------------------------------------------------- /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 | *beforeSendRequest(requestDetail) { 11 | if (requestDetail.url.indexOf('http://httpbin.org/post') === 0) { 12 | return { 13 | requestData: 'i-am-anyproxy-modified-post-data' 14 | }; 15 | } 16 | }, 17 | }; 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 httpbin.org requests to http://httpbin.org/user-agent 4 | test: 5 | curl http://httpbin.org/any-path --proxy http://127.0.0.1:8001 6 | expected response: 7 | { "user-agent": "curl/7.43.0" } 8 | */ 9 | module.exports = { 10 | *beforeSendRequest(requestDetail) { 11 | if (requestDetail.url.indexOf('http://httpbin.org') === 0) { 12 | const newRequestOptions = requestDetail.requestOptions; 13 | newRequestOptions.path = '/user-agent'; 14 | newRequestOptions.method = 'GET'; 15 | return { 16 | requestOptions: newRequestOptions 17 | }; 18 | } 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /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 | summary() { return 'a rule to modify response'; }, 12 | *beforeSendResponse(requestDetail, responseDetail) { 13 | if (requestDetail.url === 'http://httpbin.org/user-agent') { 14 | const newResponse = responseDetail.response; 15 | newResponse.body += '-- AnyProxy Hacked! --'; 16 | return new Promise((resolve, reject) => { 17 | setTimeout(() => { // delay the response for 5s 18 | resolve({ response: newResponse }); 19 | }, 5000); 20 | }); 21 | } 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /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_unauthorized_access_vulnerability.js: -------------------------------------------------------------------------------- 1 | /* 2 | 插件名称:越权访问 3 | 插件描述:利用 Cookie 替换测试是否存在越权漏洞。首先在关闭插件的情况下登录 a 账号,利用 b 账号的凭据(Cookie/Session) 4 | 替换插件中的相应字段,测试 a 的逻辑功能是否可以执行,可以执行即存在越权漏洞。 5 | */ 6 | 7 | module.exports = { 8 | *beforeSendRequest(requestDetail) { 9 | if (requestDetail.url.indexOf('http://httpbin.org') === 0) { 10 | const newRequestOptions = requestDetail.requestOptions; 11 | newRequestOptions.headers['Cookie'] = 'xxxxxx'; 12 | return { 13 | requestOptions: newRequestOptions 14 | }; 15 | } 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /setting.json: -------------------------------------------------------------------------------- 1 | {"lang":"zh-CN"} --------------------------------------------------------------------------------