diff --git a/docs/devtools.adoc b/docs/devtools.adoc new file mode 100644 index 0000000..aaf1441 --- /dev/null +++ b/docs/devtools.adoc @@ -0,0 +1,6 @@ +== Proxying Browser Developer Tools + +Similarly to proxying VNC traffic Ggr is able to proxy browser developer tools traffic in **running** Selenium session. To access developer tools - just use the following web socket URL: + + ws://ggr-host.example.com:4444/devtools/ + diff --git a/docs/index.adoc b/docs/index.adoc index 40ba8d0..a7e41cc 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -27,6 +27,7 @@ include::quota-reload.adoc[leveloffset=+1] include::video.adoc[leveloffset=+1] include::logs.adoc[leveloffset=+1] include::download.adoc[leveloffset=+1] +include::devtools.adoc[leveloffset=+1] include::tls.adoc[leveloffset=+1] include::how-it-works.adoc[leveloffset=+1] include::multiple-instances.adoc[leveloffset=+1] diff --git a/docs/log-files.adoc b/docs/log-files.adoc index ee462f7..80115ab 100644 --- a/docs/log-files.adoc +++ b/docs/log-files.adoc @@ -58,15 +58,16 @@ The following statuses are available: | SESSION_FAILED | Session attempt on specified host failed | SESSION_NOT_CREATED | Attempts to create a new session on all hosts failed. An error was returned to user. | SHUTTING_DOWN | Server is shutting down and waiting graceful shutdown timeout for currently proxied requests to finish +| UNKNOWN_DEVTOOLS_HOST | Requested to proxy devtools to host not present in quota | UNKNOWN_DOWNLOAD_HOST | Requested to proxy downloaded file to host not present in quota | UNKNOWN_LOG_HOST | Requested to proxy log to host not present in quota | UNKNOWN_VNC_HOST | Requested to proxy VNC to host not present in quota | UNKNOWN_VIDEO_HOST | Requested to proxy video to host not present in quota | UNSUPPORTED_BROWSER | Requested browser name and version is not present in quota | UNSUPPORTED_HOST_VNC_SCHEME | Invalid URL protocol specified for host in quota VNC configuration (should be vnc:// or ws://) -| VNC_CLIENT_DISCONNECTED | Client disconnected from VNC API -| VNC_ERROR | An error occurred when trying to proxy VNC traffic -| VNC_SESSION_CLOSED | Client closed VNC session +| WS_CLIENT_DISCONNECTED | Client disconnected from websocket API +| WS_ERROR | An error occurred when trying to proxy websocket traffic +| WS_SESSION_CLOSED | Client closed websocket session |=== === Custom Labels in Log File diff --git a/docs/tls.adoc b/docs/tls.adoc index dbac693..7895eec 100644 --- a/docs/tls.adoc +++ b/docs/tls.adoc @@ -55,7 +55,7 @@ server { add_header Access-Control-Allow-Credentials "true"; } - location ~ ^/vnc/ { + location ~ ^/(vnc|devtools)/ { proxy_pass http://ggr; proxy_http_version 1.1; proxy_read_timeout 950s; diff --git a/proxy.go b/proxy.go index 4dc9496..c2fe06c 100644 --- a/proxy.go +++ b/proxy.go @@ -14,6 +14,7 @@ import ( "net/http/httputil" "net/url" "reflect" + "strconv" "strings" "sync" "sync/atomic" @@ -39,7 +40,7 @@ const ( ) var paths = struct { - Ping, Status, Err, Host, Quota, Route, Proxy, VNC, Video, Logs, Download, Clipboard string + Ping, Status, Err, Host, Quota, Route, Proxy, VNC, Video, Logs, Download, Clipboard, Devtools string }{ Ping: "/ping", Status: "/wd/hub/status", @@ -53,6 +54,7 @@ var paths = struct { Logs: "/logs/", Download: "/download/", Clipboard: "/clipboard/", + Devtools: "/devtools/", } var keys = struct { @@ -671,9 +673,9 @@ func proxyWebSockets(id uint64, wsconn *websocket.Conn, sessionID string, host s } func proxyConn(id uint64, wsconn *websocket.Conn, conn net.Conn, err error, sessionID string, address string) { - log.Printf("[%d] [-] [PROXYING_TO_VNC] [-] [-] [-] [%s] [%s] [-] [-]\n", id, address, sessionID) + log.Printf("[%d] [-] [PROXYING_TO_WS] [-] [-] [-] [%s] [%s] [-] [-]", id, address, sessionID) if err != nil { - log.Printf("[%d] [-] [VNC_ERROR] [-] [-] [-] [%s] [%s] [-] [%v]\n", id, sessionID, address, err) + log.Printf("[%d] [-] [WS_ERROR] [-] [-] [-] [%s] [%s] [-] [%v]", id, sessionID, address, err) return } defer conn.Close() @@ -681,10 +683,33 @@ func proxyConn(id uint64, wsconn *websocket.Conn, conn net.Conn, err error, sess go func() { io.Copy(wsconn, conn) wsconn.Close() - log.Printf("[%d] [-] [VNC_SESSION_CLOSED] [-] [-] [-] [%s] [%s] [-] [-]\n", id, address, sessionID) + log.Printf("[%d] [-] [WS_SESSION_CLOSED] [-] [-] [-] [%s] [%s] [-] [-]", id, address, sessionID) }() io.Copy(conn, wsconn) - log.Printf("[%d] [-] [VNC_CLIENT_DISCONNECTED] [-] [-] [-] [%s] [%s] [-] [-]\n", id, address, sessionID) + log.Printf("[%d] [-] [WS_CLIENT_DISCONNECTED] [-] [-] [-] [%s] [%s] [-] [-]", id, address, sessionID) +} + +func devtools(wsconn *websocket.Conn) { + defer wsconn.Close() + confLock.RLock() + defer confLock.RUnlock() + + id := serial() + head := len(paths.Devtools) + tail := head + md5SumLength + path := wsconn.Request().URL.Path + if len(path) < tail { + log.Printf("[%d] [-] [INVALID_DEVTOOLS_REQUEST_URL] [-] [-] [%s] [-] [-] [-] [-]", id, path) + return + } + sum := path[head:tail] + h, ok := routes[sum] + if ok { + sessionID := strings.Split(path, "/")[2][md5SumLength:] + proxyWebSockets(id, wsconn, sessionID, h.Name, strconv.Itoa(h.Port), "/devtools") + } else { + log.Printf("[%d] [-] [UNKNOWN_DEVTOOLS_HOST] [-] [-] [-] [-] [%s] [-] [-]", id, sum) + } } func video(w http.ResponseWriter, r *http.Request) { @@ -759,5 +784,6 @@ func mux() http.Handler { mux.HandleFunc(paths.Logs, WithSuitableAuthentication(authenticator, logs)) mux.HandleFunc(paths.Download, WithSuitableAuthentication(authenticator, download)) mux.HandleFunc(paths.Clipboard, WithSuitableAuthentication(authenticator, clipboard)) + mux.Handle(paths.Devtools, websocket.Handler(devtools)) return mux } diff --git a/proxy_test.go b/proxy_test.go index 5f7ebe8..e4ff291 100644 --- a/proxy_test.go +++ b/proxy_test.go @@ -249,14 +249,14 @@ func TestProxyScreenVNCProtocol(t *testing.T) { }}}} updateQuota(user, browsers) - testDataReceived(vncHost, testData, t) + testDataReceived(vncHost, "vnc", testData, t) } -func testDataReceived(host Host, correctData string, t *testing.T) { +func testDataReceived(host Host, api string, correctData string, t *testing.T) { sessionID := host.Sum() + "123" origin := "http://localhost/" - u := fmt.Sprintf("ws://%s/vnc/%s", srv.Listener.Addr(), sessionID) + u := fmt.Sprintf("ws://%s/%s/%s", srv.Listener.Addr(), api, sessionID) ws, err := websocket.Dial(u, "", origin) AssertThat(t, err, Is{nil}) @@ -308,10 +308,40 @@ func TestProxyScreenWebSocketsProtocol(t *testing.T) { }}}} updateQuota(user, browsers) - testDataReceived(wsHost, testData, t) + testDataReceived(wsHost, "vnc", testData, t) } +func TestProxyDevtools(t *testing.T) { + test.Lock() + defer test.Unlock() + + const testData = "devtools-data" + mux := http.NewServeMux() + mux.Handle("/devtools/123", websocket.Handler(func(wsconn *websocket.Conn) { + wsconn.Write([]byte(testData)) + })) + + wsServer := httptest.NewServer(mux) + defer wsServer.Close() + + h, p, _ := net.SplitHostPort(wsServer.Listener.Addr().String()) + intPort, _ := strconv.Atoi(p) + wsHost := Host{Name: h, Port: intPort, Count: 1} + + browsers := Browsers{Browsers: []Browser{ + {Name: "browser", DefaultVersion: "1.0", Versions: []Version{ + {Number: "1.0", Regions: []Region{ + {Hosts: Hosts{ + wsHost, + }}, + }}, + }}}} + updateQuota(user, browsers) + + testDataReceived(wsHost, "devtools", testData, t) +} + func TestProxyVideoFileWithoutAuth(t *testing.T) { rsp, err := http.Get(gridrouter("/video/123"))