Skip to content

Commit 2e463b9

Browse files
authored
support multiple clients and reorg (#10)
## Problem Previously, 1+ connected CDP clients would race for the events coming from event. The backend would randomly "load balance" between them. ## Solution Copy events to each connected client. ## Also - some cleanup and reorg, still WIP
1 parent 9cc3998 commit 2e463b9

File tree

7 files changed

+461
-287
lines changed

7 files changed

+461
-287
lines changed

examples/helloworld/main.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"flag"
56
"fmt"
67
"log"
@@ -15,21 +16,26 @@ import (
1516
)
1617

1718
var (
18-
HTTP_CDP_Addr = "localhost:9229"
19-
HTTP_Server_Addr = "localhost:8081"
19+
HTTP_CDP_HostPort = "localhost:9229"
20+
HTTP_Proxy_HostPort = "localhost:8081"
2021
)
2122

2223
func main() {
23-
flag.StringVar(&HTTP_CDP_Addr, "http-cdp-addr", HTTP_CDP_Addr, "chrome devtools protocol listener address")
24-
flag.StringVar(&HTTP_Server_Addr, "http-proxy-addr", HTTP_Server_Addr, "HTTP server listener address")
24+
flag.StringVar(&HTTP_CDP_HostPort, "http-cdp-hostport", HTTP_CDP_HostPort, "chrome devtools protocol listener address(host:port)")
25+
flag.StringVar(&HTTP_Proxy_HostPort, "http-proxy-hostport", HTTP_Proxy_HostPort, "HTTP proxy listener address(host:port)")
2526
flag.Parse()
2627

2728
var eb = httpcdp.NewEventBus()
29+
var ctx = context.Background()
2830

2931
go func() {
30-
log.Printf("http.devtools: ListenAndServe.address=%q", HTTP_CDP_Addr)
31-
if err := http.ListenAndServe(HTTP_CDP_Addr, httpcdp.Devtools(eb)); err != nil {
32-
log.Fatalf("http.devtools: ListenAndServe.error=%q", err)
32+
log.Printf("devtools: http.ListenAndServe: hostport=%q", HTTP_CDP_HostPort)
33+
s := httpcdp.Server{
34+
Eventbus: eb,
35+
HostPort: HTTP_CDP_HostPort,
36+
}
37+
if err := s.ListenAndServe(ctx); err != nil {
38+
log.Fatalf("devtools: http.ListenAndServe: error=%q", err)
3339
}
3440
}()
3541

@@ -39,9 +45,9 @@ func main() {
3945
fmt.Fprintf(w, "hello world")
4046
})
4147

42-
log.Printf("http.proxy: ListenAndServe.address=%q", HTTP_Server_Addr)
43-
if err := http.ListenAndServe(HTTP_Server_Addr, cdphttp.Handler(eb, handler)); err != nil {
44-
log.Fatalf("http.proxy: ListenAndServe.error=%q", err)
48+
log.Printf("proxy: http.ListenAndServe: hostport=%q", HTTP_Proxy_HostPort)
49+
if err := http.ListenAndServe(HTTP_Proxy_HostPort, cdphttp.Handler(eb, handler)); err != nil {
50+
log.Fatalf("proxy: http.ListenAndServe: error=%q", err)
4551
}
4652
}()
4753

main/cdp-proxy/http.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"log"
7+
"net"
8+
"net/http"
9+
"net/http/httputil"
10+
"net/url"
11+
"time"
12+
)
13+
14+
var proxy = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15+
u := *r.URL
16+
u.Host = r.Host
17+
u.Scheme = "http"
18+
19+
if _, p, _ := net.SplitHostPort(r.Host); p == "443" {
20+
u.Scheme = "https"
21+
}
22+
23+
log.Printf("[proxy] %s", u.String())
24+
25+
if r.Method == http.MethodConnect {
26+
newTunnel(&u).ServeHTTP(w, r)
27+
} else {
28+
newForwardProxy(&u).ServeHTTP(w, r)
29+
}
30+
})
31+
32+
func newTunnel(u *url.URL) http.Handler {
33+
34+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
35+
httpErr := func(code int, err error) {
36+
log.Printf("[CONNECT]: error=%v", err)
37+
http.Error(w, http.StatusText(code), code)
38+
}
39+
log.Printf("[CONNECT]: start:%s", u.Host)
40+
defer log.Printf("[CONNECT]: done: %s", u.Host)
41+
42+
dconn, err := net.DialTimeout("tcp", r.Host, 5*time.Second)
43+
if err != nil {
44+
httpErr(http.StatusServiceUnavailable, err)
45+
return
46+
}
47+
48+
w.WriteHeader(http.StatusOK)
49+
if f, ok := w.(http.Flusher); ok {
50+
f.Flush()
51+
} else {
52+
log.Println("http.Flusher: unavailable")
53+
}
54+
55+
var sconn net.Conn
56+
if h, ok := w.(http.Hijacker); !ok {
57+
httpErr(http.StatusInternalServerError, fmt.Errorf("http.Hijacker: unavailable"))
58+
return
59+
} else {
60+
conn, _, err := h.Hijack()
61+
if err != nil {
62+
httpErr(http.StatusServiceUnavailable, err)
63+
return
64+
}
65+
sconn = conn
66+
}
67+
68+
type readWriteCloser interface {
69+
net.Conn
70+
CloseRead() error
71+
CloseWrite() error
72+
}
73+
74+
var src, dst = sconn.(readWriteCloser), dconn.(readWriteCloser)
75+
// TODO:
76+
var done = make(chan struct{})
77+
go func() {
78+
n, err := io.Copy(src, dst)
79+
log.Printf("src<-dst: n=%d error=%v", n, err)
80+
dst.CloseRead()
81+
src.CloseWrite()
82+
done <- struct{}{}
83+
}()
84+
go func() {
85+
n, err := io.Copy(dst, src)
86+
log.Printf("src->dst: n=%d error=%v", n, err)
87+
dst.CloseWrite()
88+
src.CloseRead()
89+
done <- struct{}{}
90+
}()
91+
<-done
92+
<-done
93+
})
94+
}
95+
96+
func newForwardProxy(target *url.URL) *httputil.ReverseProxy {
97+
return &httputil.ReverseProxy{
98+
Director: func(req *http.Request) {
99+
// TODO:
100+
req.Host = target.Host
101+
if _, ok := req.Header["User-Agent"]; !ok {
102+
req.Header.Set("User-Agent", "")
103+
}
104+
},
105+
// Transport: loggingTransport(http.DefaultTransport.RoundTrip),
106+
}
107+
}
108+
109+
type loggingTransport func(*http.Request) (*http.Response, error)
110+
111+
func (lt loggingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
112+
bs, _ := httputil.DumpRequestOut(r, false)
113+
log.Println("REQ:", string(bs))
114+
re, err := lt(r)
115+
bs, _ = httputil.DumpResponse(re, false)
116+
log.Println("RES:", string(bs))
117+
return re, err
118+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package httpcdp
2+
3+
import (
4+
"bytes"
5+
"sync"
6+
)
7+
8+
type bodyStore struct {
9+
m sync.Map
10+
}
11+
12+
func newStore() *bodyStore {
13+
return &bodyStore{}
14+
}
15+
16+
func (bs *bodyStore) Load(key string) (*bytes.Buffer, bool) {
17+
val, ok := bs.m.Load(key)
18+
if !ok {
19+
return nil, false
20+
}
21+
if buf, ok := val.(*bytes.Buffer); ok {
22+
return buf, true
23+
}
24+
return nil, false
25+
}
26+
27+
func (bs *bodyStore) LoadOrStore(key string, buf *bytes.Buffer) (actual *bytes.Buffer, loaded bool) {
28+
val, ok := bs.m.LoadOrStore(key, buf)
29+
return val.(*bytes.Buffer), ok
30+
}

main/cdp-proxy/httpcdp/cdp.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package httpcdp
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"encoding/json"
7+
"fmt"
8+
"log"
9+
"net/http"
10+
11+
"github.com/gorilla/websocket"
12+
)
13+
14+
// devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=localhost:9229/cdp-proxy
15+
16+
// https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27
17+
// conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf(`{"method": "Page.disable","params":{}}`)))
18+
func handleCDP(ctx context.Context, conn *websocket.Conn, e event) error {
19+
switch m := e.Method; {
20+
case m == "Page.canScreencast" ||
21+
m == "Network.canEmulateNetworkConditions" ||
22+
m == "Emulation.canEmulate":
23+
respond(conn, e.ID, `{"result":false}`)
24+
case m == "Page.getResourceTree":
25+
// Window decoration
26+
respond(conn, e.ID,
27+
`{"frameTree": { "frame":{"id":1,"url":"http://cdp-proxy","mimeType":"other"},"childFrames":[],"resources":[]}}`,
28+
)
29+
case m == "Network.getResponseBody":
30+
params, ok := e.Params.(map[string]interface{})
31+
if !ok {
32+
return nil
33+
}
34+
e.reqID, ok = params["requestId"].(string)
35+
if !ok {
36+
return nil
37+
}
38+
39+
if buf, ok := store.Load(e.reqID); !ok {
40+
respond(conn, e.ID, `{"body":"","base64Encoded":true}`)
41+
} else {
42+
result := map[string]interface{}{
43+
"base64Encoded": true,
44+
"body": base64.StdEncoding.Strict().EncodeToString(buf.Bytes()),
45+
}
46+
47+
data, err := json.Marshal(result)
48+
if err != nil {
49+
log.Printf("json.Marshal: error=%q", err)
50+
return nil
51+
}
52+
53+
// https://chromedevtools.github.io/devtools-protocol/1-2/Network#method-getResponseBody
54+
respond(conn, e.ID, string(data))
55+
}
56+
default:
57+
respond(conn, e.ID, `{}`)
58+
}
59+
60+
return nil
61+
}
62+
63+
func (s *Server) metadata(w http.ResponseWriter, r *http.Request) {
64+
// NOTE:
65+
// devtoolsFrontendUrl is not respected when opened from `chrome://inspect`
66+
var (
67+
hostPort = s.HostPort
68+
wsURL = hostPort + "/cdp"
69+
)
70+
fmt.Fprintf(w, `[{
71+
"id": "cdp-proxy",
72+
"title": "cdp-proxy",
73+
"type": "proxy",
74+
"description": "cdp-proxy requests",
75+
"faviconUrl": "https://nodejs.org/static/favicon.ico",
76+
"url": %q,
77+
"devtoolsFrontendUrl": "ws=%s",
78+
"webSocketDebuggerUrl": "ws://%s"
79+
}]`, hostPort, wsURL, wsURL)
80+
}
81+
82+
func writeConn(conn *websocket.Conn, p []byte) (int, error) {
83+
log.Printf("[CDP<-] %.120s", string(p))
84+
return len(p), conn.WriteMessage(websocket.TextMessage, p)
85+
}
86+
87+
func respond(conn *websocket.Conn, id int, p string) (int, error) {
88+
return writeConn(conn, []byte(fmt.Sprintf(`{"id":%d,"result":%s}`, id, p)))
89+
}

0 commit comments

Comments
 (0)