├── .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 | [![license](https://img.shields.io/hexpm/l/plug.svg?maxAge=2592000)](https://github.com/ouqiang/mars/blob/master/LICENSE) 4 | [![Release](https://img.shields.io/github/release/ouqiang/mars.svg?label=Release)](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 | ![列表](https://raw.githubusercontent.com/ouqiang/mars/master/screenshot/list.png) 20 | ![详情](https://raw.githubusercontent.com/ouqiang/mars/master/screenshot/detail.png) 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 | 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 | 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 --------------------------------------------------------------------------------