├── .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 | 
87 |
88 | ### 加载漏洞插件 ###
89 |
90 | 
91 |
92 | ### 报文重放 ###
93 |
94 | 
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 |
2 |
3 |
4 |
5 |
6 | {{ $t("ap.bigtab.a") }}
7 |
8 |
9 | {{ $t("ap.bigtab.b") }}
10 |
11 |
12 | {{ $t("ap.bigtab.d") }}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
101 |
102 |
119 |
--------------------------------------------------------------------------------
/client/src/components/TreeViewItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{getKey(data)}}
6 | {{data.children.length}} property
7 | {{data.children.length}} properties
8 |
9 |
10 |
11 |
12 |
13 | {{getKey(data)}}
14 | {{data.children.length}} item
15 | {{data.children.length}} items
16 |
17 |
18 |
19 |
20 | {{getKey(data)}}
21 | {{getValue(data)}}
22 |
23 |
24 |
25 |
26 |
101 |
102 |
160 |
--------------------------------------------------------------------------------
/client/src/components/filter.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
38 |
--------------------------------------------------------------------------------
/client/src/components/menu.vue:
--------------------------------------------------------------------------------
1 |
2 |
80 |
81 |
262 |
--------------------------------------------------------------------------------
/client/src/components/mock.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ $t("ap.mocklist.addbtn") }}
6 |
7 |
11 |
12 |
{{ $t("ap.mocklist.addbtn") }}
13 |
23 |
24 |
25 |
26 | {{ $t("ap.mocklist.addapibtn") }}
27 |
28 |
29 |
Request Url
30 |
31 | Request Method
32 |
33 |
34 |
35 |
Status Code
36 |
37 | Response Headers
38 |
39 | Response Body
40 |
41 |
42 |
43 |
47 |
48 |
{{ $t("ap.mocklist.currentpro") }}{{currentProject.name}}
49 |
50 | {{ $t("ap.mocklist.currenttip") }}
51 |
52 |
53 |
{{ $t("ap.mocklist.addapibtn") }}
54 |
59 |
62 |
63 |
67 |
68 |
72 |
73 |
74 |
75 |
78 | {{ $t("ap.mocklist.editbtn") }}
79 |
83 | {{ $t("ap.mocklist.delbtn") }}
84 |
85 |
86 |
87 |
88 |
89 |
90 |
264 |
--------------------------------------------------------------------------------
/client/src/components/network-detail.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Request URL: {{headers.url}}
7 | Request Method: {{headers.method}}
8 | Status Code: {{headers.statusCode}}
9 | Protocol: {{headers.protocol}}
10 |
11 |
12 |
13 | {{key}}: {{value}}
14 |
15 |
16 |
17 |
18 | {{key}}: {{value}}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {{detailContent.content}}
28 |
29 |
30 |
34 |
38 |
39 |
42 |
43 |
44 |
45 |
46 |
47 |
133 |
--------------------------------------------------------------------------------
/client/src/components/network.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
26 |
27 |
31 |
32 |
36 |
37 |
41 |
42 |
46 |
47 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
155 |
--------------------------------------------------------------------------------
/client/src/components/request.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ $t("ap.request.addbtn") }}
6 | {{ $t("ap.request.clear") }}
7 |
8 |
9 |
10 |
11 |
12 | 重放请求地址
13 |
14 |
15 |
16 |
17 |
18 |
19 |
221 |
--------------------------------------------------------------------------------
/client/src/components/rule.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $t("ap.rulepop.title") }}
5 |
6 |
7 | {{ $t("ap.rulepop.sample") }}
8 |
9 |
13 |
14 |
15 |
16 |
17 | {{ $t("ap.rulepop.name") }}
18 |
19 |
20 |
21 |
{{ $t("ap.rulepop.content") }}
22 |
23 |
24 |
25 |
29 |
30 |
31 |
{{ $t("ap.rulelist.addbtn") }}
32 |
36 |
40 |
41 |
42 |
43 |
44 |
47 |
48 |
49 |
50 |
53 | {{ $t("ap.rulelist.editbtn") }}
54 |
58 | {{ $t("ap.rulelist.delbtn") }}
59 |
64 | {{ $t("ap.rulelist.cancelbtn") }}
65 |
70 | {{ $t("ap.rulelist.usebtn") }}
71 |
72 | {{ $t("ap.rulelist.tip") }}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
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"}
--------------------------------------------------------------------------------