Skip to content

Commit

Permalink
aerokube#1063 Add Selenium-like websocket endpoint for CDP proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
BorisOsipov committed Feb 7, 2023
1 parent 1176ec6 commit c736999
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 3 deletions.
2 changes: 2 additions & 0 deletions docs/cli-flags.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ The following flags are supported by `selenoid` command:
New session attempts retry count (default 1)
-save-all-logs
Whether to save all logs without considering capabilities
-callback-urlstring
Selenoid callback url
-service-startup-timeout duration
Service startup timeout in time.Duration format (default 30s)
-session-attempt-timeout duration
Expand Down
9 changes: 8 additions & 1 deletion docs/devtools.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
. We recommend to use the most recent Chrome version possible.
====

Selenoid is proxying Chrome Developer Tools API to browser container. For every running Selenium session to access the API just use the following URL:
Selenoid is proxying Chrome Developer Tools API to browser container. For every running Selenium session to access the API just use the following URL:

```
<ws or http>://selenoid.example.com:4444/devtools/<session-id>/<method>
Expand All @@ -31,6 +31,13 @@ For example an URL to connect to current page websocket would be:
ws://selenoid.example.com:4444/devtools/<session-id>/page
```

.Accessing Developer Tools API with Selenium 4

Selenium 4 and above has functionality allowing to access Chrome DevTools features. This works just out of the box.
This feature needs full Selenoid server URL in order to work properly.
To enable this feature, set the Selenoid `-callback-url` flag to the correct Selenoid address, such as `-callback-url="https://selenoid.example.com/"`.
This can also be set through the `callbackUrl` capability.

.Accessing Developer Tools API with Webdriver.io and Puppeteer
[source,javascript]
----
Expand Down
9 changes: 9 additions & 0 deletions docs/special-capabilities.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,15 @@ s3KeyPattern: "$quota/$fileType$fileExtension"

The same key placeholders are supported. Please refer to <<Uploading Files To S3>> section for more details.

=== CDP Url: callbackUrl

This capability allows to set Selenoid server URL to enable Selenium 4 CDP access.

.Type: string
----
callbackUrl: "https://selenoid.example.com/"
----

=== Specifying Capabilities via Protocol Extensions

Some Selenium clients allow passing only a limited number of capabilities specified in https://w3c.github.io/webdriver/webdriver-spec.html[WebDriver specification]. For such cases Selenoid supports reading capabilities using https://w3c.github.io/webdriver/webdriver-spec.html#protocol-extensions[WebDriver protocol extensions] feature. The following two examples deliver the same result. Usually capabilities are passed like this:
Expand Down
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ var (
videoRecorderImage string
logOutputDir string
saveAllLogs bool
callbackUrl string
ggrHost *ggr.Host
conf *config.Config
queue *protect.Queue
Expand Down Expand Up @@ -92,6 +93,7 @@ func init() {
flag.BoolVar(&disablePrivileged, "disable-privileged", false, "Whether to disable privileged container mode")
flag.StringVar(&videoOutputDir, "video-output-dir", "video", "Directory to save recorded video to")
flag.StringVar(&videoRecorderImage, "video-recorder-image", "selenoid/video-recorder:latest-release", "Image to use as video recorder")
flag.StringVar(&callbackUrl, "callback-url", "", "Selenoid callback url")
flag.StringVar(&logOutputDir, "log-output-dir", "", "Directory to save session log to")
flag.BoolVar(&saveAllLogs, "save-all-logs", false, "Whether to save all logs without considering capabilities")
flag.DurationVar(&gracefulPeriod, "graceful-period", 300*time.Second, "graceful shutdown period in time.Duration format, e.g. 300s or 500ms")
Expand Down
53 changes: 51 additions & 2 deletions selenoid.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ func create(w http.ResponseWriter, r *http.Request) {
if logOutputDir != "" && (saveAllLogs || caps.Log) {
caps.LogName = getTemporaryFileName(logOutputDir, logFileExtension)
}
if caps.CallbackUrl == "" {
caps.CallbackUrl = callbackUrl
}
starter, ok = manager.Find(caps, requestId)
if ok {
break
Expand Down Expand Up @@ -281,12 +284,23 @@ func create(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(resp.StatusCode)
}
} else {
tee := io.TeeReader(resp.Body, w)
w.WriteHeader(resp.StatusCode)
json.NewDecoder(tee).Decode(&s)
respBody, _ := io.ReadAll(resp.Body)
json.Unmarshal(respBody, &s)
if s.ID == "" {
s.ID = s.Value.ID
}
if caps.CallbackUrl != "" && s.ID != "" {
respBody, err = addCdpCapabilities(respBody, caps.CallbackUrl, s.ID)
if err != nil {
log.Printf("[%d] [SESSION_FAILED] [%s] [%s]", requestId, u.String(), resp.Status)
jsonerror.SessionNotCreated(err).Encode(w)
queue.Drop()
cancel()
return
}
}
w.Write(respBody)
}
if s.ID == "" {
log.Printf("[%d] [SESSION_FAILED] [%s] [%s]", requestId, u.String(), resp.Status)
Expand Down Expand Up @@ -393,6 +407,41 @@ func removeSelenoidOptions(input []byte) []byte {
return ret
}

func addCdpCapabilities(input []byte, baseUrl string, sessionId string) ([]byte, error) {
var body map[string]interface{}
if err := json.Unmarshal(input, &body); err != nil {
return nil, err
}

value, ok := body["value"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("expected key 'value' of type 'map', but got %T", body["value"])
}

caps, ok := value["capabilities"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("expected key 'capabilities' of type 'map', but got %T", value["capabilities"])
}

caps["se:cdpEnable"] = true
cdpUrl, err := url.JoinPath(baseUrl, "devtools", sessionId)
if err != nil {
return nil, fmt.Errorf("cannot construct devtools url: %v", err)
}
caps["se:cdp"] = cdpUrl

var version interface{}
if v, ok := caps["browserVersion"]; ok {
version = v
} else {
version = caps["version"]
}
caps["se:cdpVersion"] = version

result, _ := json.Marshal(body)
return result, nil
}

func preprocessSessionId(sid string) string {
if ggrHost != nil {
return ggrHost.Sum() + sid
Expand Down
1 change: 1 addition & 0 deletions session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Caps struct {
Labels map[string]string `json:"labels,omitempty"`
SessionTimeout string `json:"sessionTimeout,omitempty"`
S3KeyPattern string `json:"s3KeyPattern,omitempty"`
CallbackUrl string `json:"callbackUrl,omitempty"`
ExtensionCapabilities *Caps `json:"selenoid:options,omitempty"`
}

Expand Down

0 comments on commit c736999

Please sign in to comment.