├── .gitattributes
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── cmd
├── root.go
├── server.go
└── version.go
├── conf
└── app.toml
├── deploy
└── mars.supervisor.conf
├── go.mod
├── go.sum
├── interceptor
├── example.go
└── interceptor.go
├── internal
├── app
│ ├── app.go
│ ├── config
│ │ └── config.go
│ ├── inject
│ │ └── container.go
│ └── inspector
│ │ ├── controller
│ │ ├── controller.go
│ │ └── inspector.go
│ │ └── router.go
├── common
│ ├── queue.go
│ ├── queue_test.go
│ ├── recorder
│ │ ├── body.go
│ │ ├── cert_cache.go
│ │ ├── output
│ │ │ ├── action
│ │ │ │ └── action.go
│ │ │ ├── console.go
│ │ │ └── websocket.go
│ │ ├── recorder.go
│ │ ├── request.go
│ │ ├── response.go
│ │ ├── storage
│ │ │ ├── leveldb.go
│ │ │ └── leveldb_test.go
│ │ └── transaction.go
│ ├── socket
│ │ ├── codec
│ │ │ ├── codec.go
│ │ │ ├── foo_test.go
│ │ │ ├── foo_test.proto
│ │ │ ├── json.go
│ │ │ ├── json_test.go
│ │ │ ├── protobuf.go
│ │ │ └── protobuf_test.go
│ │ ├── conn
│ │ │ ├── conn.go
│ │ │ ├── websocket.go
│ │ │ └── websocket_test.go
│ │ ├── context.go
│ │ ├── hub.go
│ │ ├── hub_test.go
│ │ ├── message
│ │ │ ├── message.pb.go
│ │ │ ├── message.proto
│ │ │ ├── registry.go
│ │ │ ├── registry_test.go
│ │ │ └── type.go
│ │ ├── router.go
│ │ └── session.go
│ └── version
│ │ └── version.go
└── statik
│ └── statik.go
├── main.go
├── screenshot
├── detail.png
└── list.png
├── script
└── package.sh
└── web
├── public
├── index.html
├── mitm-proxy.crt
├── robots.txt
└── static
│ ├── css
│ └── app.456feddc172b076dc828362d1077a2d8.css
│ ├── fonts
│ └── element-icons.6f0a763.ttf
│ └── js
│ ├── app.4ac0d8d9ce25ca25174c.js
│ ├── manifest.047c133009965c5daaca.js
│ └── vendor.99215b533571af4c9162.js
└── vue
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── README.md
├── build
├── build.js
├── check-versions.js
├── logo.png
├── utils.js
├── vue-loader.conf.js
├── webpack.base.conf.js
├── webpack.dev.conf.js
└── webpack.prod.conf.js
├── config
├── dev.env.js
├── index.js
└── prod.env.js
├── index.html
├── package.json
├── src
├── App.vue
├── assets
│ └── logo.png
├── components
│ └── common
│ │ └── navMenu.vue
├── main.js
└── socket
│ └── message.js
├── static
└── .gitkeep
└── yarn.lock
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.js linguist-language=go
2 | *.css linguist-language=go
3 | *.html linguist-language=go
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
3 |
4 | /vendor
5 | /bin
6 | /mars-build
7 | /mars-package
8 |
9 | node_modules
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:3.7
2 |
3 | RUN apk add --no-cache ca-certificates tzdata \
4 | && addgroup -S app \
5 | && adduser -S -g app app \
6 | && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
7 |
8 | WORKDIR /app
9 |
10 | COPY . .
11 |
12 | RUN chown -R app:app ./
13 |
14 | EXPOSE 8888
15 | EXPOSE 9999
16 |
17 | USER app
18 |
19 | ENTRYPOINT ["/app/mars", "server"]
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2018 qiang.ou
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | BINARY_NAME=mars
3 | MAIN_FILE=main.go
4 | RUNTIME_MODE=dev
5 |
6 | build:
7 | go build $(RACE) -o bin/${BINARY_NAME} ${MAIN_FILE}
8 |
9 | build-race: enable-race build
10 |
11 | run: build
12 | ./bin/${BINARY_NAME} server --env ${RUNTIME_MODE}
13 |
14 | run-race: enable-race run
15 |
16 | test:
17 | go test $(RACE) ./...
18 |
19 | test-race: enable-race test
20 |
21 | enable-race:
22 | $(eval RACE = -race)
23 |
24 | package:
25 | bash ./script/package.sh
26 |
27 | package-all:
28 | bash ./script/package.sh -p 'linux darwin windows'
29 |
30 | build-vue:
31 | cd web/vue && yarn run build
32 | rm -rf web/public/static
33 | cp -r web/vue/dist/* web/public/
34 |
35 | install-vue:
36 | cd web/vue && yarn install
37 |
38 | run-vue:
39 | cd web/vue && yarn run dev
40 |
41 | statik:
42 | go generate ./...
43 |
44 | clean:
45 | rm bin/${BINARY_NAME}
46 |
47 | .PHONY: clean statik run-vue install-vue build-vue package-all package enable-race
48 | .PHONY: test-race test build build-race run run-race
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mars - HTTP(S)代理, 用于抓包调试
2 |
3 | [](https://github.com/ouqiang/mars/blob/master/LICENSE)
4 | [](https://github.com/ouqiang/mars/releases)
5 |
6 |
7 |
8 | 功能特性
9 | ----
10 | * 作为普通HTTP(S)代理服务器使用
11 | * 抓包调试, web页面查看流量
12 | * 流量持久化到`leveldb`中, 可用于后期分析
13 | * 拦截请求自定义逻辑
14 |
15 |
16 | 截图
17 | ---
18 |
19 | 
20 | 
21 |
22 |
23 |
24 | ## 目录
25 |
26 | * [下载](#下载)
27 | * [安装](#安装)
28 | * [二进制安装](#二进制安装)
29 | * [源码安装](#源码安装)
30 | * [配置文件](#配置文件)
31 | * [命令](#命令)
32 | * [结合其他程序使用](#结合其他程序使用)
33 | * [Nginx](#nginx)
34 | * [frp](#frp)
35 | * [开发](#开发)
36 | * [服务端](#服务端)
37 | * [前端](#前端)
38 | * [基于已有代码开发](#基于已有代码开发)
39 | * [自定义实现](#自定义实现)
40 |
41 | ## 下载
42 |
43 | [releases](https://github.com/ouqiang/mars/releases)
44 |
45 | ## 安装
46 |
47 |
48 | ### 二进制安装
49 | 1. 解压压缩包
50 | 2. 启动: ./mars server
51 | 3. 访问代理: http://localhost:8888
52 | 4. 查看流量web页: http://localhost:9999, 客户端可扫描二维码下载根证书
53 |
54 |
55 | ### docker
56 |
57 | ```bash
58 | docker run --name mars -p 8888:8888 -p 9999:9999 -d ouqg/mars:1.0.0
59 | ```
60 |
61 | 配置: /app/conf/app.toml
62 |
63 | ### 源码安装
64 | Go版本1.11+
65 | 启用go module
66 | ```bash
67 | export GO111MODULE=on
68 | ```
69 |
70 | 启动: `make run`
71 |
72 | ## 配置文件
73 | 配置支持通过环境变量覆盖, 如`MARS_APP_PROXYPORT=8080`
74 |
75 | ```toml
76 | [app]
77 | host = "0.0.0.0"
78 | # 代理监听端口
79 | proxyPort = 8888
80 | # 查看流量web页监听端口
81 | inspectorPort = 9999
82 |
83 |
84 | [mitmProxy]
85 | # 是否开启中间人代理,不开启则盲转发
86 | enabled = true
87 | # 是否解密HTTPS, 客户端系统需安装根证书
88 | decryptHTTPS = false
89 | # 证书缓存大小
90 | certCacheSize = 1000
91 | # 数据缓存大小
92 | leveldbCacheSize = 1000
93 | ```
94 |
95 | ## 命令
96 |
97 | ### 查看版本
98 | ```bash
99 | $ ./mars version
100 |
101 | Version: v1.0.0
102 | Go Version: go1.11
103 | Git Commit: 2151a6d
104 | Built: 2018-11-18 20:04:17
105 | OS/ARCH: darwin/amd64
106 | ```
107 |
108 | ### 命令行参数
109 | ```bash
110 | $ ./mars server -h
111 | run proxy server
112 |
113 | Usage:
114 | mars server [flags]
115 |
116 | Flags:
117 | -c, --configFile string config file path (default "conf/app.toml")
118 | -e, --env string dev | prod (default "prod")
119 | -h, --help help for server
120 | ```
121 |
122 |
123 | ## 结合其他程序使用
124 |
125 | 经过`mars`的流量可在web页查看
126 |
127 | ### Nginx
128 | 请求包含特定header, 则转发给`mars`, 由`mars`访问实际的后端
129 |
130 | 原配置
131 | ```nginx
132 | proxy_pass http://172.16.10.103:8080;
133 | ```
134 |
135 | 使用`mars`后的配置
136 | ```nginx
137 | set $targetHost $host;
138 |
139 | if ($http_x_mars_debug = "1") {
140 | set $targetHost "172.16.10.103:8080";
141 | }
142 |
143 | proxy_set_header X-Mars-Host $host;
144 | proxy_set_header Host $targetHost;
145 | if ($http_x_mars_debug = "1") {
146 | proxy_pass http://127.0.0.1:8888;
147 | break;
148 | }
149 | ```
150 |
151 | ### frp
152 |
153 | 原配置
154 |
155 | ```ini
156 | [web]
157 | type = http
158 | local_ip = 127.0.0.1
159 | local_port = 80
160 | subdomain = test
161 | ```
162 |
163 | 使用`mars`后的配置
164 |
165 | ```ini
166 | [web]
167 | type = http
168 | local_ip = 127.0.0.1
169 | local_port = 8888
170 | subdomain = test
171 | host_header_rewrite = 127.0.0.1:80
172 | ```
173 |
174 | ## 开发
175 |
176 | ### 服务端
177 |
178 | #### 拦截请求
179 | 实现`Interceptor`接口, 参考`interceptor/example.go`
180 | ```go
181 | // Interceptor 拦截器
182 | type Interceptor interface {
183 | // Connect 收到客户端连接, 自定义response返回
184 | Connect(ctx *goproxy.Context, rw http.ResponseWriter)
185 | // BeforeRequest 请求发送前, 修改request
186 | BeforeRequest(ctx *goproxy.Context)
187 | // BeforeResponse 响应发送前, 修改response
188 | BeforeResponse(ctx *goproxy.Context, resp *http.Response, err error)
189 | }
190 | ```
191 |
192 | ### 自定义存储
193 | 默认存储为`leveldb`
194 |
195 | 实现`Storage`接口
196 | ```go
197 | // Storage 存取transaction接口
198 | type Storage interface {
199 | Get(txId string) (*Transaction, error)
200 | Put(*Transaction) error
201 | }
202 | ```
203 |
204 | #### 自定义输出
205 | 内置输出到console、websocket
206 | 实现`Output`接口
207 | ```go
208 | // Output 输出transaction接口
209 | type Output interface {
210 | Write(*Transaction) error
211 | }
212 | ```
213 |
214 | ### make
215 |
216 | * `make` 编译
217 | * `make run` 编译并运行
218 | * `make package` 生成当前平台的压缩包
219 | * `make package-all` 生成Windows、Linux、Mac的压缩包
220 |
221 |
222 | ## 前端
223 |
224 | ### 基于已有代码开发
225 | Vue + Element UI
226 |
227 | 1. 安装Go1.11+, Node.js, Yarn
228 | 2. 安装前端依赖 `make install-vue`
229 | 3. 启动mars: `make run`
230 | 4. 启动node server: `make run-vue`
231 | 5. App.vue中替换websocket连接地址为`http://localhost:9999`
232 | 6. 打包: `make build-vue`
233 | 7. 安装静态文件嵌入工具`go get github.com/rakyll/statik`
234 | 8. 静态文件嵌入`make statik`
235 |
236 | ### 自定义实现
237 | 基于websocket, 消息序列化协议: `json`
238 | 消息格式, 请求响应相同
239 | ```json
240 | {
241 | "type": 3000,
242 | "payload": {}
243 | }
244 | ```
245 |
246 | type取值范围
247 |
248 | - 1000 \- 1999 客户端请求
249 | - 2000 \- 2999 服务端响应
250 | - 3000\+ 服务端主动推送
251 |
252 |
253 | ### websocket连接
254 | ```javascript
255 | var ws = new WebSocket('http://localhost:9999/ws')
256 | ```
257 |
258 | ### 发送心跳
259 | > 心跳超时为30秒,连续两次超时将断开连接
260 |
261 | 请求
262 | ```json
263 | {
264 | "type": 1000,
265 | "payload": {}
266 | }
267 | ```
268 | 响应
269 | ```json
270 | {
271 | "type": 2000,
272 | "payload": {}
273 | }
274 | ```
275 |
276 | ### 服务端主动推送
277 |
278 | 只包含transaction基本信息
279 | ```json
280 | {
281 | "type": 3000,
282 | "payload": {
283 | "id": "b56cb03d-855c-48a2-81e8-bbf71f285ecf",
284 | "method": "GET",
285 | "host": "api.zhihu.com",
286 | "path": "/user-permission",
287 | "duration": 235519397,
288 | "response_status_code": 200,
289 | "response_err": "",
290 | "response_content_type": "application/json",
291 | "response_len": 135
292 | }
293 | }
294 | ```
295 |
296 | ### 获取transaction详情
297 |
298 | 请求
299 | ```json
300 | {
301 | "type": 1002,
302 | "payload": {
303 | "id": "b56cb03d-855c-48a2-81e8-bbf71f285ecf"
304 | }
305 | }
306 | ```
307 |
308 | 响应
309 | ```json
310 | {
311 | "type": 2002,
312 | "payload": {
313 | "id": "b56cb03d-855c-48a2-81e8-bbf71f285ecf",
314 | "request": {
315 | "proto": "HTTP/1.1",
316 | "method": "GET",
317 | "scheme": "https",
318 | "host": "api.zhihu.com",
319 | "path": "/user-permission",
320 | "query_param": "",
321 | "url": "https://api.zhihu.com/user-permission",
322 | "header": {
323 | "Accept": [
324 | "*/*"
325 | ],
326 | "Accept-Encoding": [
327 | "gzip"
328 | ],
329 | "Accept-Language": [
330 | "zh-Hans-CN;q=1"
331 | ],
332 | "Authorization": [
333 | "Bearer 1.1bLEjAAAAAAALAAAAYAJVTV24_lu6fCJ9XPT_pE20Kz4dyYRvaGS-ig=="
334 | ],
335 | "Connection": [
336 | "keep-alive"
337 | ],
338 | "Cookie": [
339 | "__DAYU_PP=AIzQaiFNaFRiERJrz3j2283b3752faa9; q_c1=51271b5b715943cb88a2f422af28e9c9|1538884401000|1520866854000; tgw_l7_route=b3dca7eade474617fe4df56e6c4934a3; q_c1=51271b5b715943cb88a2f422af28e9c9|1520866854000|1520866854000; q_c0=2|1:0|10:1540828001|4:q_c0|80:MS4xYkxFakFBQUFBQUFMQUFBQVlBSlZUVjI0X2x1NmZDSjlYUFRfcEUyMEt6NGR5WVJ2YUdTLWlnPT0=|8c53af477e6ab8308cceb3dde6fe1d526a61448ee32d75f28ad0f27636bcc31e; _xsrf=RT2pK2qfoxGR1wz3dOglH9qN9uYhQFVK; _zap=2f1250a9-d774-4358-9dde-e23cb7b10c29; z_c0=\"2|1:0|10:1540828001|4:z_c0|80:MS4xYkxFakFBQUFBQUFMQUFBQVlBSlZUVjI0X2x1NmZDSjlYUFRfcEUyMEt6NGR5WVJ2YUdTLWlnPT0=|e7557ab66cf63a82219c13a6851422df7ca3c045eb94a5ab346ee3e9a69894c2\"; d_c0=ADACbVFThQtLBcMEYw22mGbJ-vZ5Oy5ayzg=|1542635918; zst_82=1.0AOAhgq8kiw4LAAAASwUAADEuMI3B8lsAAAAAiqMViyRX9WuT68jWP347p59s3mQ="
340 | ],
341 | "User-Agent": [
342 | "osee2unifiedRelease/4.27.1 (iPad; iOS 12.0; Scale/2.00)"
343 | ],
344 | "X-Ab-Param": [
345 | "top_gr_model=0;top_hweb=0;ls_new_video=1;se_consulting_switch=off;top_feedre_rtt=41;top_root_web=0;tp_favsku=a;se_ad_index=9;se_wiki_box=1;top_recall_deep_user=1;top_roundtable=1;top_uit=0;tp_sft=a;zr_ans_rec=gbrank;se_gemini_service=content;top_billboard_count=1;top_multi_model=0;top_recall=1;top_root_few_topic=0;top_vd_gender=0;top_manual_tag=1;top_feedre_itemcf=31;se_majorob_style=0;top_vds_alb_pos=0;se_minor_onebox=d;top_root_ac=1;top_new_user_gift=0;top_memberfree=1;top_video_rew=0;top_ad_slot=1;top_recall_tb=4;top_video_fix_position=5;top_follow_reason=0;top_sjre=0;se_merger=1;se_refactored_search_index=0;top_billpic=0;top_local=1;top_user_gift=0;top_tagore_topic=0;top_yc=0;top_hca=0;se_rescore=1;top_newfollow=0;top_recall_follow_user=91;top_recommend_topic_card=0;top_tffrt=0;top_recall_tb_follow=71;top_card=-1;top_gr_topic_reweight=0;top_login_card=1;tp_write_pin_guide=3;top_an=0;top_recall_tb_long=51;top_spec_promo=1;pin_ef=orig;se_consulting_price=n;top_mt=0;top_quality=0;se_cm=1;top_nid=0;top_nmt=0;top_distinction=3;se_billboard=3;top_recall_tb_short=61;top_rerank_reweight=-1;top_wonderful=1;top_billupdate1=3;se_new_market_search=on;se_relevant_query=new;top_mlt_model=4;top_newfollowans=0;top_nuc=0;top_sj=2;top_vdio_rew=3;se_tf=1;top_gif=0;top_rank=0;top_tmt=0;top_lowup=1;top_tagore=1;top_video_score=1;se_correct_ab=1;top_ebook=0;top_rerank_isolation=-1;se_major_onebox=major;top_vd_rt_int=0;top_adpar=0;top_root=1;top_tuner_refactor=-1;top_v_album=1;top_no_weighing=1;top_yhgc=0;se_dl=1;top_deep_promo=0;top_fqai=2;top_rerank_repos=-1;top_tr=0;top_dtmt=2;top_raf=n;top_rerank_breakin=-1;se_product_rank_list=0;top_hqt=9;top_nad=1;top_promo=1;top_universalebook=1;top_f_r_nb=1;se_gi=0;se_ingress=on;se_websearch=0;top_test_4_liguangyi=1;top_topic_feedre=21;top_billread=1;top_billvideo=0;top_ntr=1;top_recall_core_interest=81;tp_discussion_feed_type_android=0;top_new_feed=1;top_ac_merge=0;top_slot_ad_pos=1;top_bill=0;top_feedre=1;top_pfq=5;top_retag=0;top_nszt=0;se_entity=on;se_shopsearch=1;pin_efs=orig;se_daxuechuisou=new;top_followtop=1;ls_is_use_zrec=0;se_ltr=1;top_videos_priority=-1;top_alt=0;tp_ios_topic_write_pin_guide=1;se_filter=0;top_30=0;top_feedre_cpt=101;top_vd_op=1;se_dt=1;top_billab=0;tp_discussion_feed_card_type=0;top_ab_validate=3;top_cc_at=1;top_free_content=-1;top_root_mg=1;top_is_gr=0;top_tag_isolation=0;ls_new_score=0;se_cq=0;top_fqa=0;top_feedtopiccard=0;top_retagg=0;se_engine=0;top_gr_auto_model=0;top_nucc=3;se_auto_syn=0"
346 | ],
347 | "X-Ad-Styles": [
348 | "brand_card_article_video=4;plutus_card_word_30_download=2;brand_feed_small_image=3;plutus_card_video_8_download=4;plutus_card_multi_images_30_download=2;plutus_card_image_8=1;brand_feed_hot_small_image=1;plutus_card_multi_images=5;plutus_card_window=2;plutus_card_window_8=2;brand_card_normal=3;plutus_card_image_31=2;plutus_card_multi_images_30=3;plutus_card_multi_images_8_download=1;plutus_card_word_8_download=1;plutus_card_image_31_download=2;plutus_card_slide_image_31_download=1;brand_card_question=4;brand_card_article_multi_image=5;brand_feed_active_right_image=6;brand_card_question_multi_image=5;plutus_card_small_image_8_download=1;plutus_card_image_30=2;plutus_card_word=4;plutus_card_image_30_download=2;plutus_card_word_8=1;plutus_card_video=5;plutus_card_video_8=4;plutus_card_image=14;brand_card_multi_image=2;plutus_card_small_image_8=1;plutus_card_video_30_download=2;plutus_card_image_8_download=1;plutus_card_video_30=2;brand_card_article=4;plutus_card_multi_images_8=1;brand_card_question_video=4;plutus_card_small_image=5;brand_card_video=2;plutus_card_slide_image_31=1;plutus_card_word_30=2;"
349 | ],
350 | "X-Api-Version": [
351 | "3.0.92"
352 | ],
353 | "X-App-Build": [
354 | "release"
355 | ],
356 | "X-App-Version": [
357 | "4.27.1"
358 | ],
359 | "X-App-Versioncode": [
360 | "1132"
361 | ],
362 | "X-App-Za": [
363 | "OS=iOS&Release=12.0&Model=iPad4,1&VersionName=4.27.1&VersionCode=1132&Width=1536&Height=2048&DeviceType=Pad&Brand=Apple&OperatorType="
364 | ],
365 | "X-Network-Type": [
366 | "WiFi"
367 | ],
368 | "X-Suger": [
369 | "SURGVj1EQkJDMDQ0QS0wNzg2LTQyNTctQTUzNC1FQkJEQjI1QzU3QkM7SURGQT1DODMxQkExMC00QjBBLTRCMDgtQkJBQS02OEY3NzQwRUZCRjA7VURJRD1BREFDYlZGVGhRdExCY01FWXcyMm1HYkotdlo1T3k1YXl6Zz0="
370 | ],
371 | "X-Udid": [
372 | "ADACbVFThQtLBcMEYw22mGbJ-vZ5Oy5ayzg="
373 | ],
374 | "X-Zst-82": [
375 | "1.0AOAhgq8kiw4LAAAASwUAADEuMI3B8lsAAAAAiqMViyRX9WuT68jWP347p59s3mQ="
376 | ]
377 | },
378 | "body": {
379 | "is_binary": true,
380 | "len": 0,
381 | "content_type": "application/octet-stream",
382 | "content": ""
383 | }
384 | },
385 | "response": {
386 | "proto": "HTTP/2.0",
387 | "status": "200 OK",
388 | "status_code": 200,
389 | "header": {
390 | "Cache-Control": [
391 | "private, no-store, max-age=0, no-cache, must-revalidate, post-check=0, pre-check=0"
392 | ],
393 | "Content-Length": [
394 | "135"
395 | ],
396 | "Content-Type": [
397 | "application/json"
398 | ],
399 | "Date": [
400 | "Mon, 19 Nov 2018 14:12:22 GMT"
401 | ],
402 | "Etag": [
403 | "\"557bd35b6d00324ff54d745626c212c8c4fa3d4d\""
404 | ],
405 | "Expires": [
406 | "Fri, 02 Jan 2000 00:00:00 GMT"
407 | ],
408 | "Pragma": [
409 | "no-cache"
410 | ],
411 | "Server": [
412 | "ZWS"
413 | ],
414 | "Vary": [
415 | "Accept-Encoding"
416 | ],
417 | "X-Backend-Server": [
418 | "user-credit.user-credit-web.4c1fb8f9---10.70.9.35:31024[10.70.9.35:31024]"
419 | ],
420 | "X-Rsp-Hash": [
421 | "17a4e2aa9f88c80631a29c9406db5c525598d858d3841f8cea6bb4a6e2f33aa4"
422 | ]
423 | },
424 | "body": {
425 | "is_binary": false,
426 | "len": 135,
427 | "content_type": "application/json",
428 | "content": "eyJpc19xdWVzdGlvbl9yZWRpcmVjdF9lZGl0YWJsZSI6IGZhbHNlLCAiaXNfcXVlc3Rpb25fdG9waWNfZWRpdGFibGUiOiBmYWxzZSwgImlzX3F1ZXN0aW9uX2VkaXRhYmxlIjogZmFsc2UsICJjb21tZW50X3dpdGhfcGljIjogZmFsc2V9"
429 | },
430 | "err": ""
431 | },
432 | "client_ip": "172.16.10.104",
433 | "server_ip": "118.89.204.100",
434 | "start_time": "2018-11-19T22:12:22.00511+08:00",
435 | "duration": 235519397,
436 | "err": ""
437 | }
438 | }
439 | ```
440 |
441 | ### 请求重放
442 |
443 | 请求
444 | ```json
445 | {
446 | "type": 1001,
447 | "payload": {
448 | "id": "b56cb03d-855c-48a2-81e8-bbf71f285ecf"
449 | }
450 | }
451 | ```
452 |
453 | 响应
454 | ```json
455 | {
456 | "type": 2001,
457 | "payload": {
458 | "err": ""
459 | }
460 | }
461 | ```
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | // Package cmd 命令入口
2 | package cmd
3 |
4 | import "github.com/spf13/cobra"
5 |
6 | var rootCmd = &cobra.Command{
7 | Use:"mars",
8 | Short:"HTTP(S) proxy",
9 | }
10 |
11 | // Execute 执行命令
12 | func Execute() {
13 | err := rootCmd.Execute()
14 | if err != nil {
15 |
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/cmd/server.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "path/filepath"
5 | "strings"
6 |
7 | "github.com/ouqiang/goutil"
8 | "github.com/ouqiang/mars/internal/app"
9 | "github.com/ouqiang/mars/internal/app/config"
10 | "github.com/ouqiang/mars/internal/app/inject"
11 | "github.com/ouqiang/mars/internal/common/version"
12 | log "github.com/sirupsen/logrus"
13 | "github.com/spf13/cobra"
14 | "github.com/spf13/viper"
15 | )
16 |
17 | const (
18 | // 环境变量前缀
19 | serverEnvPrefix = "MARS"
20 | // 环境变量key分隔符
21 | serverConfigKeySeparator = "_"
22 | )
23 |
24 | var serverCmd = &cobra.Command{
25 | Use: "server",
26 | Short: "run proxy server",
27 | Run: func(cmd *cobra.Command, args []string) {
28 | log.Info(version.Format())
29 | viper.BindPFlags(cmd.Flags())
30 | conf := createConfig()
31 | if conf.App.Env.IsDev() {
32 | log.SetLevel(log.DebugLevel)
33 | } else {
34 | log.SetLevel(log.InfoLevel)
35 | }
36 | container := inject.NewContainer(conf)
37 | app.New(container).Run()
38 | },
39 | }
40 |
41 | func init() {
42 | rootCmd.AddCommand(serverCmd)
43 | var env string
44 | var configFile string
45 | serverCmd.Flags().StringVarP(&env, "env", "e", "prod", "dev | prod")
46 | serverCmd.Flags().StringVarP(&configFile, "configFile", "c", "conf/app.toml", "config file path")
47 |
48 | viper.AutomaticEnv()
49 | viper.SetEnvPrefix(serverEnvPrefix)
50 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", serverConfigKeySeparator))
51 | viper.SetConfigType("toml")
52 | }
53 |
54 | // 创建配置
55 | func createConfig() *config.Config {
56 | currentDir, err := goutil.WorkDir()
57 | if err != nil {
58 | log.Fatal(err)
59 | }
60 | configFile := viper.GetString("configFile")
61 | if !filepath.IsAbs(configFile) {
62 | configFile = filepath.Join(currentDir, configFile)
63 | }
64 | viper.SetConfigFile(configFile)
65 | log.Debugf("环境变量前缀: %s", serverEnvPrefix)
66 | log.Debugf("环境变量key分隔符: %s", serverConfigKeySeparator)
67 | log.Debugf("配置文件: %s", configFile)
68 | err = viper.ReadInConfig()
69 | if err != nil {
70 | log.Fatalf("加载配置文件错误: %s", err)
71 | }
72 | conf := new(config.Config)
73 | err = viper.Unmarshal(conf)
74 | if err != nil {
75 | log.Fatalf("配置文件解析错误: %s", err)
76 | }
77 | conf.App.Env = config.RuntimeMode(viper.GetString("env"))
78 |
79 | return conf
80 | }
81 |
--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/ouqiang/mars/internal/common/version"
5 | "github.com/spf13/cobra"
6 | )
7 |
8 | var versionCmd = &cobra.Command{
9 | Use: "version",
10 | Short: "print version",
11 | Run: func(cmd *cobra.Command, args []string) {
12 | cmd.Println(version.Format())
13 | },
14 | }
15 |
16 | func init() {
17 | rootCmd.AddCommand(versionCmd)
18 | }
19 |
--------------------------------------------------------------------------------
/conf/app.toml:
--------------------------------------------------------------------------------
1 | [app]
2 | host = "0.0.0.0"
3 | proxyPort = 8888
4 | inspectorPort = 9999
5 |
6 |
7 | [mitmProxy]
8 | # 是否开启中间人代理, 不开启则盲转发
9 | enabled = true
10 | # 是否解密HTTPS, 客户端系统需安装根证书
11 | decryptHTTPS = false
12 | # 证书缓存大小
13 | certCacheSize = 1000
14 | # 数据缓存大小
15 | leveldbCacheSize = 1000
--------------------------------------------------------------------------------
/deploy/mars.supervisor.conf:
--------------------------------------------------------------------------------
1 | [program:mars]
2 | command=/path/to/mars/mars server -c conf/app.toml
3 | directory=/path/to/mars
4 | user=mars
5 | group=mars
6 | autostart=true
7 | autorestart=true
8 | startretries=3
9 | redirect_stderr = true
10 | stdout_logfile=/var/log/supervisor/mars/out.log
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ouqiang/mars
2 |
3 | require (
4 | github.com/BurntSushi/toml v0.3.1 // indirect
5 | github.com/golang/protobuf v1.2.0
6 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
7 | github.com/gorilla/websocket v1.4.0
8 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
9 | github.com/ouqiang/goproxy v1.0.2
10 | github.com/ouqiang/goutil v1.0.3
11 | github.com/posener/wstest v0.0.0-20180217133618-28272a7ea048
12 | github.com/rakyll/statik v0.1.5
13 | github.com/satori/go.uuid v1.2.0
14 | github.com/sirupsen/logrus v1.2.0
15 | github.com/spf13/cobra v0.0.3
16 | github.com/spf13/pflag v1.0.3 // indirect
17 | github.com/spf13/viper v1.2.1
18 | github.com/stretchr/testify v1.2.2
19 | github.com/syndtr/goleveldb v0.0.0-20181105012736-f9080354173f
20 | )
21 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.0.0-20180816152847-43dc61c3e9d0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | contrib.go.opencensus.io/exporter/stackdriver v0.0.0-20180421005815-665cf5131b71/go.mod h1:QeFzMJDAw8TXt5+aRaSuE8l5BwaMIOIlaVkBOPRuMuw=
3 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
5 | github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20180907232109-123a90f520a1/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo=
6 | github.com/aws/aws-sdk-go v1.13.20/go.mod h1:ZRmQr0FajVIyZ4ZzBYKG5P3ZqPz9IHG41ZoMu1ADI3k=
7 | github.com/aws/aws-xray-sdk-go v1.0.0-rc.5/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04=
8 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
9 | github.com/census-ecosystem/opencensus-go-exporter-aws v0.0.0-20180411051634-41633bc1ff6b/go.mod h1:icwlHTP1AjScKRxD/s/Qinb7mpbcoUPpqaiBvrSS/QI=
10 | github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
11 | github.com/coreos/etcd v3.3.9+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
12 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
13 | github.com/coreos/go-systemd v0.0.0-20180828140353-eee3db372b31/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
14 | github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
18 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
19 | github.com/dnaeon/go-vcr v0.0.0-20180920040454-5637cf3d8a31/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
20 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
21 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
22 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
23 | github.com/go-ini/ini v1.37.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
24 | github.com/go-sql-driver/mysql v0.0.0-20180308100310-1a676ac6e4dc/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
25 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
26 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
27 | github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
28 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
29 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
30 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
31 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
32 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
33 | github.com/google/go-cloud v0.4.0 h1:6tlq737394nnh8aY7tcUJic29Fm+ypk57c4K4eJpMz4=
34 | github.com/google/go-cloud v0.4.0/go.mod h1:TmeKmKCwPPy5hIVh91iGwvaj0EWZ1dDztImBp2ex8JQ=
35 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
36 | github.com/google/martian v2.0.0-beta.2+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
37 | github.com/googleapis/gax-go v1.0.0/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
38 | github.com/gopherjs/gopherjs v0.0.0-20180424202546-8dffc02ea1cb/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
39 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
40 | github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
41 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
42 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
43 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
44 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
45 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
46 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
47 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
48 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
49 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
50 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
51 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
52 | github.com/jtolds/gls v0.0.0-20170503224851-77f18212c9c7/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
53 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
54 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
55 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
56 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
57 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
58 | github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I=
59 | github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
60 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
61 | github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
62 | github.com/opencensus-integrations/ocsql v0.0.0-20180908125828-63b3e35325e2/go.mod h1:ozPYpNVBHZsX33jfoQPO5TlI5lqh0/3R36kirEqJKAM=
63 | github.com/ouqiang/goproxy v1.0.0 h1:gEKrW0B5EkbqAvx9xFxWnORqJy48UlKzdooTsM8rSRM=
64 | github.com/ouqiang/goproxy v1.0.0/go.mod h1:00lXwAQOblekSEdXG49aF9CR+j+g1D/RURUFo27NBHw=
65 | github.com/ouqiang/goproxy v1.0.1 h1:P3OObMUC23xOEpZrF48MpGraNlNrqZapjJB4eS5NRHs=
66 | github.com/ouqiang/goproxy v1.0.1/go.mod h1:00lXwAQOblekSEdXG49aF9CR+j+g1D/RURUFo27NBHw=
67 | github.com/ouqiang/goproxy v1.0.2 h1:ynOq3H/WOk9vgZH9NNQWH6Z71PqRRx0IuO7Ue0e0V/w=
68 | github.com/ouqiang/goproxy v1.0.2/go.mod h1:00lXwAQOblekSEdXG49aF9CR+j+g1D/RURUFo27NBHw=
69 | github.com/ouqiang/goutil v1.0.3 h1:Uz3rrAVzeqyDnvCsyLTIDnPbfQNmRSpesqKpwo6tpR0=
70 | github.com/ouqiang/goutil v1.0.3/go.mod h1:QrB1Ky4uGqcixxOx55MXweI3IA6nDZ0NtLMXbMfkur4=
71 | github.com/ouqiang/wire v0.5.1 h1:ro+4gGYJEkKClE6XhTh7pipklEZ7ivC2iydoMN2YCc8=
72 | github.com/ouqiang/wire v0.5.1/go.mod h1:zKMJPmNU+ayc5Ru0ZxH4rzPs+y7ML7e31r+CE5WuNpk=
73 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
74 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
75 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
76 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
77 | github.com/posener/wstest v0.0.0-20180217133618-28272a7ea048 h1:XJ1bEwzKDbW33q703QCy580ZEqT2/hXTrU5sUYZf5LI=
78 | github.com/posener/wstest v0.0.0-20180217133618-28272a7ea048/go.mod h1:cjC8eRbwXrr5m2069dsjp7l7b0gWqFwMTUBDLNvVqho=
79 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
80 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
81 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
82 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
83 | github.com/rakyll/statik v0.1.5 h1:Ly2UjURzxnsSYS0zI50fZ+srA+Fu7EbpV5hglvJvJG0=
84 | github.com/rakyll/statik v0.1.5/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
85 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
86 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
87 | github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
88 | github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
89 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
90 | github.com/smartystreets/assertions v0.0.0-20180301161246-7678a5452ebe/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
91 | github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
92 | github.com/smartystreets/gunit v0.0.0-20180314194857-6f0d6275bdcd/go.mod h1:XUKj4gbqj2QvJk/OdLWzyZ3FYli0f+MdpngyryX0gcw=
93 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
94 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
95 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
96 | github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
97 | github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
98 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
99 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
100 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
101 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
102 | github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
103 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
104 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
105 | github.com/spf13/viper v1.2.1 h1:bIcUwXqLseLF3BDAZduuNfekWG87ibtFxi59Bq+oI9M=
106 | github.com/spf13/viper v1.2.1/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
107 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
108 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
109 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
110 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
111 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
112 | github.com/syndtr/goleveldb v0.0.0-20181105012736-f9080354173f h1:EEVjSRihF8NIbfyCcErpSpNHEKrY3s8EAwqiPENZZn8=
113 | github.com/syndtr/goleveldb v0.0.0-20181105012736-f9080354173f/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
114 | github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
115 | github.com/ugorji/go/codec v0.0.0-20180831062425-e253f1f20942/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
116 | github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
117 | go.opencensus.io v0.12.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0=
118 | golang.org/x/crypto v0.0.0-20180718160520-a2144134853f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
119 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
120 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
121 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
122 | golang.org/x/oauth2 v0.0.0-20180603041954-1e0a3fa8ba9a/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
123 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
124 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
125 | golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg=
126 | golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
127 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
128 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
129 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
130 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
131 | golang.org/x/tools v0.0.0-20180314180217-d853e8088c62 h1:nR5jt5w7Cuz8HL4yM8UygkQNaECVd6OU1lyc1Zcp4WQ=
132 | golang.org/x/tools v0.0.0-20180314180217-d853e8088c62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
133 | golang.org/x/tools v0.0.0-20181019005945-6adeb8aab2de h1:kT+Ec4AyesVdjJuyBnwnKai/sOSk8JMYn9jetb8bO5s=
134 | golang.org/x/tools v0.0.0-20181019005945-6adeb8aab2de/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
135 | google.golang.org/api v0.0.0-20180606215403-8e9de5a6de6d/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
136 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
137 | google.golang.org/genproto v0.0.0-20180627194029-ff3583edef7d/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
138 | google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
139 | gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
140 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
141 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
142 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
143 | gopkg.in/ini.v1 v1.37.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
144 | gopkg.in/pipe.v2 v2.0.0-20140414041502-3c2ca4d52544/go.mod h1:UhTeH/yXCK/KY7TX24mqPkaQ7gZeqmWd/8SSS8B3aHw=
145 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
146 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
147 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
148 |
--------------------------------------------------------------------------------
/interceptor/example.go:
--------------------------------------------------------------------------------
1 | package interceptor
2 |
3 | import (
4 | "io"
5 | "net/http"
6 | "strings"
7 |
8 | "github.com/ouqiang/goproxy"
9 | )
10 |
11 | func init() {
12 | //Handler = new(Example)
13 | }
14 |
15 | type Example struct{}
16 |
17 | // Connect 收到客户端连接, 自定义response返回
18 | // NOTICE: HTTPS只能访问 ctx.Req.URL.Host, 不能访问Header和Body, 不能使用rw
19 | func (Example) Connect(ctx *goproxy.Context, rw http.ResponseWriter) {
20 | if strings.Contains(ctx.Req.URL.Host, "crashlytics.com") {
21 | rw.WriteHeader(http.StatusForbidden)
22 | ctx.Abort()
23 | return
24 | }
25 | io.WriteString(rw, "修改后的内容")
26 | ctx.Abort()
27 | }
28 |
29 | // BeforeRequest 请求发送前, 修改request
30 | func (Example) BeforeRequest(ctx *goproxy.Context) {
31 | ctx.Req.Header.Set("Req-Id", "123")
32 | }
33 |
34 | // BeforeResponse 响应发送前, 修改response
35 | func (Example) BeforeResponse(ctx *goproxy.Context, resp *http.Response, err error) {
36 | if err == nil {
37 | resp.Header.Set("Resp-Id", "456")
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/interceptor/interceptor.go:
--------------------------------------------------------------------------------
1 | // Package interceptor 拦截器
2 | package interceptor
3 |
4 | import "github.com/ouqiang/mars/internal/common/recorder"
5 |
6 | // Handler 拦截器handler
7 | var Handler recorder.Interceptor
8 |
--------------------------------------------------------------------------------
/internal/app/app.go:
--------------------------------------------------------------------------------
1 | // Package app 应用
2 | package app
3 |
4 | import (
5 | "net/http"
6 | "os"
7 | "os/signal"
8 | "syscall"
9 | "time"
10 |
11 | log "github.com/sirupsen/logrus"
12 |
13 | "github.com/ouqiang/mars/internal/app/inject"
14 | "github.com/ouqiang/mars/internal/app/inspector"
15 | )
16 |
17 | const (
18 | proxyServerReadTimeout = 30 * time.Second
19 | proxyServerWriteTimeout = 5 * time.Second
20 |
21 | inspectorServerReadTimeout = 30 * time.Second
22 | inspectorServerWriteTimeout = 5 * time.Second
23 | )
24 |
25 | // App 应用
26 | type App struct {
27 | container *inject.Container
28 | }
29 |
30 | // New 创建应用
31 | func New(container *inject.Container) *App {
32 | app := &App{
33 | container: container,
34 | }
35 |
36 | return app
37 | }
38 |
39 | // Run 运行应用
40 | func (app *App) Run() {
41 | go app.startProxyServer()
42 | go app.startInspectorServer()
43 | <-app.waitSignal()
44 | }
45 |
46 | // 启动代理server
47 | func (app *App) startProxyServer() {
48 | addr := app.container.Conf.App.ProxyAddr()
49 | server := &http.Server{
50 | Addr: addr,
51 | Handler: app.container.Proxy,
52 | ReadTimeout: proxyServerReadTimeout,
53 | WriteTimeout: proxyServerWriteTimeout,
54 | }
55 | log.Infof("Proxy server listen on %s", addr)
56 | err := server.ListenAndServe()
57 | if err != nil {
58 | log.Fatal(err)
59 | }
60 | }
61 |
62 | // 启动流量审查server
63 | func (app *App) startInspectorServer() {
64 | inspector.NewRouter(app.container, http.DefaultServeMux).Register()
65 | addr := app.container.Conf.App.InspectorAddr()
66 | server := &http.Server{
67 | Addr: addr,
68 | ReadTimeout: inspectorServerReadTimeout,
69 | WriteTimeout: inspectorServerWriteTimeout,
70 | }
71 | log.Infof("Inspector server listen on %s", addr)
72 | err := server.ListenAndServe()
73 | if err != nil {
74 | log.Fatal(err)
75 | }
76 | }
77 |
78 | func (app *App) waitSignal() <-chan os.Signal {
79 | ch := make(chan os.Signal)
80 | signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
81 |
82 | return ch
83 | }
84 |
--------------------------------------------------------------------------------
/internal/app/config/config.go:
--------------------------------------------------------------------------------
1 | // Package config 配置
2 | package config
3 |
4 | import (
5 | "net"
6 | "strconv"
7 | )
8 |
9 | // RuntimeMode 运行模式
10 | type RuntimeMode string
11 |
12 | func (m RuntimeMode) IsDev() bool {
13 | return m == "dev"
14 | }
15 |
16 | func (m RuntimeMode) IsProd() bool {
17 | return m == "prod"
18 | }
19 |
20 | // Config 配置
21 | type Config struct {
22 | // App 应用配置
23 | App appConfig `mapstructure:"app"`
24 | // Proxy 代理配置
25 | MITMProxy mitmProxyConfig `mapstructure:"mitmProxy"`
26 | }
27 |
28 | type appConfig struct {
29 | Env RuntimeMode
30 | Host string `mapstructure:"host"`
31 | ProxyPort int `mapstructure:"proxyPort"`
32 | InspectorPort int `mapstructure:"inspectorPort"`
33 | }
34 |
35 | type mitmProxyConfig struct {
36 | Enabled bool `mapstructure:"enabled"`
37 | DecryptHTTPS bool `mapstructure:"decryptHTTPS"`
38 | CertCacheSize int `mapstructure:"certCacheSize"`
39 | LeveldbDir string `mapstructure:"leveldbDir"`
40 | LeveldbCacheSize int `mapstructure:"leveldbCacheSize"`
41 | }
42 |
43 | // ProxyAddr 代理监听地址
44 | func (ac appConfig) ProxyAddr() string {
45 | return net.JoinHostPort(ac.Host, strconv.Itoa(ac.ProxyPort))
46 | }
47 |
48 | // InspectorAddr 审查监听地址
49 | func (ac appConfig) InspectorAddr() string {
50 | return net.JoinHostPort(ac.Host, strconv.Itoa(ac.InspectorPort))
51 | }
52 |
--------------------------------------------------------------------------------
/internal/app/inject/container.go:
--------------------------------------------------------------------------------
1 | // package inject 依赖注入
2 | package inject
3 |
4 | import (
5 | "os"
6 | "path/filepath"
7 |
8 | "github.com/ouqiang/goproxy"
9 | "github.com/ouqiang/mars/interceptor"
10 | "github.com/ouqiang/mars/internal/app/config"
11 | "github.com/ouqiang/mars/internal/common"
12 | "github.com/ouqiang/mars/internal/common/recorder"
13 | "github.com/ouqiang/mars/internal/common/recorder/output"
14 | "github.com/ouqiang/mars/internal/common/recorder/storage"
15 | "github.com/ouqiang/mars/internal/common/socket"
16 | log "github.com/sirupsen/logrus"
17 | "github.com/syndtr/goleveldb/leveldb"
18 | )
19 |
20 | // Container 容器
21 | type Container struct {
22 | Conf *config.Config
23 | Proxy *goproxy.Proxy
24 | WebSocketSessionOpts []socket.SessionOption
25 | WebSocketOutput *output.WebSocket
26 | txStorage recorder.Storage
27 | txRecorder *recorder.Recorder
28 | txOutput recorder.Output
29 | txInterceptor recorder.Interceptor
30 | }
31 |
32 | // NewContainer 创建容器
33 | func NewContainer(conf *config.Config) *Container {
34 | if conf == nil {
35 | panic("config is nil")
36 | }
37 | c := &Container{
38 | Conf: conf,
39 | txRecorder: recorder.NewRecorder(),
40 | }
41 | c.createWebSocketOutput()
42 | c.createSessionOption()
43 |
44 | c.createProxy()
45 | c.createRecorderStorage()
46 | c.createRecorderOutput()
47 | c.createRecorderInterceptor()
48 |
49 | c.txRecorder.SetProxy(c.Proxy)
50 | c.txRecorder.SetStorage(c.txStorage)
51 | c.txRecorder.SetOutput(c.txOutput)
52 | c.txRecorder.SetInterceptor(c.txInterceptor)
53 |
54 | return c
55 | }
56 |
57 | func (c *Container) createProxy() {
58 | opts := make([]goproxy.Option, 0, 3)
59 | opts = append(opts, goproxy.WithDisableKeepAlive(true))
60 | if c.Conf.MITMProxy.Enabled {
61 | opts = append(opts, goproxy.WithDelegate(c.txRecorder))
62 | }
63 | if c.Conf.MITMProxy.DecryptHTTPS {
64 | queue := common.NewQueue(c.Conf.MITMProxy.CertCacheSize)
65 | certCache := recorder.NewCertCache(queue)
66 | opts = append(opts, goproxy.WithDecryptHTTPS(certCache))
67 | }
68 | c.Proxy = goproxy.New(opts...)
69 | }
70 |
71 | func (c *Container) createRecorderStorage() {
72 | if !c.Conf.MITMProxy.Enabled {
73 | return
74 | }
75 | if c.Conf.MITMProxy.LeveldbDir == "" {
76 | c.Conf.MITMProxy.LeveldbDir = filepath.Join(os.TempDir(), "mars_leveldb")
77 | }
78 | if _, err := os.Stat(c.Conf.MITMProxy.LeveldbDir); err == nil {
79 | err = os.RemoveAll(c.Conf.MITMProxy.LeveldbDir)
80 | if err != nil {
81 | log.Fatalf("删除leveldb数据库目录错误: %s", err)
82 | }
83 | }
84 |
85 | db, err := leveldb.OpenFile(c.Conf.MITMProxy.LeveldbDir, nil)
86 | if err != nil {
87 | log.Fatalf("创建leveldb数据库错误: %s", err)
88 | }
89 | queue := common.NewQueue(c.Conf.MITMProxy.LeveldbCacheSize)
90 | c.txStorage = storage.NewLevelDB(db, queue)
91 | }
92 |
93 | func (c *Container) createRecorderOutput() {
94 | c.txOutput = c.WebSocketOutput
95 | }
96 |
97 | func (c *Container) createRecorderInterceptor() {
98 | c.txInterceptor = interceptor.Handler
99 | }
100 |
101 | func (c *Container) createWebSocketOutput() {
102 | hub := socket.NewHub(20)
103 | c.WebSocketOutput = output.NewWebSocket(hub, c.txRecorder)
104 | }
105 |
106 | func (c *Container) createSessionOption() {
107 | c.WebSocketSessionOpts = []socket.SessionOption{
108 | socket.WithSessionReceiveQueueSize(20),
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/internal/app/inspector/controller/controller.go:
--------------------------------------------------------------------------------
1 | // Package controller 控制器
2 | package controller
3 |
4 | // Controller 控制器
5 | type Controller struct {
6 | }
7 |
--------------------------------------------------------------------------------
/internal/app/inspector/controller/inspector.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | uuid "github.com/satori/go.uuid"
8 |
9 | "github.com/ouqiang/mars/internal/common/socket/conn"
10 |
11 | log "github.com/sirupsen/logrus"
12 |
13 | "github.com/ouqiang/mars/internal/common/socket"
14 |
15 | "github.com/gorilla/websocket"
16 | )
17 |
18 | var upgrader = websocket.Upgrader{
19 | HandshakeTimeout: 5 * time.Second,
20 | CheckOrigin: func(r *http.Request) bool {
21 | return true
22 | },
23 | }
24 |
25 | // Inspector 流量审查
26 | type Inspector struct {
27 | sessionOptions []socket.SessionOption
28 | sessionHandler socket.SessionHandler
29 | }
30 |
31 | // NewInspector 创建Inspector
32 | func NewInspector(sessionHandler socket.SessionHandler, opts []socket.SessionOption) *Inspector {
33 | c := &Inspector{
34 | sessionHandler: sessionHandler,
35 | sessionOptions: opts,
36 | }
37 |
38 | return c
39 | }
40 |
41 | // WebSocket 处理webSocket
42 | func (c *Inspector) WebSocket(resp http.ResponseWriter, req *http.Request) {
43 | rawConn, err := upgrader.Upgrade(resp, req, nil)
44 | if err != nil {
45 | log.Debugf("升级到websocket错误: %s", err)
46 | return
47 | }
48 | client := socket.NewSession(
49 | conn.NewWebSocket(rawConn, websocket.TextMessage),
50 | c.sessionHandler,
51 | c.sessionOptions...,
52 | )
53 | client.ID = uuid.NewV4().String()
54 | client.Run()
55 | }
56 |
--------------------------------------------------------------------------------
/internal/app/inspector/router.go:
--------------------------------------------------------------------------------
1 | // Package inspector 流量审查
2 | package inspector
3 |
4 | import (
5 | "io/ioutil"
6 | "log"
7 | "net/http"
8 |
9 | "github.com/rakyll/statik/fs"
10 |
11 | "github.com/ouqiang/mars/internal/app/inject"
12 | "github.com/ouqiang/mars/internal/app/inspector/controller"
13 | _ "github.com/ouqiang/mars/internal/statik"
14 | )
15 |
16 | const staticDir = "/public/"
17 |
18 | // router 路由
19 | type Router struct {
20 | container *inject.Container
21 | mux *http.ServeMux
22 | }
23 |
24 | // NewRouter 创建Router
25 | func NewRouter(container *inject.Container, mux *http.ServeMux) *Router {
26 | r := &Router{
27 | container: container,
28 | mux: mux,
29 | }
30 |
31 | return r
32 | }
33 |
34 | // Register 路由注册
35 | func (r *Router) Register() {
36 | r.registerStatic()
37 | c := controller.NewInspector(r.container.WebSocketOutput, r.container.WebSocketSessionOpts)
38 |
39 | r.mux.HandleFunc("/ws", c.WebSocket)
40 | }
41 |
42 | func (r *Router) registerStatic() {
43 | statikFS, err := fs.New()
44 | if err != nil {
45 | log.Fatal(err)
46 | }
47 | indexFile, err := statikFS.Open("/index.html")
48 | if err != nil {
49 | log.Fatal(err)
50 | }
51 | indexData, err := ioutil.ReadAll(indexFile)
52 | if err != nil {
53 | log.Fatal(err)
54 | }
55 | r.mux.Handle(staticDir, http.StripPrefix(staticDir, http.FileServer(statikFS)))
56 | r.mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
57 | rw.Write(indexData)
58 | })
59 | }
60 |
--------------------------------------------------------------------------------
/internal/common/queue.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "container/list"
5 | "sync"
6 | )
7 |
8 | // Queue 队列, 保存固定大小的元素
9 | type Queue struct {
10 | bucket *list.List
11 | maxSize int
12 | mu sync.Mutex
13 | }
14 |
15 | // New 创建queue
16 | func NewQueue(size int) *Queue {
17 | q := &Queue{
18 | maxSize: size,
19 | bucket: list.New(),
20 | }
21 |
22 | return q
23 | }
24 |
25 | // Add 添加元素到队列中
26 | func (q *Queue) Add(value interface{}) (removeValue interface{}) {
27 | q.mu.Lock()
28 | defer q.mu.Unlock()
29 |
30 | if q.bucket.Len() == q.maxSize {
31 | firstEle := q.bucket.Front()
32 | if firstEle != nil {
33 | removeValue = q.bucket.Remove(firstEle)
34 | }
35 | }
36 |
37 | q.bucket.PushBack(value)
38 |
39 | return removeValue
40 | }
41 |
42 | // Len 获取队列大小
43 | func (q *Queue) Len() int {
44 | q.mu.Lock()
45 | l := q.bucket.Len()
46 | q.mu.Unlock()
47 |
48 | return l
49 | }
50 |
--------------------------------------------------------------------------------
/internal/common/queue_test.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestQueue_Add(t *testing.T) {
10 | q := NewQueue(10)
11 | for i := 0; i < 100; i++ {
12 | value := q.Add(i)
13 | if i >= 10 {
14 | require.Equal(t, i-10, value.(int))
15 | }
16 | }
17 | require.Equal(t, 10, q.Len())
18 | }
19 |
--------------------------------------------------------------------------------
/internal/common/recorder/body.go:
--------------------------------------------------------------------------------
1 | package recorder
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "io/ioutil"
7 | )
8 |
9 | // Body HTTP请求、响应Body
10 | type Body struct {
11 | IsBinary bool `json:"is_binary"`
12 | Len int `json:"len"`
13 | ContentType string `json:"content_type"`
14 | Content []byte `json:"content"`
15 | }
16 |
17 | // NewBody 创建Body
18 | func NewBody() *Body {
19 | b := &Body{}
20 |
21 | return b
22 | }
23 |
24 | // 设置body内容
25 | func (b *Body) setContent(contentType string, content []byte) {
26 | b.IsBinary = IsBinaryBody(contentType)
27 | b.Len = len(content)
28 | b.Content = content
29 | b.ContentType = contentType
30 | }
31 |
32 | // body内容封装成ReadCloser
33 | func (b *Body) readCloser() io.ReadCloser {
34 | return ioutil.NopCloser(bytes.NewReader(b.Content))
35 | }
36 |
--------------------------------------------------------------------------------
/internal/common/recorder/cert_cache.go:
--------------------------------------------------------------------------------
1 | package recorder
2 |
3 | import (
4 | "crypto/tls"
5 | "sync"
6 |
7 | "github.com/ouqiang/mars/internal/common"
8 | )
9 |
10 | // CertCache 证书缓存
11 | type CertCache struct {
12 | queue *common.Queue
13 | m sync.Map
14 | }
15 |
16 | // 创建CertCache
17 | func NewCertCache(q *common.Queue) *CertCache {
18 | c := &CertCache{
19 | queue: q,
20 | }
21 |
22 | return c
23 | }
24 |
25 | // Get 获取证书
26 | func (c *CertCache) Get(host string) *tls.Certificate {
27 | value, ok := c.m.Load(host)
28 | if !ok {
29 | return nil
30 | }
31 |
32 | return value.(*tls.Certificate)
33 | }
34 |
35 | // Set 保存证书
36 | func (c *CertCache) Set(host string, cert *tls.Certificate) {
37 | removeValue := c.queue.Add(host)
38 | if removeValue != nil {
39 | c.m.Delete(removeValue.(string))
40 | }
41 | c.m.Store(host, cert)
42 | }
43 |
--------------------------------------------------------------------------------
/internal/common/recorder/output/action/action.go:
--------------------------------------------------------------------------------
1 | package action
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/ouqiang/mars/internal/common/recorder"
7 | "github.com/ouqiang/mars/internal/common/socket/message"
8 | )
9 |
10 | const (
11 | TypeRequestPing message.Type = 1000
12 | TypeRequestReplay message.Type = 1001
13 | TypeRequestTransaction message.Type = 1002
14 |
15 | TypeResponsePong message.Type = 2000
16 | TypeResponseReplay message.Type = 2001
17 | TypeResponseTransaction message.Type = 2002
18 |
19 | TypePushTransaction message.Type = 3000
20 | )
21 |
22 | type Empty struct {
23 | }
24 |
25 | type RequestReplay struct {
26 | Id string `json:"id"`
27 | }
28 |
29 | type ResponseReplay struct {
30 | Err string `json:"err"`
31 | }
32 |
33 | type RequestTransaction struct {
34 | Id string `json:"id"`
35 | }
36 |
37 | type ResponseTransaction struct {
38 | *recorder.Transaction
39 | Err string `json:"err"`
40 | }
41 |
42 | type PushTransaction struct {
43 | Id string `json:"id"`
44 | // Method 请求方法
45 | Method string `json:"method"`
46 | // Host 请求主机名
47 | Host string `json:"host"`
48 | // Path 请求path
49 | Path string `json:"path"`
50 | // Duration 耗时
51 | Duration time.Duration `json:"duration"`
52 | // ResponseStatusCode 响应状态码
53 | ResponseStatusCode int `json:"response_status_code"`
54 | // Err 错误信息
55 | ResponseErr string `json:"response_err"`
56 | // ResponseContentType 响应内容类型
57 | ResponseContentType string `json:"response_content_type"`
58 | // ResponseLen 响应长度
59 | ResponseLen int `json:"response_len"`
60 | }
61 |
--------------------------------------------------------------------------------
/internal/common/recorder/output/console.go:
--------------------------------------------------------------------------------
1 | // Package storage 存储http transaction
2 | package output
3 |
4 | import (
5 | "fmt"
6 | "io"
7 | "os"
8 | "strings"
9 | "sync"
10 |
11 | "github.com/ouqiang/mars/internal/common/recorder"
12 | )
13 |
14 | // Console 输出到终端
15 | type Console struct {
16 | m sync.Mutex
17 | builder strings.Builder
18 | writer io.Writer
19 | }
20 |
21 | // NewConsole 创建console
22 | func NewConsole() *Console {
23 | return &Console{
24 | writer: os.Stdout,
25 | }
26 | }
27 |
28 | // Write transaction输出到终端
29 | func (c *Console) Write(tx *recorder.Transaction) error {
30 | c.m.Lock()
31 | defer c.m.Unlock()
32 |
33 | c.builder.WriteString("txId: ")
34 | c.builder.WriteString(tx.Id)
35 | c.builder.WriteString("\n")
36 | c.writeRequest(tx)
37 | c.builder.WriteString("\n")
38 | c.writeResponse(tx)
39 | c.builder.WriteString("\n\n\n")
40 | io.WriteString(c.writer, c.builder.String())
41 | c.builder.Reset()
42 |
43 | return nil
44 | }
45 |
46 | func (c *Console) writeRequest(tx *recorder.Transaction) {
47 | c.builder.WriteString(fmt.Sprintf("\033[34m %s \033[0m", tx.Req.Method))
48 | c.builder.WriteString(" ")
49 | c.builder.WriteString(tx.Req.URL)
50 | c.builder.WriteString("\n")
51 | c.builder.WriteString("Server-IP: ")
52 | c.builder.WriteString(tx.ServerIP)
53 | c.builder.WriteString("\n")
54 | for key, values := range tx.Req.Header {
55 | c.builder.WriteString(key)
56 | c.builder.WriteString(": ")
57 | c.builder.WriteString(strings.Join(values, ";"))
58 | c.builder.WriteString("\n")
59 | }
60 | if !tx.Req.Body.IsBinary {
61 | c.builder.Write(tx.Req.Body.Content)
62 | c.builder.WriteString("\n")
63 | }
64 | }
65 |
66 | func (c *Console) writeResponse(tx *recorder.Transaction) {
67 | if tx.Resp.Err != "" {
68 | c.builder.WriteString(tx.Resp.Err)
69 | return
70 | }
71 | c.builder.WriteString(fmt.Sprintf("\033[32m %s %s \033[0m", tx.Resp.Proto, tx.Resp.Status))
72 | c.builder.WriteString("\n")
73 | for key, values := range tx.Resp.Header {
74 | c.builder.WriteString(key)
75 | c.builder.WriteString(": ")
76 | c.builder.WriteString(strings.Join(values, ";"))
77 | c.builder.WriteString("\n")
78 | }
79 | if !tx.Resp.Body.IsBinary {
80 | c.builder.Write(tx.Resp.Body.Content)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/internal/common/recorder/output/websocket.go:
--------------------------------------------------------------------------------
1 | package output
2 |
3 | import (
4 | "github.com/ouqiang/mars/internal/common/recorder"
5 | "github.com/ouqiang/mars/internal/common/recorder/output/action"
6 | "github.com/ouqiang/mars/internal/common/socket"
7 | "github.com/ouqiang/mars/internal/common/socket/codec"
8 | "github.com/ouqiang/mars/internal/common/socket/message"
9 | log "github.com/sirupsen/logrus"
10 | )
11 |
12 | // WebSocket 输出到WebSocket
13 | type WebSocket struct {
14 | router *socket.Router
15 | hub *socket.Hub
16 | codec codec.Codec
17 | recorder *recorder.Recorder
18 | }
19 |
20 | // NewWebSocket 创建WebSocket
21 | func NewWebSocket(hub *socket.Hub, r *recorder.Recorder) *WebSocket {
22 | ws := &WebSocket{
23 | router: socket.NewRouter(),
24 | hub: hub,
25 | codec: new(codec.JSON),
26 | recorder: r,
27 | }
28 |
29 | ws.registerRouter()
30 |
31 | return ws
32 | }
33 |
34 | func (w *WebSocket) OnConnect(session *socket.Session) {
35 | log.Debugf("webSocket建立连接: [sessionId: %s]", session.ID)
36 | w.hub.Add(session.ID, session)
37 | }
38 | func (w *WebSocket) OnMessage(session *socket.Session, data []byte) {
39 | log.Debugf("webSocket收到消息: %s", data)
40 | w.router.Dispatch(session, w.codec, data)
41 | }
42 | func (w *WebSocket) OnClose(session *socket.Session) {
43 | log.Debugf("webSocket关闭连接: [sessionId: %s]", session.ID)
44 | w.hub.Delete(session.ID)
45 | }
46 | func (w *WebSocket) OnError(session *socket.Session, err error) {
47 | log.Debugf("webSocket错误: [sessionId: %s] %s", session.ID, err)
48 | }
49 |
50 | // Write Transaction写入WebSocket
51 | func (w *WebSocket) Write(tx *recorder.Transaction) error {
52 | push := &action.PushTransaction{
53 | Id: tx.Id,
54 | Method: tx.Req.Method,
55 | Host: tx.Req.Host,
56 | Path: tx.Req.Path,
57 | Duration: tx.Duration,
58 | }
59 | if tx.Resp.Err != "" {
60 | push.ResponseErr = tx.Resp.Err
61 | } else {
62 | push.ResponseContentType = tx.Resp.Body.ContentType
63 | push.ResponseStatusCode = tx.Resp.StatusCode
64 | push.ResponseLen = tx.Resp.Body.Len
65 | }
66 | w.broadcast(action.TypePushTransaction, push)
67 |
68 | return nil
69 | }
70 |
71 | // 发送消息
72 | func (w *WebSocket) sendMessage(session *socket.Session, msgType message.Type, payload interface{}) {
73 | data, err := w.marshalMessage(msgType, payload)
74 | if err != nil {
75 | return
76 | }
77 | session.Write(data)
78 |
79 | }
80 |
81 | // 广播
82 | func (w *WebSocket) broadcast(msgType message.Type, payload interface{}) {
83 | data, err := w.marshalMessage(msgType, payload)
84 | if err != nil {
85 | return
86 | }
87 | w.hub.Broadcast(data)
88 | }
89 |
90 | // 消息序列化
91 | func (w *WebSocket) marshalMessage(msgType message.Type, payload interface{}) ([]byte, error) {
92 | data, err := w.codec.Marshal(msgType, payload)
93 | if err != nil {
94 | log.Errorf("webSocket发送消息序列化错误: [msgType: %d payload: %+v] %s", msgType, payload, err)
95 | return nil, err
96 | }
97 |
98 | return data, nil
99 | }
100 |
101 | func (w *WebSocket) registerRouter() {
102 | w.router.NotFoundHandler(func(session *socket.Session, codec codec.Codec, data []byte) {
103 | log.Warnf("webSocket路由无法解析: [sessionId: %s] %s",
104 | session.ID, data)
105 | })
106 | w.router.ErrorHandler(func(session *socket.Session, err interface{}) {
107 | log.Warnf("webSocket路由解析错误: [sessionId: %s] %+v", session.ID, err)
108 | })
109 |
110 | w.router.Register(action.TypeRequestPing, (*action.Empty)(nil), w.ping)
111 | w.router.Register(action.TypeRequestReplay, (*action.RequestReplay)(nil), w.replay)
112 | w.router.Register(action.TypeRequestTransaction, (*action.RequestTransaction)(nil), w.getTransaction)
113 | }
114 |
115 | func (w *WebSocket) ping(ctx *socket.Context) {
116 | w.sendMessage(ctx.Session, action.TypeResponsePong, new(action.Empty))
117 | }
118 |
119 | func (w *WebSocket) replay(ctx *socket.Context) {
120 | req := ctx.Payload.(*action.RequestReplay)
121 | err := w.recorder.Replay(req.Id)
122 | resp := &action.ResponseReplay{}
123 | if err != nil {
124 | resp.Err = err.Error()
125 | }
126 | w.sendMessage(ctx.Session, action.TypeResponseReplay, resp)
127 | }
128 |
129 | func (w *WebSocket) getTransaction(ctx *socket.Context) {
130 | req := ctx.Payload.(*action.RequestTransaction)
131 | tx, err := w.recorder.Storage().Get(req.Id)
132 | resp := &action.ResponseTransaction{
133 | Transaction: tx,
134 | }
135 | if err != nil {
136 | resp.Err = err.Error()
137 | }
138 | w.sendMessage(ctx.Session, action.TypeResponseTransaction, resp)
139 | }
140 |
--------------------------------------------------------------------------------
/internal/common/recorder/recorder.go:
--------------------------------------------------------------------------------
1 | package recorder
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "net/http"
7 | "net/http/httptrace"
8 | "net/url"
9 | "time"
10 |
11 | "github.com/ouqiang/goproxy"
12 | log "github.com/sirupsen/logrus"
13 | )
14 |
15 | // Storage 存取transaction接口
16 | type Storage interface {
17 | Get(txId string) (*Transaction, error)
18 | Put(*Transaction) error
19 | }
20 |
21 | // Output 输出transaction接口
22 | type Output interface {
23 | Write(*Transaction) error
24 | }
25 |
26 | // Interceptor 拦截器
27 | type Interceptor interface {
28 | // Connect 收到客户端连接, 自定义response返回, 只支持HTTP
29 | Connect(ctx *goproxy.Context, rw http.ResponseWriter)
30 | // BeforeRequest 请求发送前, 修改request
31 | BeforeRequest(ctx *goproxy.Context)
32 | // BeforeResponse 响应发送前, 修改response
33 | BeforeResponse(ctx *goproxy.Context, resp *http.Response, err error)
34 | }
35 |
36 | // Recorder 记录http transaction
37 | type Recorder struct {
38 | proxy *goproxy.Proxy
39 | storage Storage
40 | output Output
41 | interceptor Interceptor
42 | }
43 |
44 | // NewRecorder 创建recorder
45 | func NewRecorder() *Recorder {
46 | r := &Recorder{}
47 |
48 | return r
49 | }
50 |
51 | // SetProxy 设置中间人代理
52 | func (r *Recorder) SetProxy(p *goproxy.Proxy) {
53 | r.proxy = p
54 | }
55 |
56 | // SetStorage 设置transaction存储
57 | func (r *Recorder) SetStorage(s Storage) {
58 | r.storage = s
59 | }
60 |
61 | // SetOutput 设置transaction输出
62 | func (r *Recorder) SetOutput(o Output) {
63 | r.output = o
64 | }
65 |
66 | // SetInterceptor 设置拦截器
67 | func (r *Recorder) SetInterceptor(i Interceptor) {
68 | r.interceptor = i
69 | }
70 |
71 | // Storage 获取存储
72 | func (r *Recorder) Storage() Storage {
73 | return r.storage
74 | }
75 |
76 | // Connect 收到客户端连接
77 | func (r *Recorder) Connect(ctx *goproxy.Context, rw http.ResponseWriter) {
78 | if r.interceptor != nil {
79 | r.interceptor.Connect(ctx, rw)
80 | }
81 | }
82 |
83 | // Auth 代理身份认证
84 | func (r *Recorder) Auth(ctx *goproxy.Context, rw http.ResponseWriter) {}
85 |
86 | // BeforeRequest 请求发送前处理
87 | func (r *Recorder) BeforeRequest(ctx *goproxy.Context) {
88 | if host := ctx.Req.Header.Get("X-Mars-Host"); host != "" {
89 | ctx.Req.Host = host
90 | }
91 | ctx.Req.Header.Del("X-Mars-Host")
92 | ctx.Req.Header.Del("X-Mars-Debug")
93 | if r.interceptor != nil {
94 | r.interceptor.BeforeRequest(ctx)
95 | }
96 | tx := NewTransaction()
97 | tx.ClientIP, _, _ = net.SplitHostPort(ctx.Req.RemoteAddr)
98 | tx.StartTime = time.Now()
99 |
100 | tx.DumpRequest(ctx.Req)
101 |
102 | trace := &httptrace.ClientTrace{
103 | GotConn: func(info httptrace.GotConnInfo) {
104 | tx.ServerIP, _, _ = net.SplitHostPort(info.Conn.RemoteAddr().String())
105 | },
106 | }
107 | ctx.Req = ctx.Req.WithContext(httptrace.WithClientTrace(ctx.Req.Context(), trace))
108 |
109 | ctx.Data["tx"] = tx
110 | }
111 |
112 | // BeforeResponse 响应发送前处理
113 | func (r *Recorder) BeforeResponse(ctx *goproxy.Context, resp *http.Response, err error) {
114 | if r.interceptor != nil {
115 | r.interceptor.BeforeResponse(ctx, resp, err)
116 | }
117 | tx := ctx.Data["tx"].(*Transaction)
118 | tx.Duration = time.Now().Sub(tx.StartTime)
119 |
120 | tx.DumpResponse(resp, err)
121 | }
122 |
123 | // ParentProxy 设置上级代理
124 | func (r *Recorder) ParentProxy(req *http.Request) (*url.URL, error) {
125 | return http.ProxyFromEnvironment(req)
126 | }
127 |
128 | // Finish 请求结束
129 | func (r *Recorder) Finish(ctx *goproxy.Context) {
130 | value, ok := ctx.Data["tx"]
131 | if !ok {
132 | return
133 | }
134 | tx, ok := value.(*Transaction)
135 | if !ok {
136 | return
137 | }
138 | if r.storage != nil {
139 | err := r.storage.Put(tx)
140 | if err != nil {
141 | log.Warnf("请求结束#保存transaction错误: [%s] %s", ctx.Req.URL.String(), err)
142 | }
143 | }
144 | if r.output != nil {
145 | err := r.output.Write(tx)
146 | if err != nil {
147 | log.Warnf("请求结束#输出transaction错误: [%s] %s",
148 | ctx.Req.URL.String(), err)
149 | }
150 | }
151 | }
152 |
153 | // ErrorLog 记录错误日志
154 | func (r *Recorder) ErrorLog(err error) {
155 | log.Error(err)
156 | }
157 |
158 | // Replay 回放
159 | func (r *Recorder) Replay(txId string) error {
160 | tx, err := r.storage.Get(txId)
161 | if err != nil {
162 | return fmt.Errorf("回放#获取transaction错误: [txId: %s] %s", txId, err)
163 | }
164 | newReq, err := tx.Req.Restore()
165 | if err != nil {
166 | return fmt.Errorf("回放#创建请求错误: [txId: %s] %s", txId, err)
167 | }
168 | newReq.RemoteAddr = tx.ClientIP + ":80"
169 | go r.DoRequest(newReq)
170 |
171 | return nil
172 | }
173 |
174 | // DoRequest 执行请求
175 | func (r *Recorder) DoRequest(req *http.Request) {
176 | if req == nil {
177 | panic("request is nil")
178 | }
179 | ctx := &goproxy.Context{
180 | Req: req,
181 | }
182 | r.proxy.DoRequest(ctx, func(resp *http.Response, e error) {
183 | if resp != nil {
184 | resp.Body.Close()
185 | }
186 | })
187 | r.Finish(ctx)
188 | }
189 |
--------------------------------------------------------------------------------
/internal/common/recorder/request.go:
--------------------------------------------------------------------------------
1 | package recorder
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/ouqiang/goproxy"
7 | )
8 |
9 | // Request HTTP请求
10 | type Request struct {
11 | // Proto HTTP协议版本
12 | Proto string `json:"proto"`
13 | // Method 请求方法
14 | Method string `json:"method"`
15 | // Scheme 请求协议
16 | Scheme string `json:"scheme"`
17 | // Host 请求主机名
18 | Host string `json:"host"`
19 | // Path 请求path
20 | Path string `json:"path"`
21 | // QueryParam URL参数
22 | QueryParam string `json:"query_param"`
23 | // URL 完整URL
24 | URL string `json:"url"`
25 | // Header 请求Header
26 | Header http.Header `json:"header"`
27 | // Body 请求body
28 | Body *Body `json:"body"`
29 | }
30 |
31 | // NewRequest 创建请求
32 | func NewRequest() *Request {
33 | req := &Request{
34 | Body: NewBody(),
35 | }
36 |
37 | return req
38 | }
39 |
40 | // Restore 还原请求
41 | func (req *Request) Restore() (*http.Request, error) {
42 | rawReq, err := http.NewRequest(req.Method, req.URL, req.Body.readCloser())
43 | if err != nil {
44 | return nil, err
45 | }
46 | rawReq.Header = goproxy.CloneHeader(req.Header)
47 |
48 | return rawReq, nil
49 | }
50 |
--------------------------------------------------------------------------------
/internal/common/recorder/response.go:
--------------------------------------------------------------------------------
1 | package recorder
2 |
3 | import "net/http"
4 |
5 | // Response HTTP响应
6 | type Response struct {
7 | // Proto 响应协议
8 | Proto string `json:"proto"`
9 | // Status 状态状态
10 | Status string `json:"status"`
11 | // StatusCode 响应码
12 | StatusCode int `json:"status_code"`
13 | // Header 响应Header
14 | Header http.Header `json:"header"`
15 | // Body 响应Body
16 | Body *Body `json:"body"`
17 | // Err 错误信息
18 | Err string `json:"err"`
19 | }
20 |
21 | func NewResponse() *Response {
22 | resp := &Response{
23 | Body: NewBody(),
24 | }
25 |
26 | return resp
27 | }
28 |
--------------------------------------------------------------------------------
/internal/common/recorder/storage/leveldb.go:
--------------------------------------------------------------------------------
1 | package storage
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 |
7 | "github.com/ouqiang/mars/internal/common"
8 |
9 | "github.com/ouqiang/mars/internal/common/recorder"
10 | "github.com/syndtr/goleveldb/leveldb"
11 | )
12 |
13 | // LevelDB 存储到levelDB
14 | type LevelDB struct {
15 | db *leveldb.DB
16 | queue *common.Queue
17 | }
18 |
19 | // NewLevelDB 创建levelDB
20 | func NewLevelDB(db *leveldb.DB, queue *common.Queue) *LevelDB {
21 | l := &LevelDB{
22 | db: db,
23 | queue: queue,
24 | }
25 |
26 | return l
27 | }
28 |
29 | // GetBytes 获取transaction序列化的bytes
30 | func (l *LevelDB) GetBytes(txId string) ([]byte, error) {
31 | return l.db.Get([]byte(txId), nil)
32 | }
33 |
34 | // Get 获取transaction
35 | func (l *LevelDB) Get(txId string) (*recorder.Transaction, error) {
36 | data, err := l.GetBytes(txId)
37 | if err != nil {
38 | return nil, err
39 | }
40 | tx := new(recorder.Transaction)
41 | err = json.Unmarshal(data, tx)
42 | if err != nil {
43 | return nil, err
44 | }
45 |
46 | return tx, nil
47 | }
48 |
49 | // Put 保存transaction
50 | func (l *LevelDB) Put(tx *recorder.Transaction) error {
51 | if tx == nil {
52 | return errors.New("transaction is nil")
53 | }
54 | var err error
55 | removeValue := l.queue.Add(tx.Id)
56 | if removeValue != nil {
57 | err = l.db.Delete([]byte(tx.Id), nil)
58 | if err != nil {
59 | return err
60 | }
61 | }
62 | data, err := json.Marshal(tx)
63 | if err != nil {
64 | return err
65 | }
66 | err = l.db.Put([]byte(tx.Id), data, nil)
67 |
68 | return err
69 | }
70 |
--------------------------------------------------------------------------------
/internal/common/recorder/storage/leveldb_test.go:
--------------------------------------------------------------------------------
1 | package storage
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "strconv"
7 | "testing"
8 | "time"
9 |
10 | "github.com/ouqiang/goutil"
11 | "github.com/ouqiang/mars/internal/common/recorder"
12 | "github.com/stretchr/testify/require"
13 | "github.com/syndtr/goleveldb/leveldb"
14 |
15 | "github.com/ouqiang/mars/internal/common"
16 | )
17 |
18 | func TestLevelDB(t *testing.T) {
19 | queueSize := 10
20 | queue := common.NewQueue(queueSize)
21 | randString := strconv.FormatInt(time.Now().UnixNano(), 10) + strconv.Itoa(goutil.RandNumber(10000000, 99999999))
22 | dbFile := filepath.Join(os.TempDir(), randString)
23 | db, err := leveldb.OpenFile(dbFile, nil)
24 | require.NoError(t, err)
25 | defer os.RemoveAll(dbFile)
26 | defer db.Close()
27 |
28 | s := NewLevelDB(db, queue)
29 |
30 | tx := recorder.NewTransaction()
31 | err = s.Put(tx)
32 | require.NoError(t, err)
33 | tx, err = s.Get(tx.Id)
34 | require.NoError(t, err)
35 |
36 | tx, err = s.Get("not found")
37 | require.Error(t, err)
38 | }
39 |
--------------------------------------------------------------------------------
/internal/common/recorder/transaction.go:
--------------------------------------------------------------------------------
1 | // Package recorder 记录http transaction
2 | package recorder
3 |
4 | import (
5 | "bytes"
6 | "compress/gzip"
7 | "fmt"
8 | "io/ioutil"
9 | "net/http"
10 | "strings"
11 | "time"
12 |
13 | uuid "github.com/satori/go.uuid"
14 |
15 | "github.com/ouqiang/goproxy"
16 | )
17 |
18 | const (
19 | contentTypeBinary = "application/octet-stream"
20 | contentTypePlain = "text/plain"
21 | )
22 |
23 | var textMimeTypes = []string{
24 | "text/xml", "text/html", "text/css", "text/plain", "text/javascript",
25 | "application/xml", "application/json", "application/javascript", "application/x-www-form-urlencoded",
26 | "application/x-javascript",
27 | }
28 |
29 | // Transaction HTTP事务
30 | type Transaction struct {
31 | // Id 唯一id
32 | Id string `json:"id"`
33 | // Req 请求
34 | Req *Request `json:"request"`
35 | // Resp 响应
36 | Resp *Response `json:"response"`
37 | // ClientIP 客户端IP
38 | ClientIP string `json:"client_ip"`
39 | // ServerIP 服务端IP
40 | ServerIP string `json:"server_ip"`
41 | // StartTime 开始时间
42 | StartTime time.Time `json:"start_time"`
43 | // Duration 持续时间
44 | Duration time.Duration `json:"duration"`
45 | }
46 |
47 | // NewTransaction 创建HTTP事务
48 | func NewTransaction() *Transaction {
49 | tx := &Transaction{
50 | Id: uuid.NewV4().String(),
51 | Req: NewRequest(),
52 | Resp: NewResponse(),
53 | }
54 |
55 | return tx
56 | }
57 |
58 | // DumpRequest 提取request
59 | func (tx *Transaction) DumpRequest(req *http.Request) {
60 | // 设置Accept-Encoding后, http.transport不会自动解压, 需要自己处理
61 | // 强制使用gzip
62 | if req.Header.Get("Accept-Encoding") != "" {
63 | req.Header.Set("Accept-Encoding", "gzip")
64 | }
65 |
66 | tx.Req.Method = req.Method
67 | tx.Req.Header = goproxy.CloneHeader(req.Header)
68 | tx.Req.Proto = req.Proto
69 | tx.Req.URL = req.URL.String()
70 | tx.Req.Scheme = req.URL.Scheme
71 | tx.Req.Host = req.URL.Host
72 | tx.Req.Path = req.URL.Path
73 | tx.Req.QueryParam = req.URL.RawQuery
74 |
75 | var err error
76 | var body []byte
77 | req.Body, body, err = goproxy.CloneBody(req.Body)
78 | contentType := getContentType(req.Header)
79 | tx.Req.Body.setContent(contentType, body)
80 | if err != nil {
81 | body = []byte(fmt.Sprintf("复制request body错误: %s", err))
82 | tx.Req.Body.setContent(contentTypePlain, body)
83 | }
84 | }
85 |
86 | // DumpRequest 提取response
87 | func (tx *Transaction) DumpResponse(resp *http.Response, e error) {
88 | if e != nil {
89 | tx.Resp.Err = e.Error()
90 | return
91 | }
92 | tx.Resp.Proto = resp.Proto
93 | tx.Resp.Header = goproxy.CloneHeader(resp.Header)
94 | tx.Resp.Status = resp.Status
95 | tx.Resp.StatusCode = resp.StatusCode
96 |
97 | contentType := getContentType(resp.Header)
98 | if !shouldReadBody(contentType) {
99 | tx.Resp.Body.setContent(contentTypeBinary, nil)
100 | return
101 | }
102 |
103 | var err error
104 | var body []byte
105 | resp.Body, body, err = goproxy.CloneBody(resp.Body)
106 | tx.Resp.Body.setContent(contentType, body)
107 | if err != nil {
108 | body = []byte(fmt.Sprintf("复制response body错误: %s", err))
109 | tx.Resp.Body.setContent(contentTypePlain, body)
110 | return
111 | }
112 |
113 | if !strings.Contains(resp.Header.Get("Content-Encoding"), "gzip") {
114 | return
115 | }
116 |
117 | b, err := gzip.NewReader(bytes.NewReader(tx.Resp.Body.Content))
118 | if err != nil {
119 | body = []byte(fmt.Sprintf("解压response body错误: %s", err))
120 | tx.Resp.Body.setContent(contentTypePlain, body)
121 | return
122 | }
123 | body, err = ioutil.ReadAll(b)
124 | if e != nil {
125 | body = []byte(fmt.Sprintf("读取解压后的response body错误: %s", err))
126 | tx.Resp.Body.setContent(contentTypePlain, body)
127 | return
128 | }
129 | tx.Resp.Body.setContent(contentType, body)
130 | }
131 |
132 | // body是否是二进制内容
133 | func IsBinaryBody(contentType string) bool {
134 | for _, item := range textMimeTypes {
135 | if item == contentType {
136 | return false
137 | }
138 | }
139 |
140 | return true
141 | }
142 |
143 | // 是否应该读取Body内容
144 | func shouldReadBody(contentType string) bool {
145 | return strings.HasPrefix(contentType, "image/") || !IsBinaryBody(contentType)
146 | }
147 |
148 | // 获取body类型
149 | func getContentType(h http.Header) string {
150 | ct := h.Get("Content-Type")
151 | segments := strings.Split(strings.TrimSpace(ct), ";")
152 | if len(segments) > 0 && segments[0] != "" {
153 | return strings.TrimSpace(segments[0])
154 | }
155 |
156 | return contentTypeBinary
157 | }
158 |
--------------------------------------------------------------------------------
/internal/common/socket/codec/codec.go:
--------------------------------------------------------------------------------
1 | package codec
2 |
3 | import (
4 | "github.com/ouqiang/mars/internal/common/socket/message"
5 | )
6 |
7 | // Codec 编解码器
8 | type Codec interface {
9 | // Marshal 编码
10 | Marshal(msgType message.Type, payload interface{}) ([]byte, error)
11 | // Unmarshal 解码
12 | Unmarshal(data []byte, registry *message.Registry) (msgType message.Type, payload interface{}, err error)
13 | // String 名称
14 | String() string
15 | }
16 |
--------------------------------------------------------------------------------
/internal/common/socket/codec/foo_test.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // source: foo_test.proto
3 |
4 | package codec
5 |
6 | import proto "github.com/golang/protobuf/proto"
7 | import fmt "fmt"
8 | import math "math"
9 |
10 | // Reference imports to suppress errors if they are not otherwise used.
11 | var _ = proto.Marshal
12 | var _ = fmt.Errorf
13 | var _ = math.Inf
14 |
15 | // This is a compile-time assertion to ensure that this generated file
16 | // is compatible with the proto package it is being compiled against.
17 | // A compilation error at this line likely means your copy of the
18 | // proto package needs to be updated.
19 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
20 |
21 | // 测试
22 | type Foo struct {
23 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
24 | XXX_NoUnkeyedLiteral struct{} `json:"-"`
25 | XXX_unrecognized []byte `json:"-"`
26 | XXX_sizecache int32 `json:"-"`
27 | }
28 |
29 | func (m *Foo) Reset() { *m = Foo{} }
30 | func (m *Foo) String() string { return proto.CompactTextString(m) }
31 | func (*Foo) ProtoMessage() {}
32 | func (*Foo) Descriptor() ([]byte, []int) {
33 | return fileDescriptor_foo_test_b51d4077268954bd, []int{0}
34 | }
35 | func (m *Foo) XXX_Unmarshal(b []byte) error {
36 | return xxx_messageInfo_Foo.Unmarshal(m, b)
37 | }
38 | func (m *Foo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
39 | return xxx_messageInfo_Foo.Marshal(b, m, deterministic)
40 | }
41 | func (dst *Foo) XXX_Merge(src proto.Message) {
42 | xxx_messageInfo_Foo.Merge(dst, src)
43 | }
44 | func (m *Foo) XXX_Size() int {
45 | return xxx_messageInfo_Foo.Size(m)
46 | }
47 | func (m *Foo) XXX_DiscardUnknown() {
48 | xxx_messageInfo_Foo.DiscardUnknown(m)
49 | }
50 |
51 | var xxx_messageInfo_Foo proto.InternalMessageInfo
52 |
53 | func (m *Foo) GetName() string {
54 | if m != nil {
55 | return m.Name
56 | }
57 | return ""
58 | }
59 |
60 | func init() {
61 | proto.RegisterType((*Foo)(nil), "codec.Foo")
62 | }
63 |
64 | func init() { proto.RegisterFile("foo_test.proto", fileDescriptor_foo_test_b51d4077268954bd) }
65 |
66 | var fileDescriptor_foo_test_b51d4077268954bd = []byte{
67 | // 76 bytes of a gzipped FileDescriptorProto
68 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4b, 0xcb, 0xcf, 0x8f,
69 | 0x2f, 0x49, 0x2d, 0x2e, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x4d, 0xce, 0x4f, 0x49,
70 | 0x4d, 0x56, 0x92, 0xe4, 0x62, 0x76, 0xcb, 0xcf, 0x17, 0x12, 0xe2, 0x62, 0xc9, 0x4b, 0xcc, 0x4d,
71 | 0x95, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x02, 0xb3, 0x93, 0xd8, 0xc0, 0x0a, 0x8d, 0x01, 0x01,
72 | 0x00, 0x00, 0xff, 0xff, 0x0c, 0x00, 0x59, 0xbb, 0x3a, 0x00, 0x00, 0x00,
73 | }
74 |
--------------------------------------------------------------------------------
/internal/common/socket/codec/foo_test.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package codec;
4 |
5 | // 测试
6 | message Foo {
7 | string name = 1;
8 | }
--------------------------------------------------------------------------------
/internal/common/socket/codec/json.go:
--------------------------------------------------------------------------------
1 | package codec
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 |
8 | "github.com/ouqiang/mars/internal/common/socket/message"
9 | )
10 |
11 | // JSON json格式
12 | type JSON struct {
13 | // Type 类型
14 | Type message.Type `json:"type"`
15 | // Payload 消息体
16 | Payload json.RawMessage `json:"payload"`
17 | }
18 |
19 | // Marshal 编码
20 | func (JSON) Marshal(msgType message.Type, payload interface{}) ([]byte, error) {
21 | v, err := json.Marshal(payload)
22 | if err != nil {
23 | return nil, err
24 | }
25 |
26 | msg := &JSON{
27 | Type: msgType,
28 | Payload: v,
29 | }
30 |
31 | return json.Marshal(msg)
32 | }
33 |
34 | // Unmarshal 解码
35 | func (JSON) Unmarshal(data []byte, registry *message.Registry) (msgType message.Type, payload interface{}, e error) {
36 | if registry == nil {
37 | return 0, nil, errors.New("registry is nil")
38 | }
39 | jsonMsg := &JSON{}
40 | err := json.Unmarshal(data, jsonMsg)
41 | if err != nil {
42 | return 0, nil, err
43 | }
44 | value, found := registry.New(jsonMsg.Type)
45 | if !found {
46 | return 0, nil, fmt.Errorf("message type [%d] not found from registry", jsonMsg.Type)
47 | }
48 | err = json.Unmarshal(jsonMsg.Payload, value)
49 | if err != nil {
50 | return 0, nil, err
51 | }
52 |
53 | return jsonMsg.Type, value, err
54 | }
55 |
56 | func (JSON) String() string {
57 | return "json"
58 | }
59 |
--------------------------------------------------------------------------------
/internal/common/socket/codec/json_test.go:
--------------------------------------------------------------------------------
1 | package codec
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/ouqiang/mars/internal/common/socket/message"
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestJSON(t *testing.T) {
11 | type Foo struct {
12 | Name string `json:"name"`
13 | }
14 | var msgType message.Type = 1
15 | j := JSON{}
16 |
17 | data, err := j.Marshal(msgType, &Foo{
18 | Name: "json",
19 | })
20 | require.NoError(t, err)
21 | require.Equal(t, []byte(`{"type":1,"payload":{"name":"json"}}`), data)
22 | registry := message.NewRegistry()
23 | newMsgType, payload, err := j.Unmarshal(data, registry)
24 | require.Error(t, err)
25 |
26 | registry.Add(msgType, (*Foo)(nil))
27 | newMsgType, payload, err = j.Unmarshal(data, registry)
28 | require.NoError(t, err)
29 | require.Equal(t, msgType, newMsgType)
30 | foo, ok := payload.(*Foo)
31 | require.True(t, ok)
32 | require.Equal(t, "json", foo.Name)
33 | }
34 |
--------------------------------------------------------------------------------
/internal/common/socket/codec/protobuf.go:
--------------------------------------------------------------------------------
1 | package codec
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/golang/protobuf/proto"
8 | "github.com/golang/protobuf/ptypes"
9 | "github.com/ouqiang/mars/internal/common/socket/message"
10 | )
11 |
12 | // Protobuf protobuf格式
13 | type Protobuf struct{}
14 |
15 | // Marshal 编码
16 | func (Protobuf) Marshal(msgType message.Type, payload interface{}) ([]byte, error) {
17 | pb, ok := payload.(proto.Message)
18 | if !ok {
19 | return nil, errors.New("invalid protobuf payload")
20 | }
21 | data, err := ptypes.MarshalAny(pb)
22 | if err != nil {
23 | return nil, err
24 | }
25 | msg := &message.Message{
26 | Type: int32(msgType),
27 | Payload: data,
28 | }
29 |
30 | return proto.Marshal(msg)
31 | }
32 |
33 | // Unmarshal 解码
34 | func (Protobuf) Unmarshal(data []byte, registry *message.Registry) (msgType message.Type, payload interface{}, e error) {
35 | if registry == nil {
36 | return 0, nil, errors.New("registry is nil")
37 | }
38 | msg := &message.Message{}
39 | err := proto.Unmarshal(data, msg)
40 | if err != nil {
41 | return 0, nil, err
42 | }
43 | v, found := registry.New(message.Type(msg.Type))
44 | if !found {
45 | return 0, nil, fmt.Errorf("message type [%d] not found from registry", msg.Type)
46 | }
47 | pb, ok := v.(proto.Message)
48 | if !ok {
49 | return 0, nil, fmt.Errorf("registry type [%d] is not pb", msg.Type)
50 | }
51 | err = ptypes.UnmarshalAny(msg.Payload, pb)
52 | if err != nil {
53 | return 0, nil, err
54 | }
55 |
56 | return message.Type(msg.Type), pb, err
57 | }
58 |
59 | func (Protobuf) String() string {
60 | return "protobuf"
61 | }
62 |
--------------------------------------------------------------------------------
/internal/common/socket/codec/protobuf_test.go:
--------------------------------------------------------------------------------
1 | package codec
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/ouqiang/mars/internal/common/socket/message"
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestProtobuf(t *testing.T) {
11 | var msgType message.Type = 100
12 | pb := &Foo{
13 | Name: "protobuf",
14 | }
15 | p := Protobuf{}
16 | data, err := p.Marshal(msgType, pb)
17 | require.NoError(t, err)
18 |
19 | registry := message.NewRegistry()
20 | newMsgType, payload, err := p.Unmarshal(data, registry)
21 | require.Error(t, err)
22 |
23 | registry.Add(msgType, (*Foo)(nil))
24 | newMsgType, payload, err = p.Unmarshal(data, registry)
25 | require.NoError(t, err)
26 | require.Equal(t, msgType, newMsgType)
27 |
28 | foo, ok := payload.(*Foo)
29 | require.True(t, ok)
30 | require.Equal(t, foo.GetName(), pb.Name)
31 | }
32 |
--------------------------------------------------------------------------------
/internal/common/socket/conn/conn.go:
--------------------------------------------------------------------------------
1 | package conn
2 |
3 | import (
4 | "io"
5 | "net"
6 | "time"
7 | )
8 |
9 | // Conn 连接
10 | type Conn interface {
11 | // SetWriteDeadline 写入超时
12 | SetWriteDeadline(t time.Time) error
13 |
14 | // SetReadDeadline 读取超时
15 | SetReadDeadline(t time.Time) error
16 |
17 | // SetReadLimit 读取限制
18 | SetReadLimit(limit int64)
19 |
20 | // LocalAddr 本地地址
21 | LocalAddr() net.Addr
22 |
23 | // RemoteAddr 远程地址
24 | RemoteAddr() net.Addr
25 |
26 | // ReadMessage 读取消息
27 | ReadMessage() (p []byte, err error)
28 |
29 | io.Writer
30 | io.Closer
31 | }
32 |
--------------------------------------------------------------------------------
/internal/common/socket/conn/websocket.go:
--------------------------------------------------------------------------------
1 | package conn
2 |
3 | import (
4 | "net"
5 | "time"
6 |
7 | "github.com/gorilla/websocket"
8 | )
9 |
10 | // WebSocket webSocket连接
11 | type WebSocket struct {
12 | msgType int
13 | conn *websocket.Conn
14 | }
15 |
16 | var _ Conn = (*WebSocket)(nil)
17 |
18 | // NewWebSocket 创建WebSocket
19 | func NewWebSocket(conn *websocket.Conn, msgType int) *WebSocket {
20 | w := &WebSocket{
21 | conn: conn,
22 | msgType: msgType,
23 | }
24 |
25 | return w
26 | }
27 |
28 | // ReadMessage 读取消息
29 | func (w *WebSocket) ReadMessage() (p []byte, err error) {
30 | _, p, err = w.conn.ReadMessage()
31 |
32 | return
33 | }
34 |
35 | // Write 写入消息
36 | func (w *WebSocket) Write(p []byte) (n int, err error) {
37 | err = w.conn.WriteMessage(w.msgType, p)
38 | if err != nil {
39 | return 0, err
40 | }
41 |
42 | return len(p), nil
43 | }
44 |
45 | // Close 关闭连接
46 | func (w *WebSocket) Close() error {
47 | return w.conn.Close()
48 | }
49 |
50 | // SetWriteDeadline 设置写入超时时间
51 | func (w *WebSocket) SetWriteDeadline(t time.Time) error {
52 | return w.conn.SetWriteDeadline(t)
53 | }
54 |
55 | // SetReadDeadline 设置读取超时时间
56 | func (w *WebSocket) SetReadDeadline(t time.Time) error {
57 | return w.conn.SetReadDeadline(t)
58 | }
59 |
60 | // SetReadLimit 设置读取限制
61 | func (w *WebSocket) SetReadLimit(limit int64) {
62 | w.conn.SetReadLimit(limit)
63 | }
64 |
65 | // LocalAddr 本机地址
66 | func (w *WebSocket) LocalAddr() net.Addr {
67 | return w.conn.LocalAddr()
68 | }
69 |
70 | // RemoteAddr 远程地址
71 | func (w *WebSocket) RemoteAddr() net.Addr {
72 | return w.conn.RemoteAddr()
73 | }
74 |
--------------------------------------------------------------------------------
/internal/common/socket/conn/websocket_test.go:
--------------------------------------------------------------------------------
1 | package conn
2 |
3 | import (
4 | "net/http"
5 | "testing"
6 |
7 | "github.com/gorilla/websocket"
8 | "github.com/posener/wstest"
9 | "github.com/stretchr/testify/require"
10 | )
11 |
12 | func testWebSocketHandler(msgType int) http.Handler {
13 | upgrade := websocket.Upgrader{}
14 | handler := func(rw http.ResponseWriter, req *http.Request) {
15 | conn, err := upgrade.Upgrade(rw, req, nil)
16 | if err != nil {
17 | panic(err)
18 | }
19 | webSocket := NewWebSocket(conn, msgType)
20 | data, err := webSocket.ReadMessage()
21 | if err != nil {
22 | panic(err)
23 | }
24 | _, err = webSocket.Write(data)
25 | if err != nil {
26 | panic(err)
27 | }
28 | }
29 |
30 | return http.HandlerFunc(handler)
31 | }
32 |
33 | func TestWebSocket(t *testing.T) {
34 | conn, resp, err := wstest.NewDialer(testWebSocketHandler(websocket.TextMessage)).Dial("ws:/", nil)
35 | require.NoError(t, err)
36 | require.Equal(t, resp.StatusCode, http.StatusSwitchingProtocols)
37 | msg := []byte("ping")
38 | err = conn.WriteMessage(websocket.TextMessage, msg)
39 | require.NoError(t, err)
40 | _, data, err := conn.ReadMessage()
41 | require.NoError(t, err)
42 | require.Equal(t, msg, data)
43 | }
44 |
--------------------------------------------------------------------------------
/internal/common/socket/context.go:
--------------------------------------------------------------------------------
1 | package socket
2 |
3 | import (
4 | "github.com/ouqiang/mars/internal/common/socket/message"
5 | )
6 |
7 | const (
8 | abortIndex = 256
9 | )
10 |
11 | // Context 上下文信息
12 | type Context struct {
13 | // Session 对应客户端连接
14 | Session *Session
15 | // MsgType 消息类型
16 | MsgType message.Type
17 | // Payload 消息体
18 | Payload interface{}
19 | handlersChain HandlersChain
20 | index int
21 | }
22 |
23 | func newContext() *Context {
24 | ctx := &Context{
25 | index: -1,
26 | }
27 |
28 | return ctx
29 | }
30 |
31 | // Abort 中断执行
32 | func (ctx *Context) Abort() {
33 | ctx.index = abortIndex
34 | }
35 |
36 | // IsAborted 是否中断
37 | func (ctx *Context) IsAborted() bool {
38 | return ctx.index >= abortIndex
39 | }
40 |
41 | // Next 执行下一个中间件
42 | func (ctx *Context) Next() {
43 | ctx.index++
44 | l := len(ctx.handlersChain)
45 | for ; ctx.index < l; ctx.index++ {
46 | ctx.handlersChain[ctx.index](ctx)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/internal/common/socket/hub.go:
--------------------------------------------------------------------------------
1 | package socket
2 |
3 | import (
4 | "io"
5 | "sync"
6 | "sync/atomic"
7 | )
8 |
9 | // Hub session管理
10 | type Hub struct {
11 | sessions sync.Map
12 | broadcast chan []byte
13 | num int32
14 | }
15 |
16 | // NewHub 创建session集合实例
17 | func NewHub(broadcastQueueSize int) *Hub {
18 | h := &Hub{
19 | broadcast: make(chan []byte, broadcastQueueSize),
20 | }
21 |
22 | go h.run()
23 |
24 | return h
25 | }
26 |
27 | // Get 获取session
28 | func (ch *Hub) Get(sessionID string) (io.Writer, bool) {
29 | value, ok := ch.sessions.Load(sessionID)
30 | if !ok {
31 | return nil, false
32 | }
33 |
34 | return value.(io.Writer), true
35 | }
36 |
37 | // Add 添加session
38 | func (ch *Hub) Add(sessionID string, w io.Writer) {
39 | if sessionID == "" || w == nil {
40 | return
41 | }
42 | ch.sessions.Store(sessionID, w)
43 | atomic.AddInt32(&ch.num, 1)
44 | }
45 |
46 | // Delete 删除session
47 | func (ch *Hub) Delete(sessionID string) {
48 | ch.sessions.Delete(sessionID)
49 | atomic.AddInt32(&ch.num, -1)
50 | }
51 |
52 | // Broadcast 广播
53 | func (ch *Hub) Broadcast(data []byte) {
54 | ch.broadcast <- data
55 | }
56 |
57 | // Num session数量
58 | func (ch *Hub) Num() int32 {
59 | return atomic.LoadInt32(&ch.num)
60 | }
61 |
62 | // Range 遍历session
63 | func (ch *Hub) Range(f func(key, value interface{}) bool) {
64 | ch.sessions.Range(f)
65 | }
66 |
67 | // 运行
68 | func (ch *Hub) run() {
69 | for data := range ch.broadcast {
70 | ch.sessions.Range(func(key, value interface{}) bool {
71 | w := value.(io.Writer)
72 | w.Write(data)
73 | return true
74 | })
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/internal/common/socket/hub_test.go:
--------------------------------------------------------------------------------
1 | package socket
2 |
3 | import (
4 | "strconv"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stretchr/testify/require"
9 | )
10 |
11 | type testSession struct {
12 | any int
13 | }
14 |
15 | func (tc *testSession) Write(p []byte) (n int, err error) {
16 | return len(p), nil
17 | }
18 |
19 | func TestHub(t *testing.T) {
20 | hub := NewHub(50)
21 |
22 | var clientNum int64 = 10
23 | clients := make([]*testSession, 0, clientNum)
24 | var i int64 = 1
25 | for ; i <= clientNum; i++ {
26 | c := &testSession{}
27 | clients = append(clients, c)
28 | hub.Add(strconv.FormatInt(i, 10), c)
29 | }
30 | time.Sleep(1 * time.Millisecond)
31 | require.Equal(t, int32(clientNum), hub.Num())
32 |
33 | hub.Broadcast([]byte("session broadcast"))
34 |
35 | for i = 1; i <= clientNum; i++ {
36 | hub.Delete(strconv.FormatInt(i, 10))
37 | }
38 | time.Sleep(1 * time.Millisecond)
39 | require.Equal(t, int32(0), hub.Num())
40 | }
41 |
--------------------------------------------------------------------------------
/internal/common/socket/message/message.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // source: message.proto
3 |
4 | package message
5 |
6 | import proto "github.com/golang/protobuf/proto"
7 | import fmt "fmt"
8 | import math "math"
9 | import any "github.com/golang/protobuf/ptypes/any"
10 |
11 | // Reference imports to suppress errors if they are not otherwise used.
12 | var _ = proto.Marshal
13 | var _ = fmt.Errorf
14 | var _ = math.Inf
15 |
16 | // This is a compile-time assertion to ensure that this generated file
17 | // is compatible with the proto package it is being compiled against.
18 | // A compilation error at this line likely means your copy of the
19 | // proto package needs to be updated.
20 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
21 |
22 | // 消息
23 | type Message struct {
24 | Type int32 `protobuf:"varint,1,opt,name=type,proto3" json:"type,omitempty"`
25 | Payload *any.Any `protobuf:"bytes,2,opt,name=payload,proto3" json:"payload,omitempty"`
26 | XXX_NoUnkeyedLiteral struct{} `json:"-"`
27 | XXX_unrecognized []byte `json:"-"`
28 | XXX_sizecache int32 `json:"-"`
29 | }
30 |
31 | func (m *Message) Reset() { *m = Message{} }
32 | func (m *Message) String() string { return proto.CompactTextString(m) }
33 | func (*Message) ProtoMessage() {}
34 | func (*Message) Descriptor() ([]byte, []int) {
35 | return fileDescriptor_message_f669b3410bd57257, []int{0}
36 | }
37 | func (m *Message) XXX_Unmarshal(b []byte) error {
38 | return xxx_messageInfo_Message.Unmarshal(m, b)
39 | }
40 | func (m *Message) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
41 | return xxx_messageInfo_Message.Marshal(b, m, deterministic)
42 | }
43 | func (dst *Message) XXX_Merge(src proto.Message) {
44 | xxx_messageInfo_Message.Merge(dst, src)
45 | }
46 | func (m *Message) XXX_Size() int {
47 | return xxx_messageInfo_Message.Size(m)
48 | }
49 | func (m *Message) XXX_DiscardUnknown() {
50 | xxx_messageInfo_Message.DiscardUnknown(m)
51 | }
52 |
53 | var xxx_messageInfo_Message proto.InternalMessageInfo
54 |
55 | func (m *Message) GetType() int32 {
56 | if m != nil {
57 | return m.Type
58 | }
59 | return 0
60 | }
61 |
62 | func (m *Message) GetPayload() *any.Any {
63 | if m != nil {
64 | return m.Payload
65 | }
66 | return nil
67 | }
68 |
69 | func init() {
70 | proto.RegisterType((*Message)(nil), "message.Message")
71 | }
72 |
73 | func init() { proto.RegisterFile("message.proto", fileDescriptor_message_f669b3410bd57257) }
74 |
75 | var fileDescriptor_message_f669b3410bd57257 = []byte{
76 | // 124 bytes of a gzipped FileDescriptorProto
77 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xcd, 0x4d, 0x2d, 0x2e,
78 | 0x4e, 0x4c, 0x4f, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x87, 0x72, 0xa5, 0x24, 0xd3,
79 | 0xf3, 0xf3, 0xd3, 0x73, 0x52, 0xf5, 0xc1, 0xc2, 0x49, 0xa5, 0x69, 0xfa, 0x89, 0x79, 0x95, 0x10,
80 | 0x35, 0x4a, 0xbe, 0x5c, 0xec, 0xbe, 0x10, 0x55, 0x42, 0x42, 0x5c, 0x2c, 0x25, 0x95, 0x05, 0xa9,
81 | 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0xac, 0x41, 0x60, 0xb6, 0x90, 0x1e, 0x17, 0x7b, 0x41, 0x62, 0x65,
82 | 0x4e, 0x7e, 0x62, 0x8a, 0x04, 0x93, 0x02, 0xa3, 0x06, 0xb7, 0x91, 0x88, 0x1e, 0xc4, 0x2c, 0x3d,
83 | 0x98, 0x59, 0x7a, 0x8e, 0x79, 0x95, 0x41, 0x30, 0x45, 0x49, 0x6c, 0x60, 0x61, 0x63, 0x40, 0x00,
84 | 0x00, 0x00, 0xff, 0xff, 0x2a, 0x4d, 0x7d, 0xd2, 0x8a, 0x00, 0x00, 0x00,
85 | }
86 |
--------------------------------------------------------------------------------
/internal/common/socket/message/message.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package message;
4 |
5 | import "google/protobuf/any.proto";
6 |
7 | // 消息
8 | message Message {
9 | int32 type = 1; // 消息类型
10 | google.protobuf.Any payload = 2; // 消息体
11 | }
--------------------------------------------------------------------------------
/internal/common/socket/message/registry.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import "reflect"
4 |
5 | // Registry 消息类型注册
6 | type Registry struct {
7 | typeMap map[Type]reflect.Type
8 | }
9 |
10 | // NewRegistry 新建实例
11 | func NewRegistry() *Registry {
12 | r := &Registry{
13 | typeMap: make(map[Type]reflect.Type),
14 | }
15 |
16 | return r
17 | }
18 |
19 | // Add 新增
20 | func (r *Registry) Add(msgType Type, payload interface{}) {
21 | r.typeMap[msgType] = reflect.TypeOf(payload).Elem()
22 | }
23 |
24 | // New 根据类型创建实例
25 | func (r *Registry) New(msgType Type) (v interface{}, found bool) {
26 | elem, ok := r.typeMap[msgType]
27 | if !ok {
28 | return nil, false
29 | }
30 |
31 | return reflect.New(elem).Interface(), true
32 | }
33 |
--------------------------------------------------------------------------------
/internal/common/socket/message/registry_test.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/require"
8 | )
9 |
10 | func TestRegistry(t *testing.T) {
11 | r := NewRegistry()
12 |
13 | type Foo struct {
14 | Name string
15 | }
16 |
17 | var msgType Type = 10
18 | r.Add(msgType, (*Foo)(nil))
19 | v, found := r.New(msgType)
20 | require.True(t, found)
21 | require.Equal(t, reflect.TypeOf((*Foo)(nil)).Elem().Name(), reflect.TypeOf(v).Elem().Name())
22 | require.NotEqual(t, reflect.TypeOf((*Foo)(nil)).Elem().Name(), reflect.TypeOf((*Registry)(nil)).Elem().Name())
23 |
24 | v, found = r.New(Type(11))
25 | require.False(t, found)
26 | require.Nil(t, v)
27 | }
28 |
--------------------------------------------------------------------------------
/internal/common/socket/message/type.go:
--------------------------------------------------------------------------------
1 | package message
2 |
3 | // Type 消息类型
4 | type Type int32
5 |
--------------------------------------------------------------------------------
/internal/common/socket/router.go:
--------------------------------------------------------------------------------
1 | package socket
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/ouqiang/mars/internal/common/socket/codec"
7 | "github.com/ouqiang/mars/internal/common/socket/message"
8 | )
9 |
10 | // Handler 路由处理器
11 | type Handler func(ctx *Context)
12 |
13 | // HandlersChain 路由处理器链
14 | type HandlersChain []Handler
15 |
16 | // router 路由
17 | type Router struct {
18 | handlers map[message.Type]HandlersChain
19 | handlersChain HandlersChain
20 | registry *message.Registry
21 | errorHandler func(session *Session, err interface{})
22 | notFoundHandler func(session *Session, codec codec.Codec, data []byte)
23 | }
24 |
25 | // NewRouter 创建实例
26 | func NewRouter() *Router {
27 | r := &Router{
28 | handlers: make(map[message.Type]HandlersChain),
29 | registry: message.NewRegistry(),
30 | }
31 |
32 | return r
33 | }
34 |
35 | // Register 路由注册
36 | func (r *Router) Register(msgType message.Type, payload interface{}, handler Handler) *Router {
37 | if payload == nil {
38 | panic("payload type is nil")
39 | }
40 | if handler == nil {
41 | panic("handler is nil")
42 | }
43 | mergeHandlers := make(HandlersChain, len(r.handlersChain), len(r.handlersChain)+1)
44 | copy(mergeHandlers, r.handlersChain)
45 | mergeHandlers = append(mergeHandlers, handler)
46 | r.handlers[msgType] = mergeHandlers
47 | r.registry.Add(msgType, payload)
48 |
49 | return r
50 | }
51 |
52 | // Use 设置全局中间件
53 | func (r *Router) Use(middleware ...Handler) {
54 | r.handlersChain = append(r.handlersChain, middleware...)
55 | }
56 |
57 | // Dispatch 路由分发
58 | func (r *Router) Dispatch(session *Session, codec codec.Codec, data []byte) {
59 | defer func() {
60 | if err := recover(); err != nil {
61 | r.triggerError(session, err)
62 | }
63 | }()
64 | msgType, payload, err := codec.Unmarshal(data, r.registry)
65 | if err != nil {
66 | if r.notFoundHandler != nil {
67 | r.notFoundHandler(session, codec, data)
68 | return
69 | }
70 | r.triggerError(session, fmt.Errorf("路由分发, 解码错误: %s", err))
71 | return
72 | }
73 | handlersChain, ok := r.handlers[msgType]
74 | if !ok {
75 | r.triggerError(session, fmt.Errorf("路由分发, 根据消息类型找不到对应的handler 消息类型: [%d] payload: [%+v]", msgType, payload))
76 | return
77 | }
78 | ctx := newContext()
79 | ctx.handlersChain = handlersChain
80 | ctx.Session = session
81 | ctx.MsgType = msgType
82 | ctx.Payload = payload
83 | ctx.Next()
84 | }
85 |
86 | // ErrorHandler 错误处理
87 | func (r *Router) ErrorHandler(h func(session *Session, err interface{})) {
88 | r.errorHandler = h
89 | }
90 |
91 | // NotFoundHandler 未找到handler
92 | func (r *Router) NotFoundHandler(h func(session *Session, codec codec.Codec, data []byte)) {
93 | r.notFoundHandler = h
94 | }
95 |
96 | func (r *Router) triggerError(session *Session, err interface{}) {
97 | if r.errorHandler != nil {
98 | r.errorHandler(session, err)
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/internal/common/socket/session.go:
--------------------------------------------------------------------------------
1 | package socket
2 |
3 | import (
4 | "net"
5 | "sync"
6 | "sync/atomic"
7 | "time"
8 |
9 | "github.com/ouqiang/mars/internal/common/socket/conn"
10 | )
11 |
12 | // 可选项
13 | type options struct {
14 | // 读取最大字节数
15 | readLimit int64
16 | // 心跳超时时间
17 | heartBeatTimeout time.Duration
18 | // 接收队列大小
19 | receiveQueueSize int
20 | // 发送队列大小
21 | sendQueueSize int
22 | // 写入超时时间
23 | writeTimeout time.Duration
24 | // 读取超时时间
25 | readTimeout time.Duration
26 | // 心跳超时次数
27 | heartBeatTimeoutTimes int
28 | }
29 |
30 | var defaultSessionOptions = options{
31 | readLimit: 4 << 20,
32 | heartBeatTimeout: 30 * time.Second,
33 | receiveQueueSize: 50,
34 | sendQueueSize: 50,
35 | readTimeout: 2 * time.Minute,
36 | writeTimeout: 10 * time.Second,
37 | heartBeatTimeoutTimes: 2,
38 | }
39 |
40 | // SessionOption 可选项
41 | type SessionOption func(*options)
42 |
43 | // SessionHandler 事件处理
44 | type SessionHandler interface {
45 | OnConnect(*Session)
46 | OnMessage(*Session, []byte)
47 | OnClose(*Session)
48 | OnError(*Session, error)
49 | }
50 |
51 | // WithSessionHeartBeatTimeoutTimes 心跳超时次数
52 | func WithSessionHeartBeatTimeoutTimes(times int) SessionOption {
53 | return func(opt *options) {
54 | opt.heartBeatTimeoutTimes = times
55 | }
56 | }
57 |
58 | // WithSessionReadLimit 读取最大字节数
59 | func WithSessionReadLimit(limit int64) SessionOption {
60 | return func(opt *options) {
61 | opt.readLimit = limit
62 | }
63 | }
64 |
65 | // WithSessionHeartBeatTimeout 心跳超时
66 | func WithSessionHeartBeatTimeout(d time.Duration) SessionOption {
67 | return func(opt *options) {
68 | opt.heartBeatTimeout = d
69 | }
70 | }
71 |
72 | // WithSessionReceiveQueueSize 接收队列大小
73 | func WithSessionReceiveQueueSize(size int) SessionOption {
74 | return func(opt *options) {
75 | opt.receiveQueueSize = size
76 | }
77 | }
78 |
79 | // WithSessionSendQueueSize 发送队列大小
80 | func WithSessionSendQueueSize(size int) SessionOption {
81 | return func(opt *options) {
82 | opt.sendQueueSize = size
83 | }
84 | }
85 |
86 | // WithSessionWriteTimeout 写入超时
87 | func WithSessionWriteTimeout(d time.Duration) SessionOption {
88 | return func(opt *options) {
89 | opt.writeTimeout = d
90 | }
91 | }
92 |
93 | // WithSessionReadTimeout 读取超时
94 | func WithSessionReadTimeout(d time.Duration) SessionOption {
95 | return func(opt *options) {
96 | opt.readTimeout = d
97 | }
98 | }
99 |
100 | // Session 对应一个客户端连接
101 | type Session struct {
102 | ID string
103 | // Data 保存Session相关数据
104 | Data sync.Map
105 | handler SessionHandler
106 | conn conn.Conn
107 | lastActiveTime atomic.Value
108 | receiveChan chan []byte
109 | sendChan chan []byte
110 | closeChan chan struct{}
111 | closeOnce sync.Once
112 | closed atomic.Value
113 | heartBeatTimer *time.Ticker
114 | opts options
115 | }
116 |
117 | // NewSession 创建session实例
118 | func NewSession(conn conn.Conn, handler SessionHandler, opt ...SessionOption) *Session {
119 | if conn == nil {
120 | panic("conn is nil")
121 | }
122 | if handler == nil {
123 | panic("handler is nil")
124 | }
125 | opts := defaultSessionOptions
126 | for _, o := range opt {
127 | o(&opts)
128 | }
129 | s := &Session{
130 | handler: handler,
131 | opts: opts,
132 | conn: conn,
133 | receiveChan: make(chan []byte, opts.receiveQueueSize),
134 | sendChan: make(chan []byte, opts.sendQueueSize),
135 | closeChan: make(chan struct{}),
136 | heartBeatTimer: time.NewTicker(opts.heartBeatTimeout),
137 | }
138 | s.lastActiveTime.Store(time.Now())
139 | s.closed.Store(false)
140 |
141 | return s
142 | }
143 |
144 | // Run 运行
145 | func (s *Session) Run() {
146 | go s.startHeartTimer()
147 | go s.receiveMessage()
148 | go s.handleMessage()
149 | go s.sendMessage()
150 |
151 | s.handler.OnConnect(s)
152 | }
153 |
154 | // Write 写入数据
155 | func (s *Session) Write(data []byte) (n int, err error) {
156 | select {
157 | case s.sendChan <- data:
158 | default:
159 | s.Close()
160 | }
161 |
162 | return len(data), nil
163 | }
164 |
165 | // RemoteAddr 远程主机地址
166 | func (s *Session) RemoteAddr() net.Addr {
167 | return s.conn.RemoteAddr()
168 | }
169 |
170 | // Closed 连接是否已关闭
171 | func (s *Session) Closed() bool {
172 | return s.closed.Load().(bool)
173 | }
174 |
175 | // CloseNotify 连接关闭通知
176 | func (s *Session) CloseNotify() <-chan struct{} {
177 | return s.closeChan
178 | }
179 |
180 | // Close 关闭连接
181 | func (s *Session) Close() {
182 | s.closeOnce.Do(func() {
183 | s.closed.Store(true)
184 | close(s.closeChan)
185 | s.conn.Close()
186 |
187 | s.handler.OnClose(s)
188 | })
189 | }
190 |
191 | // 处理消息
192 | func (s *Session) handleMessage() {
193 | for data := range s.receiveChan {
194 | s.handler.OnMessage(s, data)
195 | }
196 | }
197 |
198 | // 发送消息
199 | func (s *Session) sendMessage() {
200 | defer s.Close()
201 | for {
202 | select {
203 | case <-s.closeChan:
204 | return
205 | case data := <-s.sendChan:
206 | s.conn.SetWriteDeadline(time.Now().Add(s.opts.writeTimeout))
207 | _, err := s.conn.Write(data)
208 | if err != nil {
209 | s.handler.OnError(s, err)
210 | return
211 | }
212 | }
213 | }
214 | }
215 |
216 | // 接收消息
217 | func (s *Session) receiveMessage() {
218 | defer func() {
219 | close(s.receiveChan)
220 | s.Close()
221 | }()
222 |
223 | for {
224 | s.conn.SetReadDeadline(time.Now().Add(s.opts.readTimeout))
225 | s.conn.SetReadLimit(s.opts.readLimit)
226 | data, err := s.conn.ReadMessage()
227 | if err != nil {
228 | s.handler.OnError(s, err)
229 | return
230 | }
231 | s.lastActiveTime.Store(time.Now())
232 | s.receiveChan <- data
233 | }
234 | }
235 |
236 | // 启动心跳定时器
237 | func (s *Session) startHeartTimer() {
238 | defer s.heartBeatTimer.Stop()
239 |
240 | timeoutTimes := 0
241 | for {
242 | select {
243 | case <-s.closeChan:
244 | return
245 | case <-s.heartBeatTimer.C:
246 | lastActiveTime := s.lastActiveTime.Load().(time.Time)
247 | if time.Now().Sub(lastActiveTime) <= s.opts.heartBeatTimeout {
248 | timeoutTimes = 0
249 | continue
250 | }
251 | timeoutTimes++
252 | if timeoutTimes >= s.opts.heartBeatTimeoutTimes {
253 | s.Close()
254 | return
255 | }
256 | }
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/internal/common/version/version.go:
--------------------------------------------------------------------------------
1 | // Package version 版本
2 | package version
3 |
4 | import "github.com/ouqiang/goutil"
5 |
6 | var (
7 | appVersion string
8 | buildDate string
9 | gitCommit string
10 | )
11 |
12 | // Init 初始化版本信息
13 | func Init(version, date, commitId string) {
14 | appVersion = version
15 | buildDate = date
16 | gitCommit = commitId
17 | }
18 |
19 | // Format 格式化版本信息
20 | func Format() string {
21 | out, err := goutil.FormatAppVersion(appVersion, gitCommit, buildDate)
22 | if err != nil {
23 | panic(err)
24 | }
25 |
26 | return out
27 | }
28 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | //go:generate statik -src=web/public -dest=internal -f
2 | // Binary mars HTTP(S)代理
3 | package main
4 |
5 | import (
6 | "github.com/ouqiang/mars/cmd"
7 | "github.com/ouqiang/mars/internal/common/version"
8 | )
9 |
10 | var (
11 | // AppVersion 应用版本
12 | AppVersion string
13 | // BuildDate 构建日期
14 | BuildDate string
15 | // GitCommit 最后提交的git commit
16 | GitCommit string
17 | )
18 |
19 | func main() {
20 | version.Init(AppVersion, BuildDate, GitCommit)
21 | cmd.Execute()
22 | }
23 |
--------------------------------------------------------------------------------
/screenshot/detail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ouqiang/mars/1e2a733b31d210140b585137c4dcc728bdd89e75/screenshot/detail.png
--------------------------------------------------------------------------------
/screenshot/list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ouqiang/mars/1e2a733b31d210140b585137c4dcc728bdd89e75/screenshot/list.png
--------------------------------------------------------------------------------
/script/package.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # 生成压缩包 xx.tar.gz或xx.zip
4 | # 使用 ./package.sh -a amd64 -p linux -v v2.0.0
5 |
6 | # 任何命令返回非0值退出
7 | set -o errexit
8 | # 使用未定义的变量退出
9 | set -o nounset
10 | # 管道中任一命令执行失败退出
11 | set -o pipefail
12 |
13 | eval $(go env)
14 |
15 | # 二进制文件名
16 | BINARY_NAME='mars'
17 | # main函数所在文件
18 | MAIN_FILE='main.go'
19 | # 需要打包的文件
20 | INCLUDE_FILE=(conf Dockerfile)
21 |
22 | # 提取git最新tag作为应用版本
23 | VERSION=''
24 | # 最新git commit id
25 | GIT_COMMIT_ID=''
26 |
27 | # 外部输入的系统
28 | INPUT_OS=()
29 | # 外部输入的架构
30 | INPUT_ARCH=()
31 | # 未指定OS,默认值
32 | DEFAULT_OS=${GOHOSTOS}
33 | # 未指定ARCH,默认值
34 | DEFAULT_ARCH=${GOHOSTARCH}
35 | # 支持的系统
36 | SUPPORT_OS=(linux darwin windows)
37 | # 支持的架构
38 | SUPPORT_ARCH=(386 amd64)
39 | # 编译参数
40 | LDFLAGS=''
41 | # 打包文件生成目录
42 | PACKAGE_DIR=''
43 | # 编译文件生成目录
44 | BUILD_DIR=''
45 |
46 | # 获取git 最新tag name
47 | git_latest_tag() {
48 | local COMMIT_ID=""
49 | local TAG_NAME=""
50 | COMMIT_ID=`git rev-list --tags --max-count=1`
51 | TAG_NAME=`git describe --tags "${COMMIT_ID}"`
52 |
53 | echo ${TAG_NAME}
54 | }
55 |
56 | # 获取git 最新commit id
57 | git_latest_commit() {
58 | echo "$(git rev-parse --short HEAD)"
59 | }
60 |
61 | # 打印信息
62 | print_message() {
63 | echo "$1"
64 | }
65 |
66 | # 打印信息后退出
67 | print_message_and_exit() {
68 | if [[ -n $1 ]]; then
69 | print_message "$1"
70 | fi
71 | exit 1
72 | }
73 |
74 | # 设置系统、CPU架构
75 | set_os_arch() {
76 | if [[ ${#INPUT_OS[@]} = 0 ]];then
77 | INPUT_OS=("${DEFAULT_OS}")
78 | fi
79 |
80 | if [[ ${#INPUT_ARCH[@]} = 0 ]];then
81 | INPUT_ARCH=("${DEFAULT_ARCH}")
82 | fi
83 |
84 | for OS in "${INPUT_OS[@]}"; do
85 | if [[ ! "${SUPPORT_OS[*]}" =~ ${OS} ]]; then
86 | print_message_and_exit "不支持的系统${OS}"
87 | fi
88 | done
89 |
90 | for ARCH in "${INPUT_ARCH[@]}";do
91 | if [[ ! "${SUPPORT_ARCH[*]}" =~ ${ARCH} ]]; then
92 | print_message_and_exit "不支持的CPU架构${ARCH}"
93 | fi
94 | done
95 | }
96 |
97 | # 初始化
98 | init() {
99 | set_os_arch
100 |
101 | if [[ -z "${VERSION}" ]];then
102 | VERSION=`git_latest_tag`
103 | fi
104 | GIT_COMMIT_ID=`git_latest_commit`
105 | LDFLAGS="-s -w -X 'main.AppVersion=${VERSION}' -X 'main.BuildDate=`date '+%Y-%m-%d %H:%M:%S'`' -X 'main.GitCommit=${GIT_COMMIT_ID}'"
106 |
107 | PACKAGE_DIR=${BINARY_NAME}-package
108 | BUILD_DIR=${BINARY_NAME}-build
109 |
110 | if [[ -d ${BUILD_DIR} ]];then
111 | rm -rf ${BUILD_DIR}
112 | fi
113 | if [[ -d ${PACKAGE_DIR} ]];then
114 | rm -rf ${PACKAGE_DIR}
115 | fi
116 |
117 | mkdir -p ${BUILD_DIR}
118 | mkdir -p ${PACKAGE_DIR}
119 | }
120 |
121 | # 编译
122 | build() {
123 | local FILENAME=''
124 | for OS in "${INPUT_OS[@]}";do
125 | for ARCH in "${INPUT_ARCH[@]}";do
126 | if [[ "${OS}" = "windows" ]];then
127 | FILENAME=${BINARY_NAME}.exe
128 | else
129 | FILENAME=${BINARY_NAME}
130 | fi
131 | env CGO_ENABLED=0 GOOS=${OS} GOARCH=${ARCH} go build -ldflags "${LDFLAGS}" -o ${BUILD_DIR}/${BINARY_NAME}-${OS}-${ARCH}/${FILENAME} ${MAIN_FILE}
132 | done
133 | done
134 | }
135 |
136 | # 打包
137 | package_binary() {
138 | cd ${BUILD_DIR}
139 |
140 | for OS in "${INPUT_OS[@]}";do
141 | for ARCH in "${INPUT_ARCH[@]}";do
142 | package_file ${BINARY_NAME}-${OS}-${ARCH}
143 | if [[ "${OS}" = "windows" ]];then
144 | zip -rq ../${PACKAGE_DIR}/${BINARY_NAME}-${VERSION}-${OS}-${ARCH}.zip ${BINARY_NAME}-${OS}-${ARCH}
145 | else
146 | tar czf ../${PACKAGE_DIR}/${BINARY_NAME}-${VERSION}-${OS}-${ARCH}.tar.gz ${BINARY_NAME}-${OS}-${ARCH}
147 | fi
148 | done
149 | done
150 |
151 | cd ${OLDPWD}
152 | }
153 |
154 | # 打包文件
155 | package_file() {
156 | if [[ "${#INCLUDE_FILE[@]}" = "0" ]];then
157 | return
158 | fi
159 | for item in "${INCLUDE_FILE[@]}"; do
160 | cp -r ../${item} $1
161 | done
162 | }
163 |
164 | # 清理
165 | clean() {
166 | if [[ -d ${BUILD_DIR} ]];then
167 | rm -rf ${BUILD_DIR}
168 | fi
169 | }
170 |
171 | # 运行
172 | run() {
173 | init
174 | build
175 | package_binary
176 | clean
177 | }
178 |
179 |
180 | # p 平台 linux darwin windows
181 | # a 架构 386 amd64
182 | # v 版本号 默认取git最新tag
183 | while getopts "p:a:v:" OPT;
184 | do
185 | case ${OPT} in
186 | p) IPS=',' read -r -a INPUT_OS <<< "${OPTARG}"
187 | ;;
188 | a) IPS=',' read -r -a INPUT_ARCH <<< "${OPTARG}"
189 | ;;
190 | v) VERSION=$OPTARG
191 | ;;
192 | *)
193 | ;;
194 | esac
195 | done
196 |
197 | run
--------------------------------------------------------------------------------
/web/public/index.html:
--------------------------------------------------------------------------------
1 |
mars
--------------------------------------------------------------------------------
/web/public/mitm-proxy.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIFdDCCA1ygAwIBAgIBATANBgkqhkiG9w0BAQsFADBZMQ4wDAYDVQQGEwVDaGlu
3 | YTEPMA0GA1UECBMGRnVKaWFuMQ8wDQYDVQQHEwZYaWFtZW4xDTALBgNVBAoTBE1h
4 | cnMxFjAUBgNVBAMTDWdvLW1pdG0tcHJveHkwIBcNMTgwMzE4MDkwMDQ0WhgPMjA2
5 | ODAzMTgwOTAwNDRaMFkxDjAMBgNVBAYTBUNoaW5hMQ8wDQYDVQQIEwZGdUppYW4x
6 | DzANBgNVBAcTBlhpYW1lbjENMAsGA1UEChMETWFyczEWMBQGA1UEAxMNZ28tbWl0
7 | bS1wcm94eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANiuppEbanTv
8 | iCs47AFIAy+AVXDhaInal4fGmN+kG1txO4YPygKGrdjokCZtkL6ZK61izFg6BLX+
9 | p65j8wnAPZPZr3Zu5vlcDM7baO9ddxtnXm/fACPEuMIvgmG7zxE9CeX3LY7tsq10
10 | hg8uKMnYGTy5Ce0hkuYn8Od0yHseGFWCmaCAHIcshbvQFxPGn42X/zWrEHDEgWtG
11 | fOlamBBTSbNza11H8udLkXlr+N+vv/P/eKjpeIf/xzPCdiUOxdD+NHCeeSgho3Sm
12 | P0T6ia4L7MVW0XUg7CseVVh+9TddO6QefmM1+AsWU/ektD+cUMtlWoDXE8idlpoZ
13 | cMVJfq/6Sa9nG280fCPjd4wFLqbR67BHQkoPjQ1vmRgs4xvD04m796dRPpTDepb/
14 | xvTTMcwgAC5tur/E5SHpr8hx9X6xGPfUUMiKyBQlSgLH4V02SjAmScxqt5AWZcT/
15 | syLHg7BhjxwBGoCwcE8zWHCJarQ0t28Z7ptyL3DXPaJ7Vd2CvLJrekvtnm9B28aU
16 | 9KOC9JL3DKzFaRrhTYb0VNLfoLV8kRJCzZI6HAwiKcAAEIXi8on6YwqLvEIxo5AL
17 | 0gTeIf/nJU2W4OY640fIdwEvcaH4Wj2bKMRaTWvQGM1TJe4hoCN/c3mVopotCb44
18 | IGC5R0XmVImVxZmdyCXJAfY1jYrWHA2ZAgMBAAGjRTBDMA4GA1UdDwEB/wQEAwIB
19 | BjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSfjEyzebvckLQu+eZjlmJF
20 | W0/ZmjANBgkqhkiG9w0BAQsFAAOCAgEAXHGvSFwtcqX6lD9rmLTPeViTIan5hG5T
21 | sEWsPp/kI6j579OavwCr7mk4nUjsKFaOEzN78C4k6s4gDWWePoJhlsXB4KefriW4
22 | gWevzwgRTrIlaw3EDb+fCle0HcsCI/wwxDD8eafUxEyqBGrhLJFiUIxvOcD+yP2Q
23 | mX3Z89Pd38Qvkb9zneJdXo2wHMq0MGKlTPdE04rg1OsuPNnSwRhtem9/E4eCtumF
24 | JoQEQtp440wpvrbZljR18Ahd+xNh6dyaD0prnrUEGsUkC1hMb3nUWmw6dZEA5rCv
25 | 8aW5ZMm9Jr7pW7yzrm8J4II1bY5v6i7+qvOFDAf1nEnVshcSCiHu6xzgtwoGtsP8
26 | mSOquiWwiceJL6q8xh6nOD3SYm2mZwA1n7Nl3mRJE/RgbwJNkveMrmZ6CKUm3N/x
27 | eqd5yhTLsD7sf3+d4B7i6fAZ+csccWaDuquVI9cXi2OoMKgIFeeVwJ1FCeLY0Nah
28 | nPlNUA0h7xKeDIHtlGsSOng6uiEVVVXGS+j9V6h+Z55AsuOAoHYOBDoXfr0Y4Bww
29 | irCRNyFcDrKoyILOOUiPxoEcclrwUBTB78JxVA8xKTbAh0aZQRZOZOz49qF4gA1d
30 | 1riiUHJIG2sD+54UEdFoR5nhZ4/RLGqQ/Kmch5VnPp7De4OzSMd/KkQDWEjR+AA1
31 | CDPlL4gNB6s=
32 | -----END CERTIFICATE-----
33 |
--------------------------------------------------------------------------------
/web/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
--------------------------------------------------------------------------------
/web/public/static/fonts/element-icons.6f0a763.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ouqiang/mars/1e2a733b31d210140b585137c4dcc728bdd89e75/web/public/static/fonts/element-icons.6f0a763.ttf
--------------------------------------------------------------------------------
/web/public/static/js/app.4ac0d8d9ce25ca25174c.js:
--------------------------------------------------------------------------------
1 | webpackJsonp([1],{FBSW:function(e,t){},NHnr:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var s=n("7+uW"),a=n("zL8q"),o=n.n(a),i=(n("tvR6"),n("mvHQ")),r=n.n(i),c=n("MJLE"),l=n.n(c),u={name:"app-nav-menu",data:function(){return{dialogVisible:!1}},methods:{showCert:function(){this.$nextTick(function(){var e=document.getElementById("qrcode");e.innerText="";var t=location.protocol+"//"+location.host+location.pathname;t.lastIndexOf("/")!==t.length-1&&(t+="/"),t+="public/mity-proxy.crt",new l.a(e,t)}),this.dialogVisible=!0},downloadCert:function(){location.href="public/mitm-proxy.crt"}}},d={render:function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",[n("el-menu",{attrs:{"background-color":"#545c64","text-color":"#fff","active-text-color":"#ffd04b",mode:"horizontal"}},[n("el-menu-item",{attrs:{index:"/"},on:{click:e.showCert}},[e._v("Root CA\n ")])],1),e._v(" "),n("el-dialog",{attrs:{title:"Download Root CA",visible:e.dialogVisible},on:{"update:visible":function(t){e.dialogVisible=t}}},[n("el-row",[n("el-col",{attrs:{span:12}},[n("div",{attrs:{id:"qrcode"}})]),e._v(" "),n("el-col",{attrs:{span:12}},[n("el-button",{attrs:{type:"primary"},on:{click:e.downloadCert}},[e._v("download")])],1)],1)],1)],1)},staticRenderFns:[]};var p=n("VU/8")(u,d,!1,function(e){n("R9ot")},"data-v-67bd304c",null).exports,v=1e3,_=1001,h=1002,f=2e3,b=2001,y=2002,m=3e3,T=n("V8mf"),g=n.n(T),x=(n("FBSW"),n("xrTZ").Base64),w=n("jMz+"),k=n("jMz+").css,S=n("jMz+").html,q={name:"App",data:function(){return{socket:null,timer:null,enableCapture:!0,captureStatusText:"stop",transactions:[],pendingTransactions:[],showMaxTransactionNum:1e3,dialogVisible:!1,dialogFullScreen:!1,activeTab:"headers",activeTx:null,activePanel:"general"}},components:{appNavMenu:p},created:function(){var e=this;this.initWebSocket(),this.heartBeat(),setInterval(function(){e.publishTransaction()},500)},destroyed:function(){this.timer&&clearInterval(this.timer),this.socket&&this.socket.close()},computed:{showTransactions:function(){return this.transactions}},methods:{getWebSocketURL:function(){var e="ws://"+location.host+location.pathname;return e.lastIndexOf("/")!==e.length-1&&(e+="/"),e},initWebSocket:function(){this.socket=new WebSocket(this.getWebSocketURL()+"ws"),this.socket.onopen=this.webSocketOpen,this.socket.onmessage=this.webSocketReceive,this.socket.onerror=this.webSocketError,this.socket.onclose=this.webSocketClose},webSocketOpen:function(){console.log("webSocket连接成功")},webSocketReceive:function(e){var t=JSON.parse(e.data);this.dispatchEvent(t.type,t.payload)},webSocketSend:function(e,t){var n={type:e,payload:t};this.socket.send(r()(n))},webSocketClose:function(){console.log("webSocket连接关闭"),this.initWebSocket()},webSocketError:function(){console.log("webSocket发生错误")},dispatchEvent:function(e,t){switch(e){case f:break;case b:this.responseReplay(t);break;case y:this.responseTransaction(t);break;case m:this.pushTransaction(t);break;default:console.log("webSocket不支持的消息类型",e,t)}},heartBeat:function(){var e=this;this.timer=setInterval(function(){e.requestPing()},2e4)},requestPing:function(){this.webSocketSend(v,{})},requestReplay:function(e){this.webSocketSend(_,{id:e})},requestTransaction:function(e){this.webSocketSend(h,{id:e})},clearTransactions:function(){this.transactions=[]},responseReplay:function(e){},responseTransaction:function(e){var t=this;e.request.body.is_image=this.isImageType(e.request.body.content_type),e.response.body.is_image=this.isImageType(e.response.body.content_type),e.request.query_param=this.parseQueryParams(e.request.query_param),this.decodeBodyIfNeed(e),e.request.body.content=this.formatCode(e.request.body.content_type,e.request.body.content),e.response.body.content=this.formatCode(e.response.body.content_type,e.response.body.content),this.activeTx=e,this.dialogVisible=!0,setTimeout(function(){t.highlightCode()},1e3)},decodeBodyIfNeed:function(e){if(!e.request.body.is_binary&&!e.request.body.is_image)switch(e.request.body.content=x.decode(e.request.body.content),e.request.body.content_type){case"application/x-www-form-urlencoded":e.request.body.content=this.parseQueryParams(e.request.body.content)}""!==e.response.err||e.response.body.is_binary||e.response.body.is_image||(e.response.body.content=x.decode(e.response.body.content))},isImageType:function(e){return 0===e.indexOf("image/")},pushTransaction:function(e){this.enableCapture&&this.pendingTransactions.push(e)},publishTransaction:function(){var e=this;0!==this.pendingTransactions.length&&(this.pendingTransactions.forEach(function(t){e.transactions.length>=e.showMaxTransactionNum&&e.transactions.pop(),e.transactions.unshift(t)}),this.pendingTransactions=[])},toggleCapture:function(){this.enableCapture=!this.enableCapture,this.enableCapture?this.captureStatusText="stop":this.captureStatusText="resume"},clickRow:function(e){this.activeTx=null,this.requestTransaction(e.id),this.activeTab="headers",this.activePanel="general"},parseQueryParams:function(e){var t={},n=e.split("&");return 0===n.length?t:(n.forEach(function(e){var n=e.split("=");switch(n.length){case 1:""!==n[0]&&(t[n[0]]="");break;case 2:""!==n[0]&&(t[n[0]]=n[1])}}),t)},highlightCode:function(){document.querySelectorAll("pre code").forEach(function(e){g.a.highlightBlock(e)})},formatCode:function(e,t){switch(e){case"text/html":case"text/plain":case"text/xml":case"application/xml":var n=this.trim(t);t=0===n.indexOf("{")&&n.lastIndexOf("}")===n.length-1?w(t):S(t);break;case"text/javascript":case"application/json":case"application/javascript":case"application/x-javascript":t=w(t);break;case"text/css":t=k(t)}return t},trim:function(e){return e.replace(/(^\s*)|(\s*$)/g,"")}},filters:{formatDuration:function(e){return(e=e/1e3/1e3)<1e3?e.toFixed(2)+"ms":(e/1e3).toFixed(2)+"s"},formatBodySize:function(e){return e<1024?e+"B":e>=1024&&e<1048576?(e/1024).toFixed(2)+"KB":(e/1048576).toFixed(2)+"M"},subString:function(e){return e.length<30?e:e.substring(e.length-27)},decodeURI:function(e){return e}}},C={render:function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("el-container",[n("el-header",[n("app-nav-menu")],1),e._v(" "),n("el-main",[n("div",{attrs:{id:"main-container"}},[n("el-dialog",{attrs:{title:"Transaction",visible:e.dialogVisible,width:"80%",center:!0},on:{"update:visible":function(t){e.dialogVisible=t}}},[e.activeTx?n("el-row",{attrs:{type:"flex",justify:"end"}},[n("el-col",{attrs:{span:4}},[n("el-button",{attrs:{type:"primary"},on:{click:function(t){e.requestReplay(e.activeTx.id)}}},[e._v("replay")])],1)],1):e._e(),e._v(" "),e.activeTx?n("el-tabs",{model:{value:e.activeTab,callback:function(t){e.activeTab=t},expression:"activeTab"}},[n("el-tab-pane",{attrs:{label:"Headers",name:"headers"}},[n("el-collapse",{model:{value:e.activePanel,callback:function(t){e.activePanel=t},expression:"activePanel"}},[n("el-collapse-item",{attrs:{title:"General",name:"general"}},[n("p",[n("span",{staticClass:"bold"},[e._v("Request URL: ")]),e._v(" "+e._s(e.activeTx.request.url)+"\n ")]),e._v(" "),n("p",[n("span",{staticClass:"bold"},[e._v("Request Method: ")]),e._v(" "+e._s(e.activeTx.request.method)+"\n ")]),e._v(" "),e.activeTx.response.err?e._e():n("p",[n("span",{staticClass:"bold"},[e._v("Status Code: ")]),e._v(" "+e._s(e.activeTx.response.status_code)+"\n ")]),e._v(" "),n("p",[e.activeTx.server_ip?n("span",{staticClass:"bold"},[e._v("Server IP: ")]):e._e(),e._v(" "+e._s(e.activeTx.server_ip)+"\n ")]),e._v(" "),n("p",[n("span",{staticClass:"bold"},[e._v("Client IP: ")]),e._v(" "+e._s(e.activeTx.client_ip)+"\n ")]),e._v(" "),e.activeTx.response.err?n("p",{staticStyle:{color:"red"}},[e._v("\n "+e._s(e.activeTx.response.err)+"\n ")]):e._e()]),e._v(" "),e.activeTx.response.err?e._e():n("el-collapse-item",{attrs:{title:"Response Headers",name:"responseHeaders"}},[e._l(e.activeTx.response.header,function(t,s){return[n("p",{key:s},[n("span",{staticClass:"bold"},[e._v(e._s(s)+": ")]),e._v(" "+e._s(e._f("decodeURI")(t.join(";")))+"\n ")])]})],2),e._v(" "),n("el-collapse-item",{attrs:{title:"Request Headers",name:"requestHeaders"}},[e._l(e.activeTx.request.header,function(t,s){return[n("p",{key:s},[n("span",{staticClass:"bold"},[e._v(e._s(s)+": ")]),e._v(" "+e._s(e._f("decodeURI")(t.join(";")))+"\n ")])]})],2),e._v(" "),n("el-collapse-item",{attrs:{title:"Query String Parameters",name:"queryStringParameters"}},[e._l(e.activeTx.request.query_param,function(t,s){return[n("p",{key:s},[n("span",{staticClass:"bold"},[e._v(e._s(s)+":")]),e._v(" "+e._s(t)+"\n ")])]})],2),e._v(" "),"GET"!==e.activeTx.request.method?n("el-collapse-item",{attrs:{title:"Request payload",name:"requestPayload"}},["application/x-www-form-urlencoded"===e.activeTx.request.body.content_type?n("span",[e._l(e.activeTx.request.body.content,function(t,s){return[n("p",{key:s},[n("span",{staticClass:"bold"},[e._v(e._s(s)+":")]),e._v(" "+e._s(t)+"\n ")])]})],2):e.activeTx.request.body.is_binary?n("span",[e._v("\n "+e._s(e.activeTx.request.body.content)+"\n ")]):n("span",[n("pre",[n("code",[e._v(e._s(e.activeTx.request.body.content))])])])]):e._e()],1)],1),e._v(" "),e.activeTx.response.err?e._e():n("el-tab-pane",{attrs:{label:"Preview",name:"preview"}},[n("p",[e.activeTx.response.body.is_image?n("span",[n("img",{attrs:{src:"data:"+e.activeTx.response.body.content_type+";base64,"+e.activeTx.response.body.content}})]):e.activeTx.response.body.is_binary?e._e():n("span",[n("pre",[n("code",[e._v(e._s(e.activeTx.response.body.content))])])])])]),e._v(" "),e.activeTx.response.err?e._e():n("el-tab-pane",{attrs:{label:"Response",name:"response"}},[n("p",[e._v("\n "+e._s(e.activeTx.response.body.content)+"\n ")])])],1):e._e()],1),e._v(" "),n("div",[n("el-row",{attrs:{type:"flex",justify:"end"}},[n("el-col",{attrs:{span:4}},[n("el-button",{attrs:{type:"primary"},on:{click:e.toggleCapture}},[e._v(e._s(e.captureStatusText))]),e._v(" "),n("el-button",{attrs:{type:"danger"},on:{click:e.clearTransactions}},[e._v("clear")])],1)],1)],1),e._v(" "),n("div",[n("table",[n("thead",[n("th",{staticStyle:{width:"20%"}},[e._v("Name")]),e._v(" "),n("th",{staticStyle:{width:"20%"}},[e._v("Host")]),e._v(" "),n("th",{staticStyle:{width:"10%"}},[e._v("Method")]),e._v(" "),n("th",{staticStyle:{width:"10%"}},[e._v("Status")]),e._v(" "),n("th",{staticStyle:{width:"20%"}},[e._v("Type")]),e._v(" "),n("th",{staticStyle:{width:"10%"}},[e._v("Size")]),e._v(" "),n("th",{staticStyle:{width:"10%"}},[e._v("Time")])]),e._v(" "),n("tbody",e._l(e.showTransactions,function(t){return n("tr",{key:t.id,on:{click:function(n){e.clickRow(t)}}},[n("td",[e._v("\n "+e._s(e._f("subString")(t.path))+"\n ")]),e._v(" "),n("td",[e._v("\n "+e._s(e._f("subString")(t.host))+"\n ")]),e._v(" "),n("td",[e._v("\n "+e._s(t.method)+"\n ")]),e._v(" "),n("td",[t.response_err?n("span",{staticStyle:{color:"red"}},[e._v("\n error\n ")]):n("span",[e._v("\n "+e._s(t.response_status_code)+"\n ")])]),e._v(" "),n("td",[e._v("\n "+e._s(t.response_content_type)+"\n ")]),e._v(" "),n("td",[e._v("\n "+e._s(e._f("formatBodySize")(t.response_len))+"\n ")]),e._v(" "),n("td",[e._v("\n "+e._s(e._f("formatDuration")(t.duration))+"\n ")])])}))])])],1)])],1)},staticRenderFns:[]};var R=n("VU/8")(q,C,!1,function(e){n("WLZP")},null,null).exports;s.default.config.productionTip=!1,s.default.use(o.a),s.default.directive("focus",{inserted:function(e){e.focus()}}),s.default.prototype.$appConfirm=function(e){this.$confirm("确定执行此操作?","提示",{confirmButtonText:"确定",cancelButtonText:"取消",type:"warning"}).then(function(){e()})},s.default.filter("formatTime",function(e){var t=function(e){return e>=10?e:"0"+e},n=new Date(e),s=n.getFullYear()+"-"+t(n.getMonth()+1)+"-"+t(n.getDate())+" "+t(n.getHours())+":"+t(n.getMinutes())+":"+t(n.getSeconds());return 0!==s.indexOf("20")?"":s}),new s.default({el:"#app",components:{App:R},template:""})},R9ot:function(e,t){},WLZP:function(e,t){},tvR6:function(e,t){}},["NHnr"]);
--------------------------------------------------------------------------------
/web/public/static/js/manifest.047c133009965c5daaca.js:
--------------------------------------------------------------------------------
1 | !function(r){var n=window.webpackJsonp;window.webpackJsonp=function(e,u,c){for(var f,i,p,l=0,a=[];l 1%", "last 2 versions", "not ie <= 8"]
7 | }
8 | }],
9 | "stage-2"
10 | ],
11 | "plugins": ["transform-vue-jsx", "transform-runtime"]
12 | }
13 |
--------------------------------------------------------------------------------
/web/vue/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/web/vue/.eslintignore:
--------------------------------------------------------------------------------
1 | /build/
2 | /config/
3 | /dist/
4 | /*.js
5 |
--------------------------------------------------------------------------------
/web/vue/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // https://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parserOptions: {
6 | parser: 'babel-eslint'
7 | },
8 | env: {
9 | browser: true,
10 | },
11 | extends: [
12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
14 | 'plugin:vue/essential',
15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md
16 | 'standard'
17 | ],
18 | // required to lint *.vue files
19 | plugins: [
20 | 'vue'
21 | ],
22 | // add your custom rules here
23 | rules: {
24 | // allow async-await
25 | 'generator-star-spacing': 'off',
26 | // allow debugger during development
27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/web/vue/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | /dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Editor directories and files
9 | .idea
10 | .vscode
11 | *.suo
12 | *.ntvs*
13 | *.njsproj
14 | *.sln
15 |
--------------------------------------------------------------------------------
/web/vue/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | "postcss-import": {},
6 | "postcss-url": {},
7 | // to edit target browsers: use "browserslist" field in package.json
8 | "autoprefixer": {}
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/web/vue/README.md:
--------------------------------------------------------------------------------
1 | # mars
2 |
3 | > HTTP(S)代理服务器
4 |
5 | ## Build Setup
6 |
7 | ``` bash
8 | # install dependencies
9 | yarn install
10 |
11 | # serve with hot reload at localhost:8080
12 | yarn run dev
13 |
14 | # build for production with minification
15 | yarn run build
16 |
17 | # build for production and view the bundle analyzer report
18 | yarn run build --report
19 | ```
20 |
21 | For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
22 |
--------------------------------------------------------------------------------
/web/vue/build/build.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('./check-versions')()
3 |
4 | process.env.NODE_ENV = 'production'
5 |
6 | const ora = require('ora')
7 | const rm = require('rimraf')
8 | const path = require('path')
9 | const chalk = require('chalk')
10 | const webpack = require('webpack')
11 | const config = require('../config')
12 | const webpackConfig = require('./webpack.prod.conf')
13 |
14 | const spinner = ora('building for production...')
15 | spinner.start()
16 |
17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
18 | if (err) throw err
19 | webpack(webpackConfig, (err, stats) => {
20 | spinner.stop()
21 | if (err) throw err
22 | process.stdout.write(stats.toString({
23 | colors: true,
24 | modules: false,
25 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
26 | chunks: false,
27 | chunkModules: false
28 | }) + '\n\n')
29 |
30 | if (stats.hasErrors()) {
31 | console.log(chalk.red(' Build failed with errors.\n'))
32 | process.exit(1)
33 | }
34 |
35 | console.log(chalk.cyan(' Build complete.\n'))
36 | console.log(chalk.yellow(
37 | ' Tip: built files are meant to be served over an HTTP server.\n' +
38 | ' Opening index.html over file:// won\'t work.\n'
39 | ))
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/web/vue/build/check-versions.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const chalk = require('chalk')
3 | const semver = require('semver')
4 | const packageConfig = require('../package.json')
5 | const shell = require('shelljs')
6 |
7 | function exec (cmd) {
8 | return require('child_process').execSync(cmd).toString().trim()
9 | }
10 |
11 | const versionRequirements = [
12 | {
13 | name: 'node',
14 | currentVersion: semver.clean(process.version),
15 | versionRequirement: packageConfig.engines.node
16 | }
17 | ]
18 |
19 | if (shell.which('npm')) {
20 | versionRequirements.push({
21 | name: 'npm',
22 | currentVersion: exec('npm --version'),
23 | versionRequirement: packageConfig.engines.npm
24 | })
25 | }
26 |
27 | module.exports = function () {
28 | const warnings = []
29 |
30 | for (let i = 0; i < versionRequirements.length; i++) {
31 | const mod = versionRequirements[i]
32 |
33 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
34 | warnings.push(mod.name + ': ' +
35 | chalk.red(mod.currentVersion) + ' should be ' +
36 | chalk.green(mod.versionRequirement)
37 | )
38 | }
39 | }
40 |
41 | if (warnings.length) {
42 | console.log('')
43 | console.log(chalk.yellow('To use this template, you must update following to modules:'))
44 | console.log()
45 |
46 | for (let i = 0; i < warnings.length; i++) {
47 | const warning = warnings[i]
48 | console.log(' ' + warning)
49 | }
50 |
51 | console.log()
52 | process.exit(1)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/web/vue/build/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ouqiang/mars/1e2a733b31d210140b585137c4dcc728bdd89e75/web/vue/build/logo.png
--------------------------------------------------------------------------------
/web/vue/build/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const config = require('../config')
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
5 | const packageConfig = require('../package.json')
6 |
7 | exports.assetsPath = function (_path) {
8 | const assetsSubDirectory = process.env.NODE_ENV === 'production'
9 | ? config.build.assetsSubDirectory
10 | : config.dev.assetsSubDirectory
11 |
12 | return path.posix.join(assetsSubDirectory, _path)
13 | }
14 |
15 | exports.cssLoaders = function (options) {
16 | options = options || {}
17 |
18 | const cssLoader = {
19 | loader: 'css-loader',
20 | options: {
21 | sourceMap: options.sourceMap
22 | }
23 | }
24 |
25 | const postcssLoader = {
26 | loader: 'postcss-loader',
27 | options: {
28 | sourceMap: options.sourceMap
29 | }
30 | }
31 |
32 | // generate loader string to be used with extract text plugin
33 | function generateLoaders (loader, loaderOptions) {
34 | const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
35 |
36 | if (loader) {
37 | loaders.push({
38 | loader: loader + '-loader',
39 | options: Object.assign({}, loaderOptions, {
40 | sourceMap: options.sourceMap
41 | })
42 | })
43 | }
44 |
45 | // Extract CSS when that option is specified
46 | // (which is the case during production build)
47 | if (options.extract) {
48 | return ExtractTextPlugin.extract({
49 | use: loaders,
50 | fallback: 'vue-style-loader'
51 | })
52 | } else {
53 | return ['vue-style-loader'].concat(loaders)
54 | }
55 | }
56 |
57 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html
58 | return {
59 | css: generateLoaders(),
60 | postcss: generateLoaders(),
61 | less: generateLoaders('less'),
62 | sass: generateLoaders('sass', { indentedSyntax: true }),
63 | scss: generateLoaders('sass'),
64 | stylus: generateLoaders('stylus'),
65 | styl: generateLoaders('stylus')
66 | }
67 | }
68 |
69 | // Generate loaders for standalone style files (outside of .vue)
70 | exports.styleLoaders = function (options) {
71 | const output = []
72 | const loaders = exports.cssLoaders(options)
73 |
74 | for (const extension in loaders) {
75 | const loader = loaders[extension]
76 | output.push({
77 | test: new RegExp('\\.' + extension + '$'),
78 | use: loader
79 | })
80 | }
81 |
82 | return output
83 | }
84 |
85 | exports.createNotifierCallback = () => {
86 | const notifier = require('node-notifier')
87 |
88 | return (severity, errors) => {
89 | if (severity !== 'error') return
90 |
91 | const error = errors[0]
92 | const filename = error.file && error.file.split('!').pop()
93 |
94 | notifier.notify({
95 | title: packageConfig.name,
96 | message: severity + ': ' + error.name,
97 | subtitle: filename || '',
98 | icon: path.join(__dirname, 'logo.png')
99 | })
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/web/vue/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const config = require('../config')
4 | const isProduction = process.env.NODE_ENV === 'production'
5 | const sourceMapEnabled = isProduction
6 | ? config.build.productionSourceMap
7 | : config.dev.cssSourceMap
8 |
9 | module.exports = {
10 | loaders: utils.cssLoaders({
11 | sourceMap: sourceMapEnabled,
12 | extract: isProduction
13 | }),
14 | cssSourceMap: sourceMapEnabled,
15 | cacheBusting: config.dev.cacheBusting,
16 | transformToRequire: {
17 | video: ['src', 'poster'],
18 | source: 'src',
19 | img: 'src',
20 | image: 'xlink:href'
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/web/vue/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const config = require('../config')
5 | const vueLoaderConfig = require('./vue-loader.conf')
6 |
7 | function resolve (dir) {
8 | return path.join(__dirname, '..', dir)
9 | }
10 |
11 | const createLintingRule = () => ({
12 | test: /\.(js|vue)$/,
13 | loader: 'eslint-loader',
14 | enforce: 'pre',
15 | include: [resolve('src'), resolve('test')],
16 | options: {
17 | formatter: require('eslint-friendly-formatter'),
18 | emitWarning: !config.dev.showEslintErrorsInOverlay
19 | }
20 | })
21 |
22 | module.exports = {
23 | context: path.resolve(__dirname, '../'),
24 | entry: {
25 | app: './src/main.js'
26 | },
27 | output: {
28 | path: config.build.assetsRoot,
29 | filename: '[name].js',
30 | publicPath: process.env.NODE_ENV === 'production'
31 | ? config.build.assetsPublicPath
32 | : config.dev.assetsPublicPath
33 | },
34 | resolve: {
35 | extensions: ['.js', '.vue', '.json'],
36 | alias: {
37 | 'vue$': 'vue/dist/vue.esm.js',
38 | '@': resolve('src'),
39 | }
40 | },
41 | module: {
42 | rules: [
43 | ...(config.dev.useEslint ? [createLintingRule()] : []),
44 | {
45 | test: /\.vue$/,
46 | loader: 'vue-loader',
47 | options: vueLoaderConfig
48 | },
49 | {
50 | test: /\.js$/,
51 | loader: 'babel-loader',
52 | include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
53 | },
54 | {
55 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
56 | loader: 'url-loader',
57 | options: {
58 | limit: 10000,
59 | name: utils.assetsPath('img/[name].[hash:7].[ext]')
60 | }
61 | },
62 | {
63 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
64 | loader: 'url-loader',
65 | options: {
66 | limit: 10000,
67 | name: utils.assetsPath('media/[name].[hash:7].[ext]')
68 | }
69 | },
70 | {
71 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
72 | loader: 'url-loader',
73 | options: {
74 | limit: 10000,
75 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
76 | }
77 | }
78 | ]
79 | },
80 | node: {
81 | // prevent webpack from injecting useless setImmediate polyfill because Vue
82 | // source contains it (although only uses it if it's native).
83 | setImmediate: false,
84 | // prevent webpack from injecting mocks to Node native modules
85 | // that does not make sense for the client
86 | dgram: 'empty',
87 | fs: 'empty',
88 | net: 'empty',
89 | tls: 'empty',
90 | child_process: 'empty'
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/web/vue/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const utils = require('./utils')
3 | const webpack = require('webpack')
4 | const config = require('../config')
5 | const merge = require('webpack-merge')
6 | const path = require('path')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
10 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
11 | const portfinder = require('portfinder')
12 |
13 | const HOST = process.env.HOST
14 | const PORT = process.env.PORT && Number(process.env.PORT)
15 |
16 | const devWebpackConfig = merge(baseWebpackConfig, {
17 | module: {
18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
19 | },
20 | // cheap-module-eval-source-map is faster for development
21 | devtool: config.dev.devtool,
22 |
23 | // these devServer options should be customized in /config/index.js
24 | devServer: {
25 | clientLogLevel: 'warning',
26 | historyApiFallback: {
27 | rewrites: [
28 | { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
29 | ],
30 | },
31 | hot: true,
32 | contentBase: false, // since we use CopyWebpackPlugin.
33 | compress: true,
34 | host: HOST || config.dev.host,
35 | port: PORT || config.dev.port,
36 | open: config.dev.autoOpenBrowser,
37 | overlay: config.dev.errorOverlay
38 | ? { warnings: false, errors: true }
39 | : false,
40 | publicPath: config.dev.assetsPublicPath,
41 | proxy: config.dev.proxyTable,
42 | quiet: true, // necessary for FriendlyErrorsPlugin
43 | watchOptions: {
44 | poll: config.dev.poll,
45 | }
46 | },
47 | plugins: [
48 | new webpack.DefinePlugin({
49 | 'process.env': require('../config/dev.env')
50 | }),
51 | new webpack.HotModuleReplacementPlugin(),
52 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
53 | new webpack.NoEmitOnErrorsPlugin(),
54 | // https://github.com/ampedandwired/html-webpack-plugin
55 | new HtmlWebpackPlugin({
56 | filename: 'index.html',
57 | template: 'index.html',
58 | inject: true
59 | }),
60 | // copy custom static assets
61 | new CopyWebpackPlugin([
62 | {
63 | from: path.resolve(__dirname, '../static'),
64 | to: config.dev.assetsSubDirectory,
65 | ignore: ['.*']
66 | }
67 | ])
68 | ]
69 | })
70 |
71 | module.exports = new Promise((resolve, reject) => {
72 | portfinder.basePort = process.env.PORT || config.dev.port
73 | portfinder.getPort((err, port) => {
74 | if (err) {
75 | reject(err)
76 | } else {
77 | // publish the new Port, necessary for e2e tests
78 | process.env.PORT = port
79 | // add port to devServer config
80 | devWebpackConfig.devServer.port = port
81 |
82 | // Add FriendlyErrorsPlugin
83 | devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
84 | compilationSuccessInfo: {
85 | messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
86 | },
87 | onErrors: config.dev.notifyOnErrors
88 | ? utils.createNotifierCallback()
89 | : undefined
90 | }))
91 |
92 | resolve(devWebpackConfig)
93 | }
94 | })
95 | })
96 |
--------------------------------------------------------------------------------
/web/vue/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 | const utils = require('./utils')
4 | const webpack = require('webpack')
5 | const config = require('../config')
6 | const merge = require('webpack-merge')
7 | const baseWebpackConfig = require('./webpack.base.conf')
8 | const CopyWebpackPlugin = require('copy-webpack-plugin')
9 | const HtmlWebpackPlugin = require('html-webpack-plugin')
10 | const ExtractTextPlugin = require('extract-text-webpack-plugin')
11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
12 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
13 |
14 | const env = require('../config/prod.env')
15 |
16 | const webpackConfig = merge(baseWebpackConfig, {
17 | module: {
18 | rules: utils.styleLoaders({
19 | sourceMap: config.build.productionSourceMap,
20 | extract: true,
21 | usePostCSS: true
22 | })
23 | },
24 | devtool: config.build.productionSourceMap ? config.build.devtool : false,
25 | output: {
26 | path: config.build.assetsRoot,
27 | filename: utils.assetsPath('js/[name].[chunkhash].js'),
28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
29 | },
30 | plugins: [
31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html
32 | new webpack.DefinePlugin({
33 | 'process.env': env
34 | }),
35 | new UglifyJsPlugin({
36 | uglifyOptions: {
37 | compress: {
38 | warnings: false
39 | }
40 | },
41 | sourceMap: config.build.productionSourceMap,
42 | parallel: true
43 | }),
44 | // extract css into its own file
45 | new ExtractTextPlugin({
46 | filename: utils.assetsPath('css/[name].[contenthash].css'),
47 | // Setting the following option to `false` will not extract CSS from codesplit chunks.
48 | // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
49 | // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
50 | // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
51 | allChunks: true,
52 | }),
53 | // Compress extracted CSS. We are using this plugin so that possible
54 | // duplicated CSS from different components can be deduped.
55 | new OptimizeCSSPlugin({
56 | cssProcessorOptions: config.build.productionSourceMap
57 | ? { safe: true, map: { inline: false } }
58 | : { safe: true }
59 | }),
60 | // generate dist index.html with correct asset hash for caching.
61 | // you can customize output by editing /index.html
62 | // see https://github.com/ampedandwired/html-webpack-plugin
63 | new HtmlWebpackPlugin({
64 | filename: config.build.index,
65 | template: 'index.html',
66 | inject: true,
67 | minify: {
68 | removeComments: true,
69 | collapseWhitespace: true,
70 | removeAttributeQuotes: true
71 | // more options:
72 | // https://github.com/kangax/html-minifier#options-quick-reference
73 | },
74 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin
75 | chunksSortMode: 'dependency'
76 | }),
77 | // keep module.id stable when vendor modules does not change
78 | new webpack.HashedModuleIdsPlugin(),
79 | // enable scope hoisting
80 | new webpack.optimize.ModuleConcatenationPlugin(),
81 | // split vendor js into its own file
82 | new webpack.optimize.CommonsChunkPlugin({
83 | name: 'vendor',
84 | minChunks (module) {
85 | // any required modules inside node_modules are extracted to vendor
86 | return (
87 | module.resource &&
88 | /\.js$/.test(module.resource) &&
89 | module.resource.indexOf(
90 | path.join(__dirname, '../node_modules')
91 | ) === 0
92 | )
93 | }
94 | }),
95 | // extract webpack runtime and module manifest to its own file in order to
96 | // prevent vendor hash from being updated whenever app bundle is updated
97 | new webpack.optimize.CommonsChunkPlugin({
98 | name: 'manifest',
99 | minChunks: Infinity
100 | }),
101 | // This instance extracts shared chunks from code splitted chunks and bundles them
102 | // in a separate chunk, similar to the vendor chunk
103 | // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
104 | new webpack.optimize.CommonsChunkPlugin({
105 | name: 'app',
106 | async: 'vendor-async',
107 | children: true,
108 | minChunks: 3
109 | }),
110 |
111 | // copy custom static assets
112 | new CopyWebpackPlugin([
113 | {
114 | from: path.resolve(__dirname, '../static'),
115 | to: config.build.assetsSubDirectory,
116 | ignore: ['.*']
117 | }
118 | ])
119 | ]
120 | })
121 |
122 | if (config.build.productionGzip) {
123 | const CompressionWebpackPlugin = require('compression-webpack-plugin')
124 |
125 | webpackConfig.plugins.push(
126 | new CompressionWebpackPlugin({
127 | asset: '[path].gz[query]',
128 | algorithm: 'gzip',
129 | test: new RegExp(
130 | '\\.(' +
131 | config.build.productionGzipExtensions.join('|') +
132 | ')$'
133 | ),
134 | threshold: 10240,
135 | minRatio: 0.8
136 | })
137 | )
138 | }
139 |
140 | if (config.build.bundleAnalyzerReport) {
141 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
142 | webpackConfig.plugins.push(new BundleAnalyzerPlugin())
143 | }
144 |
145 | module.exports = webpackConfig
146 |
--------------------------------------------------------------------------------
/web/vue/config/dev.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const merge = require('webpack-merge')
3 | const prodEnv = require('./prod.env')
4 |
5 | module.exports = merge(prodEnv, {
6 | NODE_ENV: '"development"'
7 | })
8 |
--------------------------------------------------------------------------------
/web/vue/config/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // Template version: 1.3.1
3 | // see http://vuejs-templates.github.io/webpack for documentation.
4 |
5 | const path = require('path')
6 |
7 | module.exports = {
8 | dev: {
9 |
10 | // Paths
11 | assetsSubDirectory: 'static',
12 | assetsPublicPath: '/',
13 | proxyTable: {
14 | '/api': {
15 | target: 'http://localhost:9999',
16 | changeOrigin: true,
17 | pathRewrite: {
18 | '^/api': '/api'
19 | }
20 | }
21 | },
22 |
23 | // Various Dev Server settings
24 | host: 'localhost', // can be overwritten by process.env.HOST
25 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
26 | autoOpenBrowser: false,
27 | errorOverlay: true,
28 | notifyOnErrors: true,
29 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
30 |
31 | // Use Eslint Loader?
32 | // If true, your code will be linted during bundling and
33 | // linting errors and warnings will be shown in the console.
34 | useEslint: true,
35 | // If true, eslint errors and warnings will also be shown in the error overlay
36 | // in the browser.
37 | showEslintErrorsInOverlay: false,
38 |
39 | /**
40 | * Source Maps
41 | */
42 |
43 | // https://webpack.js.org/configuration/devtool/#development
44 | devtool: 'cheap-module-eval-source-map',
45 |
46 | // If you have problems debugging vue-files in devtools,
47 | // set this to false - it *may* help
48 | // https://vue-loader.vuejs.org/en/options.html#cachebusting
49 | cacheBusting: true,
50 |
51 | cssSourceMap: true
52 | },
53 |
54 | build: {
55 | // Template for index.html
56 | index: path.resolve(__dirname, '../dist/index.html'),
57 |
58 | // Paths
59 | assetsRoot: path.resolve(__dirname, '../dist'),
60 | assetsSubDirectory: 'static',
61 | assetsPublicPath: 'public/',
62 |
63 | /**
64 | * Source Maps
65 | */
66 |
67 | productionSourceMap: false,
68 | // https://webpack.js.org/configuration/devtool/#production
69 | devtool: '#source-map',
70 |
71 | // Gzip off by default as many popular static hosts such as
72 | // Surge or Netlify already gzip all static assets for you.
73 | // Before setting to `true`, make sure to:
74 | // npm install --save-dev compression-webpack-plugin
75 | productionGzip: false,
76 | productionGzipExtensions: ['js', 'css'],
77 |
78 | // Run the build command with an extra argument to
79 | // View the bundle analyzer report after build finishes:
80 | // `npm run build --report`
81 | // Set to `true` or `false` to always turn it on or off
82 | bundleAnalyzerReport: process.env.npm_config_report
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/web/vue/config/prod.env.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | NODE_ENV: '"production"'
4 | }
5 |
--------------------------------------------------------------------------------
/web/vue/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | mars
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/web/vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gocron",
3 | "version": "1.0.0",
4 | "description": "定时任务管理系统",
5 | "author": "ouqiang ",
6 | "private": true,
7 | "scripts": {
8 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
9 | "start": "npm run dev",
10 | "lint": "eslint --ext .js,.vue src",
11 | "build": "node build/build.js"
12 | },
13 | "dependencies": {
14 | "axios": "^0.18.0",
15 | "element-ui": "^2.3.6",
16 | "highlight.js": "^9.13.1",
17 | "js-base64": "^2.4.9",
18 | "js-beautify": "^1.8.8",
19 | "qrcodejs2": "^0.0.2",
20 | "qs": "^6.5.1",
21 | "vue": "^2.5.2",
22 | "vue-router": "^3.0.1",
23 | "vuex": "^3.0.1"
24 | },
25 | "devDependencies": {
26 | "autoprefixer": "^7.1.2",
27 | "babel-core": "^6.22.1",
28 | "babel-eslint": "^8.2.1",
29 | "babel-helper-vue-jsx-merge-props": "^2.0.3",
30 | "babel-loader": "^7.1.1",
31 | "babel-plugin-syntax-jsx": "^6.18.0",
32 | "babel-plugin-transform-runtime": "^6.22.0",
33 | "babel-plugin-transform-vue-jsx": "^3.5.0",
34 | "babel-preset-env": "^1.3.2",
35 | "babel-preset-stage-2": "^6.22.0",
36 | "chalk": "^2.0.1",
37 | "copy-webpack-plugin": "^4.0.1",
38 | "css-loader": "^0.28.0",
39 | "eslint": "^4.15.0",
40 | "eslint-config-standard": "^10.2.1",
41 | "eslint-friendly-formatter": "^3.0.0",
42 | "eslint-loader": "^1.7.1",
43 | "eslint-plugin-import": "^2.7.0",
44 | "eslint-plugin-node": "^5.2.0",
45 | "eslint-plugin-promise": "^3.4.0",
46 | "eslint-plugin-standard": "^3.0.1",
47 | "eslint-plugin-vue": "^4.0.0",
48 | "extract-text-webpack-plugin": "^3.0.0",
49 | "file-loader": "^1.1.4",
50 | "friendly-errors-webpack-plugin": "^1.6.1",
51 | "html-webpack-plugin": "^2.30.1",
52 | "node-notifier": "^5.1.2",
53 | "optimize-css-assets-webpack-plugin": "^3.2.0",
54 | "ora": "^1.2.0",
55 | "portfinder": "^1.0.13",
56 | "postcss-import": "^11.0.0",
57 | "postcss-loader": "^2.0.8",
58 | "postcss-url": "^7.2.1",
59 | "rimraf": "^2.6.0",
60 | "semver": "^5.3.0",
61 | "shelljs": "^0.7.6",
62 | "uglifyjs-webpack-plugin": "^1.1.1",
63 | "url-loader": "^0.5.8",
64 | "vue-loader": "^13.3.0",
65 | "vue-style-loader": "^3.0.1",
66 | "vue-template-compiler": "^2.5.2",
67 | "webpack": "^3.6.0",
68 | "webpack-bundle-analyzer": "^2.9.0",
69 | "webpack-dev-server": "^2.9.1",
70 | "webpack-merge": "^4.1.0"
71 | },
72 | "engines": {
73 | "node": ">= 6.0.0",
74 | "npm": ">= 3.0.0"
75 | },
76 | "browserslist": [
77 | "> 1%",
78 | "last 2 versions",
79 | "not ie <= 8"
80 | ]
81 | }
82 |
--------------------------------------------------------------------------------
/web/vue/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 | replay
16 |
17 |
18 |
20 |
21 |
22 |
23 |
24 | Request URL: {{activeTx.request.url}}
25 |
26 |
27 | Request Method: {{activeTx.request.method}}
28 |
29 |
30 | Status Code: {{activeTx.response.status_code}}
31 |
32 |
33 | Server IP: {{activeTx.server_ip}}
34 |
35 |
36 | Client IP: {{activeTx.client_ip}}
37 |
38 |
39 | {{activeTx.response.err}}
40 |
41 |
42 |
43 |
44 |
45 | {{index}}: {{item.join(';') | decodeURI}}
46 |
47 |
48 |
49 |
50 |
51 |
52 | {{index}}: {{item.join(';') | decodeURI}}
53 |
54 |
55 |
56 |
57 |
58 |
59 | {{index}}: {{item}}
60 |
61 |
62 |
63 |
65 |
66 |
67 |
68 | {{index}}: {{item}}
69 |
70 |
71 |
72 |
73 | {{activeTx.request.body.content}}
74 |
75 |
76 | {{activeTx.request.body.content}}
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | {{activeTx.response.body.content}}
88 |
89 |
90 |
91 |
92 |
93 | {{activeTx.response.body.content}}
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | {{captureStatusText}}
104 | clear
105 |
106 |
107 |
108 |
109 |
110 |
111 | Name |
112 | Host |
113 | Method |
114 | Status |
115 | Type |
116 | Size |
117 | Time |
118 |
119 |
120 |
121 |
122 | {{item.path | subString}}
123 | |
124 |
125 | {{item.host | subString}}
126 | |
127 |
128 | {{item.method}}
129 | |
130 |
131 |
132 | error
133 |
134 |
135 | {{item.response_status_code}}
136 |
137 | |
138 |
139 | {{item.response_content_type}}
140 | |
141 |
142 | {{item.response_len | formatBodySize}}
143 | |
144 |
145 | {{item.duration | formatDuration}}
146 | |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
434 |
479 |
--------------------------------------------------------------------------------
/web/vue/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ouqiang/mars/1e2a733b31d210140b585137c4dcc728bdd89e75/web/vue/src/assets/logo.png
--------------------------------------------------------------------------------
/web/vue/src/components/common/navMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 | Root CA
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 | download
21 |
22 |
23 |
24 |
25 |
26 |
27 |
57 |
58 |
63 |
--------------------------------------------------------------------------------
/web/vue/src/main.js:
--------------------------------------------------------------------------------
1 | // The Vue build version to load with the `import` command
2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
3 | import Vue from 'vue'
4 | import ElementUI from 'element-ui'
5 | import 'element-ui/lib/theme-chalk/index.css'
6 | import App from './App'
7 |
8 | Vue.config.productionTip = false
9 | Vue.use(ElementUI)
10 |
11 | Vue.directive('focus', {
12 | inserted: function (el) {
13 | // 聚焦元素
14 | el.focus()
15 | }
16 | })
17 |
18 | Vue.prototype.$appConfirm = function (callback) {
19 | this.$confirm('确定执行此操作?', '提示', {
20 | confirmButtonText: '确定',
21 | cancelButtonText: '取消',
22 | type: 'warning'
23 | }).then(() => {
24 | callback()
25 | })
26 | }
27 |
28 | Vue.filter('formatTime', function (time) {
29 | const fillZero = function (num) {
30 | return num >= 10 ? num : '0' + num
31 | }
32 | const date = new Date(time)
33 |
34 | const result = date.getFullYear() + '-' +
35 | (fillZero(date.getMonth() + 1)) + '-' +
36 | fillZero(date.getDate()) + ' ' +
37 | fillZero(date.getHours()) + ':' +
38 | fillZero(date.getMinutes()) + ':' +
39 | fillZero(date.getSeconds())
40 |
41 | if (result.indexOf('20') !== 0) {
42 | return ''
43 | }
44 |
45 | return result
46 | })
47 |
48 | /* eslint-disable no-new */
49 | new Vue({
50 | el: '#app',
51 | components: { App },
52 | template: ''
53 | })
54 |
--------------------------------------------------------------------------------
/web/vue/src/socket/message.js:
--------------------------------------------------------------------------------
1 | const REQUEST_PING = 1000
2 | const REQUEST_REPLAY = 1001
3 | const REQUEST_TRANSACTION = 1002
4 |
5 | const RESPONSE_PONG = 2000
6 | const RESPONSE_REPLAY = 2001
7 | const RESPONSE_TRANSACTION = 2002
8 |
9 | const PUSH_TRANSACTION = 3000
10 |
11 | export default {
12 | REQUEST_PING,
13 | REQUEST_REPLAY,
14 | REQUEST_TRANSACTION,
15 | RESPONSE_PONG,
16 | RESPONSE_REPLAY,
17 | RESPONSE_TRANSACTION,
18 | PUSH_TRANSACTION
19 | }
20 |
--------------------------------------------------------------------------------
/web/vue/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ouqiang/mars/1e2a733b31d210140b585137c4dcc728bdd89e75/web/vue/static/.gitkeep
--------------------------------------------------------------------------------