├── .github └── workflows │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── README.md ├── docker └── run.sh ├── filter.go ├── flags.go ├── go.mod ├── go.sum ├── logger.go ├── main.go ├── protocol.go ├── proxy.go └── utils.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*" 5 | name: Release new version 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: checkout 12 | uses: actions/checkout@master 13 | - uses: azure/docker-login@v1 14 | with: 15 | username: ${{ secrets.REGISTRY_USERNAME }} 16 | password: ${{ secrets.REGISTRY_PASSWORD }} 17 | - name: setup-go 18 | uses: actions/setup-go@v1 19 | with: 20 | go-version: "1.23.x" 21 | - name: goreleaser 22 | uses: goreleaser/goreleaser-action@v6 23 | with: 24 | version: latest 25 | args: release 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | logs 3 | chrome-protocol-proxy 4 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | builds: 3 | - binary: chrome-protocol-proxy 4 | ldflags: -s -extldflags "-static" -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser 5 | env: 6 | - CGO_ENABLED=0 7 | goos: 8 | - windows 9 | - darwin 10 | - linux 11 | goarch: 12 | - amd64 13 | - arm 14 | - arm64 15 | 16 | archives: 17 | - format: tar.gz 18 | wrap_in_directory: true 19 | files: 20 | - LICENSE 21 | - README.md 22 | 23 | brews: 24 | - name: chrome-protocol-proxy 25 | caveats: "Usage: chrome-protocol-proxy -i -m -r localhost:9222" 26 | homepage: "https://github.com/wendigo/chrome-protocol-proxy" 27 | description: "chrome-protocol-proxy is small reverse websocket proxy designed for chrome debugging protocol. It's purpose is to capture messages written to and received from Chrome Debugging Protocol, coalesce requests with responses, unpack messages from Target domain and provide easy to read, colored output." 28 | directory: Formula 29 | repository: 30 | owner: wendigo 31 | name: homebrew-tap 32 | 33 | dockers: 34 | - goos: linux 35 | goarch: amd64 36 | image_templates: 37 | - "wendigo/chrome-protocol-proxy:latest" 38 | - "wendigo/chrome-protocol-proxy:{{ .Tag }}" 39 | - "wendigo/chrome-protocol-proxy:v{{ .Major }}" 40 | skip_push: false 41 | dockerfile: Dockerfile 42 | build_flag_templates: 43 | - "--label=org.label-schema.schema-version=1.0" 44 | - "--label=org.label-schema.version={{.Version}}" 45 | - "--label=org.label-schema.name={{.ProjectName}}" 46 | extra_files: 47 | - docker/run.sh 48 | 49 | scoops: 50 | - name: chrome-protocol-proxy 51 | repository: 52 | owner: wendigo 53 | name: scoop-bucket 54 | homepage: "https://github.com/wendigo/chrome-protocol-proxy" 55 | description: "chrome-protocol-proxy is small reverse websocket proxy designed for chrome debugging protocol. It's purpose is to capture messages written to and received from Chrome Debugging Protocol, coalesce requests with responses, unpack messages from Target domain and provide easy to read, colored output." 56 | license: MIT 57 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM zenika/alpine-chrome:latest 2 | ENV TERM xterm-256color 3 | COPY chrome-protocol-proxy chrome-protocol-proxy 4 | COPY docker/run.sh run.sh 5 | EXPOSE 9222 9223 6 | ENTRYPOINT ["sh", "run.sh"] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Kenneth Shaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chrome-protocol-proxy 2 | 3 | ```chrome-protocol-proxy``` is small, reverse proxy designed for working with [Chrome's DevTools protocol](https://github.com/ChromeDevTools/devtools-protocol). It captures all commands sent to and events received from Chrome, coalesce requests with responses, unpack messages from [Target domain](https://chromedevtools.github.io/debugger-protocol-viewer/tot/Target/) and provide easy to read, colored output. This tool is a fork of (and heavily inspired by) [chromedp-proxy](https://github.com/chromedp/chromedp-proxy). 4 | 5 | ![chrome-protocol-proxy screenshot](https://pbs.twimg.com/media/C9nifD2WsAEkl4s.jpg:large) 6 | 7 | # Installation 8 | 9 | ## Via homebrew 10 | 11 | ```brew install wendigo/tap/chrome-protocol-proxy``` 12 | 13 | ## Via go get 14 | 15 | ```go get -u github.com/wendigo/chrome-protocol-proxy``` 16 | 17 | ## Via docker 18 | 19 | ```docker run -t -i -p 9222:9222 wendigo/chrome-protocol-proxy:latest``` 20 | 21 | This image bundles headless Chrome in the latest version so debugger is ready to use (head to [http://localhost:9222](http://localhost:9222) to validate). 22 | 23 | # Features 24 | - colored output, 25 | - protocol frames filtering,🖖 26 | - request-response coalescing, 27 | - interprets [Target.sendMessageToTarget](https://chromedevtools.github.io/debugger-protocol-viewer/tot/Target/#method-sendMessageToTarget) requests, 28 | - interprets [Target.receivedMessageFromTarget](https://chromedevtools.github.io/debugger-protocol-viewer/tot/Target/#event-receivedMessageFromTarget) responses and events with [sessionId](https://chromium.googlesource.com/chromium/src/+/237f82767da3bbdcd8d6ad3fa4449ef6a3fe8bd3), 29 | - understands flatted sessions ([crbug.com/991325](https://bugs.chromium.org/p/chromium/issues/detail?id=991325)) 30 | - calculates and displays time delta between consecutive frames, 31 | - writes logs and splits them based on connection id and target/session id. 32 | 33 | # Configuration flags 34 | ``` 35 | -d write logs file per targetId 36 | -delta 37 | show delta time between log entries 38 | -exclude value 39 | exclude requests/responses/events matching pattern (default exclude = ) 40 | -force-color 41 | force color output regardless of TTY 42 | -i include request frames as they are sent 43 | -include value 44 | display only requests/responses/events matching pattern (default include = ) 45 | -l string 46 | listen address (default "localhost:9223") 47 | -log-dir string 48 | logs directory (default "logs") 49 | -m display time in microseconds 50 | -once 51 | debug single session 52 | -q do not show logs on stdout 53 | -r string 54 | remote address (default "localhost:9222") 55 | -s max_length 56 | shorten requests and responses to max_length 57 | -version 58 | display version information 59 | ``` 60 | 61 | # Demo 62 | [![asciicast](https://asciinema.org/a/113947.png)](https://asciinema.org/a/113947?t=0:04&autoplay=1&speed=0.4) 63 | -------------------------------------------------------------------------------- /docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | chromium-browser --headless --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage --no-sandbox --remote-debugging-address=0.0.0.0 --remote-debugging-port=9223 --no-sandbox & 3 | 4 | ./chrome-protocol-proxy -l 0.0.0.0:9222 -r localhost:9223 "$@" 5 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type argumentList struct { 10 | name string 11 | values []string 12 | } 13 | 14 | func (al *argumentList) String() string { 15 | return fmt.Sprintf("%s = %s", al.name, strings.Join(al.values, ", ")) 16 | } 17 | 18 | func (al *argumentList) Set(value string) error { 19 | al.values = append(al.values, value) 20 | return nil 21 | } 22 | 23 | var filterInclude = &argumentList{name: "include", values: []string{}} 24 | var filterExclude = &argumentList{name: "exclude", values: []string{}} 25 | 26 | func init() { 27 | flag.Var(filterInclude, "include", "display only requests/responses/events matching pattern") 28 | flag.Var(filterExclude, "exclude", "exclude requests/responses/events matching pattern") 29 | } 30 | 31 | func accept(values ...string) bool { 32 | 33 | value := strings.Join(values, "") 34 | 35 | for _, exclude := range filterExclude.values { 36 | if strings.Contains(value, exclude) { 37 | return false 38 | } 39 | } 40 | 41 | if len(filterInclude.values) == 0 { 42 | return true 43 | } 44 | 45 | for _, include := range filterInclude.values { 46 | if strings.Contains(value, include) { 47 | return true 48 | } 49 | } 50 | 51 | return false 52 | } 53 | -------------------------------------------------------------------------------- /flags.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | ) 6 | 7 | var ( 8 | flagListen = flag.String("l", "localhost:9223", "listen address") 9 | flagRemote = flag.String("r", "localhost:9222", "remote address") 10 | flagEllipsis = flag.Int("s", 0, "shorten requests and responses if above length") 11 | flagOnce = flag.Bool("once", false, "debug single session") 12 | flagShowRequests = flag.Bool("i", false, "include request frames as they are sent") 13 | flagDistributeLogs = flag.Bool("d", false, "write logs file per targetId") 14 | flagQuiet = flag.Bool("q", false, "do not show logs on stdout") 15 | flagMicroseconds = flag.Bool("m", false, "display time in microseconds") 16 | flagDelta = flag.Bool("delta", false, "show delta time between log entries") 17 | flagForceColor = flag.Bool("force-color", false, "force color output regardless of TTY") 18 | flagDirLogs = flag.String("log-dir", "logs", "logs directory") 19 | flagVersion = flag.Bool("version", false, "display version information") 20 | ) 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wendigo/chrome-protocol-proxy 2 | 3 | go 1.23.5 4 | 5 | require ( 6 | github.com/fatih/color v1.18.0 7 | github.com/gorilla/websocket v1.5.3 8 | github.com/sirupsen/logrus v1.9.3 9 | ) 10 | 11 | require ( 12 | github.com/mattn/go-colorable v0.1.14 // indirect 13 | github.com/mattn/go-isatty v0.0.20 // indirect 14 | golang.org/x/sys v0.29.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 5 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 6 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 7 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 8 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 9 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 10 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 11 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 15 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 16 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 17 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 18 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 19 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 22 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 24 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 25 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 26 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "math" 8 | "os" 9 | "path/filepath" 10 | "time" 11 | 12 | "github.com/fatih/color" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | const ( 17 | typeRequest = 1 << iota 18 | typeRequestResponse = 1 << iota 19 | typeRequestResponseError = 1 << iota 20 | typeEvent = 1 << iota 21 | ) 22 | 23 | const ( 24 | levelConnection = 1 << iota 25 | levelProtocol = 1 << iota 26 | levelTarget = 1 << iota 27 | ) 28 | 29 | const ( 30 | fieldLevel = "level" 31 | fieldType = "type" 32 | fieldTargetID = "targetID" 33 | fieldRequest = "request" 34 | fieldMethod = "method" 35 | fieldInspectorID = "inspectorId" 36 | ) 37 | 38 | const ( 39 | requestReplyFormat = "%-17s %-32s % 48s %s => %s\n" 40 | requestFormat = "%-17s %-32s % 48s %s\n" 41 | eventFormat = "%-17s %-32s % 48s %s\n" 42 | protocolFormat = "%-17s %-32s\n" 43 | timeFormat = "15:04:05.00000000" 44 | deltaFormat = "Δ%8.2fms" 45 | ) 46 | 47 | var ( 48 | responseColor = color.New(color.FgHiRed).SprintfFunc() 49 | requestColor = color.New(color.FgHiBlue).SprintFunc() 50 | requestReplyColor = color.New(color.FgHiWhite).SprintfFunc() 51 | eventsColor = color.New(color.FgGreen).SprintfFunc() 52 | eventsLabelColor = color.New(color.FgCyan).SprintfFunc() 53 | protocolColor = color.New(color.FgYellow).SprintfFunc() 54 | protocolError = color.New(color.FgHiYellow, color.BgRed).SprintfFunc() 55 | targetColor = color.New(color.FgHiWhite).SprintfFunc() 56 | methodColor = color.New(color.FgHiYellow).SprintfFunc() 57 | errorColor = color.New(color.BgRed, color.FgWhite).SprintfFunc() 58 | protocolTargetID = center("browser", 32) 59 | ) 60 | 61 | type FramesFormatter struct { 62 | lastTime int64 63 | } 64 | 65 | func (f *FramesFormatter) Format(e *logrus.Entry) ([]byte, error) { 66 | message := e.Message 67 | var timestamp string 68 | 69 | if *flagMicroseconds { 70 | timestamp = fmt.Sprintf("%d", e.Time.UnixNano()/int64(time.Millisecond)) 71 | } else { 72 | timestamp = e.Time.Format(timeFormat) 73 | } 74 | 75 | if *flagDelta { 76 | var delta string 77 | 78 | if f.lastTime == 0 { 79 | delta = fmt.Sprintf(deltaFormat, 0.00) 80 | } else { 81 | delta = fmt.Sprintf(deltaFormat, math.Abs(float64(e.Time.UnixNano()-f.lastTime)/float64(time.Millisecond))) 82 | } 83 | 84 | f.lastTime = e.Time.UnixNano() 85 | timestamp = fmt.Sprintf("%s %s", timestamp, delta) 86 | } 87 | 88 | var protocolType = -1 89 | var protocolMethod = "" 90 | 91 | protocolLevel := e.Data[fieldLevel].(int) 92 | 93 | if val, ok := e.Data[fieldType].(int); ok { 94 | protocolType = val 95 | } 96 | 97 | if val, ok := e.Data[fieldMethod].(string); ok { 98 | protocolMethod = val 99 | } 100 | 101 | if !accept(protocolMethod, message) { 102 | return []byte{}, nil 103 | } 104 | 105 | switch protocolLevel { 106 | case levelConnection: 107 | switch e.Level { 108 | case logrus.ErrorLevel: 109 | return []byte(fmt.Sprintf(protocolFormat, timestamp, errorColor(message))), nil 110 | case logrus.InfoLevel: 111 | return []byte(fmt.Sprintf(protocolFormat, timestamp, protocolColor(message))), nil 112 | } 113 | 114 | case levelProtocol, levelTarget: 115 | targetID := e.Data[fieldTargetID].(string) 116 | 117 | switch protocolType { 118 | case typeEvent: 119 | return []byte(fmt.Sprintf(eventFormat, timestamp, targetColor(targetID), eventsLabelColor(protocolMethod), eventsColor(message))), nil 120 | 121 | case typeRequest: 122 | return []byte(fmt.Sprintf(requestFormat, timestamp, targetColor(targetID), methodColor(protocolMethod), requestColor(message))), nil 123 | 124 | case typeRequestResponse: 125 | return []byte(fmt.Sprintf(requestReplyFormat, timestamp, targetColor(targetID), methodColor(protocolMethod), requestReplyColor(e.Data[fieldRequest].(string)), responseColor(message))), nil 126 | 127 | case typeRequestResponseError: 128 | return []byte(fmt.Sprintf(requestReplyFormat, timestamp, targetColor(targetID), methodColor(protocolMethod), requestReplyColor(e.Data[fieldRequest].(string)), errorColor(message))), nil 129 | } 130 | } 131 | 132 | return []byte(fmt.Sprintf("unsupported entry: %+v", e)), nil 133 | } 134 | 135 | type multiWriter struct { 136 | io.Writer 137 | writers []io.Writer 138 | } 139 | 140 | func newMultiWriter(writers ...io.Writer) *multiWriter { 141 | return &multiWriter{ 142 | Writer: io.MultiWriter(writers...), 143 | writers: writers, 144 | } 145 | } 146 | 147 | func (m *multiWriter) Close() (err error) { 148 | for _, writer := range m.writers { 149 | if v, ok := writer.(io.Closer); ok && v != os.Stdout { 150 | v.Close() 151 | } 152 | } 153 | 154 | return nil 155 | } 156 | 157 | var loggers = make(map[string]*logrus.Logger) 158 | 159 | func createLogWriter(filename string) (io.Writer, error) { 160 | 161 | if filename == "" { 162 | if *flagQuiet { 163 | return ioutil.Discard, nil 164 | } 165 | 166 | return os.Stdout, nil 167 | } 168 | 169 | logFilePath := fmt.Sprintf(*flagDirLogs+"/%s.log", filename) 170 | dir := filepath.Dir(logFilePath) 171 | 172 | if _, err := os.Stat(dir); err != nil { 173 | if err := os.MkdirAll(dir, os.ModePerm); err != nil { 174 | return nil, err 175 | } 176 | } 177 | 178 | logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm) 179 | if err != nil { 180 | return nil, err 181 | } 182 | 183 | if *flagQuiet { 184 | return newMultiWriter(logFile), nil 185 | } 186 | 187 | return newMultiWriter(logFile, os.Stdout), nil 188 | } 189 | 190 | func createLogger(name string) (*logrus.Logger, error) { 191 | if *flagForceColor { 192 | color.NoColor = false 193 | } 194 | 195 | if _, exists := loggers[name]; !exists { 196 | writer, err := createLogWriter(name) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | loggers[name] = &logrus.Logger{ 202 | Out: writer, 203 | Formatter: new(FramesFormatter), 204 | Hooks: make(logrus.LevelHooks), 205 | Level: logrus.DebugLevel, 206 | } 207 | } 208 | 209 | return loggers[name], nil 210 | } 211 | 212 | func destroyLogger(name string) error { 213 | if logger, exists := loggers[name]; exists { 214 | if closer, ok := logger.Out.(io.Closer); ok { 215 | closer.Close() 216 | } 217 | 218 | delete(loggers, name) 219 | } 220 | 221 | return nil 222 | } 223 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // This software is direct fork of https://github.com/knq/chromedp/tree/master/cmd/chromedp-proxy 2 | // with couple of features added 3 | package main 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "flag" 9 | "fmt" 10 | "log" 11 | "net/http" 12 | "net/http/httputil" 13 | "net/url" 14 | "os" 15 | "path" 16 | "strconv" 17 | "strings" 18 | 19 | "errors" 20 | 21 | "github.com/sirupsen/logrus" 22 | ) 23 | 24 | var ( 25 | version string = "unknown" 26 | commit string = "unknown" 27 | date string = "unknown" 28 | builtBy string = "unknown" 29 | ) 30 | 31 | func main() { 32 | flag.Parse() 33 | 34 | if *flagVersion { 35 | fmt.Printf("%s version %s built on %s by %s\n\nConfiguration:\n", os.Args[0], version, date, builtBy) 36 | flag.PrintDefaults() 37 | os.Exit(1) 38 | } 39 | 40 | mux := http.NewServeMux() 41 | 42 | simpleReverseProxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: *flagRemote}) 43 | 44 | mux.Handle("/json", simpleReverseProxy) 45 | mux.Handle("/", simpleReverseProxy) 46 | 47 | rootLogger, err := createLogger("connection") 48 | if err != nil { 49 | panic(fmt.Sprintf("could not create logger: %s", err)) 50 | } 51 | 52 | logger := rootLogger.WithFields(logrus.Fields{ 53 | fieldLevel: levelConnection, 54 | }) 55 | 56 | handlerFunc := func(basePath string) func(http.ResponseWriter, *http.Request) { 57 | return func(res http.ResponseWriter, req *http.Request) { 58 | 59 | stream := make(chan *protocolMessage, 1024) 60 | id := strings.ReplaceAll(strings.TrimPrefix(req.URL.Path, "/devtools/"), "/", "-") 61 | 62 | var protocolLogger *logrus.Entry 63 | 64 | if *flagDistributeLogs { 65 | logger, err := createLogger(id) 66 | if err != nil { 67 | panic(fmt.Sprintf("could not create logger: %s", err)) 68 | } 69 | 70 | protocolLogger = logger.WithFields(logrus.Fields{ 71 | fieldLevel: levelConnection, 72 | fieldInspectorID: id, 73 | }) 74 | 75 | } else { 76 | protocolLogger = logger.WithFields(logrus.Fields{ 77 | fieldInspectorID: id, 78 | }) 79 | } 80 | 81 | go dumpStream(protocolLogger, stream) 82 | 83 | endpoint := "ws://" + *flagRemote + "/devtools/" + basePath + "/" + path.Base(req.URL.Path) 84 | 85 | logger.Infof("---------- connection from %s to %s ----------", req.RemoteAddr, req.RequestURI) 86 | logger.Infof("checking protocol versions on: %s", endpoint) 87 | 88 | ver, err := checkVersion() 89 | if err != nil { 90 | protocolLogger.Errorf("could not check version: %v", err) 91 | http.Error(res, "could not check version", 500) 92 | return 93 | } 94 | 95 | logger.Infof("protocol version: %s", ver["Protocol-Version"]) 96 | logger.Infof("versions: Chrome(%s), V8(%s), Webkit(%s)", ver["Browser"], ver["V8-Version"], ver["WebKit-Version"]) 97 | logger.Infof("browser user agent: %s", ver["User-Agent"]) 98 | logger.Infof("connecting to %s... ", endpoint) 99 | 100 | // connecting to ws 101 | out, pres, err := wsDialer.Dial(endpoint, nil) 102 | if err != nil { 103 | msg := fmt.Sprintf("could not connect to %s: %v", endpoint, err) 104 | logger.Error(protocolError(msg)) 105 | http.Error(res, msg, 500) 106 | return 107 | } 108 | defer pres.Body.Close() 109 | defer out.Close() 110 | 111 | // connect incoming websocket 112 | logger.Infof("upgrading connection on %s...", req.RemoteAddr) 113 | in, err := wsUpgrader.Upgrade(res, req, nil) 114 | if err != nil { 115 | logger.Errorf("could not upgrade websocket from %s: %v", req.RemoteAddr, err) 116 | http.Error(res, "could not upgrade websocket connection", 500) 117 | return 118 | } 119 | defer in.Close() 120 | 121 | ctxt, cancel := context.WithCancel(context.Background()) 122 | defer cancel() 123 | 124 | errc := make(chan error, 1) 125 | go proxyWS(ctxt, stream, in, out, errc) 126 | go proxyWS(ctxt, stream, out, in, errc) 127 | 128 | <-errc 129 | close(stream) 130 | 131 | logger.Infof("---------- closing connection from %s to %s ----------", req.RemoteAddr, req.RequestURI) 132 | 133 | if *flagDistributeLogs { 134 | destroyLogger(id) 135 | } 136 | 137 | if *flagOnce { 138 | os.Exit(0) 139 | } 140 | } 141 | } 142 | 143 | mux.HandleFunc("/devtools/page/", handlerFunc("page")) 144 | mux.HandleFunc("/devtools/browser/", handlerFunc("browser")) 145 | 146 | log.Printf("Proxy is listening for DevTools connections on: %s", *flagListen) 147 | 148 | log.Fatal(http.ListenAndServe(*flagListen, mux)) 149 | } 150 | 151 | func dumpStream(logger *logrus.Entry, stream chan *protocolMessage) { 152 | logger.Printf("Legend: %s, %s, %s, %s, %s, %s", protocolColor("protocol informations"), 153 | eventsColor("received events"), 154 | requestColor("sent request frames"), 155 | requestReplyColor("requests params"), 156 | responseColor("received responses"), 157 | errorColor("error response."), 158 | ) 159 | 160 | requests := make(map[uint64]*protocolMessage) 161 | sessions := make(map[string]map[uint64]*protocolMessage) 162 | 163 | loop: 164 | for { 165 | select { 166 | case msg, ok := <-stream: 167 | if !ok { 168 | for sessionId := range sessions { 169 | _ = destroyLogger(fmt.Sprintf("session-%s", sessionId)) 170 | } 171 | break loop 172 | } 173 | 174 | if msg.HasSessionId() { 175 | var targetLogger *logrus.Entry 176 | 177 | targetRequests, exists := sessions[msg.TargetID()] 178 | if !exists { 179 | targetRequests = make(map[uint64]*protocolMessage) 180 | sessions[msg.TargetID()] = targetRequests 181 | } 182 | 183 | if *flagDistributeLogs { 184 | logger, err := createLogger(fmt.Sprintf("session-%s", msg.TargetID())) 185 | 186 | if err != nil { 187 | panic(fmt.Sprintf("could not create logger: %v", err)) 188 | } 189 | 190 | targetLogger = logger.WithFields(logrus.Fields{ 191 | fieldLevel: levelTarget, 192 | fieldTargetID: msg.TargetID(), 193 | }) 194 | 195 | } else { 196 | targetLogger = logger.WithFields(logrus.Fields{ 197 | fieldLevel: levelTarget, 198 | fieldTargetID: msg.TargetID(), 199 | }) 200 | } 201 | 202 | if msg.IsRequest() { 203 | requests[msg.ID] = nil 204 | 205 | if protocolMessage, err := decodeProtocolMessage(msg); err == nil { 206 | targetRequests[protocolMessage.ID] = protocolMessage 207 | 208 | if *flagShowRequests { 209 | targetLogger.WithFields(logrus.Fields{ 210 | fieldType: typeRequest, 211 | fieldMethod: protocolMessage.Method + "-(" + strconv.FormatUint(msg.ID, 10) + ")", 212 | }).Info(serialize(protocolMessage.Params)) 213 | } 214 | 215 | } else { 216 | logger.WithFields(logrus.Fields{ 217 | fieldLevel: levelConnection, 218 | }).Errorf("Could not deserialize message: %+v", err) 219 | } 220 | } else if msg.IsEvent() { 221 | if protocolMessage, err := decodeProtocolMessage(msg); err == nil { 222 | if protocolMessage.IsEvent() { 223 | targetLogger.WithFields(logrus.Fields{ 224 | fieldType: typeEvent, 225 | fieldMethod: protocolMessage.Method, 226 | }).Info(serialize(protocolMessage.Params)) 227 | } else if protocolMessage.IsResponse() { 228 | var logMessage string 229 | var logType int 230 | var logRequest string 231 | var logMethod string 232 | 233 | if protocolMessage.IsError() { 234 | logMessage = serialize(protocolMessage.Error) 235 | logType = typeRequestResponseError 236 | } else { 237 | logMessage = serialize(protocolMessage.Result) 238 | logType = typeRequestResponse 239 | } 240 | 241 | if request, ok := targetRequests[protocolMessage.ID]; ok && request != nil { 242 | delete(targetRequests, protocolMessage.ID) 243 | logRequest = serialize(request.Params) 244 | logMethod = request.Method 245 | 246 | } else { 247 | logRequest = errorColor("could not find request with id: %d", protocolMessage.ID) 248 | } 249 | 250 | if *flagShowRequests { 251 | logMethod += "*(" + strconv.FormatUint(msg.ID, 10) + ")" 252 | } else { 253 | logMethod += "*" 254 | } 255 | 256 | targetLogger.WithFields(logrus.Fields{ 257 | fieldType: logType, 258 | fieldMethod: logMethod, 259 | fieldRequest: logRequest, 260 | }).Info(logMessage) 261 | } else { 262 | targetLogger.WithFields(logrus.Fields{ 263 | fieldType: typeRequest, 264 | fieldMethod: msg.Method, 265 | }).Info("Could not understand session event: " + msg.raw) 266 | } 267 | } else { 268 | logger.WithFields(logrus.Fields{ 269 | fieldLevel: levelConnection, 270 | }).Errorf("Could not deserialize message: %+v", err) 271 | } 272 | } else if msg.IsResponse() { 273 | var logMessage string 274 | var logType int 275 | var logRequest string 276 | var logMethod string 277 | 278 | if msg.IsError() { 279 | logMessage = serialize(msg.Error) 280 | logType = typeRequestResponseError 281 | } else { 282 | logMessage = serialize(msg.Result) 283 | logType = typeRequestResponse 284 | } 285 | 286 | if request, ok := targetRequests[msg.ID]; ok && request != nil { 287 | delete(targetRequests, msg.ID) 288 | logRequest = serialize(request.Params) 289 | logMethod = request.Method 290 | 291 | } else { 292 | logRequest = errorColor("could not find request with id: %d", msg.ID) 293 | } 294 | 295 | if *flagShowRequests { 296 | logMethod += "*(" + strconv.FormatUint(msg.ID, 10) + ")" 297 | } else { 298 | logMethod += "*" 299 | } 300 | 301 | targetLogger.WithFields(logrus.Fields{ 302 | fieldType: logType, 303 | fieldMethod: logMethod, 304 | fieldRequest: logRequest, 305 | }).Info(logMessage) 306 | 307 | } else { 308 | targetLogger.WithFields(logrus.Fields{ 309 | fieldType: typeRequest, 310 | fieldMethod: msg.Method, 311 | }).Info("Could not understand session message: " + msg.raw) 312 | } 313 | 314 | } else { 315 | protocolLogger := logger.WithFields(logrus.Fields{ 316 | fieldLevel: levelProtocol, 317 | fieldTargetID: protocolTargetID, 318 | }) 319 | 320 | if msg.IsRequest() { 321 | requests[msg.ID] = msg 322 | 323 | if *flagShowRequests { 324 | protocolLogger.WithFields(logrus.Fields{ 325 | fieldType: typeRequest, 326 | fieldMethod: msg.Method + "-(" + strconv.FormatUint(msg.ID, 10) + ")", 327 | }).Info(serialize(msg.Params)) 328 | } 329 | } else if msg.IsResponse() { 330 | 331 | var logMessage string 332 | var logType int 333 | var logRequest string 334 | var logMethod string 335 | 336 | if msg.IsError() { 337 | logMessage = serialize(msg.Error) 338 | logType = typeRequestResponseError 339 | } else { 340 | logMessage = serialize(msg.Result) 341 | logType = typeRequestResponse 342 | } 343 | 344 | if request, ok := requests[msg.ID]; ok && request != nil { 345 | logRequest = serialize(request.Params) 346 | logMethod = request.Method 347 | 348 | delete(requests, msg.ID) 349 | 350 | protocolLogger.WithFields(logrus.Fields{ 351 | fieldType: logType, 352 | fieldMethod: logMethod, 353 | fieldRequest: logRequest, 354 | }).Info(logMessage) 355 | } 356 | } else if msg.IsEvent() { 357 | protocolLogger.WithFields(logrus.Fields{ 358 | fieldType: typeEvent, 359 | fieldMethod: msg.Method, 360 | }).Info(serialize(msg.Params)) 361 | } else { 362 | protocolLogger.WithFields(logrus.Fields{ 363 | fieldType: typeRequest, 364 | fieldMethod: msg.Method, 365 | }).Info("Could not understand message: " + msg.raw) 366 | } 367 | } 368 | } 369 | } 370 | } 371 | 372 | func checkVersion() (map[string]string, error) { 373 | cl := &http.Client{} 374 | req, err := http.NewRequest("GET", "http://"+*flagRemote+"/json/version", nil) 375 | if err != nil { 376 | return nil, err 377 | } 378 | 379 | res, err := cl.Do(req) 380 | if err != nil { 381 | return nil, err 382 | } 383 | 384 | defer res.Body.Close() 385 | 386 | var v map[string]string 387 | if err := json.NewDecoder(res.Body).Decode(&v); err != nil { 388 | return nil, errors.New("expected json result") 389 | } 390 | 391 | return v, nil 392 | } 393 | -------------------------------------------------------------------------------- /protocol.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | type protocolMessage struct { 6 | /** 7 | The raw message as string. 8 | */ 9 | raw string 10 | ID uint64 `json:"id"` 11 | Result map[string]interface{} `json:"result"` 12 | Error struct { 13 | Code int64 `json:"code"` 14 | Message string `json:"message"` 15 | Data string `json:"data"` 16 | } `json:"error"` 17 | Method string `json:"method"` 18 | Params map[string]interface{} `json:"params"` 19 | SessionId string `json:"sessionId"` 20 | } 21 | 22 | func (p *protocolMessage) String() string { 23 | return fmt.Sprintf( 24 | "protocolMessage{id=%d, method=%s, sessionId=%s, result=%+v, error=%+v, params=%+v}", 25 | p.ID, 26 | p.Method, 27 | p.SessionId, 28 | p.Result, 29 | p.Error, 30 | p.Params, 31 | ) 32 | } 33 | 34 | func (p *protocolMessage) IsError() bool { 35 | return p.Error.Code != 0 36 | } 37 | 38 | func (p *protocolMessage) IsResponse() bool { 39 | return p.Result != nil && p.ID > 0 40 | } 41 | 42 | func (p *protocolMessage) IsRequest() bool { 43 | return p.Method != "" && p.ID > 0 44 | } 45 | 46 | func (p *protocolMessage) IsEvent() bool { 47 | return !(p.IsRequest() || p.IsResponse()) 48 | } 49 | 50 | func (p *protocolMessage) FromTargetDomain() bool { 51 | return p.Method == "Target.sendMessageToTarget" || p.Method == "Target.receivedMessageFromTarget" 52 | } 53 | 54 | func (p *protocolMessage) HasSessionId() bool { 55 | return p.FromTargetDomain() || p.IsFlatten() 56 | } 57 | 58 | func (p *protocolMessage) IsFlatten() bool { 59 | return p.SessionId != "" 60 | } 61 | 62 | func (p *protocolMessage) TargetID() string { 63 | if p.SessionId != "" { 64 | return p.SessionId 65 | } 66 | 67 | if p.FromTargetDomain() { 68 | if val, ok := p.Params["sessionId"]; ok { 69 | return val.(string) 70 | } 71 | } 72 | 73 | return "" 74 | } 75 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "github.com/gorilla/websocket" 6 | "net/http" 7 | ) 8 | 9 | const ( 10 | incomingBufferSize = 10 * 1024 * 1024 11 | outgoingBufferSize = 25 * 1024 * 1024 12 | ) 13 | 14 | var wsUpgrader = &websocket.Upgrader{ 15 | ReadBufferSize: incomingBufferSize, 16 | WriteBufferSize: outgoingBufferSize, 17 | CheckOrigin: func(r *http.Request) bool { 18 | return true 19 | }, 20 | } 21 | 22 | var wsDialer = &websocket.Dialer{ 23 | ReadBufferSize: outgoingBufferSize, 24 | WriteBufferSize: incomingBufferSize, 25 | } 26 | 27 | func proxyWS(ctxt context.Context, stream chan *protocolMessage, in, out *websocket.Conn, errc chan error) { 28 | var mt int 29 | var buf []byte 30 | var err error 31 | 32 | for { 33 | select { 34 | default: 35 | mt, buf, err = in.ReadMessage() 36 | if err != nil { 37 | errc <- err 38 | return 39 | } 40 | 41 | if msg, derr := decodeMessage(buf); derr == nil { 42 | stream <- msg 43 | } 44 | 45 | err = out.WriteMessage(mt, buf) 46 | 47 | if err != nil { 48 | errc <- err 49 | return 50 | } 51 | 52 | case <-ctxt.Done(): 53 | return 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | func center(message string, length int) string { 10 | padding := (length - len(message)) / 2 11 | 12 | if padding < 0 { 13 | return message 14 | } 15 | 16 | return strings.Repeat(" ", padding) + message + strings.Repeat(" ", length-len(message)-padding) 17 | } 18 | 19 | func asString(value interface{}) string { 20 | if casted, ok := value.(string); ok { 21 | return casted 22 | } 23 | 24 | return fmt.Sprintf("%+v", value) 25 | } 26 | 27 | func serialize(value interface{}) string { 28 | 29 | buff, err := json.Marshal(value) 30 | if err == nil { 31 | if *flagEllipsis != 0 && len(buff) > *flagEllipsis { 32 | return string(buff[:*flagEllipsis]) + "..." 33 | } 34 | 35 | serialized := string(buff) 36 | 37 | if serialized == "null" { 38 | return "{}" 39 | } 40 | 41 | return serialized 42 | } 43 | 44 | return err.Error() 45 | } 46 | 47 | func decodeMessage(bytes []byte) (*protocolMessage, error) { 48 | var msg protocolMessage 49 | 50 | if err := json.Unmarshal(bytes, &msg); err != nil { 51 | 52 | return nil, err 53 | } 54 | 55 | msg.raw = string(bytes[:]) 56 | return &msg, nil 57 | } 58 | 59 | func decodeProtocolMessage(message *protocolMessage) (*protocolMessage, error) { 60 | if message.IsFlatten() { 61 | return message, nil 62 | } 63 | 64 | if message.FromTargetDomain() { 65 | return decodeMessage([]byte(asString(message.Params["message"]))) 66 | } 67 | 68 | return message, nil 69 | } 70 | --------------------------------------------------------------------------------