From c96542a28897bc90381cbc79e0fafd2ffbd6a5d3 Mon Sep 17 00:00:00 2001 From: Dmitry Kropachev Date: Thu, 13 Apr 2023 06:39:04 -0400 Subject: [PATCH] Add CDP support Support Chrome CDP - https://chromedevtools.github.io/devtools-protocol --- go.mod | 1 + go.sum | 3 + internal/seleniumtest/seleniumtest.go | 58 ++++++++++++++ remote.go | 109 ++++++++++++++++++++++++++ selenium.go | 9 ++- 5 files changed, 179 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2bf3347..c3e06c0 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/blang/semver v3.5.1+incompatible + github.com/chromedp/cdproto v0.0.0-20200209033844-7e00b02ea7d2 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/golang/protobuf v1.3.4 github.com/google/go-cmp v0.3.0 diff --git a/go.sum b/go.sum index 97f23d8..080aa70 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,7 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chromedp/cdproto v0.0.0-20200209033844-7e00b02ea7d2 h1:osPk40NN+GLEj2Tay/N+H/K4itKyHZ6gdrC/pXjjgQ8= github.com/chromedp/cdproto v0.0.0-20200209033844-7e00b02ea7d2/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -57,6 +58,7 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs= github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -64,6 +66,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mediabuyerbot/go-crx3 v1.3.1 h1:JG3Hlaf7FsMhTJHBt+iEO5bK1GTh/Ms/cBT2aR2kBUE= diff --git a/internal/seleniumtest/seleniumtest.go b/internal/seleniumtest/seleniumtest.go index 084af24..f6f8b2c 100644 --- a/internal/seleniumtest/seleniumtest.go +++ b/internal/seleniumtest/seleniumtest.go @@ -23,6 +23,7 @@ import ( socks5 "github.com/armon/go-socks5" "github.com/blang/semver" + "github.com/chromedp/cdproto/browser" "github.com/go-auxiliaries/selenium" "github.com/go-auxiliaries/selenium/chrome" "github.com/go-auxiliaries/selenium/firefox" @@ -1758,7 +1759,64 @@ func testChromeExtension(t *testing.T, c Config) { } } +func testExecuteChromeDPCommand(t *testing.T, c Config) { + caps := newTestCapabilities(t, c) + + wd, err := NewRemote(t, caps, c.Addr) + if err != nil { + t.Fatalf("newRemote(_, _) returned error: %v", err) + } + defer func() { + _ = wd.Quit() + }() + + res, err := wd.ExecuteChromeDPCommand("Browser.getVersion", nil) + if err != nil { + t.Fatalf("cdp execute error: %s", err.Error()) + } + + version, ok := res.(map[string]interface{}) + if !ok || version == nil { + t.Fatalf("cdp execute failed with result: %v", res) + } + + product, ok := version["product"] + if !ok { + t.Fatalf("cdp execute [Browser.getVersion] failed with result: %v", res) + } + + t.Log(product) +} + +func testGenerateCDProtoContext(t *testing.T, c Config) { + caps := newTestCapabilities(t, c) + + wd, err := NewRemote(t, caps, c.Addr) + if err != nil { + t.Fatalf("newRemote(_, _) returned error: %v", err) + } + defer func() { + _ = wd.Quit() + }() + + version := browser.GetVersion() + + _, product, _, _, _, err := version.Do(wd.GenerateCDProtoContext(context.Background())) + + if err != nil { + t.Fatalf("cdproto execute error : %s", err.Error()) + } + + if strings.Index(product, "Chrome") > 0 { + t.Log(product) + } else { + t.Fatalf("invalid chrome version %s", product) + } +} + func RunChromeTests(t *testing.T, c Config) { // Chrome-specific tests. t.Run("Extension", runTest(testChromeExtension, c)) + t.Run("ExecuteChromeDPCommand", runTest(testExecuteChromeDPCommand, c)) + t.Run("GenerateCDProtoContext", runTest(testGenerateCDProtoContext, c)) } diff --git a/remote.go b/remote.go index 4478f6d..6320ec9 100644 --- a/remote.go +++ b/remote.go @@ -5,6 +5,7 @@ package selenium import ( "bytes" + "context" "encoding/base64" "encoding/json" "errors" @@ -17,6 +18,9 @@ import ( "strings" "time" + "github.com/chromedp/cdproto/cdp" + "github.com/mailru/easyjson" + "github.com/blang/semver" "github.com/go-auxiliaries/selenium/firefox" "github.com/go-auxiliaries/selenium/log" @@ -1254,6 +1258,111 @@ func (wd *remoteWD) execScript(script string, args []interface{}, suffix string) return reply.Value, nil } +func (wd *remoteWD) execChromeDPCommandRaw(data []byte) ([]byte, error) { + return wd.execute("POST", wd.requestURL("/session/%s/goog/cdp/execute", wd.id), data) +} + +// This command is not defined in the Selenium or WebDriver documentation. +// The functionality was ported from the Python Selenium driver +// (selenium.webdriver.chrome.remote_connection.ChromeRemoteConnection). +func (wd *remoteWD) execChromeDPCommand(cmd string, params map[string]interface{}) (interface{}, error) { + // params can't be nil + if params == nil { + params = make(map[string]interface{}) + } + + data, err := json.Marshal(map[string]interface{}{ + "cmd": cmd, + "params": params, + }) + if err != nil { + return nil, err + } + + response, err := wd.execChromeDPCommandRaw(data) + if err != nil { + return nil, err + } + + reply := new(struct{ Value interface{} }) + if err = json.Unmarshal(response, reply); err != nil { + return nil, err + } + + return reply.Value, nil +} + +// CDProtoExecutor execute Chrome DevTools Protocol command through cdproto +type CDProtoExecutor struct { + *remoteWD +} + +var _ cdp.Executor = (*CDProtoExecutor)(nil) + +// Execute executes a Chrome DevTools Protocol command. +// So that CDProtoExecutor can be executor for cdproto, +// refer to https://github.com/chromedp/cdproto/blob/master/cdp/types.go +func (e CDProtoExecutor) Execute(ctx context.Context, cmd string, params easyjson.Marshaler, res easyjson.Unmarshaler) (err error) { + if e.browser != "chrome" { + return fmt.Errorf("executing a Chrome DevTools command through cdproto is only supported in Chrome, not %s", e.browser) + } + + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + body := map[string]interface{}{"cmd": cmd} + + if params == nil { + // params can't be nil + body["params"] = make(map[string]string) + } else { + body["params"] = params + } + + data, err := json.Marshal(body) + if err != nil { + return + } + + response, err := e.execChromeDPCommandRaw(data) + if err != nil { + return + } + + reply := new(struct{ Value easyjson.Unmarshaler }) + reply.Value = res + + if err = json.Unmarshal(response, reply); err != nil { + debugLog("cdproto value parse error :%+v", res) + return + } + + debugLog("cdproto value return :%+v", res) + + return +} + +func (wd *remoteWD) generateCDProtoExecutor() cdp.Executor { + return CDProtoExecutor{wd} +} + +func (wd *remoteWD) GenerateCDProtoContext(ctx context.Context) context.Context { + if ctx == nil { + ctx = context.Background() + } + return cdp.WithExecutor(ctx, wd.generateCDProtoExecutor()) +} + +func (wd *remoteWD) ExecuteChromeDPCommand(cmd string, params map[string]interface{}) (interface{}, error) { + if wd.browser != "chrome" { + return nil, fmt.Errorf("executing a Chrome DevTools command is only supported in Chrome, not %s", wd.browser) + } + return wd.execChromeDPCommand(cmd, params) +} + func (wd *remoteWD) ExecuteScript(script string, args []interface{}) (interface{}, error) { if !wd.w3cCompatible { return wd.execScript(script, args, "") diff --git a/selenium.go b/selenium.go index 0c12656..7130a66 100644 --- a/selenium.go +++ b/selenium.go @@ -1,6 +1,7 @@ package selenium import ( + "context" "time" "github.com/go-auxiliaries/selenium/chrome" @@ -413,7 +414,13 @@ type WebDriver interface { AlertText() (string, error) // SetAlertText sets the current alert text. SetAlertText(text string) error - + // ExecuteChromeDPCommand executes a Chrome DevTools Protocol command. + // See https://chromedevtools.github.io/devtools-protocol/ for available commands. + ExecuteChromeDPCommand(cmd string, params map[string]interface{}) (interface{}, error) + // GenerateCDProtoContext generates context with an executor + // which can execute a Chrome DevTools Protocol command through cdproto. + // See https://github.com/chromedp/cdproto for usage information. + GenerateCDProtoContext(ctx context.Context) context.Context // ExecuteScript executes a script. ExecuteScript(script string, args []interface{}) (interface{}, error) // ExecuteScriptAsync asynchronously executes a script.