diff --git a/allocate.go b/allocate.go index 1ae5254f..2c9d7365 100644 --- a/allocate.go +++ b/allocate.go @@ -478,10 +478,20 @@ func CombinedOutput(w io.Writer) ExecAllocatorOption { // NewRemoteAllocator creates a new context set up with a RemoteAllocator, // suitable for use with NewContext. The url should point to the browser's // websocket address, such as "ws://127.0.0.1:$PORT/devtools/browser/...". +// If the url does not contain "/devtools/browser/", it will try to detect +// the correct one by sending a request to "http://$HOST:$PORT/json/version". +// +// The url with the following formats are accepted: +// * ws://127.0.0.1:9222/ +// * http://127.0.0.1:9222/ +// +// But "ws://127.0.0.1:9222/devtools/browser/" are not accepted. +// Because it contains "/devtools/browser/" and will be considered +// as a valid websocket debugger URL. func NewRemoteAllocator(parent context.Context, url string) (context.Context, context.CancelFunc) { ctx, cancel := context.WithCancel(parent) c := &Context{Allocator: &RemoteAllocator{ - wsURL: url, + wsURL: detectURL(url), }} ctx = context.WithValue(ctx, contextKey{}, c) return ctx, cancel diff --git a/allocate_test.go b/allocate_test.go index 892826a0..f2132b1d 100644 --- a/allocate_test.go +++ b/allocate_test.go @@ -5,8 +5,10 @@ import ( "context" "fmt" "io/ioutil" + "net" "net/http" "net/http/httptest" + "net/url" "os" "os/exec" "strings" @@ -122,6 +124,55 @@ func TestSkipNewContext(t *testing.T) { func TestRemoteAllocator(t *testing.T) { t.Parallel() + tests := []struct { + name string + modifyURL func(wsURL string) string + }{ + { + name: "original wsURL", + modifyURL: func(wsURL string) string { return wsURL }, + }, + { + name: "detect from ws", + modifyURL: func(wsURL string) string { + return wsURL[0:strings.Index(wsURL, "devtools")] + }, + }, + { + name: "detect from http", + modifyURL: func(wsURL string) string { + return "http" + wsURL[2:strings.Index(wsURL, "devtools")] + }, + }, + { + name: "hostname", + modifyURL: func(wsURL string) string { + h, err := os.Hostname() + if err != nil { + t.Fatal(err) + } + u, err := url.Parse(wsURL) + if err != nil { + t.Fatal(err) + } + _, post, err := net.SplitHostPort(u.Host) + if err != nil { + t.Fatal(err) + } + u.Host = net.JoinHostPort(h, post) + u.Path = "/" + return u.String() + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testRemoteAllocator(t, tt.modifyURL) + }) + } +} + +func testRemoteAllocator(t *testing.T, modifyURL func(wsURL string) string) { tempDir, err := ioutil.TempDir("", "chromedp-runner") if err != nil { t.Fatal(err) @@ -140,6 +191,7 @@ func TestRemoteAllocator(t *testing.T) { // TODO: perhaps deduplicate this code with ExecAllocator "--user-data-dir="+tempDir, + "--remote-debugging-address=0.0.0.0", "--remote-debugging-port=0", "about:blank", ) @@ -156,7 +208,7 @@ func TestRemoteAllocator(t *testing.T) { if err != nil { t.Fatal(err) } - allocCtx, allocCancel := NewRemoteAllocator(context.Background(), wsURL) + allocCtx, allocCancel := NewRemoteAllocator(context.Background(), modifyURL(wsURL)) defer allocCancel() taskCtx, taskCancel := NewContext(allocCtx, @@ -356,7 +408,7 @@ func TestModifyCmdFunc(t *testing.T) { allocCtx, cancel := NewExecAllocator(context.Background(), append([]ExecAllocatorOption{ ModifyCmdFunc(func(cmd *exec.Cmd) { - cmd.Env = append(cmd.Env, "TZ=" + tz) + cmd.Env = append(cmd.Env, "TZ="+tz) }), }, allocOpts...)...) defer cancel() @@ -375,4 +427,3 @@ func TestModifyCmdFunc(t *testing.T) { t.Fatalf("got %s, want %s", ret, tz) } } - diff --git a/util.go b/util.go index 1c2e1bf1..2e56aca9 100644 --- a/util.go +++ b/util.go @@ -1,8 +1,11 @@ package chromedp import ( + "encoding/json" "net" + "net/http" "net/url" + "strings" "github.com/chromedp/cdproto" "github.com/chromedp/cdproto/cdp" @@ -29,6 +32,47 @@ func forceIP(urlstr string) string { return u.String() } +// detectURL detects the websocket debugger URL if the provided URL is not a +// valid websocket debugger URL. +// +// A valid websocket debugger URL is something like: +// ws://127.0.0.1:9222/devtools/browser/... +// The original URL with the following formats are accepted: +// * ws://127.0.0.1:9222/ +// * http://127.0.0.1:9222/ +func detectURL(urlstr string) string { + if strings.Contains(urlstr, "/devtools/browser/") { + return urlstr + } + + // replace the scheme and path to construct the URL like: + // http://127.0.0.1:9222/json/version + u, err := url.Parse(urlstr) + if err != nil { + return urlstr + } + u.Scheme = "http" + u.Path = "/json/version" + + // to get "webSocketDebuggerUrl" in the response + resp, err := http.Get(forceIP(u.String())) + if err != nil { + return urlstr + } + defer resp.Body.Close() + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return urlstr + } + // the browser will construct the debugger URL using the "host" header of the /json/version request. + // for example, run headless-shell in a container: docker run -d -p 9000:9222 chromedp/headless-shell:latest + // then: curl http://127.0.0.1:9000/json/version + // and the debugger URL will be something like: ws://127.0.0.1:9000/devtools/browser/... + wsURL := result["webSocketDebuggerUrl"].(string) + return wsURL +} + func runListeners(list []cancelableListener, ev interface{}) []cancelableListener { for i := 0; i < len(list); { listener := list[i]