├── web ├── client │ ├── .eslintignore │ ├── src │ │ ├── react-app-env.d.ts │ │ ├── setupTests.ts │ │ ├── App.test.tsx │ │ ├── reportWebVitals.ts │ │ ├── index.tsx │ │ ├── lib │ │ │ ├── connection.ts │ │ │ ├── utils.ts │ │ │ ├── message.ts │ │ │ └── flow.ts │ │ ├── components │ │ │ ├── FlowPreview.tsx │ │ │ ├── BreakPoint.tsx │ │ │ ├── EditFlow.tsx │ │ │ └── ViewFlow.tsx │ │ ├── App.css │ │ └── App.tsx │ ├── build │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ ├── asset-manifest.json │ │ ├── static │ │ │ ├── js │ │ │ │ ├── 2.948b8343.chunk.js.LICENSE.txt │ │ │ │ ├── runtime-main.476c72c1.js │ │ │ │ ├── 3.fdc4294f.chunk.js │ │ │ │ ├── 3.fdc4294f.chunk.js.map │ │ │ │ └── runtime-main.476c72c1.js.map │ │ │ └── css │ │ │ │ ├── main.9aa5bcb2.chunk.css │ │ │ │ └── main.9aa5bcb2.chunk.css.map │ │ └── index.html │ ├── public │ │ ├── robots.txt │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── index.html │ ├── .gitignore │ ├── tsconfig.json │ ├── .eslintrc.yml │ ├── package.json │ └── README.md ├── web.go ├── conn.go └── message.go ├── go-mitmproxy ├── mitmproxy.go ├── addon ├── decoder.go ├── mapper_test.go ├── log.go ├── dumper.go └── mapper.go ├── go.mod ├── examples ├── http-add-header │ └── main.go └── change-html │ └── main.go ├── cert ├── cert_test.go └── cert.go ├── LICENSE ├── cmd ├── dummycert │ └── main.go └── mitmproxy │ └── main.go ├── proxy ├── websocket.go ├── addon.go ├── flowencoding.go ├── helper.go ├── flow.go ├── interceptor.go ├── proxy.go ├── connection.go └── proxy_test.go ├── go.sum └── README.md /web/client/.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /web/client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /go-mitmproxy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kardianos/mitmproxy/HEAD/go-mitmproxy -------------------------------------------------------------------------------- /web/client/build/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web/client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /mitmproxy.go: -------------------------------------------------------------------------------- 1 | // Package mitmproxy implements a man-in-the-middle proxy (forward proxy). 2 | package mitmproxy 3 | -------------------------------------------------------------------------------- /web/client/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kardianos/mitmproxy/HEAD/web/client/build/favicon.ico -------------------------------------------------------------------------------- /web/client/build/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kardianos/mitmproxy/HEAD/web/client/build/logo192.png -------------------------------------------------------------------------------- /web/client/build/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kardianos/mitmproxy/HEAD/web/client/build/logo512.png -------------------------------------------------------------------------------- /web/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kardianos/mitmproxy/HEAD/web/client/public/favicon.ico -------------------------------------------------------------------------------- /web/client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kardianos/mitmproxy/HEAD/web/client/public/logo192.png -------------------------------------------------------------------------------- /web/client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kardianos/mitmproxy/HEAD/web/client/public/logo512.png -------------------------------------------------------------------------------- /web/client/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom' 6 | -------------------------------------------------------------------------------- /addon/decoder.go: -------------------------------------------------------------------------------- 1 | package addon 2 | 3 | import "github.com/kardianos/mitmproxy/proxy" 4 | 5 | // decode content-encoding then respond to client 6 | 7 | type Decoder struct { 8 | proxy.BaseAddon 9 | } 10 | 11 | func (d *Decoder) Response(f *proxy.Flow) { 12 | f.Response.ReplaceToDecodedBody() 13 | } 14 | -------------------------------------------------------------------------------- /web/client/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render, screen } from '@testing-library/react' 3 | import App from './App' 4 | 5 | test('renders learn react link', () => { 6 | render() 7 | const linkElement = screen.getByText(/learn react/i) 8 | expect(linkElement).toBeInTheDocument() 9 | }) 10 | -------------------------------------------------------------------------------- /web/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | # /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | package-lock.json 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kardianos/mitmproxy 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/andybalholm/brotli v1.0.4 7 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da 8 | github.com/gorilla/websocket v1.5.0 9 | github.com/satori/go.uuid v1.2.0 10 | github.com/sirupsen/logrus v1.8.1 11 | ) 12 | 13 | require ( 14 | golang.org/x/sys v0.0.0-20220624220833-87e55d714810 // indirect 15 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /web/client/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals' 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry) 7 | getFID(onPerfEntry) 8 | getFCP(onPerfEntry) 9 | getLCP(onPerfEntry) 10 | getTTFB(onPerfEntry) 11 | }) 12 | } 13 | } 14 | 15 | export default reportWebVitals 16 | -------------------------------------------------------------------------------- /web/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import 'bootstrap/dist/css/bootstrap.min.css' 4 | import App from './App' 5 | import reportWebVitals from './reportWebVitals' 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ) 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals() 18 | -------------------------------------------------------------------------------- /web/client/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /web/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /web/client/src/lib/connection.ts: -------------------------------------------------------------------------------- 1 | export interface IConnection { 2 | clientConn: { 3 | id: string 4 | tls: boolean 5 | address: string 6 | } 7 | serverConn: { 8 | id: string 9 | address: string 10 | peername: string 11 | } 12 | } 13 | 14 | export class ConnectionManager { 15 | private _map: Map 16 | 17 | constructor() { 18 | this._map = new Map() 19 | } 20 | 21 | get(id: string) { 22 | return this._map.get(id) 23 | } 24 | 25 | add(id: string, conn: IConnection) { 26 | this._map.set(id, conn) 27 | } 28 | 29 | delete(id: string) { 30 | this._map.delete(id) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /web/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/http-add-header/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | 6 | log "github.com/sirupsen/logrus" 7 | 8 | "github.com/kardianos/mitmproxy/proxy" 9 | ) 10 | 11 | type AddHeader struct { 12 | proxy.BaseAddon 13 | count int 14 | } 15 | 16 | func (a *AddHeader) Responseheaders(f *proxy.Flow) { 17 | a.count += 1 18 | f.Response.Header.Add("x-count", strconv.Itoa(a.count)) 19 | } 20 | 21 | func main() { 22 | opts := &proxy.Options{ 23 | Addr: ":9080", 24 | StreamLargeBodies: 1024 * 1024 * 5, 25 | } 26 | 27 | p, err := proxy.NewProxy(opts) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | p.AddAddon(&AddHeader{}) 33 | 34 | log.Fatal(p.Start()) 35 | } 36 | -------------------------------------------------------------------------------- /web/client/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es2021: true 4 | extends: 5 | - 'eslint:recommended' 6 | - 'plugin:react/recommended' 7 | - 'plugin:@typescript-eslint/recommended' 8 | parser: '@typescript-eslint/parser' 9 | parserOptions: 10 | ecmaFeatures: 11 | jsx: true 12 | ecmaVersion: 12 13 | sourceType: module 14 | plugins: 15 | - react 16 | - '@typescript-eslint' 17 | rules: 18 | indent: 19 | - error 20 | - 2 21 | linebreak-style: 22 | - error 23 | - unix 24 | quotes: 25 | - error 26 | - single 27 | semi: 28 | - error 29 | - never 30 | '@typescript-eslint/explicit-module-boundary-types': 31 | - 0 32 | '@typescript-eslint/no-explicit-any': 33 | - 0 34 | -------------------------------------------------------------------------------- /addon/mapper_test.go: -------------------------------------------------------------------------------- 1 | package addon 2 | 3 | import "testing" 4 | 5 | func TestParser(t *testing.T) { 6 | content := ` 7 | GET /index.html 8 | Host: www.baidu.com 9 | Accept: */* 10 | 11 | hello world 12 | 13 | HTTP/1.1 200 14 | 15 | ok 16 | ` 17 | p, err := newMapperParserFromString(content) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | f, err := p.parse() 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if f.Request.Method != "GET" { 27 | t.Fatal("request method error") 28 | } 29 | if f.Request.URL.String() != "http://www.baidu.com/index.html" { 30 | t.Fatal("request url error") 31 | } 32 | if f.Response.StatusCode != 200 { 33 | t.Fatal("response status code error") 34 | } 35 | if string(f.Response.Body) != "ok" { 36 | t.Fatal("response body error") 37 | } 38 | if f.Response.Header.Get("Content-Length") != "2" { 39 | t.Fatal("response header content-length error") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cert/cert_test.go: -------------------------------------------------------------------------------- 1 | package cert 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestGetStorePath(t *testing.T) { 11 | l, err := NewPathLoader("") 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | if l.StorePath == "" { 16 | t.Fatal("should have path") 17 | } 18 | } 19 | 20 | func TestNewCA(t *testing.T) { 21 | l, err := NewPathLoader("") 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | ca, err := New(l) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | data := make([]byte, 0) 31 | buf := bytes.NewBuffer(data) 32 | 33 | err = l.saveTo(buf, &ca.PrivateKey, &ca.RootCert) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | fileContent, err := ioutil.ReadFile(l.caFile()) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | if !reflect.DeepEqual(fileContent, buf.Bytes()) { 44 | t.Fatal("pem content should equal") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/change-html/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/kardianos/mitmproxy/proxy" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var titleRegexp = regexp.MustCompile("()(.*?)()") 13 | 14 | type ChangeHtml struct { 15 | proxy.BaseAddon 16 | } 17 | 18 | func (c *ChangeHtml) Response(f *proxy.Flow) { 19 | contentType := f.Response.Header.Get("Content-Type") 20 | if !strings.Contains(contentType, "text/html") { 21 | return 22 | } 23 | 24 | // change html end with: " - go-mitmproxy" 25 | f.Response.ReplaceToDecodedBody() 26 | f.Response.Body = titleRegexp.ReplaceAll(f.Response.Body, []byte("${1}${2} - go-mitmproxy${3}")) 27 | f.Response.Header.Set("Content-Length", strconv.Itoa(len(f.Response.Body))) 28 | } 29 | 30 | func main() { 31 | opts := &proxy.Options{ 32 | Addr: ":9080", 33 | StreamLargeBodies: 1024 * 1024 * 5, 34 | } 35 | 36 | p, err := proxy.NewProxy(opts) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | p.AddAddon(&ChangeHtml{}) 42 | 43 | log.Fatal(p.Start()) 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 liqiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/client/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.9aa5bcb2.chunk.css", 4 | "main.js": "/static/js/main.2abbef8f.chunk.js", 5 | "main.js.map": "/static/js/main.2abbef8f.chunk.js.map", 6 | "runtime-main.js": "/static/js/runtime-main.476c72c1.js", 7 | "runtime-main.js.map": "/static/js/runtime-main.476c72c1.js.map", 8 | "static/css/2.4659568d.chunk.css": "/static/css/2.4659568d.chunk.css", 9 | "static/js/2.948b8343.chunk.js": "/static/js/2.948b8343.chunk.js", 10 | "static/js/2.948b8343.chunk.js.map": "/static/js/2.948b8343.chunk.js.map", 11 | "static/js/3.fdc4294f.chunk.js": "/static/js/3.fdc4294f.chunk.js", 12 | "static/js/3.fdc4294f.chunk.js.map": "/static/js/3.fdc4294f.chunk.js.map", 13 | "index.html": "/index.html", 14 | "static/css/2.4659568d.chunk.css.map": "/static/css/2.4659568d.chunk.css.map", 15 | "static/css/main.9aa5bcb2.chunk.css.map": "/static/css/main.9aa5bcb2.chunk.css.map", 16 | "static/js/2.948b8343.chunk.js.LICENSE.txt": "/static/js/2.948b8343.chunk.js.LICENSE.txt" 17 | }, 18 | "entrypoints": [ 19 | "static/js/runtime-main.476c72c1.js", 20 | "static/css/2.4659568d.chunk.css", 21 | "static/js/2.948b8343.chunk.js", 22 | "static/css/main.9aa5bcb2.chunk.css", 23 | "static/js/main.2abbef8f.chunk.js" 24 | ] 25 | } -------------------------------------------------------------------------------- /web/client/src/components/FlowPreview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { shallowEqual } from '../lib/utils' 3 | import type { IFlowPreview } from '../lib/flow' 4 | 5 | interface IProps { 6 | flow: IFlowPreview 7 | isSelected: boolean 8 | onShowDetail: () => void 9 | } 10 | 11 | class FlowPreview extends React.Component<IProps> { 12 | shouldComponentUpdate(nextProps: IProps) { 13 | if (nextProps.isSelected === this.props.isSelected && shallowEqual(nextProps.flow, this.props.flow)) { 14 | return false 15 | } 16 | return true 17 | } 18 | 19 | render() { 20 | const fp = this.props.flow 21 | 22 | const classNames = [] 23 | if (this.props.isSelected) classNames.push('tr-selected') 24 | if (fp.waitIntercept) classNames.push('tr-wait-intercept') 25 | 26 | return ( 27 | <tr className={classNames.length ? classNames.join(' ') : undefined} 28 | onClick={() => { 29 | this.props.onShowDetail() 30 | }} 31 | > 32 | <td>{fp.no}</td> 33 | <td>{fp.method}</td> 34 | <td>{fp.host}</td> 35 | <td>{fp.path}</td> 36 | <td>{fp.contentType}</td> 37 | <td>{fp.statusCode}</td> 38 | <td>{fp.size}</td> 39 | <td>{fp.costTime}</td> 40 | </tr> 41 | ) 42 | } 43 | } 44 | 45 | export default FlowPreview 46 | -------------------------------------------------------------------------------- /web/client/build/static/js/2.948b8343.chunk.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2018 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /** @license React v0.20.2 14 | * scheduler.production.min.js 15 | * 16 | * Copyright (c) Facebook, Inc. and its affiliates. 17 | * 18 | * This source code is licensed under the MIT license found in the 19 | * LICENSE file in the root directory of this source tree. 20 | */ 21 | 22 | /** @license React v17.0.2 23 | * react-dom.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** @license React v17.0.2 32 | * react-jsx-runtime.production.min.js 33 | * 34 | * Copyright (c) Facebook, Inc. and its affiliates. 35 | * 36 | * This source code is licensed under the MIT license found in the 37 | * LICENSE file in the root directory of this source tree. 38 | */ 39 | 40 | /** @license React v17.0.2 41 | * react.production.min.js 42 | * 43 | * Copyright (c) Facebook, Inc. and its affiliates. 44 | * 45 | * This source code is licensed under the MIT license found in the 46 | * LICENSE file in the root directory of this source tree. 47 | */ 48 | -------------------------------------------------------------------------------- /web/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mitmproxy-client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.15.1", 7 | "@testing-library/react": "^12.1.2", 8 | "@testing-library/user-event": "^13.5.0", 9 | "@types/jest": "^27.0.3", 10 | "@types/node": "^16.11.11", 11 | "@types/react": "^17.0.37", 12 | "@types/react-dom": "^17.0.11", 13 | "bootstrap": "^5.1.3", 14 | "copy-to-clipboard": "^3.3.1", 15 | "fetch-to-curl": "^0.5.2", 16 | "react": "^17.0.2", 17 | "react-bootstrap": "^2.0.3", 18 | "react-dom": "^17.0.2", 19 | "react-json-pretty": "^2.2.0", 20 | "react-scripts": "4.0.3", 21 | "typescript": "^4.5.2", 22 | "web-vitals": "^2.1.2" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@typescript-eslint/eslint-plugin": "^4.33.0", 50 | "@typescript-eslint/parser": "^4.33.0", 51 | "eslint": "^7.32.0", 52 | "eslint-plugin-react": "^7.27.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cmd/dummycert/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/pem" 6 | "flag" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/kardianos/mitmproxy/cert" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // 生成假的/用于测试的服务器证书 15 | 16 | type Config struct { 17 | commonName string 18 | } 19 | 20 | func loadConfig() *Config { 21 | config := new(Config) 22 | flag.StringVar(&config.commonName, "commonName", "", "server commonName") 23 | flag.Parse() 24 | return config 25 | } 26 | 27 | func main() { 28 | log.SetLevel(log.InfoLevel) 29 | log.SetReportCaller(false) 30 | log.SetOutput(os.Stdout) 31 | log.SetFormatter(&log.TextFormatter{ 32 | FullTimestamp: true, 33 | }) 34 | 35 | config := loadConfig() 36 | if config.commonName == "" { 37 | log.Fatal("commonName required") 38 | } 39 | 40 | l := &cert.MemoryLoader{} 41 | ca, err := cert.New(l) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | cert, err := ca.GenerateCert(config.commonName) 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | os.Stdout.WriteString(fmt.Sprintf("%v-cert.pem\n", config.commonName)) 52 | err = pem.Encode(os.Stdout, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Certificate[0]}) 53 | if err != nil { 54 | panic(err) 55 | } 56 | os.Stdout.WriteString(fmt.Sprintf("\n%v-key.pem\n", config.commonName)) 57 | 58 | keyBytes, err := x509.MarshalPKCS8PrivateKey(&ca.PrivateKey) 59 | if err != nil { 60 | panic(err) 61 | } 62 | err = pem.Encode(os.Stdout, &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes}) 63 | if err != nil { 64 | panic(err) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /proxy/websocket.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "net/http" 7 | "net/http/httputil" 8 | "strings" 9 | 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // Currently only forwarding websocket flow. 14 | 15 | type webSocket struct{} 16 | 17 | var defaultWebSocket webSocket 18 | 19 | func (s *webSocket) ws(conn net.Conn, host string) { 20 | log := log.WithField("in", "webSocket.ws").WithField("host", host) 21 | 22 | defer conn.Close() 23 | remoteConn, err := net.Dial("tcp", host) 24 | if err != nil { 25 | logErr(log, err) 26 | return 27 | } 28 | defer remoteConn.Close() 29 | transfer(log, conn, remoteConn) 30 | } 31 | 32 | func (s *webSocket) wss(res http.ResponseWriter, req *http.Request) { 33 | log := log.WithField("in", "webSocket.wss").WithField("host", req.Host) 34 | 35 | upgradeBuf, err := httputil.DumpRequest(req, false) 36 | if err != nil { 37 | log.Errorf("DumpRequest: %v\n", err) 38 | res.WriteHeader(502) 39 | return 40 | } 41 | 42 | cconn, _, err := res.(http.Hijacker).Hijack() 43 | if err != nil { 44 | log.Errorf("Hijack: %v\n", err) 45 | res.WriteHeader(502) 46 | return 47 | } 48 | defer cconn.Close() 49 | 50 | host := req.Host 51 | if !strings.Contains(host, ":") { 52 | host = host + ":443" 53 | } 54 | conn, err := tls.Dial("tcp", host, nil) 55 | if err != nil { 56 | log.Errorf("tls.Dial: %v\n", err) 57 | return 58 | } 59 | defer conn.Close() 60 | 61 | _, err = conn.Write(upgradeBuf) 62 | if err != nil { 63 | log.Errorf("wss upgrade: %v\n", err) 64 | return 65 | } 66 | transfer(log, conn, cconn) 67 | } 68 | -------------------------------------------------------------------------------- /addon/log.go: -------------------------------------------------------------------------------- 1 | package addon 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/kardianos/mitmproxy/proxy" 7 | 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // LogAddon log connection and flow 12 | type LogAddon struct { 13 | proxy.BaseAddon 14 | } 15 | 16 | func (addon *LogAddon) ClientConnected(client *proxy.ClientConn) { 17 | log.Infof("%v client connect\n", client.Conn.RemoteAddr()) 18 | } 19 | 20 | func (addon *LogAddon) ClientDisconnected(client *proxy.ClientConn) { 21 | log.Infof("%v client disconnect\n", client.Conn.RemoteAddr()) 22 | } 23 | 24 | func (addon *LogAddon) ServerConnected(connCtx *proxy.ConnContext) { 25 | log.Infof("%v server connect %v (%v->%v)\n", connCtx.ClientConn.Conn.RemoteAddr(), connCtx.ServerConn.Address, connCtx.ServerConn.Conn.LocalAddr(), connCtx.ServerConn.Conn.RemoteAddr()) 26 | } 27 | 28 | func (addon *LogAddon) ServerDisconnected(connCtx *proxy.ConnContext) { 29 | log.Infof("%v server disconnect %v (%v->%v)\n", connCtx.ClientConn.Conn.RemoteAddr(), connCtx.ServerConn.Address, connCtx.ServerConn.Conn.LocalAddr(), connCtx.ServerConn.Conn.RemoteAddr()) 30 | } 31 | 32 | func (addon *LogAddon) Requestheaders(f *proxy.Flow) { 33 | start := time.Now() 34 | go func() { 35 | <-f.Done() 36 | var StatusCode int 37 | if f.Response != nil { 38 | StatusCode = f.Response.StatusCode 39 | } 40 | var contentLen int 41 | if f.Response != nil && f.Response.Body != nil { 42 | contentLen = len(f.Response.Body) 43 | } 44 | log.Infof("%v %v %v %v %v - %v ms\n", f.ConnContext.ClientConn.Conn.RemoteAddr(), f.Request.Method, f.Request.URL.String(), StatusCode, contentLen, time.Since(start).Milliseconds()) 45 | }() 46 | } 47 | -------------------------------------------------------------------------------- /web/client/build/static/css/main.9aa5bcb2.chunk.css: -------------------------------------------------------------------------------- 1 | .main-table-wrap{font-family:Menlo,Monaco;font-size:.8rem;display:flex;flex-flow:column;height:100vh}.table-wrap-div{flex:1 1;overflow:auto;border-top:1px solid #dee2e6}.table-wrap-div .table>:not(:first-child){border-top:0 solid #dee2e6}.table-wrap-div .table{margin-bottom:0}.table-wrap-div thead tr{border-width:0;background-color:#fff;position:-webkit-sticky;position:sticky;top:0;background:linear-gradient(0deg,#212529,#212529 2px,#fff 0,#fff)}.main-table-wrap table td{overflow:hidden;white-space:nowrap}.top-control{display:flex;align-items:center;background-color:#fff;padding:10px}.top-control>div{margin-right:20px}.main-table-wrap tbody tr.tr-selected{background-color:#2376e5;color:#fff}.main-table-wrap tbody tr.tr-wait-intercept{background-color:#d86e53;color:#fff}.flow-detail{position:fixed;top:0;right:0;height:100vh;background-color:#fff;min-width:500px;width:50%;overflow-y:auto;word-break:break-all;border-left:2px solid #dee2e6}.flow-detail .header-tabs{display:flex;position:-webkit-sticky;position:sticky;top:0;background-color:#fff;padding:5px 0}.flow-detail .header-tabs span{display:inline-block;line-height:1;padding:8px;cursor:pointer}.flow-detail .header-tabs .selected{border-bottom:2px solid #2376e5}.flow-detail .header-tabs .flow-wait-area button{margin-left:10px}.flow-detail .header-block{margin-bottom:20px}.flow-detail .header-block>p{font-weight:700}.flow-detail .header-block .header-block-content p{margin:5px 0}.flow-detail .header-block .header-block-content{margin-left:20px;line-height:1.5}.flow-detail .request-body-detail span{display:inline-block;line-height:1;padding:8px;cursor:pointer}.flow-detail .request-body-detail .selected{border-bottom:2px solid #2376e5} 2 | /*# sourceMappingURL=main.9aa5bcb2.chunk.css.map */ -------------------------------------------------------------------------------- /proxy/addon.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type Addon interface { 8 | // A client has connected to mitmproxy. Note that a connection can correspond to multiple HTTP requests. 9 | ClientConnected(*ClientConn) 10 | 11 | // A client connection has been closed (either by us or the client). 12 | ClientDisconnected(*ClientConn) 13 | 14 | // Mitmproxy has connected to a server. 15 | ServerConnected(*ConnContext) 16 | 17 | // A server connection has been closed (either by us or the server). 18 | ServerDisconnected(*ConnContext) 19 | 20 | // The TLS handshake with the server has been completed successfully. 21 | TlsEstablishedServer(*ConnContext) 22 | 23 | // HTTP request headers were successfully read. At this point, the body is empty. 24 | Requestheaders(*Flow) 25 | 26 | // The full HTTP request has been read. 27 | Request(*Flow) 28 | 29 | // HTTP response headers were successfully read. At this point, the body is empty. 30 | Responseheaders(*Flow) 31 | 32 | // The full HTTP response has been read. 33 | Response(*Flow) 34 | 35 | // Stream request body modifier 36 | StreamRequestModifier(*Flow, io.Reader) io.Reader 37 | 38 | // Stream response body modifier 39 | StreamResponseModifier(*Flow, io.Reader) io.Reader 40 | } 41 | 42 | // BaseAddon do nothing 43 | type BaseAddon struct{} 44 | 45 | func (addon *BaseAddon) ClientConnected(*ClientConn) {} 46 | func (addon *BaseAddon) ClientDisconnected(*ClientConn) {} 47 | func (addon *BaseAddon) ServerConnected(*ConnContext) {} 48 | func (addon *BaseAddon) ServerDisconnected(*ConnContext) {} 49 | 50 | func (addon *BaseAddon) TlsEstablishedServer(*ConnContext) {} 51 | 52 | func (addon *BaseAddon) Requestheaders(*Flow) {} 53 | func (addon *BaseAddon) Request(*Flow) {} 54 | func (addon *BaseAddon) Responseheaders(*Flow) {} 55 | func (addon *BaseAddon) Response(*Flow) {} 56 | func (addon *BaseAddon) StreamRequestModifier(f *Flow, in io.Reader) io.Reader { 57 | return in 58 | } 59 | func (addon *BaseAddon) StreamResponseModifier(f *Flow, in io.Reader) io.Reader { 60 | return in 61 | } 62 | -------------------------------------------------------------------------------- /web/client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /web/client/build/static/js/runtime-main.476c72c1.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(t){for(var n,i,a=t[0],c=t[1],l=t[2],p=0,s=[];p<a.length;p++)i=a[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(f&&f(t);s.length;)s.shift()();return u.push.apply(u,l||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,a=1;a<r.length;a++){var c=r[a];0!==o[c]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.e=function(e){var t=[],r=o[e];if(0!==r)if(r)t.push(r[2]);else{var n=new Promise((function(t,n){r=o[e]=[t,n]}));t.push(r[2]=n);var u,a=document.createElement("script");a.charset="utf-8",a.timeout=120,i.nc&&a.setAttribute("nonce",i.nc),a.src=function(e){return i.p+"static/js/"+({}[e]||e)+"."+{3:"fdc4294f"}[e]+".chunk.js"}(e);var c=new Error;u=function(t){a.onerror=a.onload=null,clearTimeout(l);var r=o[e];if(0!==r){if(r){var n=t&&("load"===t.type?"missing":t.type),u=t&&t.target&&t.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,r[1](c)}o[e]=void 0}};var l=setTimeout((function(){u({type:"timeout",target:a})}),12e4);a.onerror=a.onload=u,document.head.appendChild(a)}return Promise.all(t)},i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/",i.oe=function(e){throw console.error(e),e};var a=this["webpackJsonpmitmproxy-client"]=this["webpackJsonpmitmproxy-client"]||[],c=a.push.bind(a);a.push=t,a=a.slice();for(var l=0;l<a.length;l++)t(a[l]);var f=c;r()}([]); 2 | //# sourceMappingURL=runtime-main.476c72c1.js.map -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= 2 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 6 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 7 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 8 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 9 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 10 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 17 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 18 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 19 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 20 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 21 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 22 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20220624220833-87e55d714810 h1:rHZQSjJdAI4Xf5Qzeh2bBc5YJIkPFVM6oDtMFYmgws0= 24 | golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 26 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mitmproxy 2 | 3 | 4 | [Mitmproxy](https://mitmproxy.org/) implemented with golang. Intercept HTTP & HTTPS requests and responses and modify them. 5 | 6 | ## Features 7 | 8 | - Intercept HTTP & HTTPS requests and responses and modify them on the fly 9 | - SSL/TLS certificates for interception are generated on the fly 10 | - Certificates logic compatible with [mitmproxy](https://mitmproxy.org/), saved at `~/.mitmproxy`. If you used mitmproxy before and installed certificates, then you can use this go-mitmproxy directly 11 | - Addon mechanism, you can add your functions easily, refer to [examples](./examples) 12 | - Performance advantages 13 | - Golang's inherent performance advantages 14 | - Forwarding and parsing HTTPS traffic in process memory without inter-process communication such as tcp port or unix socket 15 | - Use LRU cache when generating certificates of different domain names to avoid double counting 16 | - Support `Wireshark` to analyze traffic through the environment variable `SSLKEYLOGFILE` 17 | - Support streaming when uploading/downloading large files 18 | - Web interface 19 | 20 | ## Install 21 | 22 | ``` 23 | go install github.com/kardianos/mitmproxy/cmd/go-mitmproxy@latest 24 | ``` 25 | 26 | ## Usage 27 | 28 | ### Startup 29 | 30 | ``` 31 | go-mitmproxy 32 | ``` 33 | 34 | After startup, the HTTP proxy address defaults to port 9080, and the web interface defaults to port 9081. 35 | 36 | After the first startup, the SSL/TLS certificate will be automatically generated at `~/.mitmproxy/mitmproxy-ca-cert.pem`. You can refer to this link to install: [About Certificates](https://docs.mitmproxy.org/stable/concepts-certificates/). 37 | 38 | ### Help 39 | 40 | ``` 41 | Usage of go-mitmproxy: 42 | -addr string 43 | proxy listen addr (default ":9080") 44 | -cert_path string 45 | path of generate cert files 46 | -debug int 47 | debug mode: 1 - print debug log, 2 - show debug from 48 | -dump string 49 | dump filename 50 | -dump_level int 51 | dump level: 0 - header, 1 - header + body 52 | -mapper_dir string 53 | mapper files dirpath 54 | -ssl_insecure 55 | not verify upstream server SSL/TLS certificates. 56 | -version 57 | show version 58 | -web_addr string 59 | web interface listen addr (default ":9081") 60 | ``` 61 | 62 | ## Usage as package 63 | 64 | Refer to [cmd/mitmproxy/main.go](./cmd/mitmproxy/main.go), you can add your own addon by call `AddAddon` method. 65 | 66 | For more examples, please refer to [examples](./examples) 67 | 68 | ## Web interface 69 | 70 | Has a web interface to view requests and responses. 71 | 72 | ## TODO 73 | 74 | - [ ] Support http2 75 | - [ ] Support parse websocket 76 | 77 | ## License 78 | 79 | [MIT License](./LICENSE) 80 | -------------------------------------------------------------------------------- /web/client/src/App.css: -------------------------------------------------------------------------------- 1 | .main-table-wrap { 2 | font-family: Menlo,Monaco; 3 | font-size: 0.8rem; 4 | display: flex; 5 | flex-flow: column; 6 | height: 100vh; 7 | } 8 | 9 | .table-wrap-div { 10 | flex: 1; 11 | overflow: auto; 12 | border-top: 1px solid rgb(222, 226, 230); 13 | } 14 | 15 | .table-wrap-div .table>:not(:first-child) { 16 | border-top: 0 solid rgb(222, 226, 230); 17 | } 18 | 19 | .table-wrap-div .table { 20 | margin-bottom: 0; 21 | } 22 | 23 | /* https://codepen.io/Ray-H/pen/bMedLL */ 24 | .table-wrap-div thead tr { 25 | border-width: 0; 26 | background-color: white; 27 | position: sticky; 28 | top: 0; 29 | background: linear-gradient(to top,rgb(33, 37, 41), rgb(33, 37, 41) 2px, white 1px, white 100%); 30 | } 31 | 32 | .main-table-wrap table td { 33 | overflow: hidden; 34 | white-space: nowrap; 35 | } 36 | 37 | .top-control { 38 | display: flex; 39 | align-items: center; 40 | background-color: #fff; 41 | padding: 10px; 42 | } 43 | 44 | .top-control > div { 45 | margin-right: 20px; 46 | } 47 | 48 | 49 | .main-table-wrap tbody tr.tr-selected { 50 | background-color: rgb(35, 118, 229); 51 | color: white; 52 | } 53 | 54 | .main-table-wrap tbody tr.tr-wait-intercept { 55 | background-color: rgb(216, 110, 83); 56 | color: white; 57 | } 58 | 59 | .flow-detail { 60 | position: fixed; 61 | top: 0; 62 | right: 0; 63 | 64 | height: 100vh; 65 | background-color: #fff; 66 | min-width: 500px; 67 | width: 50%; 68 | overflow-y: auto; 69 | 70 | word-break: break-all; 71 | border-left: 2px solid #dee2e6; 72 | } 73 | 74 | .flow-detail .header-tabs { 75 | display: flex; 76 | position: sticky; 77 | top: 0; 78 | background-color: white; 79 | padding: 5px 0; 80 | } 81 | 82 | .flow-detail .header-tabs span { 83 | display: inline-block; 84 | line-height: 1; 85 | padding: 8px; 86 | cursor: pointer; 87 | } 88 | 89 | .flow-detail .header-tabs .selected { 90 | border-bottom: 2px rgb(35, 118, 229) solid; 91 | } 92 | 93 | .flow-detail .header-tabs .flow-wait-area button { 94 | margin-left: 10px; 95 | } 96 | 97 | .flow-detail .header-block { 98 | margin-bottom: 20px; 99 | } 100 | 101 | .flow-detail .header-block > p { 102 | font-weight: bold; 103 | } 104 | 105 | .flow-detail .header-block .header-block-content p { 106 | margin: 5px 0; 107 | } 108 | 109 | .flow-detail .header-block .header-block-content { 110 | margin-left: 20px; 111 | line-height: 1.5; 112 | } 113 | 114 | .flow-detail .request-body-detail span { 115 | display: inline-block; 116 | line-height: 1; 117 | padding: 8px; 118 | cursor: pointer; 119 | } 120 | 121 | .flow-detail .request-body-detail .selected { 122 | border-bottom: 2px rgb(35, 118, 229) solid; 123 | } 124 | -------------------------------------------------------------------------------- /addon/dumper.go: -------------------------------------------------------------------------------- 1 | package addon 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "unicode" 11 | 12 | "github.com/kardianos/mitmproxy/proxy" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type Dumper struct { 17 | proxy.BaseAddon 18 | out io.Writer 19 | level int // 0: header 1: header + body 20 | } 21 | 22 | func NewDumper(out io.Writer, level int) *Dumper { 23 | if level != 0 && level != 1 { 24 | level = 0 25 | } 26 | return &Dumper{out: out, level: level} 27 | } 28 | 29 | func NewDumperWithFilename(filename string, level int) *Dumper { 30 | out, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) 31 | if err != nil { 32 | panic(err) 33 | } 34 | return NewDumper(out, level) 35 | } 36 | 37 | func (d *Dumper) Requestheaders(f *proxy.Flow) { 38 | go func() { 39 | <-f.Done() 40 | d.dump(f) 41 | }() 42 | } 43 | 44 | // call when <-f.Done() 45 | func (d *Dumper) dump(f *proxy.Flow) { 46 | // Reference httputil.DumpRequest. 47 | 48 | buf := bytes.NewBuffer(make([]byte, 0)) 49 | fmt.Fprintf(buf, "%s %s %s\r\n", f.Request.Method, f.Request.URL.RequestURI(), f.Request.Proto) 50 | fmt.Fprintf(buf, "Host: %s\r\n", f.Request.URL.Host) 51 | if len(f.Request.Raw().TransferEncoding) > 0 { 52 | fmt.Fprintf(buf, "Transfer-Encoding: %s\r\n", strings.Join(f.Request.Raw().TransferEncoding, ",")) 53 | } 54 | if f.Request.Raw().Close { 55 | fmt.Fprintf(buf, "Connection: close\r\n") 56 | } 57 | 58 | err := f.Request.Header.WriteSubset(buf, nil) 59 | if err != nil { 60 | log.Error(err) 61 | } 62 | buf.WriteString("\r\n") 63 | 64 | if d.level == 1 && f.Request.Body != nil && len(f.Request.Body) > 0 && canPrint(f.Request.Body) { 65 | buf.Write(f.Request.Body) 66 | buf.WriteString("\r\n\r\n") 67 | } 68 | 69 | if f.Response != nil { 70 | fmt.Fprintf(buf, "%v %v %v\r\n", f.Request.Proto, f.Response.StatusCode, http.StatusText(f.Response.StatusCode)) 71 | err = f.Response.Header.WriteSubset(buf, nil) 72 | if err != nil { 73 | log.Error(err) 74 | } 75 | buf.WriteString("\r\n") 76 | 77 | if d.level == 1 && f.Response.Body != nil && len(f.Response.Body) > 0 && f.Response.IsTextContentType() { 78 | body, err := f.Response.DecodedBody() 79 | if err == nil && body != nil && len(body) > 0 { 80 | buf.Write(body) 81 | buf.WriteString("\r\n\r\n") 82 | } 83 | } 84 | } 85 | 86 | buf.WriteString("\r\n\r\n") 87 | 88 | _, err = d.out.Write(buf.Bytes()) 89 | if err != nil { 90 | log.Error(err) 91 | } 92 | } 93 | 94 | func canPrint(content []byte) bool { 95 | for _, c := range string(content) { 96 | if !unicode.IsPrint(c) && !unicode.IsSpace(c) { 97 | return false 98 | } 99 | } 100 | return true 101 | } 102 | -------------------------------------------------------------------------------- /proxy/flowencoding.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "compress/flate" 6 | "compress/gzip" 7 | "errors" 8 | "io" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/andybalholm/brotli" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | var errEncodingNotSupport = errors.New("content-encoding not support") 17 | 18 | var textContentTypes = []string{ 19 | "text", 20 | "javascript", 21 | "json", 22 | } 23 | 24 | func (r *Response) IsTextContentType() bool { 25 | contentType := r.Header.Get("Content-Type") 26 | if contentType == "" { 27 | return false 28 | } 29 | for _, substr := range textContentTypes { 30 | if strings.Contains(contentType, substr) { 31 | return true 32 | } 33 | } 34 | return false 35 | } 36 | 37 | func (r *Response) DecodedBody() ([]byte, error) { 38 | if r.decodedBody != nil { 39 | return r.decodedBody, nil 40 | } 41 | 42 | if r.decodedErr != nil { 43 | return nil, r.decodedErr 44 | } 45 | 46 | if r.Body == nil { 47 | return nil, nil 48 | } 49 | 50 | if len(r.Body) == 0 { 51 | r.decodedBody = r.Body 52 | return r.decodedBody, nil 53 | } 54 | 55 | enc := r.Header.Get("Content-Encoding") 56 | if enc == "" || enc == "identity" { 57 | r.decodedBody = r.Body 58 | return r.decodedBody, nil 59 | } 60 | 61 | decodedBody, decodedErr := decode(enc, r.Body) 62 | if decodedErr != nil { 63 | r.decodedErr = decodedErr 64 | log.Error(r.decodedErr) 65 | return nil, decodedErr 66 | } 67 | 68 | r.decodedBody = decodedBody 69 | r.decoded = true 70 | return r.decodedBody, nil 71 | } 72 | 73 | func (r *Response) ReplaceToDecodedBody() { 74 | body, err := r.DecodedBody() 75 | if err != nil || body == nil { 76 | return 77 | } 78 | 79 | r.Body = body 80 | r.Header.Del("Content-Encoding") 81 | r.Header.Set("Content-Length", strconv.Itoa(len(body))) 82 | r.Header.Del("Transfer-Encoding") 83 | } 84 | 85 | func decode(enc string, body []byte) ([]byte, error) { 86 | if enc == "gzip" { 87 | dreader, err := gzip.NewReader(bytes.NewReader(body)) 88 | if err != nil { 89 | return nil, err 90 | } 91 | buf := bytes.NewBuffer(make([]byte, 0)) 92 | _, err = io.Copy(buf, dreader) 93 | if err != nil { 94 | return nil, err 95 | } 96 | err = dreader.Close() 97 | if err != nil { 98 | return nil, err 99 | } 100 | return buf.Bytes(), nil 101 | } else if enc == "br" { 102 | dreader := brotli.NewReader(bytes.NewReader(body)) 103 | buf := bytes.NewBuffer(make([]byte, 0)) 104 | _, err := io.Copy(buf, dreader) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return buf.Bytes(), nil 109 | } else if enc == "deflate" { 110 | dreader := flate.NewReader(bytes.NewReader(body)) 111 | buf := bytes.NewBuffer(make([]byte, 0)) 112 | _, err := io.Copy(buf, dreader) 113 | if err != nil { 114 | return nil, err 115 | } 116 | return buf.Bytes(), nil 117 | } 118 | 119 | return nil, errEncodingNotSupport 120 | } 121 | -------------------------------------------------------------------------------- /web/client/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { IRequest, IResponse } from './flow' 2 | 3 | export const isTextBody = (payload: IRequest | IResponse) => { 4 | if (!payload) return false 5 | if (!payload.header) return false 6 | if (!payload.header['Content-Type']) return false 7 | 8 | return /text|javascript|json|x-www-form-urlencoded|xml|form-data/.test(payload.header['Content-Type'].join('')) 9 | } 10 | 11 | export const getSize = (len: number) => { 12 | if (!len) return '0' 13 | if (isNaN(len)) return '0' 14 | if (len <= 0) return '0' 15 | 16 | if (len < 1024) return `${len} B` 17 | if (len < 1024 * 1024) return `${(len / 1024).toFixed(2)} KB` 18 | return `${(len / (1024 * 1024)).toFixed(2)} MB` 19 | } 20 | 21 | export const shallowEqual = (objA: any, objB: any) => { 22 | if (objA === objB) return true 23 | 24 | const keysA = Object.keys(objA) 25 | const keysB = Object.keys(objB) 26 | if (keysA.length !== keysB.length) return false 27 | 28 | for (let i = 0; i < keysA.length; i++) { 29 | const key = keysA[i] 30 | if (objB[key] === undefined || objA[key] !== objB[key]) return false 31 | } 32 | return true 33 | } 34 | 35 | export const arrayBufferToBase64 = (buf: ArrayBuffer) => { 36 | let binary = '' 37 | const bytes = new Uint8Array(buf) 38 | const len = bytes.byteLength 39 | for (let i = 0; i < len; i++) { 40 | binary += String.fromCharCode(bytes[i]) 41 | } 42 | return btoa(binary) 43 | } 44 | 45 | export const bufHexView = (buf: ArrayBuffer) => { 46 | let str = '' 47 | const bytes = new Uint8Array(buf) 48 | const len = bytes.byteLength 49 | 50 | let viewStr = '' 51 | 52 | str += '00000000: ' 53 | for (let i = 0; i < len; i++) { 54 | str += bytes[i].toString(16).padStart(2, '0') + ' ' 55 | 56 | if (bytes[i] >= 32 && bytes[i] <= 126) { 57 | viewStr += String.fromCharCode(bytes[i]) 58 | } else { 59 | viewStr += '.' 60 | } 61 | 62 | if ((i + 1) % 16 === 0) { 63 | str += ' ' + viewStr 64 | viewStr = '' 65 | str += `\n${(i + 1).toString(16).padStart(8, '0')}: ` 66 | } else if ((i + 1) % 8 === 0) { 67 | str += ' ' 68 | } 69 | } 70 | 71 | // 补充最后一行的空白 72 | if (viewStr.length > 0) { 73 | for (let i = viewStr.length; i < 16; i++) { 74 | str += ' ' + ' ' 75 | if ((i + 1) % 8 === 0) str += ' ' 76 | } 77 | str += ' ' + viewStr 78 | } 79 | 80 | return str 81 | } 82 | 83 | // https://github.com/febobo/web-interview/issues/84 84 | export function isInViewPort(element: HTMLElement) { 85 | const viewWidth = window.innerWidth || document.documentElement.clientWidth 86 | const viewHeight = window.innerHeight || document.documentElement.clientHeight 87 | const { 88 | top, 89 | right, 90 | bottom, 91 | left, 92 | } = element.getBoundingClientRect() 93 | 94 | return ( 95 | top >= 0 && 96 | left >= 0 && 97 | right <= viewWidth && 98 | bottom <= viewHeight 99 | ) 100 | } 101 | -------------------------------------------------------------------------------- /cmd/mitmproxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | rawLog "log" 7 | "os" 8 | 9 | "github.com/kardianos/mitmproxy/addon" 10 | "github.com/kardianos/mitmproxy/cert" 11 | "github.com/kardianos/mitmproxy/proxy" 12 | "github.com/kardianos/mitmproxy/web" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type Config struct { 17 | debug int 18 | version bool 19 | certPath string 20 | 21 | addr string 22 | webAddr string 23 | ssl_insecure bool 24 | 25 | dump string // dump filename 26 | dumpLevel int // dump level 27 | 28 | mapperDir string 29 | } 30 | 31 | func loadConfig() *Config { 32 | config := new(Config) 33 | 34 | flag.IntVar(&config.debug, "debug", 0, "debug mode: 1 - print debug log, 2 - show debug from") 35 | flag.BoolVar(&config.version, "version", false, "show version") 36 | flag.StringVar(&config.addr, "addr", ":9080", "proxy listen addr") 37 | flag.StringVar(&config.webAddr, "web_addr", ":9081", "web interface listen addr") 38 | flag.BoolVar(&config.ssl_insecure, "ssl_insecure", false, "not verify upstream server SSL/TLS certificates.") 39 | flag.StringVar(&config.dump, "dump", "", "dump filename") 40 | flag.IntVar(&config.dumpLevel, "dump_level", 0, "dump level: 0 - header, 1 - header + body") 41 | flag.StringVar(&config.mapperDir, "mapper_dir", "", "mapper files dirpath") 42 | flag.StringVar(&config.certPath, "cert_path", "", "path of generate cert files") 43 | flag.Parse() 44 | 45 | return config 46 | } 47 | 48 | func main() { 49 | config := loadConfig() 50 | 51 | if config.debug > 0 { 52 | rawLog.SetFlags(rawLog.LstdFlags | rawLog.Lshortfile) 53 | log.SetLevel(log.DebugLevel) 54 | } else { 55 | log.SetLevel(log.InfoLevel) 56 | } 57 | if config.debug == 2 { 58 | log.SetReportCaller(true) 59 | } 60 | log.SetOutput(os.Stdout) 61 | log.SetFormatter(&log.TextFormatter{ 62 | FullTimestamp: true, 63 | }) 64 | 65 | l, err := cert.NewPathLoader(config.certPath) 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | ca, err := cert.New(l) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | 74 | opts := &proxy.Options{ 75 | Debug: config.debug, 76 | Addr: config.addr, 77 | StreamLargeBodies: 1024 * 1024 * 5, 78 | InsecureSkipVerifyTLS: config.ssl_insecure, 79 | CA: ca, 80 | } 81 | 82 | p, err := proxy.NewProxy(opts) 83 | if err != nil { 84 | log.Fatal(err) 85 | } 86 | 87 | if config.version { 88 | fmt.Println("go-mitmproxy: " + p.Version) 89 | os.Exit(0) 90 | } 91 | 92 | log.Infof("go-mitmproxy version %v\n", p.Version) 93 | 94 | p.AddAddon(&addon.LogAddon{}) 95 | p.AddAddon(web.NewWebAddon(config.webAddr)) 96 | 97 | if config.dump != "" { 98 | dumper := addon.NewDumperWithFilename(config.dump, config.dumpLevel) 99 | p.AddAddon(dumper) 100 | } 101 | 102 | if config.mapperDir != "" { 103 | mapper := addon.NewMapper(config.mapperDir) 104 | p.AddAddon(mapper) 105 | } 106 | 107 | log.Fatal(p.Start()) 108 | } 109 | -------------------------------------------------------------------------------- /web/client/build/static/css/main.9aa5bcb2.chunk.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack://src/App.css"],"names":[],"mappings":"AAAA,iBACE,wBAAyB,CACzB,eAAiB,CACjB,YAAa,CACb,gBAAiB,CACjB,YACF,CAEA,gBACE,QAAO,CACP,aAAc,CACd,4BACF,CAEA,0CACE,0BACF,CAEA,uBACE,eACF,CAGA,yBACE,cAAe,CACf,qBAAuB,CACvB,uBAAgB,CAAhB,eAAgB,CAChB,KAAM,CACN,gEACF,CAEA,0BACE,eAAgB,CAChB,kBACF,CAEA,aACE,YAAa,CACb,kBAAmB,CACnB,qBAAsB,CACtB,YACF,CAEA,iBACE,iBACF,CAGA,sCACE,wBAAmC,CACnC,UACF,CAEA,4CACE,wBAAmC,CACnC,UACF,CAEA,aACE,cAAe,CACf,KAAM,CACN,OAAQ,CAER,YAAa,CACb,qBAAsB,CACtB,eAAgB,CAChB,SAAU,CACV,eAAgB,CAEhB,oBAAqB,CACrB,6BACF,CAEA,0BACE,YAAa,CACb,uBAAgB,CAAhB,eAAgB,CAChB,KAAM,CACN,qBAAuB,CACvB,aACF,CAEA,+BACE,oBAAqB,CACrB,aAAc,CACd,WAAY,CACZ,cACF,CAEA,oCACE,+BACF,CAEA,iDACE,gBACF,CAEA,2BACE,kBACF,CAEA,6BACE,eACF,CAEA,mDACE,YACF,CAEA,iDACE,gBAAiB,CACjB,eACF,CAEA,uCACE,oBAAqB,CACrB,aAAc,CACd,WAAY,CACZ,cACF,CAEA,4CACE,+BACF","file":"main.9aa5bcb2.chunk.css","sourcesContent":[".main-table-wrap {\n font-family: Menlo,Monaco;\n font-size: 0.8rem;\n display: flex;\n flex-flow: column;\n height: 100vh;\n}\n\n.table-wrap-div {\n flex: 1;\n overflow: auto;\n border-top: 1px solid rgb(222, 226, 230);\n}\n\n.table-wrap-div .table>:not(:first-child) {\n border-top: 0 solid rgb(222, 226, 230);\n}\n\n.table-wrap-div .table {\n margin-bottom: 0;\n}\n\n/* https://codepen.io/Ray-H/pen/bMedLL */\n.table-wrap-div thead tr {\n border-width: 0;\n background-color: white;\n position: sticky;\n top: 0;\n background: linear-gradient(to top,rgb(33, 37, 41), rgb(33, 37, 41) 2px, white 1px, white 100%);\n}\n\n.main-table-wrap table td {\n overflow: hidden;\n white-space: nowrap;\n}\n\n.top-control {\n display: flex;\n align-items: center;\n background-color: #fff;\n padding: 10px;\n}\n\n.top-control > div {\n margin-right: 20px;\n}\n\n\n.main-table-wrap tbody tr.tr-selected {\n background-color: rgb(35, 118, 229);\n color: white;\n}\n\n.main-table-wrap tbody tr.tr-wait-intercept {\n background-color: rgb(216, 110, 83);\n color: white;\n}\n\n.flow-detail {\n position: fixed;\n top: 0;\n right: 0;\n\n height: 100vh;\n background-color: #fff;\n min-width: 500px;\n width: 50%;\n overflow-y: auto;\n\n word-break: break-all;\n border-left: 2px solid #dee2e6;\n}\n\n.flow-detail .header-tabs {\n display: flex;\n position: sticky;\n top: 0;\n background-color: white;\n padding: 5px 0;\n}\n\n.flow-detail .header-tabs span {\n display: inline-block;\n line-height: 1;\n padding: 8px;\n cursor: pointer;\n}\n\n.flow-detail .header-tabs .selected {\n border-bottom: 2px rgb(35, 118, 229) solid;\n}\n\n.flow-detail .header-tabs .flow-wait-area button {\n margin-left: 10px;\n}\n\n.flow-detail .header-block {\n margin-bottom: 20px;\n}\n\n.flow-detail .header-block > p {\n font-weight: bold;\n}\n\n.flow-detail .header-block .header-block-content p {\n margin: 5px 0;\n}\n\n.flow-detail .header-block .header-block-content {\n margin-left: 20px;\n line-height: 1.5;\n}\n\n.flow-detail .request-body-detail span {\n display: inline-block;\n line-height: 1;\n padding: 8px;\n cursor: pointer;\n}\n\n.flow-detail .request-body-detail .selected {\n border-bottom: 2px rgb(35, 118, 229) solid;\n}\n"]} -------------------------------------------------------------------------------- /proxy/helper.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | "os" 8 | "strings" 9 | "sync" 10 | 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var normalErrMsgs []string = []string{ 15 | "read: connection reset by peer", 16 | "write: broken pipe", 17 | "i/o timeout", 18 | "net/http: TLS handshake timeout", 19 | "io: read/write on closed pipe", 20 | "connect: connection refused", 21 | "connect: connection reset by peer", 22 | "use of closed network connection", 23 | } 24 | 25 | // Only print unexpected error messages. 26 | func logErr(log *log.Entry, err error) (loged bool) { 27 | msg := err.Error() 28 | 29 | for _, str := range normalErrMsgs { 30 | if strings.Contains(msg, str) { 31 | log.Debug(err) 32 | return 33 | } 34 | } 35 | 36 | log.Error(err) 37 | loged = true 38 | return 39 | } 40 | 41 | // Forward traffic. 42 | func transfer(log *log.Entry, server, client io.ReadWriteCloser) { 43 | done := make(chan struct{}) 44 | defer close(done) 45 | 46 | errChan := make(chan error) 47 | go func() { 48 | _, err := io.Copy(server, client) 49 | log.Debugln("client copy end", err) 50 | client.Close() 51 | select { 52 | case <-done: 53 | return 54 | case errChan <- err: 55 | return 56 | } 57 | }() 58 | go func() { 59 | _, err := io.Copy(client, server) 60 | log.Debugln("server copy end", err) 61 | server.Close() 62 | 63 | if clientConn, ok := client.(*wrapClientConn); ok { 64 | err := clientConn.Conn.(*net.TCPConn).CloseRead() 65 | log.Debugln("clientConn.Conn.(*net.TCPConn).CloseRead()", err) 66 | } 67 | 68 | select { 69 | case <-done: 70 | return 71 | case errChan <- err: 72 | return 73 | } 74 | }() 75 | 76 | for i := 0; i < 2; i++ { 77 | if err := <-errChan; err != nil { 78 | logErr(log, err) 79 | return 80 | } 81 | } 82 | } 83 | 84 | // Try to read Reader into the buffer. 85 | // If the buffer limit size is reached then read from buffered data and rest of connection. 86 | // Otherwise just return buffer. 87 | func readerToBuffer(r io.Reader, limit int64) ([]byte, io.Reader, error) { 88 | buf := bytes.NewBuffer(make([]byte, 0)) 89 | lr := io.LimitReader(r, limit) 90 | 91 | _, err := io.Copy(buf, lr) 92 | if err != nil { 93 | return nil, nil, err 94 | } 95 | 96 | // If limit is reached. 97 | if int64(buf.Len()) == limit { 98 | // Return new Reader. 99 | return nil, io.MultiReader(bytes.NewBuffer(buf.Bytes()), r), nil 100 | } 101 | 102 | // Return buffer. 103 | return buf.Bytes(), nil, nil 104 | } 105 | 106 | // Wireshark parse https setup. 107 | var tlsKeyLogWriter io.Writer 108 | var tlsKeyLogOnce sync.Once 109 | 110 | func getTLSKeyLogWriter() io.Writer { 111 | tlsKeyLogOnce.Do(func() { 112 | logfile := os.Getenv("SSLKEYLOGFILE") 113 | if logfile == "" { 114 | return 115 | } 116 | 117 | writer, err := os.OpenFile(logfile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) 118 | if err != nil { 119 | log.Debugf("getTlsKeyLogWriter OpenFile error: %v", err) 120 | return 121 | } 122 | 123 | tlsKeyLogWriter = writer 124 | }) 125 | return tlsKeyLogWriter 126 | } 127 | -------------------------------------------------------------------------------- /proxy/flow.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | 10 | uuid "github.com/satori/go.uuid" 11 | ) 12 | 13 | // flow http request 14 | type Request struct { 15 | Method string `json:"method"` 16 | URL *url.URL `json:"url"` 17 | Proto string `json:"proto"` 18 | Header http.Header `json:"header"` 19 | Body []byte `json:"-"` 20 | 21 | raw *http.Request `json:"-"` 22 | } 23 | 24 | func newRequest(req *http.Request) *Request { 25 | return &Request{ 26 | Method: req.Method, 27 | URL: req.URL, 28 | Proto: req.Proto, 29 | Header: req.Header, 30 | raw: req, 31 | } 32 | } 33 | 34 | func (r *Request) Raw() *http.Request { 35 | return r.raw 36 | } 37 | 38 | func (req *Request) MarshalJSON() ([]byte, error) { 39 | return json.Marshal(req) 40 | } 41 | 42 | func (req *Request) UnmarshalJSON(data []byte) error { 43 | r := make(map[string]any) 44 | err := json.Unmarshal(data, &r) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | rawurl, ok := r["url"].(string) 50 | if !ok { 51 | return errors.New("url parse error") 52 | } 53 | u, err := url.Parse(rawurl) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | rawheader, ok := r["header"].(map[string]any) 59 | if !ok { 60 | return errors.New("rawheader parse error") 61 | } 62 | 63 | header := make(map[string][]string) 64 | for k, v := range rawheader { 65 | vals, ok := v.([]any) 66 | if !ok { 67 | return errors.New("header parse error") 68 | } 69 | 70 | svals := make([]string, 0) 71 | for _, val := range vals { 72 | sval, ok := val.(string) 73 | if !ok { 74 | return errors.New("header parse error") 75 | } 76 | svals = append(svals, sval) 77 | } 78 | header[k] = svals 79 | } 80 | 81 | *req = Request{ 82 | Method: r["method"].(string), 83 | URL: u, 84 | Proto: r["proto"].(string), 85 | Header: header, 86 | } 87 | return nil 88 | } 89 | 90 | // flow http response 91 | type Response struct { 92 | StatusCode int `json:"statusCode"` 93 | Header http.Header `json:"header"` 94 | Body []byte `json:"-"` 95 | BodyReader io.Reader 96 | 97 | close bool // connection close 98 | 99 | decodedBody []byte 100 | decoded bool // decoded reports whether the response was sent compressed but was decoded to decodedBody. 101 | decodedErr error 102 | } 103 | 104 | // flow 105 | type Flow struct { 106 | Id uuid.UUID 107 | ConnContext *ConnContext 108 | Request *Request 109 | Response *Response 110 | 111 | // https://docs.mitmproxy.org/stable/overview-features/#streaming 112 | // 如果为 true,则不缓冲 Request.Body 和 Response.Body,且不进入之后的 Addon.Request 和 Addon.Response 113 | Stream bool 114 | 115 | done chan struct{} 116 | } 117 | 118 | func newFlow() *Flow { 119 | return &Flow{ 120 | Id: uuid.NewV4(), 121 | done: make(chan struct{}), 122 | } 123 | } 124 | 125 | func (f *Flow) Done() <-chan struct{} { 126 | return f.done 127 | } 128 | 129 | func (f *Flow) finish() { 130 | close(f.done) 131 | } 132 | 133 | func (f *Flow) MarshalJSON() ([]byte, error) { 134 | j := make(map[string]any) 135 | j["id"] = f.Id 136 | j["request"] = f.Request 137 | j["response"] = f.Response 138 | return json.Marshal(j) 139 | } 140 | -------------------------------------------------------------------------------- /web/client/public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> 6 | <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 | <meta name="theme-color" content="#000000" /> 8 | <meta 9 | name="description" 10 | content="Web site created using create-react-app" 11 | /> 12 | <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> 13 | <!-- 14 | manifest.json provides metadata used when your web app is installed on a 15 | user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ 16 | --> 17 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 18 | <!-- 19 | Notice the use of %PUBLIC_URL% in the tags above. 20 | It will be replaced with the URL of the `public` folder during the build. 21 | Only files inside the `public` folder can be referenced from the HTML. 22 | 23 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 24 | work correctly both with client-side routing and a non-root public URL. 25 | Learn how to configure a non-root public URL by running `npm run build`. 26 | --> 27 | <title>go-mitmproxy 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "net/http" 7 | "sync" 8 | 9 | "github.com/gorilla/websocket" 10 | "github.com/kardianos/mitmproxy/proxy" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | //go:embed client/build 15 | var assets embed.FS 16 | 17 | type WebAddon struct { 18 | proxy.BaseAddon 19 | upgrader *websocket.Upgrader 20 | 21 | conns []*concurrentConn 22 | connsMu sync.RWMutex 23 | } 24 | 25 | func NewWebAddon(addr string) *WebAddon { 26 | web := new(WebAddon) 27 | web.upgrader = &websocket.Upgrader{ 28 | CheckOrigin: func(r *http.Request) bool { 29 | return true 30 | }, 31 | } 32 | 33 | serverMux := new(http.ServeMux) 34 | serverMux.HandleFunc("/echo", web.echo) 35 | 36 | fsys, err := fs.Sub(assets, "client/build") 37 | if err != nil { 38 | panic(err) 39 | } 40 | serverMux.Handle("/", http.FileServer(http.FS(fsys))) 41 | 42 | server := &http.Server{Addr: addr, Handler: serverMux} 43 | web.conns = make([]*concurrentConn, 0) 44 | 45 | go func() { 46 | log.Infof("web interface start listen at %v\n", addr) 47 | err := server.ListenAndServe() 48 | log.Error(err) 49 | }() 50 | 51 | return web 52 | } 53 | 54 | func (web *WebAddon) echo(w http.ResponseWriter, r *http.Request) { 55 | c, err := web.upgrader.Upgrade(w, r, nil) 56 | if err != nil { 57 | log.Print("upgrade:", err) 58 | return 59 | } 60 | 61 | conn := newConn(c) 62 | web.addConn(conn) 63 | defer func() { 64 | web.removeConn(conn) 65 | c.Close() 66 | }() 67 | 68 | conn.readloop() 69 | } 70 | 71 | func (web *WebAddon) addConn(c *concurrentConn) { 72 | web.connsMu.Lock() 73 | web.conns = append(web.conns, c) 74 | web.connsMu.Unlock() 75 | } 76 | 77 | func (web *WebAddon) removeConn(conn *concurrentConn) { 78 | web.connsMu.Lock() 79 | defer web.connsMu.Unlock() 80 | 81 | index := -1 82 | for i, c := range web.conns { 83 | if conn == c { 84 | index = i 85 | break 86 | } 87 | } 88 | 89 | if index == -1 { 90 | return 91 | } 92 | web.conns = append(web.conns[:index], web.conns[index+1:]...) 93 | } 94 | 95 | func (web *WebAddon) forEachConn(do func(c *concurrentConn)) bool { 96 | web.connsMu.RLock() 97 | conns := web.conns 98 | web.connsMu.RUnlock() 99 | if len(conns) == 0 { 100 | return false 101 | } 102 | for _, c := range conns { 103 | do(c) 104 | } 105 | return true 106 | } 107 | 108 | func (web *WebAddon) sendFlow(f *proxy.Flow, msgFn func() *messageFlow) bool { 109 | web.connsMu.RLock() 110 | conns := web.conns 111 | web.connsMu.RUnlock() 112 | 113 | if len(conns) == 0 { 114 | return false 115 | } 116 | 117 | msg := msgFn() 118 | for _, c := range conns { 119 | c.writeMessage(msg, f) 120 | } 121 | 122 | return true 123 | } 124 | 125 | func (web *WebAddon) Requestheaders(f *proxy.Flow) { 126 | if f.ConnContext.ClientConn.TLS { 127 | web.forEachConn(func(c *concurrentConn) { 128 | c.trySendConnMessage(f) 129 | }) 130 | } 131 | 132 | web.sendFlow(f, func() *messageFlow { 133 | return newMessageFlow(messageTypeRequest, f) 134 | }) 135 | } 136 | 137 | func (web *WebAddon) Request(f *proxy.Flow) { 138 | web.sendFlow(f, func() *messageFlow { 139 | return newMessageFlow(messageTypeRequestBody, f) 140 | }) 141 | } 142 | 143 | func (web *WebAddon) Responseheaders(f *proxy.Flow) { 144 | if !f.ConnContext.ClientConn.TLS { 145 | web.forEachConn(func(c *concurrentConn) { 146 | c.trySendConnMessage(f) 147 | }) 148 | } 149 | 150 | web.sendFlow(f, func() *messageFlow { 151 | return newMessageFlow(messageTypeResponse, f) 152 | }) 153 | } 154 | 155 | func (web *WebAddon) Response(f *proxy.Flow) { 156 | web.sendFlow(f, func() *messageFlow { 157 | return newMessageFlow(messageTypeResponseBody, f) 158 | }) 159 | } 160 | 161 | func (web *WebAddon) ServerDisconnected(connCtx *proxy.ConnContext) { 162 | web.forEachConn(func(c *concurrentConn) { 163 | c.whenConnClose(connCtx) 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /web/client/src/components/BreakPoint.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button from 'react-bootstrap/Button' 3 | import Modal from 'react-bootstrap/Modal' 4 | import Form from 'react-bootstrap/Form' 5 | import Row from 'react-bootstrap/Row' 6 | import Col from 'react-bootstrap/Col' 7 | 8 | type Method = 'ALL' | 'GET' | 'POST' | 'PUT' | 'DELETE' | '' 9 | type Action = 1 | 2 | 3 10 | interface IRule { 11 | method: Method 12 | url: string 13 | action: Action 14 | } 15 | 16 | interface IState { 17 | show: boolean 18 | rule: IRule 19 | haveRules: boolean 20 | } 21 | 22 | interface IProps { 23 | onSave: (rules: IRule[]) => void 24 | } 25 | 26 | class BreakPoint extends React.Component { 27 | constructor(props: IProps) { 28 | super(props) 29 | 30 | this.state = { 31 | show: false, 32 | 33 | rule: { 34 | method: 'ALL', 35 | url: '', 36 | action: 1, 37 | }, 38 | 39 | haveRules: false, 40 | } 41 | 42 | this.handleClose = this.handleClose.bind(this) 43 | this.handleShow = this.handleShow.bind(this) 44 | this.handleSave = this.handleSave.bind(this) 45 | } 46 | 47 | handleClose() { 48 | this.setState({ show: false }) 49 | } 50 | 51 | handleShow() { 52 | this.setState({ show: true }) 53 | } 54 | 55 | handleSave() { 56 | const { rule } = this.state 57 | const rules: IRule[] = [] 58 | if (rule.url) { 59 | rules.push({ 60 | method: rule.method === 'ALL' ? '' : rule.method, 61 | url: rule.url, 62 | action: rule.action, 63 | }) 64 | } 65 | 66 | this.props.onSave(rules) 67 | this.handleClose() 68 | 69 | this.setState({ haveRules: rules.length ? true : false }) 70 | } 71 | 72 | render() { 73 | const { rule, haveRules } = this.state 74 | const variant = haveRules ? 'success' : 'primary' 75 | 76 | return ( 77 |
78 | 79 | 80 | 81 | 82 | Set BreakPoint 83 | 84 | 85 | 86 | 87 | Method 88 | 89 | { this.setState({ rule: { ...rule, method: e.target.value as Method } }) }}> 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | URL 101 | { this.setState({ rule: { ...rule, url: e.target.value } }) }} /> 102 | 103 | 104 | 105 | Action 106 | 107 | { this.setState({ rule: { ...rule, action: parseInt(e.target.value) as Action } }) }}> 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 120 | 123 | 124 | 125 |
126 | ) 127 | } 128 | } 129 | 130 | export default BreakPoint 131 | -------------------------------------------------------------------------------- /web/client/build/static/js/3.fdc4294f.chunk.js: -------------------------------------------------------------------------------- 1 | (this["webpackJsonpmitmproxy-client"]=this["webpackJsonpmitmproxy-client"]||[]).push([[3],{67:function(e,t,n){"use strict";n.r(t),n.d(t,"getCLS",(function(){return T})),n.d(t,"getFCP",(function(){return g})),n.d(t,"getFID",(function(){return C})),n.d(t,"getLCP",(function(){return k})),n.d(t,"getTTFB",(function(){return D}));var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},p=-1,v=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){f((function(e){var t=e.timeStamp;p=t}),!0)},l=function(){return p<0&&(p=v(),d(),s((function(){setTimeout((function(){p=v(),d()}),0)}))),{get firstHiddenTime(){return p}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime-1&&e(t)},r=u("CLS",0),a=0,o=[],p=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},v=c("layout-shift",p);v&&(n=m(i,r,t),f((function(){v.takeRecords().map(p),n(!0)})),s((function(){a=0,y=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),p=u("FID"),v=function(e){e.startTimeperformance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("pageshow",t)}}}]); 2 | //# sourceMappingURL=3.fdc4294f.chunk.js.map -------------------------------------------------------------------------------- /web/client/build/index.html: -------------------------------------------------------------------------------- 1 | go-mitmproxy
-------------------------------------------------------------------------------- /web/client/src/lib/message.ts: -------------------------------------------------------------------------------- 1 | import type { IConnection } from './connection' 2 | import type { Flow, IFlowRequest, IRequest, IResponse } from './flow' 3 | 4 | export enum MessageType { 5 | CONN = 0, 6 | CONN_CLOSE = 5, 7 | REQUEST = 1, 8 | REQUEST_BODY = 2, 9 | RESPONSE = 3, 10 | RESPONSE_BODY = 4, 11 | } 12 | 13 | const allMessageBytes = [ 14 | MessageType.CONN, 15 | MessageType.CONN_CLOSE, 16 | MessageType.REQUEST, 17 | MessageType.REQUEST_BODY, 18 | MessageType.RESPONSE, 19 | MessageType.RESPONSE_BODY, 20 | ] 21 | 22 | export interface IMessage { 23 | type: MessageType 24 | id: string 25 | waitIntercept: boolean 26 | content?: ArrayBuffer | IFlowRequest | IResponse | IConnection 27 | } 28 | 29 | // type: 0/1/2/3/4 30 | // messageFlow 31 | // version 1 byte + type 1 byte + id 36 byte + waitIntercept 1 byte + content left bytes 32 | export const parseMessage = (data: ArrayBuffer): IMessage | null => { 33 | if (data.byteLength < 39) return null 34 | const meta = new Int8Array(data.slice(0, 39)) 35 | const version = meta[0] 36 | if (version !== 2) return null 37 | const type = meta[1] as MessageType 38 | if (!allMessageBytes.includes(type)) return null 39 | const id = new TextDecoder().decode(data.slice(2, 38)) 40 | const waitIntercept = meta[38] === 1 41 | 42 | const resp: IMessage = { 43 | type, 44 | id, 45 | waitIntercept, 46 | } 47 | if (data.byteLength === 39) return resp 48 | if (type === MessageType.REQUEST_BODY || type === MessageType.RESPONSE_BODY) { 49 | resp.content = data.slice(39) 50 | return resp 51 | } 52 | 53 | const contentStr = new TextDecoder().decode(data.slice(39)) 54 | let content: any 55 | try { 56 | content = JSON.parse(contentStr) 57 | } catch (err) { 58 | return null 59 | } 60 | 61 | resp.content = content 62 | return resp 63 | } 64 | 65 | export enum SendMessageType { 66 | CHANGE_REQUEST = 11, 67 | CHANGE_RESPONSE = 12, 68 | DROP_REQUEST = 13, 69 | DROP_RESPONSE = 14, 70 | CHANGE_BREAK_POINT_RULES = 21, 71 | } 72 | 73 | // type: 11/12/13/14 74 | // messageEdit 75 | // version 1 byte + type 1 byte + id 36 byte + header len 4 byte + header content bytes + body len 4 byte + [body content bytes] 76 | export const buildMessageEdit = (messageType: SendMessageType, flow: Flow) => { 77 | if (messageType === SendMessageType.DROP_REQUEST || messageType === SendMessageType.DROP_RESPONSE) { 78 | const view = new Uint8Array(38) 79 | view[0] = 1 80 | view[1] = messageType 81 | view.set(new TextEncoder().encode(flow.id), 2) 82 | return view 83 | } 84 | 85 | let header: Omit | Omit 86 | let body: ArrayBuffer | Uint8Array | undefined 87 | 88 | if (messageType === SendMessageType.CHANGE_REQUEST) { 89 | ({ body, ...header } = flow.request) 90 | } else if (messageType === SendMessageType.CHANGE_RESPONSE) { 91 | ({ body, ...header } = flow.response as IResponse) 92 | } else { 93 | throw new Error('invalid message type') 94 | } 95 | 96 | if (body instanceof ArrayBuffer) body = new Uint8Array(body) 97 | const bodyLen = (body && body.byteLength) ? body.byteLength : 0 98 | 99 | if ('Content-Encoding' in header.header) delete header.header['Content-Encoding'] 100 | if ('Transfer-Encoding' in header.header) delete header.header['Transfer-Encoding'] 101 | header.header['Content-Length'] = [String(bodyLen)] 102 | 103 | const headerBytes = new TextEncoder().encode(JSON.stringify(header)) 104 | const len = 2 + 36 + 4 + headerBytes.byteLength + 4 + bodyLen 105 | const data = new ArrayBuffer(len) 106 | const view = new Uint8Array(data) 107 | view[0] = 1 108 | view[1] = messageType 109 | view.set(new TextEncoder().encode(flow.id), 2) 110 | view.set(headerBytes, 2 + 36 + 4) 111 | if (bodyLen) view.set(body as Uint8Array, 2 + 36 + 4 + headerBytes.byteLength + 4) 112 | 113 | const view2 = new DataView(data) 114 | view2.setUint32(2 + 36, headerBytes.byteLength) 115 | view2.setUint32(2 + 36 + 4 + headerBytes.byteLength, bodyLen) 116 | 117 | return view 118 | } 119 | 120 | // type: 21 121 | // messageMeta 122 | // version 1 byte + type 1 byte + content left bytes 123 | export const buildMessageMeta = (messageType: SendMessageType, rules: any) => { 124 | if (messageType !== SendMessageType.CHANGE_BREAK_POINT_RULES) { 125 | throw new Error('invalid message type') 126 | } 127 | 128 | const rulesBytes = new TextEncoder().encode(JSON.stringify(rules)) 129 | const view = new Uint8Array(2 + rulesBytes.byteLength) 130 | view[0] = 1 131 | view[1] = messageType 132 | view.set(rulesBytes, 2) 133 | 134 | return view 135 | } 136 | -------------------------------------------------------------------------------- /web/conn.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | 7 | "github.com/gorilla/websocket" 8 | "github.com/kardianos/mitmproxy/proxy" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type breakPointRule struct { 13 | Method string `json:"method"` 14 | URL string `json:"url"` 15 | Action int `json:"action"` // 1 - change request 2 - change response 3 - both 16 | } 17 | 18 | type concurrentConn struct { 19 | conn *websocket.Conn 20 | mu sync.Mutex 21 | 22 | sendConnMessageMap map[string]bool 23 | 24 | waitChans map[string]chan interface{} 25 | waitChansMu sync.Mutex 26 | 27 | breakPointRules []*breakPointRule 28 | } 29 | 30 | func newConn(c *websocket.Conn) *concurrentConn { 31 | return &concurrentConn{ 32 | conn: c, 33 | sendConnMessageMap: make(map[string]bool), 34 | waitChans: make(map[string]chan interface{}), 35 | } 36 | } 37 | 38 | func (c *concurrentConn) trySendConnMessage(f *proxy.Flow) { 39 | c.mu.Lock() 40 | defer c.mu.Unlock() 41 | 42 | key := f.ConnContext.ID().String() 43 | if send := c.sendConnMessageMap[key]; send { 44 | return 45 | } 46 | c.sendConnMessageMap[key] = true 47 | msg := newMessageFlow(messageTypeConn, f) 48 | err := c.conn.WriteMessage(websocket.BinaryMessage, msg.bytes()) 49 | if err != nil { 50 | log.Error(err) 51 | return 52 | } 53 | } 54 | 55 | func (c *concurrentConn) whenConnClose(connCtx *proxy.ConnContext) { 56 | c.mu.Lock() 57 | defer c.mu.Unlock() 58 | 59 | delete(c.sendConnMessageMap, connCtx.ID().String()) 60 | 61 | msg := newMessageConnClose(connCtx) 62 | err := c.conn.WriteMessage(websocket.BinaryMessage, msg.bytes()) 63 | if err != nil { 64 | log.Error(err) 65 | return 66 | } 67 | } 68 | 69 | func (c *concurrentConn) writeMessage(msg *messageFlow, f *proxy.Flow) { 70 | if c.isIntercpt(f, msg) { 71 | msg.waitIntercept = 1 72 | } 73 | 74 | c.mu.Lock() 75 | err := c.conn.WriteMessage(websocket.BinaryMessage, msg.bytes()) 76 | c.mu.Unlock() 77 | if err != nil { 78 | log.Error(err) 79 | return 80 | } 81 | 82 | if msg.waitIntercept == 1 { 83 | c.waitIntercept(f, msg) 84 | } 85 | } 86 | 87 | func (c *concurrentConn) readloop() { 88 | for { 89 | mt, data, err := c.conn.ReadMessage() 90 | if err != nil { 91 | log.Error(err) 92 | break 93 | } 94 | 95 | if mt != websocket.BinaryMessage { 96 | log.Warn("not BinaryMessage, skip") 97 | continue 98 | } 99 | 100 | msg := parseMessage(data) 101 | if msg == nil { 102 | log.Warn("parseMessage error, skip") 103 | continue 104 | } 105 | 106 | if msgEdit, ok := msg.(*messageEdit); ok { 107 | ch := c.initWaitChan(msgEdit.id.String()) 108 | go func(m *messageEdit, ch chan<- interface{}) { 109 | ch <- m 110 | }(msgEdit, ch) 111 | } else if msgMeta, ok := msg.(*messageMeta); ok { 112 | c.breakPointRules = msgMeta.breakPointRules 113 | } else { 114 | log.Warn("invalid message, skip") 115 | } 116 | } 117 | } 118 | 119 | func (c *concurrentConn) initWaitChan(key string) chan interface{} { 120 | c.waitChansMu.Lock() 121 | defer c.waitChansMu.Unlock() 122 | 123 | if ch, ok := c.waitChans[key]; ok { 124 | return ch 125 | } 126 | ch := make(chan interface{}) 127 | c.waitChans[key] = ch 128 | return ch 129 | } 130 | 131 | // Determine if it should intercept. 132 | func (c *concurrentConn) isIntercpt(f *proxy.Flow, after *messageFlow) bool { 133 | if after.mType != messageTypeRequestBody && after.mType != messageTypeResponseBody { 134 | return false 135 | } 136 | 137 | if len(c.breakPointRules) == 0 { 138 | return false 139 | } 140 | 141 | var action int 142 | if after.mType == messageTypeRequestBody { 143 | action = 1 144 | } else { 145 | action = 2 146 | } 147 | 148 | for _, rule := range c.breakPointRules { 149 | if rule.URL == "" { 150 | continue 151 | } 152 | if action&rule.Action == 0 { 153 | continue 154 | } 155 | if rule.Method != "" && rule.Method != f.Request.Method { 156 | continue 157 | } 158 | if strings.Contains(f.Request.URL.String(), rule.URL) { 159 | return true 160 | } 161 | } 162 | 163 | return false 164 | } 165 | 166 | // Intercept. 167 | func (c *concurrentConn) waitIntercept(f *proxy.Flow, after *messageFlow) { 168 | ch := c.initWaitChan(f.Id.String()) 169 | msg := (<-ch).(*messageEdit) 170 | 171 | // Drop. 172 | if msg.mType == messageTypeDropRequest || msg.mType == messageTypeDropResponse { 173 | f.Response = &proxy.Response{ 174 | StatusCode: 502, 175 | } 176 | return 177 | } 178 | 179 | // change 180 | if msg.mType == messageTypeChangeRequest { 181 | f.Request.Method = msg.request.Method 182 | f.Request.URL = msg.request.URL 183 | f.Request.Header = msg.request.Header 184 | f.Request.Body = msg.request.Body 185 | } else if msg.mType == messageTypeChangeResponse { 186 | f.Response.StatusCode = msg.response.StatusCode 187 | f.Response.Header = msg.response.Header 188 | f.Response.Body = msg.response.Body 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /proxy/interceptor.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "crypto/tls" 7 | "net" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/kardianos/mitmproxy/cert" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // Similar to standard library server, run through current process memory socket data, without tcp or unix socket. 16 | 17 | type pipeAddr struct { 18 | remoteAddr string 19 | } 20 | 21 | func (pipeAddr) Network() string { return "pipe" } 22 | func (a *pipeAddr) String() string { return a.remoteAddr } 23 | 24 | // add Peek method for conn 25 | type pipeConn struct { 26 | net.Conn 27 | r *bufio.Reader 28 | host string // server host:port 29 | remoteAddr string // client ip:port 30 | connContext *ConnContext 31 | } 32 | 33 | func newPipeConn(c net.Conn, req *http.Request) *pipeConn { 34 | connContext := req.Context().Value(connContextKey).(*ConnContext) 35 | pipeConn := &pipeConn{ 36 | Conn: c, 37 | r: bufio.NewReader(c), 38 | host: req.Host, 39 | remoteAddr: req.RemoteAddr, 40 | connContext: connContext, 41 | } 42 | connContext.pipeConn = pipeConn 43 | return pipeConn 44 | } 45 | 46 | func (c *pipeConn) Peek(n int) ([]byte, error) { 47 | return c.r.Peek(n) 48 | } 49 | 50 | func (c *pipeConn) Read(data []byte) (int, error) { 51 | return c.r.Read(data) 52 | } 53 | 54 | func (c *pipeConn) RemoteAddr() net.Addr { 55 | return &pipeAddr{remoteAddr: c.remoteAddr} 56 | } 57 | 58 | // Setup client and server communication. 59 | func newPipes(req *http.Request) (net.Conn, *pipeConn) { 60 | client, srv := net.Pipe() 61 | server := newPipeConn(srv, req) 62 | return client, server 63 | } 64 | 65 | // mock net.Listener 66 | type middleListener struct { 67 | connChan chan net.Conn 68 | doneChan chan struct{} 69 | } 70 | 71 | func (l *middleListener) Accept() (net.Conn, error) { 72 | select { 73 | case c := <-l.connChan: 74 | return c, nil 75 | case <-l.doneChan: 76 | return nil, http.ErrServerClosed 77 | } 78 | } 79 | func (l *middleListener) Close() error { return nil } 80 | func (l *middleListener) Addr() net.Addr { return nil } 81 | 82 | // middle: man-in-the-middle server 83 | type middle struct { 84 | proxy *Proxy 85 | ca cert.Getter 86 | listener *middleListener 87 | server *http.Server 88 | } 89 | 90 | func newMiddle(proxy *Proxy) (*middle, error) { 91 | m := &middle{ 92 | proxy: proxy, 93 | ca: proxy.Opts.CA, 94 | listener: &middleListener{ 95 | connChan: make(chan net.Conn), 96 | doneChan: make(chan struct{}), 97 | }, 98 | } 99 | 100 | server := &http.Server{ 101 | Handler: m, 102 | ConnContext: func(ctx context.Context, c net.Conn) context.Context { 103 | return context.WithValue(ctx, connContextKey, c.(*tls.Conn).NetConn().(*pipeConn).connContext) 104 | }, 105 | TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), // Disable http2. 106 | TLSConfig: &tls.Config{ 107 | SessionTicketsDisabled: true, // Set to true, ensure GetCertificate is always called. 108 | GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { 109 | connCtx := clientHello.Context().Value(connContextKey).(*ConnContext) 110 | if err := connCtx.tlsHandshake(clientHello); err != nil { 111 | return nil, err 112 | } 113 | 114 | for _, addon := range connCtx.proxy.Addons { 115 | addon.TlsEstablishedServer(connCtx) 116 | } 117 | 118 | return m.ca.GetCert(clientHello.ServerName) 119 | }, 120 | }, 121 | } 122 | m.server = server 123 | return m, nil 124 | } 125 | 126 | func (m *middle) start() error { 127 | return m.server.ServeTLS(m.listener, "", "") 128 | } 129 | 130 | func (m *middle) close() error { 131 | err := m.server.Close() 132 | close(m.listener.doneChan) 133 | return err 134 | } 135 | 136 | func (m *middle) dial(req *http.Request) (net.Conn, error) { 137 | pipeClientConn, pipeServerConn := newPipes(req) 138 | err := pipeServerConn.connContext.initServerTcpConn(req) 139 | if err != nil { 140 | pipeClientConn.Close() 141 | pipeServerConn.Close() 142 | return nil, err 143 | } 144 | go m.intercept(pipeServerConn) 145 | return pipeClientConn, nil 146 | } 147 | 148 | func (m *middle) ServeHTTP(res http.ResponseWriter, req *http.Request) { 149 | if strings.EqualFold(req.Header.Get("Connection"), "Upgrade") && strings.EqualFold(req.Header.Get("Upgrade"), "websocket") { 150 | // wss 151 | defaultWebSocket.wss(res, req) 152 | return 153 | } 154 | 155 | if req.URL.Scheme == "" { 156 | req.URL.Scheme = "https" 157 | } 158 | if req.URL.Host == "" { 159 | req.URL.Host = req.Host 160 | } 161 | m.proxy.ServeHTTP(res, req) 162 | } 163 | 164 | // Parse connect flow. 165 | // In case of tls flow, listener.Accept => Middle.ServeHTTP 166 | // Otherwise assume ws flow. 167 | func (m *middle) intercept(pipeServerConn *pipeConn) { 168 | buf, err := pipeServerConn.Peek(3) 169 | if err != nil { 170 | log.Errorf("Peek error: %v\n", err) 171 | pipeServerConn.Close() 172 | return 173 | } 174 | 175 | // https://github.com/mitmproxy/mitmproxy/blob/main/mitmproxy/net/tls.py is_tls_record_magic 176 | if buf[0] == 0x16 && buf[1] == 0x03 && buf[2] <= 0x03 { 177 | // tls 178 | pipeServerConn.connContext.ClientConn.TLS = true 179 | pipeServerConn.connContext.initHttpsServerConn() 180 | m.listener.connChan <- pipeServerConn 181 | } else { 182 | // ws 183 | defaultWebSocket.ws(pipeServerConn, pipeServerConn.host) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /addon/mapper.go: -------------------------------------------------------------------------------- 1 | package addon 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | "path/filepath" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/kardianos/mitmproxy/proxy" 14 | log "github.com/sirupsen/logrus" 15 | ) 16 | 17 | var httpsRegexp = regexp.MustCompile(`^https://`) 18 | 19 | type Mapper struct { 20 | proxy.BaseAddon 21 | reqResMap map[string]*proxy.Response 22 | } 23 | 24 | func NewMapper(dirname string) *Mapper { 25 | infos, err := ioutil.ReadDir(dirname) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | filenames := make([]string, 0) 31 | 32 | for _, info := range infos { 33 | if info.IsDir() { 34 | continue 35 | } 36 | if !strings.HasSuffix(info.Name(), ".map.txt") { 37 | continue 38 | } 39 | 40 | filenames = append(filenames, filepath.Join(dirname, info.Name())) 41 | } 42 | 43 | if len(filenames) == 0 { 44 | return &Mapper{ 45 | reqResMap: make(map[string]*proxy.Response), 46 | } 47 | } 48 | 49 | ch := make(chan interface{}, len(filenames)) 50 | for _, filename := range filenames { 51 | go func(filename string, ch chan<- interface{}) { 52 | f, err := parseFlowFromFile(filename) 53 | if err != nil { 54 | log.Error(err) 55 | ch <- err 56 | return 57 | } 58 | ch <- f 59 | }(filename, ch) 60 | } 61 | 62 | reqResMap := make(map[string]*proxy.Response) 63 | 64 | for i := 0; i < len(filenames); i++ { 65 | flowOrErr := <-ch 66 | if f, ok := flowOrErr.(*proxy.Flow); ok { 67 | key := buildReqKey(f.Request) 68 | log.Infof("add request mapper: %v", key) 69 | reqResMap[key] = f.Response 70 | } 71 | } 72 | 73 | return &Mapper{ 74 | reqResMap: reqResMap, 75 | } 76 | } 77 | 78 | func parseFlowFromFile(filename string) (*proxy.Flow, error) { 79 | p, err := newMapperParserFromFile(filename) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return p.parse() 84 | } 85 | 86 | func (c *Mapper) Request(f *proxy.Flow) { 87 | key := buildReqKey(f.Request) 88 | if resp, ok := c.reqResMap[key]; ok { 89 | f.Response = resp 90 | } 91 | } 92 | 93 | func buildReqKey(req *proxy.Request) string { 94 | url := req.URL.String() 95 | url = httpsRegexp.ReplaceAllString(url, "http://") 96 | key := req.Method + " " + url 97 | return key 98 | } 99 | 100 | type mapperParser struct { 101 | lines []string 102 | url string 103 | request *proxy.Request 104 | response *proxy.Response 105 | } 106 | 107 | func newMapperParserFromFile(filename string) (*mapperParser, error) { 108 | bytes, err := ioutil.ReadFile(filename) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | return newMapperParserFromString(string(bytes)) 114 | } 115 | 116 | func newMapperParserFromString(content string) (*mapperParser, error) { 117 | content = strings.TrimSpace(content) 118 | lines := strings.Split(content, "\n") 119 | if len(lines) == 0 { 120 | return nil, errors.New("no lines") 121 | } 122 | 123 | return &mapperParser{ 124 | lines: lines, 125 | }, nil 126 | } 127 | 128 | func (p *mapperParser) parse() (*proxy.Flow, error) { 129 | if err := p.parseRequest(); err != nil { 130 | return nil, err 131 | } 132 | 133 | if err := p.parseResponse(); err != nil { 134 | return nil, err 135 | } 136 | 137 | return &proxy.Flow{ 138 | Request: p.request, 139 | Response: p.response, 140 | }, nil 141 | } 142 | 143 | func (p *mapperParser) parseRequest() error { 144 | if err := p.parseReqHead(); err != nil { 145 | return err 146 | } 147 | 148 | if header, err := p.parseHeader(); err != nil { 149 | return err 150 | } else { 151 | p.request.Header = header 152 | } 153 | 154 | // parse url 155 | if !strings.HasPrefix(p.url, "http") { 156 | host := p.request.Header.Get("host") 157 | if host == "" { 158 | return errors.New("no request host") 159 | } 160 | p.url = "http://" + host + p.url 161 | } 162 | url, err := url.Parse(p.url) 163 | if err != nil { 164 | return err 165 | } 166 | p.request.URL = url 167 | 168 | p.parseReqBody() 169 | 170 | return nil 171 | } 172 | 173 | func (p *mapperParser) parseReqHead() error { 174 | line, _ := p.getLine() 175 | re := regexp.MustCompile(`^(GET|POST|PUT|DELETE)\s+?(.+)`) 176 | matches := re.FindStringSubmatch(line) 177 | if len(matches) == 0 { 178 | return errors.New("request head parse error") 179 | } 180 | 181 | p.request = &proxy.Request{ 182 | Method: matches[1], 183 | } 184 | p.url = matches[2] 185 | 186 | return nil 187 | } 188 | 189 | func (p *mapperParser) parseHeader() (http.Header, error) { 190 | header := make(http.Header) 191 | re := regexp.MustCompile(`^([\w-]+?):\s*(.+)$`) 192 | 193 | for { 194 | line, ok := p.getLine() 195 | if !ok { 196 | break 197 | } 198 | line = strings.TrimSpace(line) 199 | if line == "" { 200 | break 201 | } 202 | matches := re.FindStringSubmatch(line) 203 | if len(matches) == 0 { 204 | return nil, errors.New("request header parse error") 205 | } 206 | 207 | key := matches[1] 208 | val := matches[2] 209 | header.Add(key, val) 210 | } 211 | 212 | return header, nil 213 | } 214 | 215 | func (p *mapperParser) parseReqBody() { 216 | bodyLines := make([]string, 0) 217 | 218 | for { 219 | line, ok := p.getLine() 220 | if !ok { 221 | break 222 | } 223 | 224 | if len(bodyLines) == 0 { 225 | line = strings.TrimSpace(line) 226 | if line == "" { 227 | continue 228 | } 229 | } 230 | 231 | if strings.HasPrefix(line, "HTTP/1.1 ") { 232 | p.lines = append([]string{line}, p.lines...) 233 | break 234 | } 235 | bodyLines = append(bodyLines, line) 236 | } 237 | 238 | body := strings.Join(bodyLines, "\n") 239 | body = strings.TrimSpace(body) 240 | p.request.Body = []byte(body) 241 | } 242 | 243 | func (p *mapperParser) parseResponse() error { 244 | if err := p.parseResHead(); err != nil { 245 | return err 246 | } 247 | 248 | if header, err := p.parseHeader(); err != nil { 249 | return err 250 | } else { 251 | p.response.Header = header 252 | } 253 | 254 | // all left content 255 | body := strings.Join(p.lines, "\n") 256 | body = strings.TrimSpace(body) 257 | p.response.Body = []byte(body) 258 | p.response.Header.Set("Content-Length", strconv.Itoa(len(p.response.Body))) 259 | 260 | return nil 261 | } 262 | 263 | func (p *mapperParser) parseResHead() error { 264 | line, ok := p.getLine() 265 | if !ok { 266 | return errors.New("response no head line") 267 | } 268 | 269 | re := regexp.MustCompile(`^HTTP/1\.1\s+?(\d+)`) 270 | matches := re.FindStringSubmatch(line) 271 | if len(matches) == 0 { 272 | return errors.New("response head parse error") 273 | } 274 | 275 | code, _ := strconv.Atoi(matches[1]) 276 | p.response = &proxy.Response{ 277 | StatusCode: code, 278 | } 279 | 280 | return nil 281 | } 282 | 283 | func (p *mapperParser) getLine() (string, bool) { 284 | if len(p.lines) == 0 { 285 | return "", false 286 | } 287 | 288 | line := p.lines[0] 289 | p.lines = p.lines[1:] 290 | return line, true 291 | } 292 | -------------------------------------------------------------------------------- /web/client/src/components/EditFlow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button from 'react-bootstrap/Button' 3 | import Modal from 'react-bootstrap/Modal' 4 | import Form from 'react-bootstrap/Form' 5 | import Alert from 'react-bootstrap/Alert' 6 | import { SendMessageType, buildMessageEdit } from '../lib/message' 7 | import { isTextBody } from '../lib/utils' 8 | import type { Flow, Header, IRequest, IResponse } from '../lib/flow' 9 | 10 | const stringifyRequest = (request: IRequest) => { 11 | const firstLine = `${request.method} ${request.url}` 12 | const headerLines = Object.keys(request.header).map(key => { 13 | const valstr = request.header[key].join(' \t ') // for parse convenience 14 | return `${key}: ${valstr}` 15 | }).join('\n') 16 | 17 | let bodyLines = '' 18 | if (request.body && isTextBody(request)) bodyLines = new TextDecoder().decode(request.body) 19 | 20 | return `${firstLine}\n\n${headerLines}\n\n${bodyLines}` 21 | } 22 | 23 | const parseRequest = (content: string): IRequest | undefined => { 24 | const firstIndex = content.indexOf('\n\n') 25 | if (firstIndex <= 0) return 26 | 27 | const firstLine = content.slice(0, firstIndex) 28 | const [method, url] = firstLine.split(' ') 29 | if (!method || !url) return 30 | 31 | const secondIndex = content.indexOf('\n\n', firstIndex + 2) 32 | if (secondIndex <= 0) return 33 | const headerLines = content.slice(firstIndex + 2, secondIndex) 34 | const header: Header = {} 35 | for (const line of headerLines.split('\n')) { 36 | const [key, vals] = line.split(': ') 37 | if (!key || !vals) return 38 | header[key] = vals.split(' \t ') 39 | } 40 | 41 | const bodyLines = content.slice(secondIndex + 2) 42 | let body: ArrayBuffer | undefined 43 | if (bodyLines) body = new TextEncoder().encode(bodyLines) 44 | 45 | return { 46 | method, 47 | url, 48 | proto: '', 49 | header, 50 | body, 51 | } 52 | } 53 | 54 | const stringifyResponse = (response: IResponse) => { 55 | const firstLine = `${response.statusCode}` 56 | const headerLines = Object.keys(response.header).map(key => { 57 | const valstr = response.header[key].join(' \t ') // for parse convenience 58 | return `${key}: ${valstr}` 59 | }).join('\n') 60 | 61 | let bodyLines = '' 62 | if (response.body && isTextBody(response)) bodyLines = new TextDecoder().decode(response.body) 63 | 64 | return `${firstLine}\n\n${headerLines}\n\n${bodyLines}` 65 | } 66 | 67 | const parseResponse = (content: string): IResponse | undefined => { 68 | const firstIndex = content.indexOf('\n\n') 69 | if (firstIndex <= 0) return 70 | 71 | const firstLine = content.slice(0, firstIndex) 72 | const statusCode = parseInt(firstLine) 73 | if (isNaN(statusCode)) return 74 | 75 | const secondIndex = content.indexOf('\n\n', firstIndex + 2) 76 | if (secondIndex <= 0) return 77 | const headerLines = content.slice(firstIndex + 2, secondIndex) 78 | const header: Header = {} 79 | for (const line of headerLines.split('\n')) { 80 | const [key, vals] = line.split(': ') 81 | if (!key || !vals) return 82 | header[key] = vals.split(' \t ') 83 | } 84 | 85 | const bodyLines = content.slice(secondIndex + 2) 86 | let body: ArrayBuffer | undefined 87 | if (bodyLines) body = new TextEncoder().encode(bodyLines) 88 | 89 | return { 90 | statusCode, 91 | header, 92 | body, 93 | } 94 | } 95 | 96 | 97 | interface IProps { 98 | flow: Flow 99 | onChangeRequest: (request: IRequest) => void 100 | onChangeResponse: (response: IResponse) => void 101 | onMessage: (msg: ArrayBufferLike) => void 102 | } 103 | 104 | interface IState { 105 | show: boolean 106 | alertMsg: string 107 | content: string 108 | } 109 | 110 | class EditFlow extends React.Component { 111 | constructor(props: IProps) { 112 | super(props) 113 | 114 | this.state = { 115 | show: false, 116 | alertMsg: '', 117 | content: '', 118 | } 119 | 120 | this.handleClose = this.handleClose.bind(this) 121 | this.handleShow = this.handleShow.bind(this) 122 | this.handleSave = this.handleSave.bind(this) 123 | } 124 | 125 | showAlert(msg: string) { 126 | this.setState({ alertMsg: msg }) 127 | } 128 | 129 | handleClose() { 130 | this.setState({ show: false }) 131 | } 132 | 133 | handleShow() { 134 | const { flow } = this.props 135 | const when = flow.response ? 'response' : 'request' 136 | 137 | let content = '' 138 | if (when === 'request') { 139 | content = stringifyRequest(flow.request) 140 | } else { 141 | content = stringifyResponse(flow.response as IResponse) 142 | } 143 | 144 | this.setState({ show: true, alertMsg: '', content }) 145 | } 146 | 147 | handleSave() { 148 | const { flow } = this.props 149 | const when = flow.response ? 'response' : 'request' 150 | 151 | const { content } = this.state 152 | 153 | if (when === 'request') { 154 | const request = parseRequest(content) 155 | if (!request) { 156 | this.showAlert('parse error') 157 | return 158 | } 159 | 160 | this.props.onChangeRequest(request) 161 | this.handleClose() 162 | } else { 163 | const response = parseResponse(content) 164 | if (!response) { 165 | this.showAlert('parse error') 166 | return 167 | } 168 | 169 | this.props.onChangeResponse(response) 170 | this.handleClose() 171 | } 172 | } 173 | 174 | render() { 175 | const { flow } = this.props 176 | if (!flow.waitIntercept) return null 177 | 178 | const { alertMsg } = this.state 179 | 180 | const when = flow.response ? 'response' : 'request' 181 | 182 | return ( 183 |
184 | 185 | 186 | 187 | 192 | 193 | 198 | 199 | 200 | 201 | 202 | Edit {when === 'request' ? 'Request' : 'Response'} 203 | 204 | 205 | 206 | 207 | { this.setState({ content: e.target.value }) }} /> 208 | 209 | { 210 | !alertMsg ? null : {alertMsg} 211 | } 212 | 213 | 214 | 215 | 218 | 221 | 222 | 223 | 224 |
225 | ) 226 | } 227 | } 228 | 229 | export default EditFlow 230 | -------------------------------------------------------------------------------- /web/message.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/json" 7 | "errors" 8 | 9 | "github.com/kardianos/mitmproxy/proxy" 10 | uuid "github.com/satori/go.uuid" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // message: 15 | 16 | // type: 0/1/2/3/4/5 17 | // messageFlow 18 | // version 1 byte + type 1 byte + id 36 byte + waitIntercept 1 byte + content left bytes 19 | 20 | // type: 11/12/13/14 21 | // messageEdit 22 | // version 1 byte + type 1 byte + id 36 byte + header len 4 byte + header content bytes + body len 4 byte + [body content bytes] 23 | 24 | // type: 21 25 | // messageMeta 26 | // version 1 byte + type 1 byte + content left bytes 27 | 28 | const messageVersion = 2 29 | 30 | type messageType byte 31 | 32 | const ( 33 | messageTypeConn messageType = 0 34 | messageTypeConnClose messageType = 5 35 | messageTypeRequest messageType = 1 36 | messageTypeRequestBody messageType = 2 37 | messageTypeResponse messageType = 3 38 | messageTypeResponseBody messageType = 4 39 | 40 | messageTypeChangeRequest messageType = 11 41 | messageTypeChangeResponse messageType = 12 42 | messageTypeDropRequest messageType = 13 43 | messageTypeDropResponse messageType = 14 44 | 45 | messageTypeChangeBreakPointRules messageType = 21 46 | ) 47 | 48 | var allMessageTypes = []messageType{ 49 | messageTypeConn, 50 | messageTypeConnClose, 51 | messageTypeRequest, 52 | messageTypeRequestBody, 53 | messageTypeResponse, 54 | messageTypeResponseBody, 55 | messageTypeChangeRequest, 56 | messageTypeChangeResponse, 57 | messageTypeDropRequest, 58 | messageTypeDropResponse, 59 | messageTypeChangeBreakPointRules, 60 | } 61 | 62 | func validMessageType(t byte) bool { 63 | for _, v := range allMessageTypes { 64 | if t == byte(v) { 65 | return true 66 | } 67 | } 68 | return false 69 | } 70 | 71 | type message interface { 72 | bytes() []byte 73 | } 74 | 75 | type messageFlow struct { 76 | mType messageType 77 | id uuid.UUID 78 | waitIntercept byte 79 | content []byte 80 | } 81 | 82 | func newMessageFlow(mType messageType, f *proxy.Flow) *messageFlow { 83 | var content []byte 84 | var err error = nil 85 | 86 | if mType == messageTypeConn { 87 | content, err = json.Marshal(f.ConnContext) 88 | } else if mType == messageTypeRequest { 89 | m := make(map[string]interface{}) 90 | m["request"] = f.Request 91 | m["connId"] = f.ConnContext.ID().String() 92 | content, err = json.Marshal(m) 93 | } else if mType == messageTypeRequestBody { 94 | content = f.Request.Body 95 | } else if mType == messageTypeResponse { 96 | content, err = json.Marshal(f.Response) 97 | } else if mType == messageTypeResponseBody { 98 | content, err = f.Response.DecodedBody() 99 | } else { 100 | panic(errors.New("invalid message type")) 101 | } 102 | 103 | if err != nil { 104 | panic(err) 105 | } 106 | 107 | id := f.Id 108 | if mType == messageTypeConn { 109 | id = f.ConnContext.ID() 110 | } 111 | 112 | return &messageFlow{ 113 | mType: mType, 114 | id: id, 115 | content: content, 116 | } 117 | } 118 | 119 | func newMessageConnClose(connCtx *proxy.ConnContext) *messageFlow { 120 | return &messageFlow{ 121 | mType: messageTypeConnClose, 122 | id: connCtx.ID(), 123 | } 124 | } 125 | 126 | func (m *messageFlow) bytes() []byte { 127 | buf := bytes.NewBuffer(make([]byte, 0)) 128 | buf.WriteByte(byte(messageVersion)) 129 | buf.WriteByte(byte(m.mType)) 130 | buf.WriteString(m.id.String()) // len: 36 131 | buf.WriteByte(m.waitIntercept) 132 | buf.Write(m.content) 133 | return buf.Bytes() 134 | } 135 | 136 | type messageEdit struct { 137 | mType messageType 138 | id uuid.UUID 139 | request *proxy.Request 140 | response *proxy.Response 141 | } 142 | 143 | func parseMessageEdit(data []byte) *messageEdit { 144 | // 2 + 36 145 | if len(data) < 38 { 146 | return nil 147 | } 148 | 149 | mType := (messageType)(data[1]) 150 | 151 | id, err := uuid.FromString(string(data[2:38])) 152 | if err != nil { 153 | return nil 154 | } 155 | 156 | msg := &messageEdit{ 157 | mType: mType, 158 | id: id, 159 | } 160 | 161 | if mType == messageTypeDropRequest || mType == messageTypeDropResponse { 162 | return msg 163 | } 164 | 165 | // 2 + 36 + 4 + 4 166 | if len(data) < 46 { 167 | return nil 168 | } 169 | 170 | hl := (int)(binary.BigEndian.Uint32(data[38:42])) 171 | if 42+hl+4 > len(data) { 172 | return nil 173 | } 174 | headerContent := data[42 : 42+hl] 175 | 176 | bl := (int)(binary.BigEndian.Uint32(data[42+hl : 42+hl+4])) 177 | if 42+hl+4+bl != len(data) { 178 | return nil 179 | } 180 | bodyContent := data[42+hl+4:] 181 | 182 | if mType == messageTypeChangeRequest { 183 | req := new(proxy.Request) 184 | err := json.Unmarshal(headerContent, req) 185 | if err != nil { 186 | return nil 187 | } 188 | req.Body = bodyContent 189 | msg.request = req 190 | } else if mType == messageTypeChangeResponse { 191 | res := new(proxy.Response) 192 | err := json.Unmarshal(headerContent, res) 193 | if err != nil { 194 | return nil 195 | } 196 | res.Body = bodyContent 197 | msg.response = res 198 | } else { 199 | return nil 200 | } 201 | 202 | return msg 203 | } 204 | 205 | func (m *messageEdit) bytes() []byte { 206 | buf := bytes.NewBuffer(make([]byte, 0)) 207 | buf.WriteByte(byte(messageVersion)) 208 | buf.WriteByte(byte(m.mType)) 209 | buf.WriteString(m.id.String()) // len: 36 210 | 211 | if m.mType == messageTypeChangeRequest { 212 | headerContent, err := json.Marshal(m.request) 213 | if err != nil { 214 | panic(err) 215 | } 216 | hl := make([]byte, 4) 217 | binary.BigEndian.PutUint32(hl, (uint32)(len(headerContent))) 218 | buf.Write(hl) 219 | 220 | bodyContent := m.request.Body 221 | bl := make([]byte, 4) 222 | binary.BigEndian.PutUint32(bl, (uint32)(len(bodyContent))) 223 | buf.Write(bl) 224 | buf.Write(bodyContent) 225 | } else if m.mType == messageTypeChangeResponse { 226 | headerContent, err := json.Marshal(m.response) 227 | if err != nil { 228 | panic(err) 229 | } 230 | hl := make([]byte, 4) 231 | binary.BigEndian.PutUint32(hl, (uint32)(len(headerContent))) 232 | buf.Write(hl) 233 | 234 | bodyContent := m.response.Body 235 | bl := make([]byte, 4) 236 | binary.BigEndian.PutUint32(bl, (uint32)(len(bodyContent))) 237 | buf.Write(bl) 238 | buf.Write(bodyContent) 239 | } 240 | 241 | return buf.Bytes() 242 | } 243 | 244 | type messageMeta struct { 245 | mType messageType 246 | breakPointRules []*breakPointRule 247 | } 248 | 249 | func parseMessageMeta(data []byte) *messageMeta { 250 | content := data[2:] 251 | rules := make([]*breakPointRule, 0) 252 | err := json.Unmarshal(content, &rules) 253 | if err != nil { 254 | return nil 255 | } 256 | 257 | return &messageMeta{ 258 | mType: messageType(data[1]), 259 | breakPointRules: rules, 260 | } 261 | } 262 | 263 | func (m *messageMeta) bytes() []byte { 264 | buf := bytes.NewBuffer(make([]byte, 0)) 265 | buf.WriteByte(byte(messageVersion)) 266 | buf.WriteByte(byte(m.mType)) 267 | 268 | content, err := json.Marshal(m.breakPointRules) 269 | if err != nil { 270 | panic(err) 271 | } 272 | buf.Write(content) 273 | 274 | return buf.Bytes() 275 | } 276 | 277 | func parseMessage(data []byte) message { 278 | if len(data) < 2 { 279 | return nil 280 | } 281 | 282 | if data[0] != messageVersion { 283 | return nil 284 | } 285 | 286 | if !validMessageType(data[1]) { 287 | return nil 288 | } 289 | 290 | mType := (messageType)(data[1]) 291 | 292 | if mType == messageTypeChangeRequest || mType == messageTypeChangeResponse || mType == messageTypeDropRequest || mType == messageTypeDropResponse { 293 | return parseMessageEdit(data) 294 | } else if mType == messageTypeChangeBreakPointRules { 295 | return parseMessageMeta(data) 296 | } else { 297 | log.Warnf("invalid message type %v", mType) 298 | return nil 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /web/client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Table from 'react-bootstrap/Table' 3 | import Form from 'react-bootstrap/Form' 4 | import Button from 'react-bootstrap/Button' 5 | import './App.css' 6 | 7 | import BreakPoint from './components/BreakPoint' 8 | import FlowPreview from './components/FlowPreview' 9 | import ViewFlow from './components/ViewFlow' 10 | 11 | import { Flow, FlowManager } from './lib/flow' 12 | import { parseMessage, SendMessageType, buildMessageMeta, MessageType } from './lib/message' 13 | import { isInViewPort } from './lib/utils' 14 | import { ConnectionManager, IConnection } from './lib/connection' 15 | 16 | interface IState { 17 | flows: Flow[] 18 | flow: Flow | null 19 | wsStatus: 'open' | 'close' | 'connecting' 20 | } 21 | 22 | const wsReconnIntervals = [1, 1, 2, 2, 4, 4, 8, 8, 16, 16, 32, 32] 23 | 24 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 25 | interface IProps {} 26 | 27 | class App extends React.Component { 28 | private connMgr: ConnectionManager 29 | private flowMgr: FlowManager 30 | private ws: WebSocket | null 31 | private wsUnmountClose: boolean 32 | private tableBottomRef: React.RefObject 33 | 34 | private wsReconnCount = -1 35 | 36 | constructor(props: IProps) { 37 | super(props) 38 | 39 | this.connMgr = new ConnectionManager() 40 | this.flowMgr = new FlowManager() 41 | 42 | this.state = { 43 | flows: this.flowMgr.showList(), 44 | flow: null, 45 | wsStatus: 'close', 46 | } 47 | 48 | this.ws = null 49 | this.wsUnmountClose = false 50 | this.tableBottomRef = React.createRef() 51 | } 52 | 53 | componentDidMount() { 54 | this.initWs() 55 | } 56 | 57 | componentWillUnmount() { 58 | if (this.ws) { 59 | this.wsUnmountClose = true 60 | this.ws.close() 61 | this.ws = null 62 | } 63 | } 64 | 65 | initWs() { 66 | if (this.ws) return 67 | 68 | this.setState({ wsStatus: 'connecting' }) 69 | 70 | let host 71 | if (process.env.NODE_ENV === 'development') { 72 | host = 'localhost:9081' 73 | } else { 74 | host = new URL(document.URL).host 75 | } 76 | this.ws = new WebSocket(`ws://${host}/echo`) 77 | this.ws.binaryType = 'arraybuffer' 78 | 79 | this.ws.onopen = () => { 80 | this.wsReconnCount = -1 81 | this.setState({ wsStatus: 'open' }) 82 | } 83 | 84 | this.ws.onerror = evt => { 85 | console.error('ERROR:', evt) 86 | this.ws?.close() 87 | } 88 | 89 | this.ws.onclose = () => { 90 | this.setState({ wsStatus: 'close' }) 91 | if (this.wsUnmountClose) return 92 | 93 | this.wsReconnCount++ 94 | this.ws = null 95 | const waitSeconds = wsReconnIntervals[this.wsReconnCount] || wsReconnIntervals[wsReconnIntervals.length - 1] 96 | console.info(`will reconnect after ${waitSeconds} seconds`) 97 | setTimeout(() => { 98 | this.initWs() 99 | }, waitSeconds * 1000) 100 | } 101 | 102 | this.ws.onmessage = evt => { 103 | const msg = parseMessage(evt.data) 104 | if (!msg) { 105 | console.error('parse error:', evt.data) 106 | return 107 | } 108 | // console.log('msg:', msg) 109 | 110 | if (msg.type === MessageType.CONN) { 111 | this.connMgr.add(msg.id, msg.content as IConnection) 112 | this.setState({ flows: this.state.flows }) 113 | } 114 | else if (msg.type === MessageType.CONN_CLOSE) { 115 | this.connMgr.delete(msg.id) 116 | } 117 | else if (msg.type === MessageType.REQUEST) { 118 | const flow = new Flow(msg, this.connMgr) 119 | flow.getConn() 120 | this.flowMgr.add(flow) 121 | 122 | let shouldScroll = false 123 | if (this.tableBottomRef?.current && isInViewPort(this.tableBottomRef.current)) { 124 | shouldScroll = true 125 | } 126 | this.setState({ flows: this.flowMgr.showList() }, () => { 127 | if (shouldScroll) { 128 | this.tableBottomRef?.current?.scrollIntoView({ behavior: 'auto' }) 129 | } 130 | }) 131 | } 132 | else if (msg.type === MessageType.REQUEST_BODY) { 133 | const flow = this.flowMgr.get(msg.id) 134 | if (!flow) return 135 | flow.addRequestBody(msg) 136 | this.setState({ flows: this.state.flows }) 137 | } 138 | else if (msg.type === MessageType.RESPONSE) { 139 | const flow = this.flowMgr.get(msg.id) 140 | if (!flow) return 141 | flow.getConn() 142 | flow.addResponse(msg) 143 | this.setState({ flows: this.state.flows }) 144 | } 145 | else if (msg.type === MessageType.RESPONSE_BODY) { 146 | const flow = this.flowMgr.get(msg.id) 147 | if (!flow || !flow.response) return 148 | flow.addResponseBody(msg) 149 | this.setState({ flows: this.state.flows }) 150 | } 151 | } 152 | } 153 | 154 | render() { 155 | const { flows } = this.state 156 | return ( 157 |
158 |
159 |
163 |
164 | { 167 | const value = e.target.value 168 | this.flowMgr.changeFilterLazy(value, () => { 169 | this.setState({ flows: this.flowMgr.showList() }) 170 | }) 171 | }} 172 | > 173 | 174 |
175 | 176 | { 177 | const msg = buildMessageMeta(SendMessageType.CHANGE_BREAK_POINT_RULES, rules) 178 | if (this.ws) this.ws.send(msg) 179 | }} /> 180 | 181 | status: {this.state.wsStatus} 182 |
183 | 184 |
185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | { 200 | flows.map(f => { 201 | const fp = f.preview() 202 | 203 | return ( 204 | { 209 | this.setState({ flow: f }) 210 | }} 211 | /> 212 | ) 213 | }) 214 | } 215 | 216 |
NoMethodHostPathTypeStatusSizeTime
217 |
218 |
219 | 220 | { this.setState({ flow: null }) }} 223 | onReRenderFlows={() => { this.setState({ flows: this.state.flows }) }} 224 | onMessage={msg => { if (this.ws) this.ws.send(msg) }} 225 | /> 226 |
227 | ) 228 | } 229 | } 230 | 231 | export default App 232 | -------------------------------------------------------------------------------- /proxy/proxy.go: -------------------------------------------------------------------------------- 1 | // Package proxy implements the MitM Proxy (Forward Proxy). 2 | package proxy 3 | 4 | import ( 5 | "bytes" 6 | "context" 7 | "io" 8 | "net" 9 | "net/http" 10 | 11 | "github.com/kardianos/mitmproxy/cert" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type Options struct { 16 | Debug int 17 | Addr string 18 | StreamLargeBodies int64 // When the request or response body is larger then this in bytes, turn into stream model. 19 | InsecureSkipVerifyTLS bool 20 | CA cert.Getter 21 | } 22 | 23 | type Proxy struct { 24 | Opts *Options 25 | Version string 26 | Addons []Addon 27 | 28 | server *http.Server 29 | interceptor *middle 30 | } 31 | 32 | func NewProxy(opts *Options) (*Proxy, error) { 33 | if opts.StreamLargeBodies <= 0 { 34 | opts.StreamLargeBodies = 1024 * 1024 * 5 // default: 5mb 35 | } 36 | 37 | proxy := &Proxy{ 38 | Opts: opts, 39 | Version: "1.3.1", 40 | Addons: make([]Addon, 0), 41 | } 42 | 43 | proxy.server = &http.Server{ 44 | Addr: opts.Addr, 45 | Handler: proxy, 46 | ConnContext: func(ctx context.Context, c net.Conn) context.Context { 47 | wc := c.(*wrapClientConn) 48 | connCtx := newConnContext(wc, proxy) 49 | for _, addon := range proxy.Addons { 50 | addon.ClientConnected(connCtx.ClientConn) 51 | } 52 | wc.connCtx = connCtx 53 | return context.WithValue(ctx, connContextKey, connCtx) 54 | }, 55 | } 56 | 57 | interceptor, err := newMiddle(proxy) 58 | if err != nil { 59 | return nil, err 60 | } 61 | proxy.interceptor = interceptor 62 | 63 | return proxy, nil 64 | } 65 | 66 | func (proxy *Proxy) AddAddon(addon Addon) { 67 | proxy.Addons = append(proxy.Addons, addon) 68 | } 69 | 70 | func (proxy *Proxy) Start() error { 71 | addr := proxy.server.Addr 72 | if addr == "" { 73 | addr = ":http" 74 | } 75 | ln, err := net.Listen("tcp", addr) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | go proxy.interceptor.start() 81 | 82 | log.Infof("Proxy start listen at %v\n", proxy.server.Addr) 83 | pln := &wrapListener{ 84 | Listener: ln, 85 | proxy: proxy, 86 | } 87 | return proxy.server.Serve(pln) 88 | } 89 | 90 | func (proxy *Proxy) Close() error { 91 | err := proxy.server.Close() 92 | proxy.interceptor.close() 93 | return err 94 | } 95 | 96 | func (proxy *Proxy) Shutdown(ctx context.Context) error { 97 | err := proxy.server.Shutdown(ctx) 98 | proxy.interceptor.close() 99 | return err 100 | } 101 | 102 | func (proxy *Proxy) ServeHTTP(res http.ResponseWriter, req *http.Request) { 103 | if req.Method == "CONNECT" { 104 | proxy.handleConnect(res, req) 105 | return 106 | } 107 | 108 | log := log.WithFields(log.Fields{ 109 | "in": "Proxy.ServeHTTP", 110 | "url": req.URL, 111 | "method": req.Method, 112 | }) 113 | 114 | if !req.URL.IsAbs() || req.URL.Host == "" { 115 | res.WriteHeader(400) 116 | _, err := io.WriteString(res, "This is a proxy server and cannot initiate requests directly.") 117 | if err != nil { 118 | log.Error(err) 119 | } 120 | return 121 | } 122 | 123 | reply := func(response *Response, body io.Reader) { 124 | if response.Header != nil { 125 | for key, value := range response.Header { 126 | for _, v := range value { 127 | res.Header().Add(key, v) 128 | } 129 | } 130 | } 131 | if response.close { 132 | res.Header().Add("Connection", "close") 133 | } 134 | res.WriteHeader(response.StatusCode) 135 | 136 | if body != nil { 137 | _, err := io.Copy(res, body) 138 | if err != nil { 139 | logErr(log, err) 140 | } 141 | } 142 | if response.BodyReader != nil { 143 | _, err := io.Copy(res, response.BodyReader) 144 | if err != nil { 145 | logErr(log, err) 146 | } 147 | } 148 | if response.Body != nil && len(response.Body) > 0 { 149 | _, err := res.Write(response.Body) 150 | if err != nil { 151 | logErr(log, err) 152 | } 153 | } 154 | } 155 | 156 | // when addons panic 157 | defer func() { 158 | if err := recover(); err != nil { 159 | log.Warnf("Recovered: %v\n", err) 160 | } 161 | }() 162 | 163 | f := newFlow() 164 | f.Request = newRequest(req) 165 | f.ConnContext = req.Context().Value(connContextKey).(*ConnContext) 166 | defer f.finish() 167 | 168 | // trigger addon event Requestheaders 169 | for _, addon := range proxy.Addons { 170 | addon.Requestheaders(f) 171 | if f.Response != nil { 172 | reply(f.Response, nil) 173 | return 174 | } 175 | } 176 | 177 | // Read request body 178 | var reqBody io.Reader = req.Body 179 | if !f.Stream { 180 | reqBuf, r, err := readerToBuffer(req.Body, proxy.Opts.StreamLargeBodies) 181 | reqBody = r 182 | if err != nil { 183 | log.Error(err) 184 | res.WriteHeader(502) 185 | return 186 | } 187 | 188 | if reqBuf == nil { 189 | log.Warnf("request body size >= %v\n", proxy.Opts.StreamLargeBodies) 190 | f.Stream = true 191 | } else { 192 | f.Request.Body = reqBuf 193 | 194 | // trigger addon event Request 195 | for _, addon := range proxy.Addons { 196 | addon.Request(f) 197 | if f.Response != nil { 198 | reply(f.Response, nil) 199 | return 200 | } 201 | } 202 | reqBody = bytes.NewReader(f.Request.Body) 203 | } 204 | } 205 | 206 | for _, addon := range proxy.Addons { 207 | reqBody = addon.StreamRequestModifier(f, reqBody) 208 | } 209 | proxyReq, err := http.NewRequest(f.Request.Method, f.Request.URL.String(), reqBody) 210 | if err != nil { 211 | log.Error(err) 212 | res.WriteHeader(502) 213 | return 214 | } 215 | 216 | for key, value := range f.Request.Header { 217 | for _, v := range value { 218 | proxyReq.Header.Add(key, v) 219 | } 220 | } 221 | 222 | f.ConnContext.initHttpServerConn() 223 | proxyRes, err := f.ConnContext.ServerConn.client.Do(proxyReq) 224 | if err != nil { 225 | logErr(log, err) 226 | res.WriteHeader(502) 227 | return 228 | } 229 | 230 | if proxyRes.Close { 231 | f.ConnContext.closeAfterResponse = true 232 | } 233 | 234 | defer proxyRes.Body.Close() 235 | 236 | f.Response = &Response{ 237 | StatusCode: proxyRes.StatusCode, 238 | Header: proxyRes.Header, 239 | close: proxyRes.Close, 240 | } 241 | 242 | // trigger addon event Responseheaders 243 | for _, addon := range proxy.Addons { 244 | addon.Responseheaders(f) 245 | if f.Response.Body != nil { 246 | reply(f.Response, nil) 247 | return 248 | } 249 | } 250 | 251 | // Read response body 252 | var resBody io.Reader = proxyRes.Body 253 | if !f.Stream { 254 | resBuf, r, err := readerToBuffer(proxyRes.Body, proxy.Opts.StreamLargeBodies) 255 | resBody = r 256 | if err != nil { 257 | log.Error(err) 258 | res.WriteHeader(502) 259 | return 260 | } 261 | if resBuf == nil { 262 | log.Warnf("response body size >= %v\n", proxy.Opts.StreamLargeBodies) 263 | f.Stream = true 264 | } else { 265 | f.Response.Body = resBuf 266 | 267 | // trigger addon event Response 268 | for _, addon := range proxy.Addons { 269 | addon.Response(f) 270 | } 271 | } 272 | } 273 | for _, addon := range proxy.Addons { 274 | resBody = addon.StreamResponseModifier(f, resBody) 275 | } 276 | 277 | reply(f.Response, resBody) 278 | } 279 | 280 | func (proxy *Proxy) handleConnect(res http.ResponseWriter, req *http.Request) { 281 | log := log.WithFields(log.Fields{ 282 | "in": "Proxy.handleConnect", 283 | "host": req.Host, 284 | }) 285 | 286 | conn, err := proxy.interceptor.dial(req) 287 | if err != nil { 288 | log.Error(err) 289 | res.WriteHeader(502) 290 | return 291 | } 292 | defer conn.Close() 293 | 294 | cconn, _, err := res.(http.Hijacker).Hijack() 295 | if err != nil { 296 | log.Error(err) 297 | res.WriteHeader(502) 298 | return 299 | } 300 | 301 | // cconn.(*net.TCPConn).SetLinger(0) // send RST other than FIN when finished, to avoid TIME_WAIT state 302 | // cconn.(*net.TCPConn).SetKeepAlive(false) 303 | defer cconn.Close() 304 | 305 | _, err = io.WriteString(cconn, "HTTP/1.1 200 Connection Established\r\n\r\n") 306 | if err != nil { 307 | log.Error(err) 308 | return 309 | } 310 | 311 | transfer(log, conn, cconn) 312 | } 313 | -------------------------------------------------------------------------------- /cert/cert.go: -------------------------------------------------------------------------------- 1 | // Package cert fetches the root certificate for MitM proxy. 2 | package cert 3 | 4 | import ( 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/pem" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "io/ioutil" 15 | "math/big" 16 | "net" 17 | "os" 18 | "path/filepath" 19 | "strings" 20 | "sync" 21 | "time" 22 | 23 | "github.com/golang/groupcache/lru" 24 | "github.com/golang/groupcache/singleflight" 25 | log "github.com/sirupsen/logrus" 26 | ) 27 | 28 | // reference 29 | // https://docs.mitmproxy.org/stable/concepts-certificates/ 30 | // https://github.com/mitmproxy/mitmproxy/blob/master/mitmproxy/certs.py 31 | 32 | var errCaNotFound = errors.New("ca not found") 33 | 34 | type Getter interface { 35 | GetCert(commonName string) (*tls.Certificate, error) 36 | } 37 | 38 | type CA struct { 39 | PrivateKey rsa.PrivateKey 40 | RootCert x509.Certificate 41 | 42 | cacheMu sync.Mutex 43 | cache *lru.Cache 44 | 45 | group *singleflight.Group 46 | } 47 | 48 | type Loader interface { 49 | Load() (*rsa.PrivateKey, *x509.Certificate, error) 50 | } 51 | 52 | func New(l Loader) (*CA, error) { 53 | key, cert, err := l.Load() 54 | if err != nil { 55 | return nil, err 56 | } 57 | return &CA{ 58 | PrivateKey: *key, 59 | RootCert: *cert, 60 | cache: lru.New(100), 61 | group: new(singleflight.Group), 62 | }, nil 63 | } 64 | 65 | func createCert() (*rsa.PrivateKey, *x509.Certificate, error) { 66 | key, err := rsa.GenerateKey(rand.Reader, 2048) 67 | if err != nil { 68 | return nil, nil, err 69 | } 70 | 71 | template := &x509.Certificate{ 72 | SerialNumber: big.NewInt(time.Now().UnixNano() / 100000), 73 | Subject: pkix.Name{ 74 | CommonName: "mitmproxy", 75 | Organization: []string{"mitmproxy"}, 76 | }, 77 | NotBefore: time.Now().Add(-time.Hour * 48), 78 | NotAfter: time.Now().Add(time.Hour * 24 * 365 * 3), 79 | BasicConstraintsValid: true, 80 | IsCA: true, 81 | SignatureAlgorithm: x509.SHA256WithRSA, 82 | KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, 83 | ExtKeyUsage: []x509.ExtKeyUsage{ 84 | x509.ExtKeyUsageServerAuth, 85 | x509.ExtKeyUsageClientAuth, 86 | x509.ExtKeyUsageEmailProtection, 87 | x509.ExtKeyUsageTimeStamping, 88 | x509.ExtKeyUsageCodeSigning, 89 | x509.ExtKeyUsageMicrosoftCommercialCodeSigning, 90 | x509.ExtKeyUsageMicrosoftServerGatedCrypto, 91 | x509.ExtKeyUsageNetscapeServerGatedCrypto, 92 | }, 93 | } 94 | 95 | certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) 96 | if err != nil { 97 | return nil, nil, err 98 | } 99 | cert, err := x509.ParseCertificate(certBytes) 100 | if err != nil { 101 | return nil, nil, err 102 | } 103 | 104 | return key, cert, nil 105 | } 106 | 107 | type MemoryLoader struct{} 108 | 109 | func (m *MemoryLoader) Load() (*rsa.PrivateKey, *x509.Certificate, error) { 110 | return createCert() 111 | } 112 | 113 | type PathLoader struct { 114 | StorePath string 115 | } 116 | 117 | func (p *PathLoader) Load() (*rsa.PrivateKey, *x509.Certificate, error) { 118 | if key, cert, err := p.load(); err != nil { 119 | if err != errCaNotFound { 120 | return nil, nil, err 121 | } 122 | } else { 123 | log.Debug("load root ca") 124 | return key, cert, nil 125 | } 126 | 127 | key, cert, err := p.create() 128 | if err != nil { 129 | return nil, nil, err 130 | } 131 | log.Debug("create root ca") 132 | return key, cert, nil 133 | } 134 | 135 | func NewPathLoader(path string) (*PathLoader, error) { 136 | if path == "" { 137 | homeDir, err := os.UserHomeDir() 138 | if err != nil { 139 | return nil, err 140 | } 141 | path = filepath.Join(homeDir, ".mitmproxy") 142 | } 143 | 144 | if !filepath.IsAbs(path) { 145 | dir, err := os.Getwd() 146 | if err != nil { 147 | return nil, err 148 | } 149 | path = filepath.Join(dir, path) 150 | } 151 | 152 | stat, err := os.Stat(path) 153 | if err != nil { 154 | if os.IsNotExist(err) { 155 | err = os.MkdirAll(path, os.ModePerm) 156 | if err != nil { 157 | return nil, err 158 | } 159 | } else { 160 | return nil, err 161 | } 162 | } else { 163 | if !stat.Mode().IsDir() { 164 | return nil, fmt.Errorf("path %v not a folder", path) 165 | } 166 | } 167 | 168 | return &PathLoader{StorePath: path}, nil 169 | } 170 | 171 | // The certificate and the private key in PEM format. 172 | func (p *PathLoader) caFile() string { 173 | return filepath.Join(p.StorePath, "mitmproxy-ca.pem") 174 | } 175 | 176 | func (p *PathLoader) load() (*rsa.PrivateKey, *x509.Certificate, error) { 177 | caFile := p.caFile() 178 | stat, err := os.Stat(caFile) 179 | if err != nil { 180 | if os.IsNotExist(err) { 181 | return nil, nil, errCaNotFound 182 | } 183 | return nil, nil, err 184 | } 185 | 186 | if !stat.Mode().IsRegular() { 187 | return nil, nil, fmt.Errorf("%v not a file", caFile) 188 | } 189 | 190 | data, err := ioutil.ReadFile(caFile) 191 | if err != nil { 192 | return nil, nil, err 193 | } 194 | 195 | keyDERBlock, data := pem.Decode(data) 196 | if keyDERBlock == nil { 197 | return nil, nil, fmt.Errorf("%v 中不存在 PRIVATE KEY", caFile) 198 | } 199 | certDERBlock, _ := pem.Decode(data) 200 | if certDERBlock == nil { 201 | return nil, nil, fmt.Errorf("%v 中不存在 CERTIFICATE", caFile) 202 | } 203 | 204 | var privateKey *rsa.PrivateKey 205 | key, err := x509.ParsePKCS8PrivateKey(keyDERBlock.Bytes) 206 | if err != nil { 207 | // fix #14 208 | if strings.Contains(err.Error(), "use ParsePKCS1PrivateKey instead") { 209 | privateKey, err = x509.ParsePKCS1PrivateKey(keyDERBlock.Bytes) 210 | if err != nil { 211 | return nil, nil, err 212 | } 213 | } else { 214 | return nil, nil, err 215 | } 216 | } else { 217 | if v, ok := key.(*rsa.PrivateKey); ok { 218 | privateKey = v 219 | } else { 220 | return nil, nil, errors.New("found unknown rsa private key type in PKCS#8 wrapping") 221 | } 222 | } 223 | 224 | x509Cert, err := x509.ParseCertificate(certDERBlock.Bytes) 225 | if err != nil { 226 | return nil, nil, err 227 | } 228 | 229 | return privateKey, x509Cert, nil 230 | } 231 | 232 | func (p *PathLoader) create() (*rsa.PrivateKey, *x509.Certificate, error) { 233 | key, cert, err := createCert() 234 | if err != nil { 235 | return nil, nil, err 236 | } 237 | 238 | if err := p.save(key, cert); err != nil { 239 | return nil, nil, err 240 | } 241 | return key, cert, nil 242 | } 243 | 244 | func (p *PathLoader) saveTo(out io.Writer, key *rsa.PrivateKey, cert *x509.Certificate) error { 245 | keyBytes, err := x509.MarshalPKCS8PrivateKey(key) 246 | if err != nil { 247 | return err 248 | } 249 | err = pem.Encode(out, &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes}) 250 | if err != nil { 251 | return err 252 | } 253 | 254 | return pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) 255 | } 256 | 257 | func (p *PathLoader) saveCertTo(out io.Writer, key *rsa.PrivateKey, cert *x509.Certificate) error { 258 | return pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) 259 | } 260 | 261 | func (p *PathLoader) save(key *rsa.PrivateKey, cert *x509.Certificate) error { 262 | file, err := os.Create(p.caFile()) 263 | if err != nil { 264 | return err 265 | } 266 | defer file.Close() 267 | return p.saveTo(file, key, cert) 268 | } 269 | 270 | func (ca *CA) GetCert(commonName string) (*tls.Certificate, error) { 271 | ca.cacheMu.Lock() 272 | if val, ok := ca.cache.Get(commonName); ok { 273 | ca.cacheMu.Unlock() 274 | log.Debugf("ca GetCert: %v", commonName) 275 | return val.(*tls.Certificate), nil 276 | } 277 | ca.cacheMu.Unlock() 278 | 279 | val, err := ca.group.Do(commonName, func() (interface{}, error) { 280 | cert, err := ca.GenerateCert(commonName) 281 | if err == nil { 282 | ca.cacheMu.Lock() 283 | ca.cache.Add(commonName, cert) 284 | ca.cacheMu.Unlock() 285 | } 286 | return cert, err 287 | }) 288 | 289 | if err != nil { 290 | return nil, err 291 | } 292 | 293 | return val.(*tls.Certificate), nil 294 | } 295 | 296 | // TODO: Should support multiple SubjectAltName. 297 | func (ca *CA) GenerateCert(commonName string) (*tls.Certificate, error) { 298 | log.Debugf("ca DummyCert: %v", commonName) 299 | template := &x509.Certificate{ 300 | SerialNumber: big.NewInt(time.Now().UnixNano() / 100000), 301 | Subject: pkix.Name{ 302 | CommonName: commonName, 303 | Organization: []string{"mitmproxy"}, 304 | }, 305 | NotBefore: time.Now().Add(-time.Hour * 48), 306 | NotAfter: time.Now().Add(time.Hour * 24 * 365), 307 | SignatureAlgorithm: x509.SHA256WithRSA, 308 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, 309 | } 310 | 311 | ip := net.ParseIP(commonName) 312 | if ip != nil { 313 | template.IPAddresses = []net.IP{ip} 314 | } else { 315 | template.DNSNames = []string{commonName} 316 | } 317 | 318 | certBytes, err := x509.CreateCertificate(rand.Reader, template, &ca.RootCert, &ca.PrivateKey.PublicKey, &ca.PrivateKey) 319 | if err != nil { 320 | return nil, err 321 | } 322 | 323 | cert := &tls.Certificate{ 324 | Certificate: [][]byte{certBytes}, 325 | PrivateKey: &ca.PrivateKey, 326 | } 327 | 328 | return cert, nil 329 | } 330 | -------------------------------------------------------------------------------- /web/client/build/static/js/3.fdc4294f.chunk.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../../../addon/web/client/node_modules/web-vitals/dist/web-vitals.js"],"names":["e","t","n","i","r","name","value","delta","entries","id","concat","Date","now","Math","floor","random","a","PerformanceObserver","supportedEntryTypes","includes","self","getEntries","map","observe","type","buffered","o","document","visibilityState","removeEventListener","addEventListener","c","persisted","u","f","s","m","timeStamp","v","setTimeout","p","disconnect","startTime","firstHiddenTime","push","window","performance","getEntriesByName","requestAnimationFrame","d","l","h","hadRecentInput","length","takeRecords","g","passive","capture","y","T","S","E","entryType","target","cancelable","processingStart","forEach","w","L","b","F","once","P","getEntriesByType","timing","max","navigationStart","responseStart","readyState"],"mappings":"2HAAA,+MAAIA,EAAEC,EAAEC,EAAEC,EAAEC,EAAE,SAASJ,EAAEC,GAAG,MAAM,CAACI,KAAKL,EAAEM,WAAM,IAASL,GAAG,EAAEA,EAAEM,MAAM,EAAEC,QAAQ,GAAGC,GAAG,MAAMC,OAAOC,KAAKC,MAAM,KAAKF,OAAOG,KAAKC,MAAM,cAAcD,KAAKE,UAAU,QAAQC,EAAE,SAAShB,EAAEC,GAAG,IAAI,GAAGgB,oBAAoBC,oBAAoBC,SAASnB,GAAG,CAAC,GAAG,gBAAgBA,KAAK,2BAA2BoB,MAAM,OAAO,IAAIlB,EAAE,IAAIe,qBAAqB,SAASjB,GAAG,OAAOA,EAAEqB,aAAaC,IAAIrB,MAAM,OAAOC,EAAEqB,QAAQ,CAACC,KAAKxB,EAAEyB,UAAS,IAAKvB,GAAG,MAAMF,MAAM0B,EAAE,SAAS1B,EAAEC,GAAG,IAAIC,EAAE,SAASA,EAAEC,GAAG,aAAaA,EAAEqB,MAAM,WAAWG,SAASC,kBAAkB5B,EAAEG,GAAGF,IAAI4B,oBAAoB,mBAAmB3B,GAAE,GAAI2B,oBAAoB,WAAW3B,GAAE,MAAO4B,iBAAiB,mBAAmB5B,GAAE,GAAI4B,iBAAiB,WAAW5B,GAAE,IAAK6B,EAAE,SAAS/B,GAAG8B,iBAAiB,YAAY,SAAS7B,GAAGA,EAAE+B,WAAWhC,EAAEC,MAAK,IAAKgC,EAAE,SAASjC,EAAEC,EAAEC,GAAG,IAAIC,EAAE,OAAO,SAASC,GAAGH,EAAEK,OAAO,IAAIF,GAAGF,KAAKD,EAAEM,MAAMN,EAAEK,OAAOH,GAAG,IAAIF,EAAEM,YAAO,IAASJ,KAAKA,EAAEF,EAAEK,MAAMN,EAAEC,OAAOiC,GAAG,EAAEC,EAAE,WAAW,MAAM,WAAWR,SAASC,gBAAgB,EAAE,KAAKQ,EAAE,WAAWV,GAAG,SAAS1B,GAAG,IAAIC,EAAED,EAAEqC,UAAUH,EAAEjC,KAAI,IAAKqC,EAAE,WAAW,OAAOJ,EAAE,IAAIA,EAAEC,IAAIC,IAAIL,GAAG,WAAWQ,YAAY,WAAWL,EAAEC,IAAIC,MAAM,OAAO,CAAC,sBAAsB,OAAOF,KAAKM,EAAE,SAASxC,EAAEC,GAAG,IAAIC,EAAEC,EAAEmC,IAAIZ,EAAEtB,EAAE,OAAO8B,EAAE,SAASlC,GAAG,2BAA2BA,EAAEK,OAAO+B,GAAGA,EAAEK,aAAazC,EAAE0C,UAAUvC,EAAEwC,kBAAkBjB,EAAEpB,MAAMN,EAAE0C,UAAUhB,EAAElB,QAAQoC,KAAK5C,GAAGE,GAAE,MAAOiC,EAAEU,OAAOC,aAAaA,YAAYC,kBAAkBD,YAAYC,iBAAiB,0BAA0B,GAAGX,EAAED,EAAE,KAAKnB,EAAE,QAAQkB,IAAIC,GAAGC,KAAKlC,EAAE+B,EAAEjC,EAAE0B,EAAEzB,GAAGkC,GAAGD,EAAEC,GAAGJ,GAAG,SAAS5B,GAAGuB,EAAEtB,EAAE,OAAOF,EAAE+B,EAAEjC,EAAE0B,EAAEzB,GAAG+C,uBAAuB,WAAWA,uBAAuB,WAAWtB,EAAEpB,MAAMwC,YAAYlC,MAAMT,EAAEkC,UAAUnC,GAAE,cAAe+C,GAAE,EAAGC,GAAG,EAAEC,EAAE,SAASnD,EAAEC,GAAGgD,IAAIT,GAAG,SAASxC,GAAGkD,EAAElD,EAAEM,SAAS2C,GAAE,GAAI,IAAI/C,EAAEC,EAAE,SAASF,GAAGiD,GAAG,GAAGlD,EAAEC,IAAIiC,EAAE9B,EAAE,MAAM,GAAG+B,EAAE,EAAEC,EAAE,GAAGE,EAAE,SAAStC,GAAG,IAAIA,EAAEoD,eAAe,CAAC,IAAInD,EAAEmC,EAAE,GAAGjC,EAAEiC,EAAEA,EAAEiB,OAAO,GAAGlB,GAAGnC,EAAE0C,UAAUvC,EAAEuC,UAAU,KAAK1C,EAAE0C,UAAUzC,EAAEyC,UAAU,KAAKP,GAAGnC,EAAEM,MAAM8B,EAAEQ,KAAK5C,KAAKmC,EAAEnC,EAAEM,MAAM8B,EAAE,CAACpC,IAAImC,EAAED,EAAE5B,QAAQ4B,EAAE5B,MAAM6B,EAAED,EAAE1B,QAAQ4B,EAAElC,OAAOiD,EAAEnC,EAAE,eAAesB,GAAGa,IAAIjD,EAAE+B,EAAE9B,EAAE+B,EAAEjC,GAAGyB,GAAG,WAAWyB,EAAEG,cAAchC,IAAIgB,GAAGpC,GAAE,MAAO6B,GAAG,WAAWI,EAAE,EAAEe,GAAG,EAAEhB,EAAE9B,EAAE,MAAM,GAAGF,EAAE+B,EAAE9B,EAAE+B,EAAEjC,QAAQsD,EAAE,CAACC,SAAQ,EAAGC,SAAQ,GAAIC,EAAE,IAAI/C,KAAKgD,EAAE,SAASxD,EAAEC,GAAGJ,IAAIA,EAAEI,EAAEH,EAAEE,EAAED,EAAE,IAAIS,KAAKiD,EAAE/B,qBAAqBgC,MAAMA,EAAE,WAAW,GAAG5D,GAAG,GAAGA,EAAEC,EAAEwD,EAAE,CAAC,IAAItD,EAAE,CAAC0D,UAAU,cAAczD,KAAKL,EAAEwB,KAAKuC,OAAO/D,EAAE+D,OAAOC,WAAWhE,EAAEgE,WAAWtB,UAAU1C,EAAEqC,UAAU4B,gBAAgBjE,EAAEqC,UAAUpC,GAAGE,EAAE+D,SAAS,SAASlE,GAAGA,EAAEI,MAAMD,EAAE,KAAKgE,EAAE,SAASnE,GAAG,GAAGA,EAAEgE,WAAW,CAAC,IAAI/D,GAAGD,EAAEqC,UAAU,KAAK,IAAI1B,KAAKmC,YAAYlC,OAAOZ,EAAEqC,UAAU,eAAerC,EAAEwB,KAAK,SAASxB,EAAEC,GAAG,IAAIC,EAAE,WAAWyD,EAAE3D,EAAEC,GAAGG,KAAKD,EAAE,WAAWC,KAAKA,EAAE,WAAWyB,oBAAoB,YAAY3B,EAAEqD,GAAG1B,oBAAoB,gBAAgB1B,EAAEoD,IAAIzB,iBAAiB,YAAY5B,EAAEqD,GAAGzB,iBAAiB,gBAAgB3B,EAAEoD,GAA9N,CAAkOtD,EAAED,GAAG2D,EAAE1D,EAAED,KAAK4D,EAAE,SAAS5D,GAAG,CAAC,YAAY,UAAU,aAAa,eAAekE,SAAS,SAASjE,GAAG,OAAOD,EAAEC,EAAEkE,EAAEZ,OAAOa,EAAE,SAASlE,EAAEgC,GAAG,IAAIC,EAAEC,EAAEE,IAAIE,EAAEpC,EAAE,OAAO6C,EAAE,SAASjD,GAAGA,EAAE0C,UAAUN,EAAEO,kBAAkBH,EAAElC,MAAMN,EAAEiE,gBAAgBjE,EAAE0C,UAAUF,EAAEhC,QAAQoC,KAAK5C,GAAGmC,GAAE,KAAMe,EAAElC,EAAE,cAAciC,GAAGd,EAAEF,EAAE/B,EAAEsC,EAAEN,GAAGgB,GAAGxB,GAAG,WAAWwB,EAAEI,cAAchC,IAAI2B,GAAGC,EAAET,gBAAe,GAAIS,GAAGnB,GAAG,WAAW,IAAIf,EAAEwB,EAAEpC,EAAE,OAAO+B,EAAEF,EAAE/B,EAAEsC,EAAEN,GAAG/B,EAAE,GAAGF,GAAG,EAAED,EAAE,KAAK4D,EAAE9B,kBAAkBd,EAAEiC,EAAE9C,EAAEyC,KAAK5B,GAAG6C,QAAQQ,EAAE,GAAGC,EAAE,SAAStE,EAAEC,GAAG,IAAIC,EAAEC,EAAEmC,IAAIJ,EAAE9B,EAAE,OAAO+B,EAAE,SAASnC,GAAG,IAAIC,EAAED,EAAE0C,UAAUzC,EAAEE,EAAEwC,kBAAkBT,EAAE5B,MAAML,EAAEiC,EAAE1B,QAAQoC,KAAK5C,IAAIE,KAAKkC,EAAEpB,EAAE,2BAA2BmB,GAAG,GAAGC,EAAE,CAAClC,EAAE+B,EAAEjC,EAAEkC,EAAEjC,GAAG,IAAIuC,EAAE,WAAW6B,EAAEnC,EAAEzB,MAAM2B,EAAEkB,cAAchC,IAAIa,GAAGC,EAAEK,aAAa4B,EAAEnC,EAAEzB,KAAI,EAAGP,GAAE,KAAM,CAAC,UAAU,SAASgE,SAAS,SAASlE,GAAG8B,iBAAiB9B,EAAEwC,EAAE,CAAC+B,MAAK,EAAGd,SAAQ,OAAQ/B,EAAEc,GAAE,GAAIT,GAAG,SAAS5B,GAAG+B,EAAE9B,EAAE,OAAOF,EAAE+B,EAAEjC,EAAEkC,EAAEjC,GAAG+C,uBAAuB,WAAWA,uBAAuB,WAAWd,EAAE5B,MAAMwC,YAAYlC,MAAMT,EAAEkC,UAAUgC,EAAEnC,EAAEzB,KAAI,EAAGP,GAAE,cAAesE,EAAE,SAASxE,GAAG,IAAIC,EAAEC,EAAEE,EAAE,QAAQH,EAAE,WAAW,IAAI,IAAIA,EAAE6C,YAAY2B,iBAAiB,cAAc,IAAI,WAAW,IAAIzE,EAAE8C,YAAY4B,OAAOzE,EAAE,CAAC6D,UAAU,aAAapB,UAAU,GAAG,IAAI,IAAIxC,KAAKF,EAAE,oBAAoBE,GAAG,WAAWA,IAAID,EAAEC,GAAGW,KAAK8D,IAAI3E,EAAEE,GAAGF,EAAE4E,gBAAgB,IAAI,OAAO3E,EAAhL,GAAqL,GAAGC,EAAEI,MAAMJ,EAAEK,MAAMN,EAAE4E,cAAc3E,EAAEI,MAAM,GAAGJ,EAAEI,MAAMwC,YAAYlC,MAAM,OAAOV,EAAEM,QAAQ,CAACP,GAAGD,EAAEE,GAAG,MAAMF,MAAM,aAAa2B,SAASmD,WAAWvC,WAAWtC,EAAE,GAAG6B,iBAAiB,WAAW7B","file":"static/js/3.fdc4294f.chunk.js","sourcesContent":["var e,t,n,i,r=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:\"v2-\".concat(Date.now(),\"-\").concat(Math.floor(8999999999999*Math.random())+1e12)}},a=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if(\"first-input\"===e&&!(\"PerformanceEventTiming\"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},o=function(e,t){var n=function n(i){\"pagehide\"!==i.type&&\"hidden\"!==document.visibilityState||(e(i),t&&(removeEventListener(\"visibilitychange\",n,!0),removeEventListener(\"pagehide\",n,!0)))};addEventListener(\"visibilitychange\",n,!0),addEventListener(\"pagehide\",n,!0)},c=function(e){addEventListener(\"pageshow\",(function(t){t.persisted&&e(t)}),!0)},u=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},f=-1,s=function(){return\"hidden\"===document.visibilityState?0:1/0},m=function(){o((function(e){var t=e.timeStamp;f=t}),!0)},v=function(){return f<0&&(f=s(),m(),c((function(){setTimeout((function(){f=s(),m()}),0)}))),{get firstHiddenTime(){return f}}},p=function(e,t){var n,i=v(),o=r(\"FCP\"),f=function(e){\"first-contentful-paint\"===e.name&&(m&&m.disconnect(),e.startTime-1&&e(t)},f=r(\"CLS\",0),s=0,m=[],v=function(e){if(!e.hadRecentInput){var t=m[0],i=m[m.length-1];s&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(s+=e.value,m.push(e)):(s=e.value,m=[e]),s>f.value&&(f.value=s,f.entries=m,n())}},h=a(\"layout-shift\",v);h&&(n=u(i,f,t),o((function(){h.takeRecords().map(v),n(!0)})),c((function(){s=0,l=-1,f=r(\"CLS\",0),n=u(i,f,t)})))},g={passive:!0,capture:!0},y=new Date,T=function(i,r){e||(e=r,t=i,n=new Date,S(removeEventListener),E())},E=function(){if(t>=0&&t1e12?new Date:performance.now())-e.timeStamp;\"pointerdown\"==e.type?function(e,t){var n=function(){T(e,t),r()},i=function(){r()},r=function(){removeEventListener(\"pointerup\",n,g),removeEventListener(\"pointercancel\",i,g)};addEventListener(\"pointerup\",n,g),addEventListener(\"pointercancel\",i,g)}(t,e):T(t,e)}},S=function(e){[\"mousedown\",\"keydown\",\"touchstart\",\"pointerdown\"].forEach((function(t){return e(t,w,g)}))},L=function(n,f){var s,m=v(),p=r(\"FID\"),d=function(e){e.startTimeperformance.now())return;n.entries=[t],e(n)}catch(e){}},\"complete\"===document.readyState?setTimeout(t,0):addEventListener(\"pageshow\",t)};export{h as getCLS,p as getFCP,L as getFID,F as getLCP,P as getTTFB};\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /web/client/src/lib/flow.ts: -------------------------------------------------------------------------------- 1 | import type { ConnectionManager, IConnection } from './connection' 2 | import { IMessage, MessageType } from './message' 3 | import { arrayBufferToBase64, bufHexView, getSize, isTextBody } from './utils' 4 | 5 | export type Header = Record 6 | 7 | export interface IRequest { 8 | method: string 9 | url: string 10 | proto: string 11 | header: Header 12 | body?: ArrayBuffer 13 | } 14 | 15 | export interface IFlowRequest { 16 | connId: string 17 | request: IRequest 18 | } 19 | 20 | export interface IResponse { 21 | statusCode: number 22 | header: Header 23 | body?: ArrayBuffer 24 | } 25 | 26 | export interface IPreviewBody { 27 | type: 'image' | 'json' | 'binary' 28 | data: string | null 29 | } 30 | 31 | export interface IFlowPreview { 32 | no: number 33 | id: string 34 | waitIntercept: boolean 35 | host: string 36 | path: string 37 | method: string 38 | statusCode: string 39 | size: string 40 | costTime: string 41 | contentType: string 42 | } 43 | 44 | export class Flow { 45 | public no: number 46 | public id: string 47 | public connId: string 48 | public waitIntercept: boolean 49 | public request: IRequest 50 | public response: IResponse | null = null 51 | 52 | public url: URL 53 | private path: string 54 | private _size = 0 55 | private size = '0' 56 | private headerContentLengthExist = false 57 | private contentType = '' 58 | 59 | private startTime = Date.now() 60 | private endTime = 0 61 | private costTime = '(pending)' 62 | 63 | public static curNo = 0 64 | 65 | private status: MessageType = MessageType.REQUEST 66 | 67 | private _isTextRequest: boolean | null 68 | private _isTextResponse: boolean | null 69 | private _requestBody: string | null 70 | private _hexviewRequestBody: string | null = null 71 | private _responseBody: string | null 72 | 73 | private _previewResponseBody: IPreviewBody | null = null 74 | private _previewRequestBody: IPreviewBody | null = null 75 | private _hexviewResponseBody: string | null = null 76 | 77 | private connMgr: ConnectionManager; 78 | private conn: IConnection | undefined; 79 | 80 | constructor(msg: IMessage, connMgr: ConnectionManager) { 81 | this.no = ++Flow.curNo 82 | this.id = msg.id 83 | this.waitIntercept = msg.waitIntercept 84 | 85 | const flowRequestMsg = msg.content as IFlowRequest 86 | this.connId = flowRequestMsg.connId 87 | this.request = flowRequestMsg.request 88 | 89 | this.url = new URL(this.request.url) 90 | this.path = this.url.pathname + this.url.search 91 | 92 | this._isTextRequest = null 93 | this._isTextResponse = null 94 | this._requestBody = null 95 | this._responseBody = null 96 | 97 | this.connMgr = connMgr 98 | } 99 | 100 | public addRequestBody(msg: IMessage): Flow { 101 | this.status = MessageType.REQUEST_BODY 102 | this.waitIntercept = msg.waitIntercept 103 | this.request.body = msg.content as ArrayBuffer 104 | return this 105 | } 106 | 107 | public addResponse(msg: IMessage): Flow { 108 | this.status = MessageType.RESPONSE 109 | this.waitIntercept = msg.waitIntercept 110 | this.response = msg.content as IResponse 111 | 112 | if (this.response && this.response.header) { 113 | if (this.response.header['Content-Type'] != null) { 114 | this.contentType = this.response.header['Content-Type'][0].split(';')[0] 115 | if (this.contentType.includes('javascript')) this.contentType = 'javascript' 116 | } 117 | if (this.response.header['Content-Length'] != null) { 118 | this.headerContentLengthExist = true 119 | this._size = parseInt(this.response.header['Content-Length'][0]) 120 | this.size = getSize(this._size) 121 | } 122 | } 123 | 124 | return this 125 | } 126 | 127 | public addResponseBody(msg: IMessage): Flow { 128 | this.status = MessageType.RESPONSE_BODY 129 | this.waitIntercept = msg.waitIntercept 130 | if (this.response) this.response.body = msg.content as ArrayBuffer 131 | this.endTime = Date.now() 132 | this.costTime = String(this.endTime - this.startTime) + ' ms' 133 | 134 | if (!this.headerContentLengthExist && this.response && this.response.body) { 135 | this._size = this.response.body.byteLength 136 | this.size = getSize(this._size) 137 | } 138 | return this 139 | } 140 | 141 | public preview(): IFlowPreview { 142 | return { 143 | no: this.no, 144 | id: this.id, 145 | waitIntercept: this.waitIntercept, 146 | host: this.url.host, 147 | path: this.path, 148 | method: this.request.method, 149 | statusCode: this.response ? String(this.response.statusCode) : '(pending)', 150 | size: this.size, 151 | costTime: this.costTime, 152 | contentType: this.contentType, 153 | } 154 | } 155 | 156 | public isTextRequest(): boolean { 157 | if (this._isTextRequest !== null) return this._isTextRequest 158 | this._isTextRequest = isTextBody(this.request) 159 | return this._isTextRequest 160 | } 161 | 162 | public requestBody(): string { 163 | if (this._requestBody !== null) return this._requestBody 164 | if (!this.isTextRequest()) { 165 | this._requestBody = '' 166 | return this._requestBody 167 | } 168 | if (this.status < MessageType.REQUEST_BODY) return '' 169 | this._requestBody = new TextDecoder().decode(this.request.body) 170 | return this._requestBody 171 | } 172 | 173 | public hexviewRequestBody(): string | null { 174 | if (this._hexviewRequestBody !== null) return this._hexviewRequestBody 175 | if (this.status < MessageType.REQUEST_BODY) return null 176 | if (!(this.request?.body?.byteLength)) return null 177 | 178 | this._hexviewRequestBody = bufHexView(this.request.body) 179 | return this._hexviewRequestBody 180 | } 181 | 182 | public isTextResponse(): boolean | null { 183 | if (this.status < MessageType.RESPONSE) return null 184 | if (this._isTextResponse !== null) return this._isTextResponse 185 | this._isTextResponse = isTextBody(this.response as IResponse) 186 | return this._isTextResponse 187 | } 188 | 189 | public responseBody(): string { 190 | if (this._responseBody !== null) return this._responseBody 191 | if (this.status < MessageType.RESPONSE) return '' 192 | if (!this.isTextResponse()) { 193 | this._responseBody = '' 194 | return this._responseBody 195 | } 196 | if (this.status < MessageType.RESPONSE_BODY) return '' 197 | this._responseBody = new TextDecoder().decode(this.response?.body) 198 | return this._responseBody 199 | } 200 | 201 | public previewResponseBody(): IPreviewBody | null { 202 | if (this._previewResponseBody) return this._previewResponseBody 203 | 204 | if (this.status < MessageType.RESPONSE_BODY) return null 205 | if (!(this.response?.body?.byteLength)) return null 206 | 207 | let contentType: string | undefined 208 | if (this.response.header['Content-Type']) contentType = this.response.header['Content-Type'][0] 209 | if (!contentType) return null 210 | 211 | if (contentType.startsWith('image/')) { 212 | this._previewResponseBody = { 213 | type: 'image', 214 | data: arrayBufferToBase64(this.response.body), 215 | } 216 | } 217 | else if (contentType.includes('application/json')) { 218 | this._previewResponseBody = { 219 | type: 'json', 220 | data: this.responseBody(), 221 | } 222 | } 223 | 224 | return this._previewResponseBody 225 | } 226 | 227 | public previewRequestBody(): IPreviewBody | null { 228 | if (this._previewRequestBody) return this._previewRequestBody 229 | 230 | if (this.status < MessageType.REQUEST_BODY) return null 231 | if (!(this.request.body?.byteLength)) return null 232 | 233 | if (!this.isTextRequest()) { 234 | this._previewRequestBody = { 235 | type: 'binary', 236 | data: this.hexviewRequestBody(), 237 | } 238 | } else if (/json/.test(this.request.header['Content-Type'].join(''))) { 239 | this._previewRequestBody = { 240 | type: 'json', 241 | data: this.requestBody(), 242 | } 243 | } 244 | 245 | return this._previewRequestBody 246 | } 247 | 248 | public hexviewResponseBody(): string | null { 249 | if (this._hexviewResponseBody !== null) return this._hexviewResponseBody 250 | 251 | if (this.status < MessageType.RESPONSE_BODY) return null 252 | if (!(this.response?.body?.byteLength)) return null 253 | 254 | this._hexviewResponseBody = bufHexView(this.response.body) 255 | return this._hexviewResponseBody 256 | } 257 | 258 | public getConn(): IConnection | undefined { 259 | if (this.conn) return this.conn 260 | this.conn = this.connMgr.get(this.connId) 261 | return this.conn 262 | } 263 | } 264 | 265 | export class FlowManager { 266 | private items: Flow[] 267 | private _map: Map 268 | private filterText: string 269 | private filterTimer: number | null 270 | private num: number 271 | private max: number 272 | 273 | constructor() { 274 | this.items = [] 275 | this._map = new Map() 276 | this.filterText = '' 277 | this.filterTimer = null 278 | this.num = 0 279 | 280 | this.max = 1000 281 | } 282 | 283 | showList() { 284 | let text = this.filterText 285 | if (text) text = text.trim() 286 | if (!text) return this.items 287 | 288 | // regexp 289 | if (text.startsWith('/') && text.endsWith('/')) { 290 | text = text.slice(1, text.length - 1).trim() 291 | if (!text) return this.items 292 | try { 293 | const reg = new RegExp(text) 294 | return this.items.filter(item => { 295 | return reg.test(item.request.url) 296 | }) 297 | } catch (err) { 298 | return this.items 299 | } 300 | } 301 | 302 | return this.items.filter(item => { 303 | return item.request.url.includes(text) 304 | }) 305 | } 306 | 307 | add(item: Flow) { 308 | item.no = ++this.num 309 | this.items.push(item) 310 | this._map.set(item.id, item) 311 | 312 | if (this.items.length > this.max) { 313 | const oldest = this.items.shift() 314 | if (oldest) this._map.delete(oldest.id) 315 | } 316 | } 317 | 318 | get(id: string) { 319 | return this._map.get(id) 320 | } 321 | 322 | changeFilter(text: string) { 323 | this.filterText = text 324 | } 325 | 326 | changeFilterLazy(text: string, callback: () => void) { 327 | if (this.filterTimer) { 328 | clearTimeout(this.filterTimer) 329 | this.filterTimer = null 330 | } 331 | 332 | this.filterTimer = setTimeout(() => { 333 | this.filterText = text 334 | callback() 335 | }, 300) as any 336 | } 337 | 338 | clear() { 339 | this.items = [] 340 | this._map = new Map() 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /proxy/connection.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "crypto/tls" 7 | "encoding/json" 8 | "errors" 9 | "net" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | "time" 14 | 15 | uuid "github.com/satori/go.uuid" 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | // client connection 20 | type ClientConn struct { 21 | ID uuid.UUID `json:"id"` 22 | Conn *wrapClientConn `json:"-"` 23 | TLS bool `json:"tls"` 24 | } 25 | 26 | func newClientConn(c *wrapClientConn) *ClientConn { 27 | return &ClientConn{ 28 | ID: uuid.NewV4(), 29 | Conn: c, 30 | TLS: false, 31 | } 32 | } 33 | 34 | func (c *ClientConn) MarshalJSON() ([]byte, error) { 35 | m := struct { 36 | ID uuid.UUID `json:"id"` 37 | Address string `json:"address"` 38 | TLS bool `json:"tls"` 39 | }{ 40 | ID: c.ID, 41 | Address: c.Conn.RemoteAddr().String(), 42 | TLS: c.TLS, 43 | } 44 | return json.Marshal(m) 45 | } 46 | 47 | // server connection 48 | type ServerConn struct { 49 | ID uuid.UUID `json:"id"` 50 | Address string `json:"address"` 51 | Conn net.Conn `json:"-"` 52 | 53 | tlsHandshaked chan struct{} 54 | tlsHandshakeErr error 55 | tlsConn *tls.Conn 56 | tlsState *tls.ConnectionState 57 | client *http.Client 58 | } 59 | 60 | func newServerConn() *ServerConn { 61 | return &ServerConn{ 62 | ID: uuid.NewV4(), 63 | tlsHandshaked: make(chan struct{}), 64 | } 65 | } 66 | 67 | func (c *ServerConn) MarshalJSON() ([]byte, error) { 68 | m := struct { 69 | ID uuid.UUID `json:"id"` 70 | Address string `json:"address"` 71 | PeerName string `json:"peername"` 72 | }{ 73 | ID: c.ID, 74 | Address: c.Address, 75 | PeerName: c.Conn.LocalAddr().String(), 76 | } 77 | return json.Marshal(m) 78 | } 79 | 80 | func (c *ServerConn) TLSState() *tls.ConnectionState { 81 | <-c.tlsHandshaked 82 | return c.tlsState 83 | } 84 | 85 | // connection context ctx key 86 | var connContextKey = new(struct{}) 87 | 88 | // connection context 89 | type ConnContext struct { 90 | ClientConn *ClientConn `json:"clientConn"` 91 | ServerConn *ServerConn `json:"serverConn"` 92 | 93 | proxy *Proxy 94 | pipeConn *pipeConn 95 | closeAfterResponse bool // after http response, http server will close the connection 96 | } 97 | 98 | func newConnContext(c *wrapClientConn, proxy *Proxy) *ConnContext { 99 | clientConn := newClientConn(c) 100 | return &ConnContext{ 101 | ClientConn: clientConn, 102 | proxy: proxy, 103 | } 104 | } 105 | 106 | func (connCtx *ConnContext) ID() uuid.UUID { 107 | return connCtx.ClientConn.ID 108 | } 109 | 110 | func (connCtx *ConnContext) initHttpServerConn() { 111 | if connCtx.ServerConn != nil { 112 | return 113 | } 114 | if connCtx.ClientConn.TLS { 115 | return 116 | } 117 | 118 | serverConn := newServerConn() 119 | serverConn.client = &http.Client{ 120 | Transport: &http.Transport{ 121 | Proxy: http.ProxyFromEnvironment, 122 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 123 | c, err := (&net.Dialer{}).DialContext(ctx, network, addr) 124 | if err != nil { 125 | return nil, err 126 | } 127 | cw := &wrapServerConn{ 128 | Conn: c, 129 | proxy: connCtx.proxy, 130 | connCtx: connCtx, 131 | } 132 | serverConn.Conn = cw 133 | serverConn.Address = addr 134 | defer func() { 135 | for _, addon := range connCtx.proxy.Addons { 136 | addon.ServerConnected(connCtx) 137 | } 138 | }() 139 | return cw, nil 140 | }, 141 | ForceAttemptHTTP2: false, // disable http2 142 | DisableCompression: true, // To get the original response from the server, set Transport.DisableCompression to true. 143 | TLSClientConfig: &tls.Config{ 144 | InsecureSkipVerify: connCtx.proxy.Opts.InsecureSkipVerifyTLS, 145 | KeyLogWriter: getTLSKeyLogWriter(), 146 | }, 147 | }, 148 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 149 | // Disable automatic redirects. 150 | return http.ErrUseLastResponse 151 | }, 152 | } 153 | connCtx.ServerConn = serverConn 154 | } 155 | 156 | func (connCtx *ConnContext) initServerTcpConn(req *http.Request) error { 157 | log.Debugln("in initServerTcpConn") 158 | ServerConn := newServerConn() 159 | connCtx.ServerConn = ServerConn 160 | ServerConn.Address = connCtx.pipeConn.host 161 | 162 | // test is use proxy 163 | clientReq := &http.Request{URL: &url.URL{Scheme: "https", Host: ServerConn.Address}} 164 | proxyUrl, err := http.ProxyFromEnvironment(clientReq) 165 | if err != nil { 166 | return err 167 | } 168 | var plainConn net.Conn 169 | if proxyUrl != nil { 170 | plainConn, err = getProxyConn(proxyUrl, ServerConn.Address) 171 | } else { 172 | plainConn, err = (&net.Dialer{}).DialContext(context.Background(), "tcp", ServerConn.Address) 173 | } 174 | if err != nil { 175 | return err 176 | } 177 | ServerConn.Conn = &wrapServerConn{ 178 | Conn: plainConn, 179 | proxy: connCtx.proxy, 180 | connCtx: connCtx, 181 | } 182 | 183 | for _, addon := range connCtx.proxy.Addons { 184 | addon.ServerConnected(connCtx) 185 | } 186 | 187 | return nil 188 | } 189 | 190 | // connect proxy when set https_proxy env 191 | // ref: http/transport.go dialConn func 192 | func getProxyConn(proxyUrl *url.URL, address string) (net.Conn, error) { 193 | conn, err := (&net.Dialer{}).DialContext(context.Background(), "tcp", proxyUrl.Host) 194 | if err != nil { 195 | return nil, err 196 | } 197 | connectReq := &http.Request{ 198 | Method: "CONNECT", 199 | URL: &url.URL{Opaque: address}, 200 | Host: address, 201 | } 202 | connectCtx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) 203 | defer cancel() 204 | didReadResponse := make(chan struct{}) // closed after CONNECT write+read is done or fails 205 | var resp *http.Response 206 | // Write the CONNECT request & read the response. 207 | go func() { 208 | defer close(didReadResponse) 209 | err = connectReq.Write(conn) 210 | if err != nil { 211 | return 212 | } 213 | // Okay to use and discard buffered reader here, because 214 | // TLS server will not speak until spoken to. 215 | br := bufio.NewReader(conn) 216 | resp, err = http.ReadResponse(br, connectReq) 217 | }() 218 | select { 219 | case <-connectCtx.Done(): 220 | conn.Close() 221 | <-didReadResponse 222 | return nil, connectCtx.Err() 223 | case <-didReadResponse: 224 | // resp or err now set 225 | } 226 | if err != nil { 227 | conn.Close() 228 | return nil, err 229 | } 230 | if resp.StatusCode != 200 { 231 | _, text, ok := strings.Cut(resp.Status, " ") 232 | conn.Close() 233 | if !ok { 234 | return nil, errors.New("unknown status code") 235 | } 236 | return nil, errors.New(text) 237 | } 238 | return conn, nil 239 | } 240 | 241 | func (connCtx *ConnContext) initHttpsServerConn() { 242 | if !connCtx.ClientConn.TLS { 243 | return 244 | } 245 | connCtx.ServerConn.client = &http.Client{ 246 | Transport: &http.Transport{ 247 | DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 248 | <-connCtx.ServerConn.tlsHandshaked 249 | return connCtx.ServerConn.tlsConn, connCtx.ServerConn.tlsHandshakeErr 250 | }, 251 | ForceAttemptHTTP2: false, // disable http2 252 | DisableCompression: true, // To get the original response from the server, set Transport.DisableCompression to true. 253 | }, 254 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 255 | // Disable automatic redirects. 256 | return http.ErrUseLastResponse 257 | }, 258 | } 259 | } 260 | 261 | func (connCtx *ConnContext) tlsHandshake(clientHello *tls.ClientHelloInfo) error { 262 | cfg := &tls.Config{ 263 | InsecureSkipVerify: connCtx.proxy.Opts.InsecureSkipVerifyTLS, 264 | KeyLogWriter: getTLSKeyLogWriter(), 265 | ServerName: clientHello.ServerName, 266 | NextProtos: []string{"http/1.1"}, // todo: h2 267 | // CurvePreferences: clientHello.SupportedCurves, // todo: 如果打开会出错 268 | CipherSuites: clientHello.CipherSuites, 269 | } 270 | if len(clientHello.SupportedVersions) > 0 { 271 | minVersion := clientHello.SupportedVersions[0] 272 | maxVersion := clientHello.SupportedVersions[0] 273 | for _, version := range clientHello.SupportedVersions { 274 | if version < minVersion { 275 | minVersion = version 276 | } 277 | if version > maxVersion { 278 | maxVersion = version 279 | } 280 | } 281 | cfg.MinVersion = minVersion 282 | cfg.MaxVersion = maxVersion 283 | } 284 | 285 | tlsConn := tls.Client(connCtx.ServerConn.Conn, cfg) 286 | err := tlsConn.HandshakeContext(context.Background()) 287 | if err != nil { 288 | connCtx.ServerConn.tlsHandshakeErr = err 289 | close(connCtx.ServerConn.tlsHandshaked) 290 | return err 291 | } 292 | 293 | connCtx.ServerConn.tlsConn = tlsConn 294 | tlsState := tlsConn.ConnectionState() 295 | connCtx.ServerConn.tlsState = &tlsState 296 | close(connCtx.ServerConn.tlsHandshaked) 297 | 298 | return nil 299 | } 300 | 301 | // wrap tcpConn for remote client 302 | type wrapClientConn struct { 303 | net.Conn 304 | proxy *Proxy 305 | connCtx *ConnContext 306 | closed bool 307 | closeErr error 308 | } 309 | 310 | func (c *wrapClientConn) Close() error { 311 | if c.closed { 312 | return c.closeErr 313 | } 314 | log.Debugln("in wrapClientConn close", c.connCtx.ClientConn.Conn.RemoteAddr()) 315 | 316 | c.closed = true 317 | c.closeErr = c.Conn.Close() 318 | 319 | for _, addon := range c.proxy.Addons { 320 | addon.ClientDisconnected(c.connCtx.ClientConn) 321 | } 322 | 323 | if c.connCtx.ServerConn != nil && c.connCtx.ServerConn.Conn != nil { 324 | c.connCtx.ServerConn.Conn.Close() 325 | } 326 | 327 | return c.closeErr 328 | } 329 | 330 | // wrap tcpListener for remote client 331 | type wrapListener struct { 332 | net.Listener 333 | proxy *Proxy 334 | } 335 | 336 | func (l *wrapListener) Accept() (net.Conn, error) { 337 | c, err := l.Listener.Accept() 338 | if err != nil { 339 | return nil, err 340 | } 341 | 342 | return &wrapClientConn{ 343 | Conn: c, 344 | proxy: l.proxy, 345 | }, nil 346 | } 347 | 348 | // wrap tcpConn for remote server 349 | type wrapServerConn struct { 350 | net.Conn 351 | proxy *Proxy 352 | connCtx *ConnContext 353 | closed bool 354 | closeErr error 355 | } 356 | 357 | func (c *wrapServerConn) Close() error { 358 | if c.closed { 359 | return c.closeErr 360 | } 361 | log.Debugln("in wrapServerConn close", c.connCtx.ClientConn.Conn.RemoteAddr()) 362 | 363 | c.closed = true 364 | c.closeErr = c.Conn.Close() 365 | 366 | for _, addon := range c.proxy.Addons { 367 | addon.ServerDisconnected(c.connCtx) 368 | } 369 | 370 | if !c.connCtx.ClientConn.TLS { 371 | c.connCtx.ClientConn.Conn.Conn.(*net.TCPConn).CloseRead() 372 | return c.closeErr 373 | } 374 | if !c.connCtx.closeAfterResponse { 375 | c.connCtx.pipeConn.Close() 376 | } 377 | 378 | return c.closeErr 379 | } 380 | -------------------------------------------------------------------------------- /web/client/build/static/js/runtime-main.476c72c1.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../webpack/bootstrap"],"names":["webpackJsonpCallback","data","moduleId","chunkId","chunkIds","moreModules","executeModules","i","resolves","length","Object","prototype","hasOwnProperty","call","installedChunks","push","modules","parentJsonpFunction","shift","deferredModules","apply","checkDeferredModules","result","deferredModule","fulfilled","j","depId","splice","__webpack_require__","s","installedModules","1","exports","module","l","e","promises","installedChunkData","promise","Promise","resolve","reject","onScriptComplete","script","document","createElement","charset","timeout","nc","setAttribute","src","p","jsonpScriptSrc","error","Error","event","onerror","onload","clearTimeout","chunk","errorType","type","realSrc","target","message","name","request","undefined","setTimeout","head","appendChild","all","m","c","d","getter","o","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","oe","err","console","jsonpArray","this","oldJsonpFunction","slice"],"mappings":"aACE,SAASA,EAAqBC,GAQ7B,IAPA,IAMIC,EAAUC,EANVC,EAAWH,EAAK,GAChBI,EAAcJ,EAAK,GACnBK,EAAiBL,EAAK,GAIHM,EAAI,EAAGC,EAAW,GACpCD,EAAIH,EAASK,OAAQF,IACzBJ,EAAUC,EAASG,GAChBG,OAAOC,UAAUC,eAAeC,KAAKC,EAAiBX,IAAYW,EAAgBX,IACpFK,EAASO,KAAKD,EAAgBX,GAAS,IAExCW,EAAgBX,GAAW,EAE5B,IAAID,KAAYG,EACZK,OAAOC,UAAUC,eAAeC,KAAKR,EAAaH,KACpDc,EAAQd,GAAYG,EAAYH,IAKlC,IAFGe,GAAqBA,EAAoBhB,GAEtCO,EAASC,QACdD,EAASU,OAATV,GAOD,OAHAW,EAAgBJ,KAAKK,MAAMD,EAAiBb,GAAkB,IAGvDe,IAER,SAASA,IAER,IADA,IAAIC,EACIf,EAAI,EAAGA,EAAIY,EAAgBV,OAAQF,IAAK,CAG/C,IAFA,IAAIgB,EAAiBJ,EAAgBZ,GACjCiB,GAAY,EACRC,EAAI,EAAGA,EAAIF,EAAed,OAAQgB,IAAK,CAC9C,IAAIC,EAAQH,EAAeE,GACG,IAA3BX,EAAgBY,KAAcF,GAAY,GAE3CA,IACFL,EAAgBQ,OAAOpB,IAAK,GAC5Be,EAASM,EAAoBA,EAAoBC,EAAIN,EAAe,KAItE,OAAOD,EAIR,IAAIQ,EAAmB,GAKnBhB,EAAkB,CACrBiB,EAAG,GAGAZ,EAAkB,GAQtB,SAASS,EAAoB1B,GAG5B,GAAG4B,EAAiB5B,GACnB,OAAO4B,EAAiB5B,GAAU8B,QAGnC,IAAIC,EAASH,EAAiB5B,GAAY,CACzCK,EAAGL,EACHgC,GAAG,EACHF,QAAS,IAUV,OANAhB,EAAQd,GAAUW,KAAKoB,EAAOD,QAASC,EAAQA,EAAOD,QAASJ,GAG/DK,EAAOC,GAAI,EAGJD,EAAOD,QAKfJ,EAAoBO,EAAI,SAAuBhC,GAC9C,IAAIiC,EAAW,GAKXC,EAAqBvB,EAAgBX,GACzC,GAA0B,IAAvBkC,EAGF,GAAGA,EACFD,EAASrB,KAAKsB,EAAmB,QAC3B,CAEN,IAAIC,EAAU,IAAIC,SAAQ,SAASC,EAASC,GAC3CJ,EAAqBvB,EAAgBX,GAAW,CAACqC,EAASC,MAE3DL,EAASrB,KAAKsB,EAAmB,GAAKC,GAGtC,IACII,EADAC,EAASC,SAASC,cAAc,UAGpCF,EAAOG,QAAU,QACjBH,EAAOI,QAAU,IACbnB,EAAoBoB,IACvBL,EAAOM,aAAa,QAASrB,EAAoBoB,IAElDL,EAAOO,IA1DV,SAAwB/C,GACvB,OAAOyB,EAAoBuB,EAAI,cAAgB,GAAGhD,IAAUA,GAAW,IAAM,CAAC,EAAI,YAAYA,GAAW,YAyD1FiD,CAAejD,GAG5B,IAAIkD,EAAQ,IAAIC,MAChBZ,EAAmB,SAAUa,GAE5BZ,EAAOa,QAAUb,EAAOc,OAAS,KACjCC,aAAaX,GACb,IAAIY,EAAQ7C,EAAgBX,GAC5B,GAAa,IAAVwD,EAAa,CACf,GAAGA,EAAO,CACT,IAAIC,EAAYL,IAAyB,SAAfA,EAAMM,KAAkB,UAAYN,EAAMM,MAChEC,EAAUP,GAASA,EAAMQ,QAAUR,EAAMQ,OAAOb,IACpDG,EAAMW,QAAU,iBAAmB7D,EAAU,cAAgByD,EAAY,KAAOE,EAAU,IAC1FT,EAAMY,KAAO,iBACbZ,EAAMQ,KAAOD,EACbP,EAAMa,QAAUJ,EAChBH,EAAM,GAAGN,GAEVvC,EAAgBX,QAAWgE,IAG7B,IAAIpB,EAAUqB,YAAW,WACxB1B,EAAiB,CAAEmB,KAAM,UAAWE,OAAQpB,MAC1C,MACHA,EAAOa,QAAUb,EAAOc,OAASf,EACjCE,SAASyB,KAAKC,YAAY3B,GAG5B,OAAOJ,QAAQgC,IAAInC,IAIpBR,EAAoB4C,EAAIxD,EAGxBY,EAAoB6C,EAAI3C,EAGxBF,EAAoB8C,EAAI,SAAS1C,EAASiC,EAAMU,GAC3C/C,EAAoBgD,EAAE5C,EAASiC,IAClCvD,OAAOmE,eAAe7C,EAASiC,EAAM,CAAEa,YAAY,EAAMC,IAAKJ,KAKhE/C,EAAoBoD,EAAI,SAAShD,GACX,qBAAXiD,QAA0BA,OAAOC,aAC1CxE,OAAOmE,eAAe7C,EAASiD,OAAOC,YAAa,CAAEC,MAAO,WAE7DzE,OAAOmE,eAAe7C,EAAS,aAAc,CAAEmD,OAAO,KAQvDvD,EAAoBwD,EAAI,SAASD,EAAOE,GAEvC,GADU,EAAPA,IAAUF,EAAQvD,EAAoBuD,IAC/B,EAAPE,EAAU,OAAOF,EACpB,GAAW,EAAPE,GAA8B,kBAAVF,GAAsBA,GAASA,EAAMG,WAAY,OAAOH,EAChF,IAAII,EAAK7E,OAAO8E,OAAO,MAGvB,GAFA5D,EAAoBoD,EAAEO,GACtB7E,OAAOmE,eAAeU,EAAI,UAAW,CAAET,YAAY,EAAMK,MAAOA,IACtD,EAAPE,GAA4B,iBAATF,EAAmB,IAAI,IAAIM,KAAON,EAAOvD,EAAoB8C,EAAEa,EAAIE,EAAK,SAASA,GAAO,OAAON,EAAMM,IAAQC,KAAK,KAAMD,IAC9I,OAAOF,GAIR3D,EAAoB+D,EAAI,SAAS1D,GAChC,IAAI0C,EAAS1C,GAAUA,EAAOqD,WAC7B,WAAwB,OAAOrD,EAAgB,SAC/C,WAA8B,OAAOA,GAEtC,OADAL,EAAoB8C,EAAEC,EAAQ,IAAKA,GAC5BA,GAIR/C,EAAoBgD,EAAI,SAASgB,EAAQC,GAAY,OAAOnF,OAAOC,UAAUC,eAAeC,KAAK+E,EAAQC,IAGzGjE,EAAoBuB,EAAI,IAGxBvB,EAAoBkE,GAAK,SAASC,GAA2B,MAApBC,QAAQ3C,MAAM0C,GAAYA,GAEnE,IAAIE,EAAaC,KAAK,gCAAkCA,KAAK,iCAAmC,GAC5FC,EAAmBF,EAAWlF,KAAK2E,KAAKO,GAC5CA,EAAWlF,KAAOf,EAClBiG,EAAaA,EAAWG,QACxB,IAAI,IAAI7F,EAAI,EAAGA,EAAI0F,EAAWxF,OAAQF,IAAKP,EAAqBiG,EAAW1F,IAC3E,IAAIU,EAAsBkF,EAI1B9E,I","file":"static/js/runtime-main.476c72c1.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tfunction webpackJsonpCallback(data) {\n \t\tvar chunkIds = data[0];\n \t\tvar moreModules = data[1];\n \t\tvar executeModules = data[2];\n\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(data);\n\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n\n \t\t// add entry modules from loaded chunk to deferred list\n \t\tdeferredModules.push.apply(deferredModules, executeModules || []);\n\n \t\t// run deferred modules when all chunks ready\n \t\treturn checkDeferredModules();\n \t};\n \tfunction checkDeferredModules() {\n \t\tvar result;\n \t\tfor(var i = 0; i < deferredModules.length; i++) {\n \t\t\tvar deferredModule = deferredModules[i];\n \t\t\tvar fulfilled = true;\n \t\t\tfor(var j = 1; j < deferredModule.length; j++) {\n \t\t\t\tvar depId = deferredModule[j];\n \t\t\t\tif(installedChunks[depId] !== 0) fulfilled = false;\n \t\t\t}\n \t\t\tif(fulfilled) {\n \t\t\t\tdeferredModules.splice(i--, 1);\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = deferredModule[0]);\n \t\t\t}\n \t\t}\n\n \t\treturn result;\n \t}\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// undefined = chunk not loaded, null = chunk preloaded/prefetched\n \t// Promise = chunk loading, 0 = chunk loaded\n \tvar installedChunks = {\n \t\t1: 0\n \t};\n\n \tvar deferredModules = [];\n\n \t// script path function\n \tfunction jsonpScriptSrc(chunkId) {\n \t\treturn __webpack_require__.p + \"static/js/\" + ({}[chunkId]||chunkId) + \".\" + {\"3\":\"fdc4294f\"}[chunkId] + \".chunk.js\"\n \t}\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n \t// This file contains only the entry chunk.\n \t// The chunk loading function for additional chunks\n \t__webpack_require__.e = function requireEnsure(chunkId) {\n \t\tvar promises = [];\n\n\n \t\t// JSONP chunk loading for javascript\n\n \t\tvar installedChunkData = installedChunks[chunkId];\n \t\tif(installedChunkData !== 0) { // 0 means \"already installed\".\n\n \t\t\t// a Promise means \"currently loading\".\n \t\t\tif(installedChunkData) {\n \t\t\t\tpromises.push(installedChunkData[2]);\n \t\t\t} else {\n \t\t\t\t// setup Promise in chunk cache\n \t\t\t\tvar promise = new Promise(function(resolve, reject) {\n \t\t\t\t\tinstalledChunkData = installedChunks[chunkId] = [resolve, reject];\n \t\t\t\t});\n \t\t\t\tpromises.push(installedChunkData[2] = promise);\n\n \t\t\t\t// start chunk loading\n \t\t\t\tvar script = document.createElement('script');\n \t\t\t\tvar onScriptComplete;\n\n \t\t\t\tscript.charset = 'utf-8';\n \t\t\t\tscript.timeout = 120;\n \t\t\t\tif (__webpack_require__.nc) {\n \t\t\t\t\tscript.setAttribute(\"nonce\", __webpack_require__.nc);\n \t\t\t\t}\n \t\t\t\tscript.src = jsonpScriptSrc(chunkId);\n\n \t\t\t\t// create error before stack unwound to get useful stacktrace later\n \t\t\t\tvar error = new Error();\n \t\t\t\tonScriptComplete = function (event) {\n \t\t\t\t\t// avoid mem leaks in IE.\n \t\t\t\t\tscript.onerror = script.onload = null;\n \t\t\t\t\tclearTimeout(timeout);\n \t\t\t\t\tvar chunk = installedChunks[chunkId];\n \t\t\t\t\tif(chunk !== 0) {\n \t\t\t\t\t\tif(chunk) {\n \t\t\t\t\t\t\tvar errorType = event && (event.type === 'load' ? 'missing' : event.type);\n \t\t\t\t\t\t\tvar realSrc = event && event.target && event.target.src;\n \t\t\t\t\t\t\terror.message = 'Loading chunk ' + chunkId + ' failed.\\n(' + errorType + ': ' + realSrc + ')';\n \t\t\t\t\t\t\terror.name = 'ChunkLoadError';\n \t\t\t\t\t\t\terror.type = errorType;\n \t\t\t\t\t\t\terror.request = realSrc;\n \t\t\t\t\t\t\tchunk[1](error);\n \t\t\t\t\t\t}\n \t\t\t\t\t\tinstalledChunks[chunkId] = undefined;\n \t\t\t\t\t}\n \t\t\t\t};\n \t\t\t\tvar timeout = setTimeout(function(){\n \t\t\t\t\tonScriptComplete({ type: 'timeout', target: script });\n \t\t\t\t}, 120000);\n \t\t\t\tscript.onerror = script.onload = onScriptComplete;\n \t\t\t\tdocument.head.appendChild(script);\n \t\t\t}\n \t\t}\n \t\treturn Promise.all(promises);\n \t};\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n \t// on error function for async loading\n \t__webpack_require__.oe = function(err) { console.error(err); throw err; };\n\n \tvar jsonpArray = this[\"webpackJsonpmitmproxy-client\"] = this[\"webpackJsonpmitmproxy-client\"] || [];\n \tvar oldJsonpFunction = jsonpArray.push.bind(jsonpArray);\n \tjsonpArray.push = webpackJsonpCallback;\n \tjsonpArray = jsonpArray.slice();\n \tfor(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);\n \tvar parentJsonpFunction = oldJsonpFunction;\n\n\n \t// run deferred modules from other chunks\n \tcheckDeferredModules();\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /web/client/src/components/ViewFlow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button from 'react-bootstrap/Button' 3 | import FormCheck from 'react-bootstrap/FormCheck' 4 | import fetchToCurl from 'fetch-to-curl' 5 | import copy from 'copy-to-clipboard' 6 | import JSONPretty from 'react-json-pretty' 7 | import { isTextBody } from '../lib/utils' 8 | import type { Flow, IResponse } from '../lib/flow' 9 | import EditFlow from './EditFlow' 10 | 11 | interface Iprops { 12 | flow: Flow | null 13 | onClose: () => void 14 | onReRenderFlows: () => void 15 | onMessage: (msg: ArrayBufferLike) => void 16 | } 17 | 18 | interface IState { 19 | flowTab: 'Headers' | 'Preview' | 'Response' | 'Hexview' | 'Detail' 20 | copied: boolean 21 | requestBodyViewTab: 'Raw' | 'Preview' 22 | responseBodyLineBreak: boolean 23 | } 24 | 25 | class ViewFlow extends React.Component { 26 | constructor(props: Iprops) { 27 | super(props) 28 | 29 | this.state = { 30 | flowTab: 'Detail', 31 | copied: false, 32 | requestBodyViewTab: 'Raw', 33 | responseBodyLineBreak: false, 34 | } 35 | } 36 | 37 | preview() { 38 | const { flow } = this.props 39 | if (!flow) return null 40 | const response = flow.response 41 | if (!response) return null 42 | 43 | if (!(response.body && response.body.byteLength)) { 44 | return
No response
45 | } 46 | 47 | const pv = flow.previewResponseBody() 48 | if (!pv) return
Not support preview
49 | 50 | if (pv.type === 'image') { 51 | return 52 | } 53 | else if (pv.type === 'json') { 54 | return
55 | } 56 | 57 | return
Not support preview
58 | } 59 | 60 | requestBodyPreview() { 61 | const { flow } = this.props 62 | if (!flow) return null 63 | 64 | const pv = flow.previewRequestBody() 65 | if (!pv) return
Not support preview
66 | 67 | if (pv.type === 'json') { 68 | return
69 | } 70 | else if (pv.type === 'binary') { 71 | return
{pv.data}
72 | } 73 | 74 | return
Not support preview
75 | } 76 | 77 | hexview() { 78 | const { flow } = this.props 79 | if (!flow) return null 80 | const response = flow.response 81 | if (!response) return null 82 | 83 | if (!(response.body && response.body.byteLength)) { 84 | return
No response
85 | } 86 | 87 | return
{flow.hexviewResponseBody()}
88 | } 89 | 90 | detail() { 91 | const { flow } = this.props 92 | if (!flow) return null 93 | 94 | const conn = flow.getConn() 95 | if (!conn) return null 96 | 97 | return ( 98 |
99 |
100 |

Server Connection

101 |
102 |

Address: {conn.serverConn.address}

103 |

Resolved Address: {conn.serverConn.peername}

104 |
105 |
106 |
107 |

Client Connection

108 |
109 |

Address: {conn.clientConn.address}

110 |
111 |
112 |
113 | ) 114 | } 115 | 116 | render() { 117 | if (!this.props.flow) return null 118 | 119 | const flow = this.props.flow 120 | const flowTab = this.state.flowTab 121 | 122 | const request = flow.request 123 | const response: IResponse = (flow.response || {}) as any 124 | 125 | // Query String Parameters 126 | const searchItems: Array<{ key: string; value: string }> = [] 127 | if (flow.url && flow.url.search) { 128 | flow.url.searchParams.forEach((value, key) => { 129 | searchItems.push({ key, value }) 130 | }) 131 | } 132 | 133 | return ( 134 |
135 |
136 | { this.props.onClose() }}>x 137 | { this.setState({ flowTab: 'Detail' }) }}>Detail 138 | { this.setState({ flowTab: 'Headers' }) }}>Headers 139 | { this.setState({ flowTab: 'Preview' }) }}>Preview 140 | { this.setState({ flowTab: 'Response' }) }}>Response 141 | { this.setState({ flowTab: 'Hexview' }) }}>Hexview 142 | 143 | { 146 | flow.request.method = request.method 147 | flow.request.url = request.url 148 | flow.request.header = request.header 149 | if (isTextBody(flow.request)) flow.request.body = request.body 150 | this.props.onReRenderFlows() 151 | }} 152 | onChangeResponse={response => { 153 | if (!flow.response) flow.response = {} as IResponse 154 | 155 | flow.response.statusCode = response.statusCode 156 | flow.response.header = response.header 157 | if (isTextBody(flow.response)) flow.response.body = response.body 158 | this.props.onReRenderFlows() 159 | }} 160 | onMessage={msg => { 161 | this.props.onMessage(msg) 162 | flow.waitIntercept = false 163 | this.props.onReRenderFlows() 164 | }} 165 | /> 166 | 167 |
168 | 169 |
170 | { 171 | !(flowTab === 'Headers') ? null : 172 |
173 |

192 | 193 |
194 |

General

195 |
196 |

Request URL: {request.url}

197 |

Request Method: {request.method}

198 |

Status Code: {`${response.statusCode || '(pending)'}`}

199 |
200 |
201 | 202 | { 203 | !(response.header) ? null : 204 |
205 |

Response Headers

206 |
207 | { 208 | Object.keys(response.header).map(key => { 209 | return ( 210 |

{key}: {response.header[key].join(' ')}

211 | ) 212 | }) 213 | } 214 |
215 |
216 | } 217 | 218 |
219 |

Request Headers

220 |
221 | { 222 | !(request.header) ? null : 223 | Object.keys(request.header).map(key => { 224 | return ( 225 |

{key}: {request.header[key].join(' ')}

226 | ) 227 | }) 228 | } 229 |
230 |
231 | 232 | { 233 | !(searchItems.length) ? null : 234 |
235 |

Query String Parameters

236 |
237 | { 238 | searchItems.map(({ key, value }) => { 239 | return ( 240 |

{key}: {value}

241 | ) 242 | }) 243 | } 244 |
245 |
246 | } 247 | 248 | { 249 | !(request.body && request.body.byteLength) ? null : 250 |
251 |

Request Body

252 |
253 |
254 |
255 | { this.setState({ requestBodyViewTab: 'Raw' }) }}>Raw 256 | { this.setState({ requestBodyViewTab: 'Preview' }) }}>Preview 257 |
258 | 259 | { 260 | !(this.state.requestBodyViewTab === 'Raw') ? null : 261 |
262 | { 263 | !(flow.isTextRequest()) ? Not text Request : flow.requestBody() 264 | } 265 |
266 | } 267 | 268 | { 269 | !(this.state.requestBodyViewTab === 'Preview') ? null : 270 |
{this.requestBodyPreview()}
271 | } 272 |
273 |
274 |
275 | } 276 | 277 |
278 | } 279 | 280 | { 281 | !(flowTab === 'Response') ? null : 282 | !(response.body && response.body.byteLength) ?
No response
: 283 | !(flow.isTextResponse()) ?
Not text response
: 284 |
285 |
286 | { 291 | this.setState({ responseBodyLineBreak: e.target.checked }) 292 | }} 293 | label="自动换行"> 294 |
295 |
296 | {flow.responseBody()} 297 |
298 |
299 | } 300 | 301 | { 302 | !(flowTab === 'Preview') ? null : 303 |
{this.preview()}
304 | } 305 | 306 | { 307 | !(flowTab === 'Hexview') ? null : 308 |
{this.hexview()}
309 | } 310 | 311 | { 312 | !(flowTab === 'Detail') ? null : 313 |
{this.detail()}
314 | } 315 |
316 | 317 |
318 | ) 319 | } 320 | } 321 | 322 | export default ViewFlow 323 | -------------------------------------------------------------------------------- /proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "errors" 8 | "io" 9 | "io/ioutil" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | "testing" 17 | "time" 18 | 19 | "github.com/kardianos/mitmproxy/cert" 20 | ) 21 | 22 | func handleError(t *testing.T, err error) { 23 | t.Helper() 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | } 28 | 29 | func testSendRequest(t *testing.T, endpoint string, client *http.Client, bodyWant string) { 30 | t.Helper() 31 | req, err := http.NewRequest("GET", endpoint, nil) 32 | handleError(t, err) 33 | if client == nil { 34 | client = http.DefaultClient 35 | } 36 | resp, err := client.Do(req) 37 | handleError(t, err) 38 | defer resp.Body.Close() 39 | body, err := ioutil.ReadAll(resp.Body) 40 | handleError(t, err) 41 | if string(body) != bodyWant { 42 | t.Fatalf("expected %s, but got %s", bodyWant, body) 43 | } 44 | } 45 | 46 | type testProxyHelper struct { 47 | server *http.Server 48 | proxyAddr string 49 | 50 | ln net.Listener 51 | tlsPlainLn net.Listener 52 | tlsLn net.Listener 53 | httpEndpoint string 54 | httpsEndpoint string 55 | testOrderAddonInstance *testOrderAddon 56 | testProxy *Proxy 57 | getProxyClient func() *http.Client 58 | } 59 | 60 | func (helper *testProxyHelper) init(t *testing.T) { 61 | t.Helper() 62 | 63 | mux := http.NewServeMux() 64 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 65 | w.Write([]byte("ok")) 66 | }) 67 | helper.server.Handler = mux 68 | 69 | // start http server 70 | ln, err := net.Listen("tcp", "127.0.0.1:0") 71 | handleError(t, err) 72 | helper.ln = ln 73 | 74 | // start https server 75 | tlsPlainLn, err := net.Listen("tcp", "127.0.0.1:0") 76 | handleError(t, err) 77 | helper.tlsPlainLn = tlsPlainLn 78 | l := &cert.MemoryLoader{} 79 | ca, err := cert.New(l) 80 | handleError(t, err) 81 | cert, err := ca.GetCert("localhost") 82 | handleError(t, err) 83 | tlsConfig := &tls.Config{ 84 | Certificates: []tls.Certificate{*cert}, 85 | } 86 | helper.tlsLn = tls.NewListener(tlsPlainLn, tlsConfig) 87 | 88 | httpEndpoint := "http://" + ln.Addr().String() + "/" 89 | httpsPort := tlsPlainLn.Addr().(*net.TCPAddr).Port 90 | httpsEndpoint := "https://localhost:" + strconv.Itoa(httpsPort) + "/" 91 | helper.httpEndpoint = httpEndpoint 92 | helper.httpsEndpoint = httpsEndpoint 93 | 94 | // start proxy 95 | testProxy, err := NewProxy(&Options{ 96 | Addr: helper.proxyAddr, // some random port 97 | InsecureSkipVerifyTLS: true, 98 | CA: ca, 99 | }) 100 | handleError(t, err) 101 | testProxy.AddAddon(&interceptAddon{}) 102 | testOrderAddonInstance := &testOrderAddon{ 103 | orders: make([]string, 0), 104 | } 105 | testProxy.AddAddon(testOrderAddonInstance) 106 | helper.testOrderAddonInstance = testOrderAddonInstance 107 | helper.testProxy = testProxy 108 | 109 | getProxyClient := func() *http.Client { 110 | return &http.Client{ 111 | Transport: &http.Transport{ 112 | TLSClientConfig: &tls.Config{ 113 | InsecureSkipVerify: true, 114 | }, 115 | Proxy: func(r *http.Request) (*url.URL, error) { 116 | return url.Parse("http://127.0.0.1" + helper.proxyAddr) 117 | }, 118 | }, 119 | } 120 | } 121 | helper.getProxyClient = getProxyClient 122 | } 123 | 124 | // addon for test intercept 125 | type interceptAddon struct { 126 | BaseAddon 127 | } 128 | 129 | func (addon *interceptAddon) Request(f *Flow) { 130 | // intercept request, should not send request to real endpoint 131 | if f.Request.URL.Path == "/intercept-request" { 132 | f.Response = &Response{ 133 | StatusCode: 200, 134 | Body: []byte("intercept-request"), 135 | } 136 | } 137 | } 138 | 139 | func (addon *interceptAddon) Response(f *Flow) { 140 | if f.Request.URL.Path == "/intercept-response" { 141 | f.Response = &Response{ 142 | StatusCode: 200, 143 | Body: []byte("intercept-response"), 144 | } 145 | } 146 | } 147 | 148 | // addon for test functions' execute order 149 | type testOrderAddon struct { 150 | BaseAddon 151 | orders []string 152 | mu sync.Mutex 153 | } 154 | 155 | func (addon *testOrderAddon) reset() { 156 | addon.mu.Lock() 157 | defer addon.mu.Unlock() 158 | addon.orders = make([]string, 0) 159 | } 160 | 161 | func (addon *testOrderAddon) contains(t *testing.T, name string) { 162 | t.Helper() 163 | addon.mu.Lock() 164 | defer addon.mu.Unlock() 165 | for _, n := range addon.orders { 166 | if name == n { 167 | return 168 | } 169 | } 170 | t.Fatalf("expected contains %s, but not", name) 171 | } 172 | 173 | func (addon *testOrderAddon) before(t *testing.T, a, b string) { 174 | t.Helper() 175 | addon.mu.Lock() 176 | defer addon.mu.Unlock() 177 | aIndex, bIndex := -1, -1 178 | for i, n := range addon.orders { 179 | if a == n { 180 | aIndex = i 181 | } else if b == n { 182 | bIndex = i 183 | } 184 | } 185 | if aIndex == -1 { 186 | t.Fatalf("expected contains %s, but not", a) 187 | } 188 | if bIndex == -1 { 189 | t.Fatalf("expected contains %s, but not", b) 190 | } 191 | if aIndex > bIndex { 192 | t.Fatalf("expected %s executed before %s, but not", a, b) 193 | } 194 | } 195 | 196 | func (addon *testOrderAddon) ClientConnected(*ClientConn) { 197 | addon.mu.Lock() 198 | defer addon.mu.Unlock() 199 | addon.orders = append(addon.orders, "ClientConnected") 200 | } 201 | func (addon *testOrderAddon) ClientDisconnected(*ClientConn) { 202 | addon.mu.Lock() 203 | defer addon.mu.Unlock() 204 | addon.orders = append(addon.orders, "ClientDisconnected") 205 | } 206 | func (addon *testOrderAddon) ServerConnected(*ConnContext) { 207 | addon.mu.Lock() 208 | defer addon.mu.Unlock() 209 | addon.orders = append(addon.orders, "ServerConnected") 210 | } 211 | func (addon *testOrderAddon) ServerDisconnected(*ConnContext) { 212 | addon.mu.Lock() 213 | defer addon.mu.Unlock() 214 | addon.orders = append(addon.orders, "ServerDisconnected") 215 | } 216 | func (addon *testOrderAddon) TlsEstablishedServer(*ConnContext) { 217 | addon.mu.Lock() 218 | defer addon.mu.Unlock() 219 | addon.orders = append(addon.orders, "TlsEstablishedServer") 220 | } 221 | func (addon *testOrderAddon) Requestheaders(*Flow) { 222 | addon.mu.Lock() 223 | defer addon.mu.Unlock() 224 | addon.orders = append(addon.orders, "Requestheaders") 225 | } 226 | func (addon *testOrderAddon) Request(*Flow) { 227 | addon.mu.Lock() 228 | defer addon.mu.Unlock() 229 | addon.orders = append(addon.orders, "Request") 230 | } 231 | func (addon *testOrderAddon) Responseheaders(*Flow) { 232 | addon.mu.Lock() 233 | defer addon.mu.Unlock() 234 | addon.orders = append(addon.orders, "Responseheaders") 235 | } 236 | func (addon *testOrderAddon) Response(*Flow) { 237 | addon.mu.Lock() 238 | defer addon.mu.Unlock() 239 | addon.orders = append(addon.orders, "Response") 240 | } 241 | func (addon *testOrderAddon) StreamRequestModifier(f *Flow, in io.Reader) io.Reader { 242 | addon.mu.Lock() 243 | defer addon.mu.Unlock() 244 | addon.orders = append(addon.orders, "StreamRequestModifier") 245 | return in 246 | } 247 | func (addon *testOrderAddon) StreamResponseModifier(f *Flow, in io.Reader) io.Reader { 248 | addon.mu.Lock() 249 | defer addon.mu.Unlock() 250 | addon.orders = append(addon.orders, "StreamResponseModifier") 251 | return in 252 | } 253 | 254 | func TestProxy(t *testing.T) { 255 | helper := &testProxyHelper{ 256 | server: &http.Server{}, 257 | proxyAddr: ":29080", 258 | } 259 | helper.init(t) 260 | httpEndpoint := helper.httpEndpoint 261 | httpsEndpoint := helper.httpsEndpoint 262 | testOrderAddonInstance := helper.testOrderAddonInstance 263 | testProxy := helper.testProxy 264 | getProxyClient := helper.getProxyClient 265 | defer helper.ln.Close() 266 | go helper.server.Serve(helper.ln) 267 | defer helper.tlsPlainLn.Close() 268 | go helper.server.Serve(helper.tlsLn) 269 | go testProxy.Start() 270 | time.Sleep(time.Millisecond * 10) // wait for test proxy startup 271 | 272 | t.Run("test http server", func(t *testing.T) { 273 | testSendRequest(t, httpEndpoint, nil, "ok") 274 | }) 275 | 276 | t.Run("test https server", func(t *testing.T) { 277 | t.Run("should generate not trusted error", func(t *testing.T) { 278 | _, err := http.Get(httpsEndpoint) 279 | if err == nil { 280 | t.Fatal("should have error") 281 | } 282 | var want x509.UnknownAuthorityError 283 | es := err.Error() 284 | switch { 285 | default: 286 | t.Fatal("should get not trusted error, but got", es) 287 | case errors.As(err, &want): 288 | case strings.Contains(es, "certificate is not trusted"): 289 | case strings.Contains(es, "certificate signed by unknown authority"): 290 | } 291 | }) 292 | 293 | t.Run("should get ok when InsecureSkipVerify", func(t *testing.T) { 294 | client := &http.Client{ 295 | Transport: &http.Transport{ 296 | TLSClientConfig: &tls.Config{ 297 | InsecureSkipVerify: true, 298 | }, 299 | }, 300 | } 301 | testSendRequest(t, httpsEndpoint, client, "ok") 302 | }) 303 | }) 304 | 305 | t.Run("test proxy", func(t *testing.T) { 306 | proxyClient := getProxyClient() 307 | 308 | t.Run("can proxy http", func(t *testing.T) { 309 | testSendRequest(t, httpEndpoint, proxyClient, "ok") 310 | }) 311 | 312 | t.Run("can proxy https", func(t *testing.T) { 313 | testSendRequest(t, httpsEndpoint, proxyClient, "ok") 314 | }) 315 | 316 | t.Run("can intercept request", func(t *testing.T) { 317 | t.Run("http", func(t *testing.T) { 318 | testSendRequest(t, httpEndpoint+"intercept-request", proxyClient, "intercept-request") 319 | }) 320 | t.Run("https", func(t *testing.T) { 321 | testSendRequest(t, httpsEndpoint+"intercept-request", proxyClient, "intercept-request") 322 | }) 323 | }) 324 | 325 | t.Run("can intercept request with wrong host", func(t *testing.T) { 326 | t.Run("http", func(t *testing.T) { 327 | httpEndpoint := "http://some-wrong-host/" 328 | testSendRequest(t, httpEndpoint+"intercept-request", proxyClient, "intercept-request") 329 | }) 330 | t.Run("https can't", func(t *testing.T) { 331 | httpsEndpoint := "https://some-wrong-host/" 332 | _, err := http.Get(httpsEndpoint + "intercept-request") 333 | if err == nil { 334 | t.Fatal("should have error") 335 | } 336 | if !strings.Contains(err.Error(), "dial tcp") { 337 | t.Fatal("should get dial error, but got", err.Error()) 338 | } 339 | }) 340 | }) 341 | 342 | t.Run("can intercept response", func(t *testing.T) { 343 | t.Run("http", func(t *testing.T) { 344 | testSendRequest(t, httpEndpoint+"intercept-response", proxyClient, "intercept-response") 345 | }) 346 | t.Run("https", func(t *testing.T) { 347 | testSendRequest(t, httpsEndpoint+"intercept-response", proxyClient, "intercept-response") 348 | }) 349 | }) 350 | }) 351 | 352 | t.Run("test proxy when DisableKeepAlives", func(t *testing.T) { 353 | proxyClient := getProxyClient() 354 | proxyClient.Transport.(*http.Transport).DisableKeepAlives = true 355 | 356 | t.Run("http", func(t *testing.T) { 357 | testSendRequest(t, httpEndpoint, proxyClient, "ok") 358 | }) 359 | 360 | t.Run("https", func(t *testing.T) { 361 | testSendRequest(t, httpsEndpoint, proxyClient, "ok") 362 | }) 363 | }) 364 | 365 | t.Run("should trigger disconnect functions when DisableKeepAlives", func(t *testing.T) { 366 | proxyClient := getProxyClient() 367 | proxyClient.Transport.(*http.Transport).DisableKeepAlives = true 368 | 369 | t.Run("http", func(t *testing.T) { 370 | time.Sleep(time.Millisecond * 10) 371 | testOrderAddonInstance.reset() 372 | testSendRequest(t, httpEndpoint, proxyClient, "ok") 373 | time.Sleep(time.Millisecond * 10) 374 | testOrderAddonInstance.contains(t, "ClientDisconnected") 375 | testOrderAddonInstance.contains(t, "ServerDisconnected") 376 | }) 377 | 378 | t.Run("https", func(t *testing.T) { 379 | time.Sleep(time.Millisecond * 10) 380 | testOrderAddonInstance.reset() 381 | testSendRequest(t, httpsEndpoint, proxyClient, "ok") 382 | time.Sleep(time.Millisecond * 10) 383 | testOrderAddonInstance.contains(t, "ClientDisconnected") 384 | testOrderAddonInstance.contains(t, "ServerDisconnected") 385 | }) 386 | }) 387 | 388 | t.Run("should not have eof error when DisableKeepAlives", func(t *testing.T) { 389 | proxyClient := getProxyClient() 390 | proxyClient.Transport.(*http.Transport).DisableKeepAlives = true 391 | t.Run("http", func(t *testing.T) { 392 | for i := 0; i < 10; i++ { 393 | testSendRequest(t, httpEndpoint, proxyClient, "ok") 394 | } 395 | }) 396 | t.Run("https", func(t *testing.T) { 397 | for i := 0; i < 10; i++ { 398 | testSendRequest(t, httpsEndpoint, proxyClient, "ok") 399 | } 400 | }) 401 | }) 402 | 403 | t.Run("should trigger disconnect functions when client side trigger off", func(t *testing.T) { 404 | proxyClient := getProxyClient() 405 | var clientConn net.Conn 406 | proxyClient.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { 407 | c, err := (&net.Dialer{}).DialContext(ctx, network, addr) 408 | clientConn = c 409 | return c, err 410 | } 411 | 412 | t.Run("http", func(t *testing.T) { 413 | time.Sleep(time.Millisecond * 10) 414 | testOrderAddonInstance.reset() 415 | testSendRequest(t, httpEndpoint, proxyClient, "ok") 416 | clientConn.Close() 417 | time.Sleep(time.Millisecond * 10) 418 | testOrderAddonInstance.contains(t, "ClientDisconnected") 419 | testOrderAddonInstance.contains(t, "ServerDisconnected") 420 | testOrderAddonInstance.before(t, "ClientDisconnected", "ServerDisconnected") 421 | }) 422 | 423 | t.Run("https", func(t *testing.T) { 424 | time.Sleep(time.Millisecond * 10) 425 | testOrderAddonInstance.reset() 426 | testSendRequest(t, httpsEndpoint, proxyClient, "ok") 427 | clientConn.Close() 428 | time.Sleep(time.Millisecond * 10) 429 | testOrderAddonInstance.contains(t, "ClientDisconnected") 430 | testOrderAddonInstance.contains(t, "ServerDisconnected") 431 | testOrderAddonInstance.before(t, "ClientDisconnected", "ServerDisconnected") 432 | }) 433 | }) 434 | } 435 | 436 | func TestProxyWhenServerNotKeepAlive(t *testing.T) { 437 | server := &http.Server{} 438 | server.SetKeepAlivesEnabled(false) 439 | helper := &testProxyHelper{ 440 | server: server, 441 | proxyAddr: ":29081", 442 | } 443 | helper.init(t) 444 | httpEndpoint := helper.httpEndpoint 445 | httpsEndpoint := helper.httpsEndpoint 446 | testOrderAddonInstance := helper.testOrderAddonInstance 447 | testProxy := helper.testProxy 448 | getProxyClient := helper.getProxyClient 449 | defer helper.ln.Close() 450 | go helper.server.Serve(helper.ln) 451 | defer helper.tlsPlainLn.Close() 452 | go helper.server.Serve(helper.tlsLn) 453 | go testProxy.Start() 454 | time.Sleep(time.Millisecond * 10) // wait for test proxy startup 455 | 456 | t.Run("should not have eof error when server side DisableKeepAlives", func(t *testing.T) { 457 | proxyClient := getProxyClient() 458 | t.Run("http", func(t *testing.T) { 459 | for i := 0; i < 10; i++ { 460 | testSendRequest(t, httpEndpoint, proxyClient, "ok") 461 | } 462 | }) 463 | t.Run("https", func(t *testing.T) { 464 | for i := 0; i < 10; i++ { 465 | testSendRequest(t, httpsEndpoint, proxyClient, "ok") 466 | } 467 | }) 468 | }) 469 | 470 | t.Run("should trigger disconnect functions when server DisableKeepAlives", func(t *testing.T) { 471 | proxyClient := getProxyClient() 472 | 473 | t.Run("http", func(t *testing.T) { 474 | time.Sleep(time.Millisecond * 10) 475 | testOrderAddonInstance.reset() 476 | testSendRequest(t, httpEndpoint, proxyClient, "ok") 477 | time.Sleep(time.Millisecond * 10) 478 | testOrderAddonInstance.contains(t, "ClientDisconnected") 479 | testOrderAddonInstance.contains(t, "ServerDisconnected") 480 | testOrderAddonInstance.before(t, "ServerDisconnected", "ClientDisconnected") 481 | }) 482 | 483 | t.Run("https", func(t *testing.T) { 484 | time.Sleep(time.Millisecond * 10) 485 | testOrderAddonInstance.reset() 486 | testSendRequest(t, httpsEndpoint, proxyClient, "ok") 487 | time.Sleep(time.Millisecond * 10) 488 | testOrderAddonInstance.contains(t, "ClientDisconnected") 489 | testOrderAddonInstance.contains(t, "ServerDisconnected") 490 | testOrderAddonInstance.before(t, "ServerDisconnected", "ClientDisconnected") 491 | }) 492 | }) 493 | } 494 | 495 | func TestProxyWhenServerKeepAliveButCloseImmediately(t *testing.T) { 496 | helper := &testProxyHelper{ 497 | server: &http.Server{ 498 | IdleTimeout: time.Millisecond * 10, 499 | }, 500 | proxyAddr: ":29082", 501 | } 502 | helper.init(t) 503 | httpEndpoint := helper.httpEndpoint 504 | httpsEndpoint := helper.httpsEndpoint 505 | testOrderAddonInstance := helper.testOrderAddonInstance 506 | testProxy := helper.testProxy 507 | getProxyClient := helper.getProxyClient 508 | defer helper.ln.Close() 509 | go helper.server.Serve(helper.ln) 510 | defer helper.tlsPlainLn.Close() 511 | go helper.server.Serve(helper.tlsLn) 512 | go testProxy.Start() 513 | time.Sleep(time.Millisecond * 10) // wait for test proxy startup 514 | 515 | t.Run("should not have eof error when server close connection immediately", func(t *testing.T) { 516 | proxyClient := getProxyClient() 517 | t.Run("http", func(t *testing.T) { 518 | for i := 0; i < 10; i++ { 519 | testSendRequest(t, httpEndpoint, proxyClient, "ok") 520 | } 521 | }) 522 | t.Run("http wait server closed", func(t *testing.T) { 523 | for i := 0; i < 10; i++ { 524 | testSendRequest(t, httpEndpoint, proxyClient, "ok") 525 | time.Sleep(time.Millisecond * 20) 526 | } 527 | }) 528 | t.Run("https", func(t *testing.T) { 529 | for i := 0; i < 10; i++ { 530 | testSendRequest(t, httpsEndpoint, proxyClient, "ok") 531 | } 532 | }) 533 | t.Run("https wait server closed", func(t *testing.T) { 534 | for i := 0; i < 10; i++ { 535 | testSendRequest(t, httpsEndpoint, proxyClient, "ok") 536 | time.Sleep(time.Millisecond * 20) 537 | } 538 | }) 539 | }) 540 | 541 | t.Run("should trigger disconnect functions when server close connection immediately", func(t *testing.T) { 542 | proxyClient := getProxyClient() 543 | 544 | t.Run("http", func(t *testing.T) { 545 | time.Sleep(time.Millisecond * 10) 546 | testOrderAddonInstance.reset() 547 | testSendRequest(t, httpEndpoint, proxyClient, "ok") 548 | time.Sleep(time.Millisecond * 20) 549 | testOrderAddonInstance.contains(t, "ClientDisconnected") 550 | testOrderAddonInstance.contains(t, "ServerDisconnected") 551 | testOrderAddonInstance.before(t, "ServerDisconnected", "ClientDisconnected") 552 | }) 553 | 554 | t.Run("https", func(t *testing.T) { 555 | time.Sleep(time.Millisecond * 10) 556 | testOrderAddonInstance.reset() 557 | testSendRequest(t, httpsEndpoint, proxyClient, "ok") 558 | time.Sleep(time.Millisecond * 20) 559 | testOrderAddonInstance.contains(t, "ClientDisconnected") 560 | testOrderAddonInstance.contains(t, "ServerDisconnected") 561 | testOrderAddonInstance.before(t, "ServerDisconnected", "ClientDisconnected") 562 | }) 563 | }) 564 | } 565 | 566 | func TestProxyClose(t *testing.T) { 567 | t.Skip("go1.19 hang") 568 | helper := &testProxyHelper{ 569 | server: &http.Server{}, 570 | proxyAddr: ":29083", 571 | } 572 | helper.init(t) 573 | httpEndpoint := helper.httpEndpoint 574 | httpsEndpoint := helper.httpsEndpoint 575 | testProxy := helper.testProxy 576 | getProxyClient := helper.getProxyClient 577 | defer helper.ln.Close() 578 | go helper.server.Serve(helper.ln) 579 | defer helper.tlsPlainLn.Close() 580 | go helper.server.Serve(helper.tlsLn) 581 | 582 | errCh := make(chan error) 583 | go func() { 584 | err := testProxy.Start() 585 | errCh <- err 586 | }() 587 | time.Sleep(time.Millisecond * 10) // wait for test proxy startup 588 | 589 | proxyClient := getProxyClient() 590 | testSendRequest(t, httpEndpoint, proxyClient, "ok") 591 | testSendRequest(t, httpsEndpoint, proxyClient, "ok") 592 | 593 | if err := testProxy.Close(); err != nil { 594 | t.Fatalf("close got error %v", err) 595 | } 596 | 597 | select { 598 | case err := <-errCh: 599 | if err != http.ErrServerClosed { 600 | t.Fatalf("expected ErrServerClosed error, but got %v", err) 601 | } 602 | case <-time.After(time.Millisecond * 10): 603 | t.Fatal("close timeout") 604 | } 605 | } 606 | 607 | func TestProxyShutdown(t *testing.T) { 608 | t.Skip("go1.19 hang") 609 | helper := &testProxyHelper{ 610 | server: &http.Server{}, 611 | proxyAddr: ":29084", 612 | } 613 | helper.init(t) 614 | httpEndpoint := helper.httpEndpoint 615 | httpsEndpoint := helper.httpsEndpoint 616 | testProxy := helper.testProxy 617 | getProxyClient := helper.getProxyClient 618 | defer helper.ln.Close() 619 | go helper.server.Serve(helper.ln) 620 | defer helper.tlsPlainLn.Close() 621 | go helper.server.Serve(helper.tlsLn) 622 | 623 | errCh := make(chan error) 624 | go func() { 625 | err := testProxy.Start() 626 | errCh <- err 627 | }() 628 | 629 | time.Sleep(time.Millisecond * 10) // wait for test proxy startup 630 | 631 | proxyClient := getProxyClient() 632 | testSendRequest(t, httpEndpoint, proxyClient, "ok") 633 | testSendRequest(t, httpsEndpoint, proxyClient, "ok") 634 | 635 | if err := testProxy.Shutdown(context.TODO()); err != nil { 636 | t.Fatalf("shutdown got error %v", err) 637 | } 638 | 639 | select { 640 | case err := <-errCh: 641 | if err != http.ErrServerClosed { 642 | t.Fatalf("expected ErrServerClosed error, but got %v", err) 643 | } 644 | case <-time.After(time.Millisecond * 10): 645 | t.Fatal("shutdown timeout") 646 | } 647 | } 648 | --------------------------------------------------------------------------------