diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c1a8aed..31bf403c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,20 +9,23 @@ jobs: golang: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Golang - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: - go-version: 1.14.x + go-version: ~1.22.5 - - uses: actions/cache@v1 + - uses: actions/cache@v3 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Test run: ci/test.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52f621bd..7f9797f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,20 +8,23 @@ jobs: golang: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Golang - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: - go-version: 1.14.x + go-version: ~1.22.5 - - uses: actions/cache@v1 + - uses: actions/cache@v3 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Test run: ci/test.sh @@ -29,7 +32,7 @@ jobs: run: ci/build.sh - name: Prepare release version - run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF:10} + run: echo "RELEASE_VERSION=${GITHUB_REF:10}" >> $GITHUB_ENV - uses: AButler/upload-release-assets@v2.0 with: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..eec20e8e --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,19 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + days-before-stale: 60 + days-before-close: 7 + stale-issue-label: stale + exempt-issue-labels: new-feature,bug,improvement,docs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 23bad07c..af68e5ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,14 +7,14 @@ jobs: golang: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Golang - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: - go-version: 1.14.x + go-version: ~1.22.5 - - uses: actions/cache@v1 + - uses: actions/cache@v3 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} @@ -27,6 +27,6 @@ jobs: - name: Build run: ci/build.sh - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 with: flags: go diff --git a/Dockerfile b/Dockerfile index 692bafb5..f820c492 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,9 @@ -FROM alpine:3.8 +FROM --platform=$BUILDPLATFORM alpine:3 RUN apk add -U ca-certificates tzdata mailcap && rm -Rf /var/cache/apk/* -COPY selenoid /usr/bin + +ARG TARGETARCH +COPY dist/selenoid_linux_$TARGETARCH /usr/bin/selenoid EXPOSE 4444 ENTRYPOINT ["/usr/bin/selenoid", "-listen", ":4444", "-conf", "/etc/selenoid/browsers.json", "-video-output-dir", "/opt/selenoid/video/"] diff --git a/README.md b/README.md index cc5da93c..f7727528 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,17 @@ [![Docker Pulls](https://img.shields.io/docker/pulls/aerokube/selenoid.svg)](https://hub.docker.com/r/aerokube/selenoid) [![StackOverflow Tag](https://img.shields.io/badge/stackoverflow-selenoid-orange.svg?style=flat)](https://stackoverflow.com/questions/tagged/selenoid) +**UNMAINTAINED**. Consider https://aerokube.com/moon/latest as alternative. + Selenoid is a powerful implementation of [Selenium](http://github.com/SeleniumHQ/selenium) hub using [Docker](https://docker.com/) containers to launch browsers. ![Selenoid Animation](docs/img/selenoid-animation.gif) ## Features ### One-command Installation -Start browser automation in minutes by copy-pasting just **one command**: +Start browser automation in minutes by downloading [Configuration Manager](https://github.com/aerokube/cm/releases) binary and running just **one command**: ``` -$ curl -s https://aerokube.com/cm/bash | bash \ - && ./cm selenoid start --vnc --tmpfs 128 +$ ./cm selenoid start --vnc --tmpfs 128 ``` **That's it!** You can now use Selenoid instead of Selenium server. Specify the following Selenium URL in tests: ``` @@ -67,5 +68,5 @@ Selenoid was initially created to be deployed on hardware servers or virtual mac ## Known Users -[![JetBrains](docs/img/logo/jetbrains.png)](http://jetbrains.com/) [![Yandex](docs/img/logo/yandex.png)](https://yandex.com/company/) [![Sberbank Technology](docs/img/logo/sbertech.png)](http://sber-tech.com/) [![ThoughtWorks](docs/img/logo/thoughtworks.png)](https://thoughtworks.com/) [![VK.com](docs/img/logo/vk.png)](https://vk.com/) [![SuperJob](docs/img/logo/superjob.png)](http://superjob.ru/) [![PropellerAds](docs/img/logo/propellerads.png)](http://propellerads.com/) [![AlfaBank](docs/img/logo/alfabank.png)](https://alfabank.com/) [![3CX](docs/img/logo/3cx.png)](https://www.3cx.com/) [![IQ Option](docs/img/logo/iq_option.png)](https://iqoption.com/) [![Mail.Ru Group](docs/img/logo/mail_ru.png)](https://corp.mail.ru/en/) [![Newegg.Com](docs/img/logo/newegg.png)](https://newegg.com/) [![Badoo](docs/img/logo/badoo.png)](https://badoo.com/team/) [![BCS](docs/img/logo/bcs.png)](https://bcs.ru/) [![Quality Lab](docs/img/logo/quality-lab.png)](https://quality-lab.ru) [![AT Consulting](docs/img/logo/at-consulting.png)](https://www.at-consulting.ru/) [![Royal Caribbean International](docs/img/logo/royal-caribbean.png)](https://www.royalcaribbean.com/) [![Sixt](docs/img/logo/sixt.png)](https://sixt.com/) [![Testjar](docs/img/logo/testjar.png)](http://www.testjar.com/) [![Flipdish](docs/img/logo/flipdish.png)](https://www.flipdish.com/) +[![JetBrains](docs/img/logo/jetbrains.png)](http://jetbrains.com/) [![Yandex](docs/img/logo/yandex.png)](https://yandex.com/company/) [![Sberbank Technology](docs/img/logo/sbertech.png)](http://sber-tech.com/) [![ThoughtWorks](docs/img/logo/thoughtworks.png)](https://thoughtworks.com/) [![VK.com](docs/img/logo/vk.png)](https://vk.com/) [![SuperJob](docs/img/logo/superjob.png)](http://superjob.ru/) [![PropellerAds](docs/img/logo/propellerads.png)](http://propellerads.com/) [![AlfaBank](docs/img/logo/alfabank.png)](https://alfabank.com/) [![3CX](docs/img/logo/3cx.png)](https://www.3cx.com/) [![IQ Option](docs/img/logo/iq_option.png)](https://iqoption.com/) [![Mail.Ru Group](docs/img/logo/mail_ru.png)](https://corp.mail.ru/en/) [![Newegg.Com](docs/img/logo/newegg.png)](https://newegg.com/) [![Badoo](docs/img/logo/badoo.png)](https://badoo.com/team/) [![BCS](docs/img/logo/bcs.png)](https://bcs.ru/) [![Quality Lab](docs/img/logo/quality-lab.png)](https://quality-lab.ru) [![AT Consulting](docs/img/logo/at-consulting.png)](https://www.at-consulting.ru/) [![Royal Caribbean International](docs/img/logo/royal-caribbean.png)](https://www.royalcaribbean.com/) [![Sixt](docs/img/logo/sixt.png)](https://sixt.com/) [![Testjar](docs/img/logo/testjar.png)](http://www.testjar.com/) [![Flipdish](docs/img/logo/flipdish.png)](https://www.flipdish.com/) [![RiAdvice](docs/img/logo/riadvice.png)](https://riadvice.tn/) diff --git a/ci/build.sh b/ci/build.sh index 922a2295..89e4ddf5 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -1,6 +1,7 @@ #!/bin/bash +set -e + export GO111MODULE="on" -go get -u github.com/mitchellh/gox # cross compile -GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-X main.buildStamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.gitRevision=`git describe --tags || git rev-parse HEAD` -s -w" -gox -os "linux darwin windows" -arch "amd64" -osarch="windows/386" -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags "-X main.buildStamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.gitRevision=`git describe --tags || git rev-parse HEAD` -s -w" +go install github.com/mitchellh/gox@latest # cross compile +CGO_ENABLED=0 gox -os "linux darwin windows" -arch "amd64" -osarch="darwin/arm64" -osarch="darwin/arm64" -osarch="linux/arm64" -osarch="windows/386" -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags "-X main.buildStamp=`date -u '+%Y-%m-%d_%I:%M:%S%p'` -X main.gitRevision=`git describe --tags || git rev-parse HEAD` -s -w" diff --git a/ci/docker-push.sh b/ci/docker-push.sh index 2391b712..097009b3 100755 --- a/ci/docker-push.sh +++ b/ci/docker-push.sh @@ -2,12 +2,5 @@ set -e -docker build -t $GITHUB_REPOSITORY . -docker tag $GITHUB_REPOSITORY $GITHUB_REPOSITORY:$1 -docker tag $GITHUB_REPOSITORY selenoid/hub:$1 -docker tag $GITHUB_REPOSITORY aandryashin/selenoid:$1 docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" -docker push $GITHUB_REPOSITORY -docker push $GITHUB_REPOSITORY:$1 -docker push selenoid/hub:$1 -docker push aandryashin/selenoid:$1 +docker buildx build --pull --push -t "$GITHUB_REPOSITORY" -t "$GITHUB_REPOSITORY:$1" -t "selenoid/hub:$1" --platform linux/amd64,linux/arm64 . diff --git a/ci/test.sh b/ci/test.sh index ae1da579..796a126b 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -1,4 +1,9 @@ #!/bin/bash +set -e + export GO111MODULE="on" -go test -tags 's3 metadata' -v -race -coverprofile=coverage.txt -covermode=atomic -coverpkg github.com/aerokube/selenoid,github.com/aerokube/selenoid/session,github.com/aerokube/selenoid/config,github.com/aerokube/selenoid/protect,github.com/aerokube/selenoid/service,github.com/aerokube/selenoid/upload +go test -tags 's3 metadata' -v -race -coverprofile=coverage.txt -covermode=atomic -coverpkg github.com/aerokube/selenoid,github.com/aerokube/selenoid/session,github.com/aerokube/selenoid/config,github.com/aerokube/selenoid/protect,github.com/aerokube/selenoid/service,github.com/aerokube/selenoid/upload,github.com/aerokube/selenoid/info,github.com/aerokube/selenoid/jsonerror + +go install golang.org/x/vuln/cmd/govulncheck@latest +"$(go env GOPATH)"/bin/govulncheck -tags production ./... diff --git a/config/config.go b/config/config.go index 58a90248..92dd8bda 100644 --- a/config/config.go +++ b/config/config.go @@ -1,17 +1,16 @@ package config import ( - "log" - "encoding/json" "fmt" - "io/ioutil" + "log" + "os" "strings" "sync" + "time" "github.com/aerokube/selenoid/session" "github.com/docker/docker/api/types/container" - "time" ) // Session - session id and vnc flag @@ -22,6 +21,7 @@ type Session struct { VNC bool `json:"vnc"` Screen string `json:"screen"` Caps session.Caps `json:"caps"` + Started time.Time `json:"started"` } // Sessions - used count and individual sessions for quota user @@ -85,7 +85,7 @@ func NewConfig() *Config { } func loadJSON(filename string, v interface{}) error { - buf, err := ioutil.ReadFile(filename) + buf, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("read error: %v", err) } @@ -182,6 +182,7 @@ func (config *Config) State(sessions *session.Map, limit, queued, pending int) * VNC: vnc, Screen: session.Caps.ScreenResolution, Caps: session.Caps, + Started: session.Started, } if ctr != nil { sess.Container = ctr.ID diff --git a/config_test.go b/config_test.go index f848afb0..39e7deec 100644 --- a/config_test.go +++ b/config_test.go @@ -2,20 +2,19 @@ package main import ( "fmt" - "io/ioutil" "log" "os" "testing" - . "github.com/aandryashin/matchers" "github.com/aerokube/selenoid/config" "github.com/aerokube/selenoid/session" + assert "github.com/stretchr/testify/require" ) const testLogConf = "config/container-logs.json" func configfile(s string) string { - tmp, err := ioutil.TempFile("", "config") + tmp, err := os.CreateTemp("", "config") if err != nil { log.Fatal(err) } @@ -35,15 +34,16 @@ func TestConfig(t *testing.T) { defer os.Remove(confFile) conf := config.NewConfig() err := conf.Load(confFile, testLogConf) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) } func TestConfigError(t *testing.T) { confFile := configfile(`{}`) - os.Remove(confFile) + _ = os.Remove(confFile) conf := config.NewConfig() err := conf.Load(confFile, testLogConf) - AssertThat(t, err.Error(), EqualTo{fmt.Sprintf("browsers config: read error: open %s: no such file or directory", confFile)}) + assert.Error(t, err) + assert.Equal(t, err.Error(), fmt.Sprintf("browsers config: read error: open %s: no such file or directory", confFile)) } func TestLogConfigError(t *testing.T) { @@ -51,7 +51,7 @@ func TestLogConfigError(t *testing.T) { defer os.Remove(confFile) conf := config.NewConfig() err := conf.Load(confFile, "some-missing-file") - AssertThat(t, err, Not{nil}) + assert.Error(t, err) } func TestConfigParseError(t *testing.T) { @@ -59,7 +59,8 @@ func TestConfigParseError(t *testing.T) { defer os.Remove(confFile) var conf config.Config err := conf.Load(confFile, testLogConf) - AssertThat(t, err.Error(), EqualTo{"browsers config: parse error: unexpected end of JSON input"}) + assert.Error(t, err) + assert.Equal(t, err.Error(), "browsers config: parse error: unexpected end of JSON input") } func TestConfigEmptyState(t *testing.T) { @@ -69,42 +70,42 @@ func TestConfigEmptyState(t *testing.T) { conf.Load(confFile, testLogConf) state := conf.State(session.NewMap(), 0, 0, 0) - AssertThat(t, state.Total, EqualTo{0}) - AssertThat(t, state.Queued, EqualTo{0}) - AssertThat(t, state.Pending, EqualTo{0}) - AssertThat(t, state.Used, EqualTo{0}) + assert.Equal(t, state.Total, 0) + assert.Equal(t, state.Queued, 0) + assert.Equal(t, state.Pending, 0) + assert.Equal(t, state.Used, 0) } func TestConfigNonEmptyState(t *testing.T) { confFile := configfile(`{}`) defer os.Remove(confFile) conf := config.NewConfig() - conf.Load(confFile, testLogConf) + _ = conf.Load(confFile, testLogConf) sessions := session.NewMap() sessions.Put("0", &session.Session{Caps: session.Caps{Name: "firefox", Version: "49.0"}, Quota: "unknown"}) state := conf.State(sessions, 1, 0, 0) - AssertThat(t, state.Total, EqualTo{1}) - AssertThat(t, state.Queued, EqualTo{0}) - AssertThat(t, state.Pending, EqualTo{0}) - AssertThat(t, state.Used, EqualTo{1}) - AssertThat(t, state.Browsers["firefox"]["49.0"]["unknown"].Count, EqualTo{1}) + assert.Equal(t, state.Total, 1) + assert.Equal(t, state.Queued, 0) + assert.Equal(t, state.Pending, 0) + assert.Equal(t, state.Used, 1) + assert.Equal(t, state.Browsers["firefox"]["49.0"]["unknown"].Count, 1) } func TestConfigEmptyVersions(t *testing.T) { confFile := configfile(`{"firefox":{}}`) defer os.Remove(confFile) conf := config.NewConfig() - conf.Load(confFile, testLogConf) + _ = conf.Load(confFile, testLogConf) sessions := session.NewMap() sessions.Put("0", &session.Session{Caps: session.Caps{Name: "firefox", Version: "49.0"}, Quota: "unknown"}) state := conf.State(sessions, 1, 0, 0) - AssertThat(t, state.Total, EqualTo{1}) - AssertThat(t, state.Queued, EqualTo{0}) - AssertThat(t, state.Pending, EqualTo{0}) - AssertThat(t, state.Used, EqualTo{1}) - AssertThat(t, state.Browsers["firefox"]["49.0"]["unknown"].Count, EqualTo{1}) + assert.Equal(t, state.Total, 1) + assert.Equal(t, state.Queued, 0) + assert.Equal(t, state.Pending, 0) + assert.Equal(t, state.Used, 1) + assert.Equal(t, state.Browsers["firefox"]["49.0"]["unknown"].Count, 1) } func TestConfigNonEmptyVersions(t *testing.T) { @@ -116,11 +117,11 @@ func TestConfigNonEmptyVersions(t *testing.T) { sessions := session.NewMap() sessions.Put("0", &session.Session{Caps: session.Caps{Name: "firefox", Version: "49.0"}, Quota: "unknown"}) state := conf.State(sessions, 1, 0, 0) - AssertThat(t, state.Total, EqualTo{1}) - AssertThat(t, state.Queued, EqualTo{0}) - AssertThat(t, state.Pending, EqualTo{0}) - AssertThat(t, state.Used, EqualTo{1}) - AssertThat(t, state.Browsers["firefox"]["49.0"]["unknown"].Count, EqualTo{1}) + assert.Equal(t, state.Total, 1) + assert.Equal(t, state.Queued, 0) + assert.Equal(t, state.Pending, 0) + assert.Equal(t, state.Used, 1) + assert.Equal(t, state.Browsers["firefox"]["49.0"]["unknown"].Count, 1) } func TestConfigFindMissingBrowser(t *testing.T) { @@ -130,7 +131,7 @@ func TestConfigFindMissingBrowser(t *testing.T) { conf.Load(confFile, testLogConf) _, _, ok := conf.Find("firefox", "") - AssertThat(t, ok, Is{false}) + assert.False(t, ok) } func TestConfigFindDefaultVersionError(t *testing.T) { @@ -140,65 +141,70 @@ func TestConfigFindDefaultVersionError(t *testing.T) { conf.Load(confFile, testLogConf) _, _, ok := conf.Find("firefox", "") - AssertThat(t, ok, Is{false}) + assert.False(t, ok) } func TestConfigFindDefaultVersion(t *testing.T) { confFile := configfile(`{"firefox":{"default":"49.0"}}`) defer os.Remove(confFile) conf := config.NewConfig() - conf.Load(confFile, testLogConf) + err := conf.Load(confFile, testLogConf) + assert.NoError(t, err) _, v, ok := conf.Find("firefox", "") - AssertThat(t, ok, Is{false}) - AssertThat(t, v, EqualTo{"49.0"}) + assert.False(t, ok) + assert.Equal(t, v, "49.0") } func TestConfigFindFoundByEmptyPrefix(t *testing.T) { confFile := configfile(`{"firefox":{"default":"49.0","versions":{"49.0":{}}}}`) defer os.Remove(confFile) conf := config.NewConfig() - conf.Load(confFile, testLogConf) + err := conf.Load(confFile, testLogConf) + assert.NoError(t, err) _, v, ok := conf.Find("firefox", "") - AssertThat(t, ok, Is{true}) - AssertThat(t, v, EqualTo{"49.0"}) + assert.True(t, ok) + assert.Equal(t, v, "49.0") } func TestConfigFindFoundByPrefix(t *testing.T) { confFile := configfile(`{"firefox":{"default":"49.0","versions":{"49.0":{}}}}`) defer os.Remove(confFile) conf := config.NewConfig() - conf.Load(confFile, testLogConf) + err := conf.Load(confFile, testLogConf) + assert.NoError(t, err) _, v, ok := conf.Find("firefox", "49") - AssertThat(t, ok, Is{true}) - AssertThat(t, v, EqualTo{"49.0"}) + assert.True(t, ok) + assert.Equal(t, v, "49.0") } func TestConfigFindFoundByMatch(t *testing.T) { confFile := configfile(`{"firefox":{"default":"49.0","versions":{"49.0":{}}}}`) defer os.Remove(confFile) conf := config.NewConfig() - conf.Load(confFile, testLogConf) + err := conf.Load(confFile, testLogConf) + assert.NoError(t, err) _, v, ok := conf.Find("firefox", "49.0") - AssertThat(t, ok, Is{true}) - AssertThat(t, v, EqualTo{"49.0"}) + assert.True(t, ok) + assert.Equal(t, v, "49.0") } func TestConfigFindImage(t *testing.T) { confFile := configfile(`{"firefox":{"default":"49.0","versions":{"49.0":{"image":"image","port":"5555", "path":"/"}}}}`) defer os.Remove(confFile) conf := config.NewConfig() - conf.Load(confFile, testLogConf) + err := conf.Load(confFile, testLogConf) + assert.NoError(t, err) b, v, ok := conf.Find("firefox", "49.0") - AssertThat(t, ok, Is{true}) - AssertThat(t, v, EqualTo{"49.0"}) - AssertThat(t, b.Image, EqualTo{"image"}) - AssertThat(t, b.Port, EqualTo{"5555"}) - AssertThat(t, b.Path, EqualTo{"/"}) + assert.True(t, ok) + assert.Equal(t, v, "49.0") + assert.Equal(t, b.Image, "image") + assert.Equal(t, b.Port, "5555") + assert.Equal(t, b.Path, "/") } func TestConfigConcurrentLoad(t *testing.T) { @@ -208,10 +214,10 @@ func TestConfigConcurrentLoad(t *testing.T) { done := make(chan struct{}) go func() { - conf.Load(confFile, testLogConf) + _ = conf.Load(confFile, testLogConf) done <- struct{}{} }() - conf.Load(confFile, testLogConf) + _ = conf.Load(confFile, testLogConf) <-done } @@ -220,15 +226,14 @@ func TestConfigConcurrentLoadAndRead(t *testing.T) { defer os.Remove(confFile) conf := config.NewConfig() err := conf.Load(confFile, testLogConf) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) done := make(chan string) go func() { browser, _, _ := conf.Find("firefox", "") done <- browser.Tmpfs["/tmp"] }() - conf.Load(confFile, testLogConf) + err = conf.Load(confFile, testLogConf) + assert.NoError(t, err) <-done } @@ -237,9 +242,7 @@ func TestConfigConcurrentRead(t *testing.T) { defer os.Remove(confFile) var conf config.Config err := conf.Load(confFile, testLogConf) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) done := make(chan string) go func() { browser, _, _ := conf.Find("firefox", "") diff --git a/doc.go b/doc.go index 84ecab89..a70b7154 100644 --- a/doc.go +++ b/doc.go @@ -1,4 +1,4 @@ /* -Selenoid is a powerful implementation of Selenium Hub using Docker or standalone web driver binaries to start and launch browsers. Documentation has moved to: http://github.com/aandryashin/selenoid +Selenoid is a powerful implementation of Selenium Hub using Docker or standalone web driver binaries to start and launch browsers. Documentation has moved to: https://aerokube.com/selenoid/latest. */ package main diff --git a/docs/browser-image-information.adoc b/docs/browser-image-information.adoc index 2ac295bf..9177407b 100644 --- a/docs/browser-image-information.adoc +++ b/docs/browser-image-information.adoc @@ -1,306 +1,3 @@ == Browser Image information -NOTE: Build files for all these images are open-sourced and stored in https://github.com/aerokube/selenoid-images[repository]. - -=== Firefox - -.Firefox Images with Selenium Server -|=== -| Image | VNC Image | Selenium Version | Firefox Version | Client Version - -| selenoid/firefox:3.6 | selenoid/vnc:firefox_3.6 | 2.20.0 | 3.6.16 i386 (dialogs may not work) .7+<.^| -**Java:** 2.53.1 and below -**Python:** not supported -**selenium-webdriver.js:** not supported -| selenoid/firefox:4.0 | selenoid/vnc:firefox_4.0 | 2.20.0 | 4.0.1 i386 -| selenoid/firefox:5.0 | selenoid/vnc:firefox_5.0 | 2.20.0 | 5.0.1 i386 -| selenoid/firefox:6.0 | selenoid/vnc:firefox_6.0 | 2.20.0 | 6.0.2 i386 -| selenoid/firefox:7.0 | selenoid/vnc:firefox_7.0 | 2.20.0 | 7.0.1 i386 -| selenoid/firefox:8.0 | selenoid/vnc:firefox_8.0 | 2.20.0 | 8.0.1 i386 -| selenoid/firefox:9.0 | selenoid/vnc:firefox_9.0 | 2.20.0 | 9.0.1 -| selenoid/firefox:10.0 | selenoid/vnc:firefox_10.0 | 2.32.0 | 10.0.2 .13+<.^| -**Java:** any modern version -**Python:** not supported -**selenium-webdriver.js:** not supported -| selenoid/firefox:11.0 | selenoid/vnc:firefox_11.0 | 2.32.0 | 11.0 -| selenoid/firefox:12.0 | selenoid/vnc:firefox_12.0 | 2.32.0 | 12.0 -| selenoid/firefox:13.0 | selenoid/vnc:firefox_13.0 | 2.32.0 | 13.0 -| selenoid/firefox:14.0 | selenoid/vnc:firefox_14.0 | 2.32.0 | 14.0.1 -| selenoid/firefox:15.0 | selenoid/vnc:firefox_15.0 | 2.32.0 | 15.0.1 -| selenoid/firefox:16.0 | selenoid/vnc:firefox_16.0 | 2.32.0 | 16.0.2 -| selenoid/firefox:17.0 | selenoid/vnc:firefox_17.0 | 2.32.0 | 17.0.1 -| selenoid/firefox:18.0 | selenoid/vnc:firefox_18.0 | 2.32.0 | 18.0.2 -| selenoid/firefox:19.0 | selenoid/vnc:firefox_19.0 | 2.32.0 | 19.0.2 -| selenoid/firefox:20.0 | selenoid/vnc:firefox_20.0 | 2.32.0 | 20.0 -| selenoid/firefox:21.0 | selenoid/vnc:firefox_21.0 | 2.32.0 | 21.0 -| selenoid/firefox:22.0 | selenoid/vnc:firefox_22.0 | 2.32.0 | 22.0 -| selenoid/firefox:23.0 | selenoid/vnc:firefox_23.0 | 2.35.0 | 23.0.1 .25+<.^| Any modern client version -| selenoid/firefox:24.0 | selenoid/vnc:firefox_24.0 | 2.39.0 | 24.0 -| selenoid/firefox:25.0 | selenoid/vnc:firefox_25.0 | 2.39.0 | 25.0.1 -| selenoid/firefox:26.0 | selenoid/vnc:firefox_26.0 | 2.39.0 | 26.0 -| selenoid/firefox:27.0 | selenoid/vnc:firefox_27.0 | 2.40.0 | 27.0.1 -| selenoid/firefox:28.0 | selenoid/vnc:firefox_28.0 | 2.41.0 | 28.0 -| selenoid/firefox:29.0 | selenoid/vnc:firefox_29.0 | 2.43.1 | 29.0.1 -| selenoid/firefox:30.0 | selenoid/vnc:firefox_30.0 | 2.43.1 | 30.0 -| selenoid/firefox:31.0 | selenoid/vnc:firefox_31.0 | 2.44.0 | 31.0 -| selenoid/firefox:32.0 | selenoid/vnc:firefox_32.0 | 2.44.0 | 32.0.3 -| selenoid/firefox:33.0 | selenoid/vnc:firefox_33.0 | 2.44.0 | 33.0.3 -| selenoid/firefox:34.0 | selenoid/vnc:firefox_34.0 | 2.45.0 | 34.0.5 -| selenoid/firefox:35.0 | selenoid/vnc:firefox_35.0 | 2.45.0 | 35.0.1 -| selenoid/firefox:36.0 | selenoid/vnc:firefox_36.0 | 2.45.0 | 36.0.1 -| selenoid/firefox:37.0 | selenoid/vnc:firefox_37.0 | 2.45.0 | 37.0.2 -| selenoid/firefox:38.0 | selenoid/vnc:firefox_38.0 | 2.45.0 | 38.0.5 -| selenoid/firefox:39.0 | selenoid/vnc:firefox_39.0 | 2.45.0 | 39.0.3 -| selenoid/firefox:40.0 | selenoid/vnc:firefox_40.0 | 2.45.0 | 40.0.3 -| selenoid/firefox:41.0 | selenoid/vnc:firefox_41.0 | 2.45.0 | 41.0.2 -| selenoid/firefox:42.0 | selenoid/vnc:firefox_42.0 | 2.47.1 | 42.0 -| selenoid/firefox:43.0 | selenoid/vnc:firefox_43.0 | 2.53.1 | 43.0.4 -| selenoid/firefox:44.0 | selenoid/vnc:firefox_44.0 | 2.53.1 | 44.0.2 -| selenoid/firefox:45.0 | selenoid/vnc:firefox_45.0 | 2.53.1 | 45.0.2 -| selenoid/firefox:46.0 | selenoid/vnc:firefox_46.0 | 2.53.1 | 46.0.1 -| selenoid/firefox:47.0 | selenoid/vnc:firefox_47.0 | 2.53.1 | 47.0.1 -|=== - -WARNING: Firefox 53.0+ images require Selenium client 3.4.0 or newer. - -.Firefox Images with Selenoid -|=== -| Image | VNC Image | Selenoid Version | Geckodriver Version | Firefox Version | Client Version - -| selenoid/firefox:48.0 | selenoid/vnc:firefox_48.0 | 1.10.0 | 0.13.0 | 48.0.2 (page load timeout, native events and proxies don't work) .32+<.^| -**Java, selenium-webdriver.js**: 3.4.0 and above -**Python**: 3.5.0 and above -| selenoid/firefox:49.0 | selenoid/vnc:firefox_49.0 | 1.10.0 | 0.13.0 | 49.0.2 (page load timeout, native events and switching between windows don't work) -| selenoid/firefox:50.0 | selenoid/vnc:firefox_50.0 | 1.10.0 | 0.13.0 | 50.0.2 (page load timeout, native events, switching windows and proxies don't work) -| selenoid/firefox:51.0 | selenoid/vnc:firefox_51.0 | 1.10.0 | 0.14.0 | 51.0.1 (page load timeout, native events, switching windows and proxies don't work) -| selenoid/firefox:52.0 | selenoid/vnc:firefox_52.0 | 1.10.0 | 0.15.0 | 52.0.2 (page load timeout, native events, switching windows and proxies don't work) -| selenoid/firefox:53.0 | selenoid/vnc:firefox_53.0 | 1.10.0 | 0.16.0 | 53.0.2 (switching windows may not work) -| selenoid/firefox:54.0 | selenoid/vnc:firefox_54.0 | 1.10.0 | 0.17.0 | 54.0.1 (switching windows may not work) -| selenoid/firefox:55.0 | selenoid/vnc:firefox_55.0 | 1.10.0 | 0.18.0 | 55.0.1 (switching windows may not work) -| selenoid/firefox:56.0 | selenoid/vnc:firefox_56.0 | 1.10.0 | 0.19.1 | 56.0.1 -| selenoid/firefox:57.0 | selenoid/vnc:firefox_57.0 | 1.10.0 | 0.19.1 | 57.0 -| selenoid/firefox:58.0 | selenoid/vnc:firefox_58.0 | 1.10.0 | 0.20.1 | 58.0 -| selenoid/firefox:59.0 | selenoid/vnc:firefox_59.0 | 1.10.0 | 0.20.1 | 59.0.1 -| selenoid/firefox:60.0 | selenoid/vnc:firefox_60.0 | 1.10.0 | 0.21.0 | 60.0.2 -| selenoid/firefox:61.0 | selenoid/vnc:firefox_61.0 | 1.10.0 | 0.21.0 | 61.0 -| selenoid/firefox:62.0 | selenoid/vnc:firefox_62.0 | 1.10.0 | 0.22.0 | 62.0 -| selenoid/firefox:63.0 | selenoid/vnc:firefox_63.0 | 1.8.1 | 0.23.0 | 63.0 -| selenoid/firefox:64.0 | selenoid/vnc:firefox_64.0 | 1.8.4 | 0.23.0 | 64.0 -| selenoid/firefox:65.0 | selenoid/vnc:firefox_65.0 | 1.9.0 | 0.24.0 | 65.0 -| selenoid/firefox:66.0 | selenoid/vnc:firefox_66.0 | 1.9.1 | 0.24.0 | 66.0.1 -| selenoid/firefox:67.0 | selenoid/vnc:firefox_67.0 | 1.9.1 | 0.24.0 | 67.0 -| selenoid/firefox:68.0 | selenoid/vnc:firefox_68.0 | 1.9.2 | 0.24.0 | 68.0 -| selenoid/firefox:69.0 | selenoid/vnc:firefox_69.0 | 1.9.2 | 0.24.0 | 69.0 -| selenoid/firefox:70.0 | selenoid/vnc:firefox_70.0 | 1.9.2 | 0.26.0 | 70.0 -| selenoid/firefox:71.0 | selenoid/vnc:firefox_71.0 | 1.9.3 | 0.26.0 | 71.0 -| selenoid/firefox:72.0 | selenoid/vnc:firefox_72.0 | 1.9.3 | 0.26.0 | 72.0 -| selenoid/firefox:73.0 | selenoid/vnc:firefox_73.0 | 1.10.0 | 0.26.0 | 73.0 -| selenoid/firefox:74.0 | selenoid/vnc:firefox_74.0 | 1.10.0 | 0.26.0 | 74.0.1 -| selenoid/firefox:75.0 | selenoid/vnc:firefox_75.0 | 1.10.0 | 0.26.0 | 75.0 -| selenoid/firefox:76.0 | selenoid/vnc:firefox_76.0 | 1.10.0 | 0.26.0 | 76.0 -| selenoid/firefox:77.0 | selenoid/vnc:firefox_77.0 | 1.10.0 | 0.26.0 | 77.0.1 -| selenoid/firefox:78.0 | selenoid/vnc:firefox_78.0 | 1.10.0 | 0.26.0 | 78.0.1 -| selenoid/firefox:79.0 | selenoid/vnc:firefox_79.0 | 1.10.0 | 0.27.0 | 79.0 -|=== - - -=== Chrome - -.Chrome Images -|=== -| Image | VNC Image | Chromedriver version | Chrome version - -| selenoid/chrome:48.0 | selenoid/vnc:chrome_48.0 | 2.21 | 48.0.2564.116 -| selenoid/chrome:49.0 | selenoid/vnc:chrome_49.0 | 2.22 | 49.0.2623.112 -| selenoid/chrome:50.0 | selenoid/vnc:chrome_50.0 | 2.22 | 50.0.2661.102 -| selenoid/chrome:51.0 | selenoid/vnc:chrome_51.0 | 2.23 | 51.0.2704.106 -| selenoid/chrome:52.0 | selenoid/vnc:chrome_52.0 | 2.24 | 52.0.2743.116 -| selenoid/chrome:53.0 | selenoid/vnc:chrome_53.0 | 2.26 | 53.0.2785.143 -| selenoid/chrome:54.0 | selenoid/vnc:chrome_54.0 | 2.27 | 54.0.2840.100 -| selenoid/chrome:55.0 | selenoid/vnc:chrome_55.0 | 2.28 | 55.0.2883.87 -| selenoid/chrome:56.0 | selenoid/vnc:chrome_56.0 | 2.29 | 56.0.2924.87 -| selenoid/chrome:57.0 | selenoid/vnc:chrome_57.0 | 2.29 | 57.0.2987.110 -| selenoid/chrome:58.0 | selenoid/vnc:chrome_58.0 | 2.29 | 58.0.3029.81 -| selenoid/chrome:59.0 | selenoid/vnc:chrome_59.0 | 2.30 | 59.0.3071.86 -| selenoid/chrome:60.0 | selenoid/vnc:chrome_60.0 | 2.31 | 60.0.3112.90 -| selenoid/chrome:61.0 | selenoid/vnc:chrome_61.0 | 2.32 | 61.0.3163.79 -| selenoid/chrome:62.0 | selenoid/vnc:chrome_62.0 | 2.33 | 62.0.3202.62 -| selenoid/chrome:63.0 | selenoid/vnc:chrome_63.0 | 2.33 | 63.0.3239.84 -| selenoid/chrome:64.0 | selenoid/vnc:chrome_64.0 | 2.35 | 64.0.3282.119 -| selenoid/chrome:65.0 | selenoid/vnc:chrome_65.0 | 2.38 | 65.0.3325.181 -| selenoid/chrome:66.0 | selenoid/vnc:chrome_66.0 | 2.38 | 66.0.3359.117 -| selenoid/chrome:67.0 | selenoid/vnc:chrome_67.0 | 2.39 | 67.0.3396.62 -| selenoid/chrome:68.0 | selenoid/vnc:chrome_68.0 | 2.41 | 68.0.3440.106 -| selenoid/chrome:69.0 | selenoid/vnc:chrome_69.0 | 2.42 | 69.0.3497.100 -| selenoid/chrome:70.0 | selenoid/vnc:chrome_70.0 | 2.44 | 70.0.3538.110 -| selenoid/chrome:71.0 | selenoid/vnc:chrome_71.0 | 2.44 | 71.0.3578.80 -| selenoid/chrome:72.0 | selenoid/vnc:chrome_72.0 | 2.46 | 72.0.3626.121 -| selenoid/chrome:73.0 | selenoid/vnc:chrome_73.0 | 73.0.3683.68 | 73.0.3683.75 -| selenoid/chrome:74.0 | selenoid/vnc:chrome_74.0 | 74.0.3729.6 | 74.0.3729.157 -| selenoid/chrome:75.0 | selenoid/vnc:chrome_75.0 | 75.0.3770.90 | 75.0.3770.90 -| selenoid/chrome:76.0 | selenoid/vnc:chrome_76.0 | 76.0.3809.87 | 76.0.3809.68 -| selenoid/chrome:77.0 | selenoid/vnc:chrome_77.0 | 77.0.3865.40 | 77.0.3865.75 -| selenoid/chrome:78.0 | selenoid/vnc:chrome_78.0 | 78.0.3904.87 | 78.0.3904.70 -| selenoid/chrome:79.0 | selenoid/vnc:chrome_79.0 | 79.0.3945.36 | 79.0.3945.79 -| selenoid/chrome:80.0 | selenoid/vnc:chrome_80.0 | 80.0.3987.106 | 80.0.3987.132 -| selenoid/chrome:81.0 | selenoid/vnc:chrome_81.0 | 81.0.4044.138 | 81.0.4044.138 -| - | - | - | 82.0.x.x (release skipped by development team) -| selenoid/chrome:83.0 | selenoid/vnc:chrome_83.0 | 83.0.4103.39 | 83.0.4103.61 -| selenoid/chrome:84.0 | selenoid/vnc:chrome_84.0 | 84.0.4147.30 | 84.0.4147.89 -|=== - -[NOTE] -==== -. These images work with any modern Selenium client version. -. Images for older Chrome versions were not built because we have no Debian packages. If you have such packages - we could create more images. -==== - -=== Chrome Mobile - -WARNING: Hardware server or virtual machine with nested virtualization support is required to run Chrome Mobile images. - -.Chrome Mobile Images -|=== -| Image | Android version | Appium version | Chromedriver version | Chrome version - -| selenoid/chrome-mobile:73.0 | 8.1 | 1.13.0 | 73.0.3683.68 | 73.0.3683.90 -| selenoid/chrome-mobile:74.0 | 8.1 | 1.13.0 | 74.0.3729.6 | 74.0.3729.157 -| selenoid/chrome-mobile:75.0 | 8.1 | 1.13.0 | 75.0.3770.8 | 75.0.3770.89 -| selenoid/chrome-mobile:76.0 | 9.0 | 1.16.0 | 76.0.3809.126 | 76.0.3809.132 -| selenoid/chrome-mobile:77.0 | 9.0 | 1.16.0 | 77.0.3865.40 | 77.0.3865.116 -| selenoid/chrome-mobile:78.0 | 9.0 | 1.16.0 | 78.0.3904.105 | 78.0.3904.96 -| selenoid/chrome-mobile:79.0 | 9.0 | 1.16.0 | 79.0.3945.36 | 79.0.3945.93 -|=== - -An example `browsers.json` for Chrome Mobile images looks like the following: -[source,javascript] ----- -{ - "chrome": { - "default": "mobile-75.0", - "versions": { - "mobile-75.0": { - "image": "selenoid/chrome-mobile:75.0", - "port": "4444", - "path": "/wd/hub" - } - } - } -} ----- - -=== Opera - -.Opera Presto Images -|=== -| Image | VNC Image | Selenium version | Opera version - -| selenoid/opera:12.16 | selenoid/vnc:opera_12.16 | 2.37.0 | 12.16.1860 (dialogs and probably async JS don't work) -|=== - -[WARNING] -==== -Due to bug in *Operadriver* to work with *Opera Blink* images you need to pass additional capability: -[source,javascript] -{"browserName": "opera", "operaOptions": {"binary": "/usr/bin/opera"}} - -We do not consider these images really stable. Many of base operations like working with proxies may not work. -==== - -.Opera Blink Images -|=== -| Image | VNC Image | Operadriver version | Opera version - -| selenoid/opera:33.0 | selenoid/vnc:opera_33.0 | 0.2.2 | 33.0.1990.115 -| selenoid/opera:34.0 | selenoid/vnc:opera_34.0 | 0.2.2 | 34.0.2036.50 -| selenoid/opera:35.0 | selenoid/vnc:opera_35.0 | 0.2.2 | 35.0.2066.92 -| selenoid/opera:36.0 | selenoid/vnc:opera_36.0 | 0.2.2 | 36.0.2130.65 -| selenoid/opera:37.0 | selenoid/vnc:opera_37.0 | 0.2.2 | 37.0.2178.54 -| selenoid/opera:38.0 | selenoid/vnc:opera_38.0 | 0.2.2 | 38.0.2220.41 -| selenoid/opera:39.0 | selenoid/vnc:opera_39.0 | 0.2.2 | 39.0.2256.71 -| selenoid/opera:40.0 | selenoid/vnc:opera_40.0 | 0.2.2 | 40.0.2308.90 -| selenoid/opera:41.0 | selenoid/vnc:opera_41.0 | 2.27 | 41.0.2353.69 -| selenoid/opera:42.0 | selenoid/vnc:opera_42.0 | 2.27 | 42.0.2393.94 -| selenoid/opera:43.0 | selenoid/vnc:opera_43.0 | 2.27 | 43.0.2442.991 -| selenoid/opera:44.0 | selenoid/vnc:opera_44.0 | 2.27 | 44.0.2510.857 -| selenoid/opera:45.0 | selenoid/vnc:opera_45.0 | 2.27 | 45.0.2552.635 -| selenoid/opera:46.0 | selenoid/vnc:opera_46.0 | 2.27 | 46.0.2597.26 -| selenoid/opera:47.0 | selenoid/vnc:opera_47.0 | 2.29 | 47.0.2631.39 -| selenoid/opera:48.0 | selenoid/vnc:opera_48.0 | 2.30 | 48.0.2685.35 -| selenoid/opera:49.0 | selenoid/vnc:opera_49.0 | 2.32 | 49.0.2725.39 -| selenoid/opera:50.0 | selenoid/vnc:opera_50.0 | 2.32 | 50.0.2762.45 -| selenoid/opera:51.0 | selenoid/vnc:opera_51.0 | 2.33 | 51.0.2830.26 -| selenoid/opera:52.0 | selenoid/vnc:opera_52.0 | 2.35 | 52.0.2871.37 -| selenoid/opera:53.0 | selenoid/vnc:opera_53.0 | 2.36 | 53.0.2907.68 -| selenoid/opera:54.0 | selenoid/vnc:opera_54.0 | 2.37 | 54.0.2952.46 -| selenoid/opera:55.0 | selenoid/vnc:opera_55.0 | 2.37 | 55.0.2994.37 -| selenoid/opera:56.0 | selenoid/vnc:opera_56.0 | 2.40 | 56.0.3051.31 -| selenoid/opera:57.0 | selenoid/vnc:opera_57.0 | 2.41 | 57.0.3098.76 -| selenoid/opera:58.0 | selenoid/vnc:opera_58.0 | 2.42 | 58.0.3135.79 -| - | - | - | 59.0.x.x (no stable release exists) -| selenoid/opera:60.0 | selenoid/vnc:opera_60.0 | 2.45 | 60.0.3255.56 -| - | - | - | 61.0.x.x (no stable release exists) -| selenoid/opera:62.0 | selenoid/vnc:opera_62.0 | 75.0.3770.100 | 62.0.3331.99 (need to use browserName = chrome) -| selenoid/opera:63.0 | selenoid/vnc:opera_63.0 | 76.0.3809.132 | 63.0.3368.91 -| selenoid/opera:64.0 | selenoid/vnc:opera_64.0 | 77.0.3865.120 | 64.0.3417.73 -| selenoid/opera:65.0 | selenoid/vnc:opera_65.0 | 78.0.3904.87 | 65.0.3467.42 -| selenoid/opera:66.0 | selenoid/vnc:opera_66.0 | 79.0.3945.79 | 66.0.3515.36 -| selenoid/opera:67.0 | selenoid/vnc:opera_67.0 | 80.0.3987.100 | 67.0.3575.53 -| selenoid/opera:68.0 | selenoid/vnc:opera_68.0 | 81.0.4044.113 | 68.0.3618.104 -| selenoid/opera:69.0 | selenoid/vnc:opera_69.0 | 83.0.4103.97 | 69.0.3686.49 -|=== - -[NOTE] -==== -. These images work with any modern Selenium client version. -. Images for older Opera versions were not built because we have no Debian packages. If you have such packages - we could create more images. -==== - -=== Internet Explorer and Microsoft Edge - -We don't build ready to use images for these browsers because of Windows license limitations. However we provide detailed instructions on building such images https://github.com/aerokube/windows-images[here]. The same approach can be used for preparing images with Firefox, Chrome and Opera under Windows. - -=== Android - -WARNING: Hardware server or virtual machine with nested virtualization support is required to run Android images. - -.Android Images -|=== -| Image | Android version | Appium version - -| selenoid/android:4.4 | 4.4 | 1.13.0 -| selenoid/android:5.0 | 5.0 | 1.13.0 -| selenoid/android:5.1 | 5.1 | 1.13.0 -| selenoid/android:6.0 | 6.0 | 1.13.0 -| selenoid/android:7.0 | 7.0 | 1.13.0 -| selenoid/android:7.1 | 7.1 | 1.13.0 -| selenoid/android:8.0 | 8.0 | 1.13.0 -| selenoid/android:8.1 | 8.1 | 1.13.0 -| selenoid/android:9.0 | 9.0 | 1.16.0 -| selenoid/android:10.0 | 10.0 | 1.16.0 -|=== - -[NOTE] -==== -. These images include VNC server and Android Quick Boot snapshot. -. Neither Chromedriver nor Chrome Mobile are installed. To test hybrid apps build your own image using provided automation script. -==== - -An example `browsers.json` for Android images looks like the following: -[source,javascript] ----- -{ - "android": { - "default": "6.0", - "versions": { - "6.0": { - "image": "selenoid/android:6.0", - "port": "4444", - "path": "/wd/hub" - } - } - } -} ----- - -An example Java test can be found https://github.com/aerokube/demo-tests/blob/master/src/test/java/com/aerokube/selenoid/AndroidRemoteApkTest.java[here]. +Moved to a https://aerokube.com/images/latest/[dedicated page]. diff --git a/docs/cloud-platforms.adoc b/docs/cloud-platforms.adoc deleted file mode 100644 index 2187df2f..00000000 --- a/docs/cloud-platforms.adoc +++ /dev/null @@ -1,54 +0,0 @@ -== Cloud Platforms - -Selenoid ready-to-use images are present in popular cloud platforms and can be run in a few clicks. This section contains step-by-step instructions on launching Selenoid in these platforms. - -=== Google Cloud - -Selenoid can be run from https://console.cloud.google.com/marketplace[Google Cloud Marketplace] as follows: - -. Go to https://console.cloud.google.com/marketplace/details/aerokube/selenoid[Selenoid page] and click the **Launch on Compute Engine** button: -+ -image:img/google-cloud-marketplace-page.png[google-cloud-marketplace] - -. Type virtual machine name, size and click the **Deploy** button: -+ -image:img/google-cloud-vm-settings.png[google-cloud-vm-settings] - -. Wait for virtual machine to start. Selenoid user interface is available at `http://:8080/`. Selenium tests should be run against `http://:4444/wd/hub`. - -=== DigitalOcean - -Selenoid can be run from https://marketplace.digitalocean.com/[DigitalOcean Marketplace] as follows: - -. Go to https://marketplace.digitalocean.com/apps/selenoid[Selenoid page]: -+ -image:img/digitalocean-marketplace-page.png[digitalocean-marketplace] - -. Click on **Create Selenoid Droplet** button. -. Choose a plan, i.e. virtual machine size: -+ -image:img/digitalocean-vm-flavor.png[digitalocean-vm-flavor] -. Choose a data center region near you: -+ -image:img/digitalocean-vm-datacenter.png[digitalocean-vm-datacenter] - -. Select or add an SSH key for your droplet. -. Select a name for your virtual machine: -+ -image:img/digitalocean-vm-name.png[digitalocean-vm-name] -. Click on **Create Droplet** button. -. Wait for virtual machine to start. Selenoid user interface is available at `http://:8080/`. Selenium tests should be run against `http://:4444/wd/hub`. - -=== Yandex Cloud - -Selenoid can be run from https://cloud.yandex.com/marketplace[Yandex Cloud Marketplace] as follows: - -. Go to https://cloud.yandex.com/marketplace/products/f2e1m50sdg87g8eh716h[Selenoid page] and click the **Launch** button: -+ -image:img/yandex-cloud-marketplace-page.png[yandex-cloud-marketplace] - -. Type virtual machine name, select disk size, available computing resources and click the **Create VM** button: -+ -image:img/yandex-cloud-vm-settings.png[yandex-cloud-vm-settings] - -. Wait for virtual machine to start. Selenoid user interface is available at `http://:8080/`. Selenium tests should be run against `http://:4444/wd/hub`. diff --git a/docs/contributing.adoc b/docs/contributing.adoc index 4f810785..f941df74 100644 --- a/docs/contributing.adoc +++ b/docs/contributing.adoc @@ -30,8 +30,9 @@ To build Docker container type: [source,bash] ---- -$ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -$ docker build -t selenoid:latest . +$ mkdir -p dist +$ GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o dist/selenoid_linux_amd64 +$ docker buildx build --pull --platform linux/amd64 -t selenoid:latest . ---- ==== @@ -42,7 +43,7 @@ Locally can be generated with: [source,bash] ---- -$ docker run --rm -v `pwd`/docs/:/documents/ \ +$ docker run --rm -v ./docs/:/documents/ \ asciidoctor/docker-asciidoctor \ asciidoctor -D /documents/output/ index.adoc ---- diff --git a/docs/docker-settings.adoc b/docs/docker-settings.adoc index 6f182afb..32ff267b 100644 --- a/docs/docker-settings.adoc +++ b/docs/docker-settings.adoc @@ -12,7 +12,7 @@ # ifconfig | grep eth0 eth0 Link encap:Ethernet HWaddr 00:25:90:eb:fb:3e + -Now we are setting this Mac-adress for `docker0` virtual interface: +Now we are setting this Mac-address for `docker0` virtual interface: # ip link set docker0 address 00:25:90:eb:fb:3e diff --git a/docs/faq.adoc b/docs/faq.adoc index ed69a696..23e8cd8e 100644 --- a/docs/faq.adoc +++ b/docs/faq.adoc @@ -56,6 +56,10 @@ This depends on your tests. We recommend to start with 1 CPU and 1 Gb of memory The only difference between these images - is a running VNC server (`x11vnc`) consuming approximately 20 Megabytes of RAM in idle state which is negligible compared to browser memory consumption. +**VNC is consuming all my container CPU** + +On RedHat-based distributions you should set `LimitNOFILE=1048576` for `containerd.service` + === Features not Working **Selenoid does not start: open config/browsers.json: no such file or directory** @@ -92,10 +96,8 @@ When running Selenoid as Docker container video feature can be not working (beca **Can't get VNC feature to work: Disconnected** -Please check the following: +Please check that you have `enableVNC = true` capability in your tests -. You have `enableVNC = true` capability in your tests -. You are using browser images with `vnc` in their name, e.g. `selenoid/vnc:firefox:58.0`. **Seeing black screen with a cross in VNC window** diff --git a/docs/file-download.adoc b/docs/file-download.adoc index 677a2fe8..66ad2879 100644 --- a/docs/file-download.adoc +++ b/docs/file-download.adoc @@ -57,8 +57,15 @@ driver.navigate().to("http://example.com/myfile.odt"); . Files are accessible only when browser session is running. ==== -Your tests may need to download files with browsers. To analyze these files a common requirement is then to somehow extract downloaded files from browser containers. A possible solution can be dealing with container volumes. But Selenoid provides a `/download` API and dramatically simplifies downloading files. Having a running session `f2bcd32b-d932-4cdc-a639-687ab8e4f840` you can access all downloaded files using an URL: +Your tests may need to download files with browsers. To analyze these files a common requirement is then to somehow extract downloaded files from browser containers. A possible solution can be dealing with container volumes. But Selenoid provides a `/download` API and dramatically simplifies downloading files. Having a running session `f2bcd32b-d932-4cdc-a639-687ab8e4f840` you can access all downloaded files using an HTTP request: ``` -http://selenoid-host.example.com:4444/download/f2bcd32b-d932-4cdc-a639-687ab8e4f840/myfile.txt +GET http://selenoid-host.example.com:4444/download/f2bcd32b-d932-4cdc-a639-687ab8e4f840/myfile.txt ``` In order for this feature to work an HTTP file server should be listening inside browser container on port `8080`. Download directory inside container to be used in tests is usually `~/Downloads`. + +Similarly to delete downloaded files replace GET request with DELETE: + +[source] +---- +DELETE http://selenoid-host.example.com:4444/download/f2bcd32b-d932-4cdc-a639-687ab8e4f840/myfile.txt +---- diff --git a/docs/img/logo/riadvice.png b/docs/img/logo/riadvice.png new file mode 100644 index 00000000..34e1248b Binary files /dev/null and b/docs/img/logo/riadvice.png differ diff --git a/docs/index.adoc b/docs/index.adoc index 2f90e430..8a316c17 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -22,8 +22,6 @@ It is using Docker to launch browsers. Please refer to https://github.com/aeroku include::quick-start-guide.adoc[leveloffset=+1] include::starting-selenoid-manually.adoc[leveloffset=+1] include::faq.adoc[leveloffset=+1] -include::windows.adoc[leveloffset=+1] -include::browser-images.adoc[leveloffset=+1] == Main Features include::video.adoc[leveloffset=+1] @@ -55,6 +53,3 @@ include::contributing.adoc[] [appendix] include::browser-image-information.adoc[] - -[appendix] -include::cloud-platforms.adoc[] diff --git a/docs/logs.adoc b/docs/logs.adoc index a3fd0a1e..df37632a 100644 --- a/docs/logs.adoc +++ b/docs/logs.adoc @@ -19,8 +19,8 @@ $ docker run -d --name selenoid \ -p 4444:4444 \ -v /var/run/docker.sock:/var/run/docker.sock \ --v `pwd`/config/:/etc/selenoid/:ro \ --v `pwd`/logs/:/opt/selenoid/logs/ \ +-v /your/directory/config/:/etc/selenoid/:ro \ +-v /your/directory/logs/:/opt/selenoid/logs/ \ aerokube/selenoid:latest-release -log-output-dir /opt/selenoid/logs ---- diff --git a/docs/quick-start-guide.adoc b/docs/quick-start-guide.adoc index 1001d361..88290830 100644 --- a/docs/quick-start-guide.adoc +++ b/docs/quick-start-guide.adoc @@ -1,7 +1,6 @@ == Quick Start Guide === Start Selenoid -==== Option 1: you have a workstation, virtual machine or server . Make sure you have recent https://www.docker.com/[Docker] version installed. . Download http://aerokube.com/cm/latest/[Configuration Manager] (Selenoid quick installation binary) for your platform from https://github.com/aerokube/cm/releases/latest[releases] page. @@ -12,15 +11,21 @@ . Run one command to start Selenoid: $ ./cm selenoid start --vnc ++ +[WARNING] +==== +Running this command with `sudo` can lead to broken installation. Recommended way is running Selenoid as regular user. On Linux to have permissions to access Docker you may need to add your user to `docker` group: +[source,bash] +---- +$ sudo usermod -aG docker $USER +---- +==== ++ . Optionally run one more command to start Selenoid UI: $ ./cm selenoid-ui start -==== Option 2: you have Google Cloud, DigitalOcean or Yandex Cloud - -We deliver ready to use images with Selenoid and Selenoid UI. Take a look at <> section for more details. - === Run Your Tests . Run your tests against Selenoid like you do with regular Selenium hub: diff --git a/docs/starting-selenoid-manually.adoc b/docs/starting-selenoid-manually.adoc index 4ad79a0e..1d3fe318 100644 --- a/docs/starting-selenoid-manually.adoc +++ b/docs/starting-selenoid-manually.adoc @@ -11,14 +11,14 @@ . Create `config/browsers.json` configuration file with content: + .config/browsers.json -[source,javascript] +[source,json] ---- { "firefox": { "default": "57.0", "versions": { "57.0": { - "image": "selenoid/vnc:firefox_57.0", + "image": "selenoid/firefox:88.0", "port": "4444", "path": "/wd/hub" } @@ -31,7 +31,7 @@ NOTE: For Chrome and Opera images "path" is "/" instead of "/wd/hub" + . Pull browser Docker image: + -`$ docker pull selenoid/vnc:firefox_57.0`. +`$ docker pull selenoid/firefox:88.0`. === Browser Images We maintain a set of prebuilt Docker container images for different browsers including: @@ -48,20 +48,21 @@ Feel free to create issues or request images for new versions. ==== . Complete list of browser images can be found in <> . These images support all UTF-8 locales. This can be important if you want to save files using national alphabet symbols. You can enable your preferred locale in browsers configuration file by setting respective environment variables: -``` +[source,json] +---- { "chrome": { "default": "64.0", "versions": { "64.0": { - "image": "selenoid/vnc:chrome_64.0", + "image": "selenoid/chrome:90.0", //... "env" : ["LANG=ru_RU.UTF-8", "LANGUAGE=ru:en", "LC_ALL=ru_RU.UTF-8"] } } } } -``` +---- ==== === Start Selenoid @@ -107,7 +108,7 @@ docker run -d \ --name selenoid \ -p 4444:4444 \ -v /var/run/docker.sock:/var/run/docker.sock \ --v `pwd`/config/:/etc/selenoid/:ro \ +-v /your/directory/config/:/etc/selenoid/:ro \ aerokube/selenoid:latest-release ---- diff --git a/docs/video.adoc b/docs/video.adoc index 5e5dc82a..bfe5bb06 100644 --- a/docs/video.adoc +++ b/docs/video.adoc @@ -21,7 +21,7 @@ $ docker pull selenoid/video-recorder:latest-release . When running Selenoid **in Docker container**: .. Mount a directory from the host machine (e.g. `~/.aerokube/selenoid/video`) to store video files to `/opt/selenoid/video`. .. Pass an additional `OVERRIDE_VIDEO_OUTPUT_DIR` environment variable with absolute path to video directory on host machine. This is required because video recorder container, automatically created by Selenoid should save video to host machine video storage directory. -. When running Selenoid **as a binary** - videos will be stored in `video` directory inside current working directory. +. When running Selenoid **as a binary** - videos will be stored in `video` directory inside current working directory. + .Example Docker Command ---- @@ -29,9 +29,9 @@ $ docker run -d \ --name selenoid \ -p 4444:4444 \ -v /var/run/docker.sock:/var/run/docker.sock \ --v `pwd`/config/:/etc/selenoid/:ro \ --v `pwd`/video/:/opt/selenoid/video/ \ --e OVERRIDE_VIDEO_OUTPUT_DIR=`pwd`/video/ \ +-v /your/directory/config/:/etc/selenoid/:ro \ +-v /your/directory/video/:/opt/selenoid/video/ \ +-e OVERRIDE_VIDEO_OUTPUT_DIR=/your/directory/video/ \ aerokube/selenoid:latest-release ---- diff --git a/go.mod b/go.mod index 744258b9..a22b07d1 100644 --- a/go.mod +++ b/go.mod @@ -1,20 +1,50 @@ module github.com/aerokube/selenoid - go 1.14 +go 1.22 require ( - github.com/aandryashin/matchers v0.0.0-20161126170413-435295ea180e - github.com/aerokube/ggr v0.0.0-20181215175518-4a2e23fa1769 - github.com/aerokube/util v0.0.0-20190701120823-161c21b50f69 - github.com/aws/aws-sdk-go v1.20.12 - github.com/docker/docker v0.7.3-0.20190629173937-e105a74c5419 - github.com/docker/go-connections v0.4.0 - github.com/docker/go-units v0.4.0 - github.com/gorilla/websocket v1.4.2 - github.com/imdario/mergo v0.3.6 - github.com/mafredri/cdp v0.21.0 - github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c - github.com/pkg/errors v0.8.1 - golang.org/x/net v0.0.0-20190311183353-d8887717615a - gopkg.in/yaml.v2 v2.2.2 // indirect + github.com/aerokube/ggr v0.0.0-20240420103110-fc913c480489 + github.com/aws/aws-sdk-go v1.53.5 + github.com/docker/docker v26.1.5+incompatible + github.com/docker/go-connections v0.5.0 + github.com/docker/go-units v0.5.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.1 + github.com/imdario/mergo v0.3.15 + github.com/mafredri/cdp v0.34.1 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.9.0 + golang.org/x/net v0.25.0 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect + go.opentelemetry.io/otel v1.22.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect + go.opentelemetry.io/otel/metric v1.22.0 // indirect + go.opentelemetry.io/otel/sdk v1.22.0 // indirect + go.opentelemetry.io/otel/trace v1.22.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.1 // indirect ) diff --git a/go.sum b/go.sum index 8d34a115..891c98e4 100644 --- a/go.sum +++ b/go.sum @@ -1,103 +1,185 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc= -github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/aandryashin/matchers v0.0.0-20160729131923-5eb67beb188b/go.mod h1:cbmYNkm9xeQlNoWEPtOUcvNok2gSD7ErMnYkRW+eHi8= -github.com/aandryashin/matchers v0.0.0-20161126170413-435295ea180e h1:ogUKYFNcdYUIBSLibE4+EjbTJazoHr5JsWWx21Lpn8c= -github.com/aandryashin/matchers v0.0.0-20161126170413-435295ea180e/go.mod h1:cbmYNkm9xeQlNoWEPtOUcvNok2gSD7ErMnYkRW+eHi8= -github.com/aandryashin/reloader v0.0.0-20161127125235-da4f1b43ce40/go.mod h1:gvg2/m9OQ4ZwK4Qk/mnfgokCb4qDN4BGyle+QGw4VOc= -github.com/abbot/go-http-auth v0.0.0-20161224193827-d45c47bedec7/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM= -github.com/aerokube/ggr v0.0.0-20181215175518-4a2e23fa1769 h1:5UpE5f0xRoCgAcxFnT8h3EraWjg4cdX+HM6Hlh99LD4= -github.com/aerokube/ggr v0.0.0-20181215175518-4a2e23fa1769/go.mod h1:c4gg5wIR9PyppUSe4swKbNiSLO0vivf7THFs3rkcBIU= -github.com/aerokube/util v0.0.0-20190701120823-161c21b50f69 h1:fweUpmRn4GxKz+TFP9gmYs/k0unLEl5DJauqjwws7yI= -github.com/aerokube/util v0.0.0-20190701120823-161c21b50f69/go.mod h1:xhDPwF7O2YfPEAqVQNmeGldyPgNRC/V6ZRP/GtIr7Y4= -github.com/aws/aws-sdk-go v1.20.12 h1:xV7xfLSkiqd7JOnLlfER+Jz8kI98rAGJvtXssYkCRs4= -github.com/aws/aws-sdk-go v1.20.12/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/aerokube/ggr v0.0.0-20240420103110-fc913c480489 h1:o921DQC1wmxmYftt6xpDmoIlEOUqxPh9bLi5uB25Nps= +github.com/aerokube/ggr v0.0.0-20240420103110-fc913c480489/go.mod h1:soFdGlpMBKP88KMnnCranonPRqNw9O0FasvXvaO8IGs= +github.com/aws/aws-sdk-go v1.53.5 h1:1OcVWMjGlwt7EU5OWmmEEXqaYfmX581EK317QJZXItM= +github.com/aws/aws-sdk-go v1.53.5/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v0.7.3-0.20190629173937-e105a74c5419 h1:gui8HtKTBl1hF+CPmrprMfJVO4zjVEdwy4BLtZs24t4= -github.com/docker/docker v0.7.3-0.20190629173937-e105a74c5419/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g= +github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/mafredri/cdp v0.21.0 h1:HHvwNtvQr+6Y91bqnDoO7q9j/KrYudoDvEmvsA34a9M= -github.com/mafredri/cdp v0.21.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE= -github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= -github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c h1:MUyE44mTvnI5A0xrxIxaMqoWFzPfQvtE2IWUollMDMs= -github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mafredri/cdp v0.34.1 h1:EeLNc+6pkDx2hrAm1arIjiofoH0fM5On1uAFzcuUn+o= +github.com/mafredri/cdp v0.34.1/go.mod h1:Dbsh7eY/zhQlsddEDWzZGOztv9Jf2gzKq47M7a2P3C4= +github.com/mafredri/go-lint v0.0.0-20180911205320-920981dfc79e/go.mod h1:k/zdyxI3q6dup24o8xpYjJKTCf2F7rfxLp6w/efTiWs= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -golang.org/x/crypto v0.0.0-20161221235747-f6b343c37ca8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 h1:W18sezcAYs+3tDZX4F80yctqa12jcP1PUS2gQu1zTPU= +google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97/go.mod h1:iargEX0SFPm3xcfMI0d1domjg0ZF4Aa0p2awqyxhvF0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY= +google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/info/info.go b/info/info.go new file mode 100644 index 00000000..868471ea --- /dev/null +++ b/info/info.go @@ -0,0 +1,27 @@ +package info + +import ( + "net" + "net/http" + "time" +) + +func RequestInfo(r *http.Request) (string, string) { + const unknownUser = "unknown" + user := "" + if u, _, ok := r.BasicAuth(); ok { + user = u + } else { + user = unknownUser + } + remote := r.Header.Get("X-Forwarded-For") + if remote != "" { + return user, remote + } + remote, _, _ = net.SplitHostPort(r.RemoteAddr) + return user, remote +} + +func SecondsSince(start time.Time) float64 { + return time.Now().Sub(start).Seconds() +} diff --git a/jsonerror/jsonerror.go b/jsonerror/jsonerror.go new file mode 100644 index 00000000..db04aad5 --- /dev/null +++ b/jsonerror/jsonerror.go @@ -0,0 +1,52 @@ +package jsonerror + +import ( + "encoding/json" + "fmt" + "net/http" +) + +type SeleniumError struct { + Name string + Err error + Status int +} + +func (se *SeleniumError) Error() string { + return fmt.Errorf("%s: %v", se.Name, se.Err).Error() +} + +func newSeleniumError(name string, err error, status int) *SeleniumError { + return &SeleniumError{ + Name: name, + Err: err, + Status: status, + } +} + +func InvalidArgument(err error) *SeleniumError { + return newSeleniumError("invalid argument", err, http.StatusBadRequest) +} + +func InvalidSessionID(err error) *SeleniumError { + return newSeleniumError("invalid session id", err, http.StatusNotFound) +} + +func SessionNotCreated(err error) *SeleniumError { + return newSeleniumError("session not created", err, http.StatusInternalServerError) +} + +func UnknownError(err error) *SeleniumError { + return newSeleniumError("unknown error", err, http.StatusInternalServerError) +} + +func (se *SeleniumError) Encode(w http.ResponseWriter) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(se.Status) + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "value": map[string]string{ + "error": se.Name, + "message": se.Err.Error(), + }, + }) +} diff --git a/main.go b/main.go index e5f72e6a..a4c0e9dc 100644 --- a/main.go +++ b/main.go @@ -4,32 +4,31 @@ import ( "context" "encoding/json" "flag" + "fmt" + "github.com/aerokube/selenoid/info" + "github.com/docker/docker/api" "log" "net" "net/http" "os" "os/signal" "path" + "path/filepath" "strconv" "strings" "syscall" "time" - "golang.org/x/net/websocket" - - "fmt" - - "path/filepath" - ggr "github.com/aerokube/ggr/config" "github.com/aerokube/selenoid/config" + "github.com/aerokube/selenoid/jsonerror" "github.com/aerokube/selenoid/protect" "github.com/aerokube/selenoid/service" "github.com/aerokube/selenoid/session" "github.com/aerokube/selenoid/upload" - "github.com/aerokube/util" - "github.com/aerokube/util/docker" "github.com/docker/docker/client" + "github.com/pkg/errors" + "golang.org/x/net/websocket" ) var ( @@ -188,7 +187,7 @@ func init() { } ip, _, _ := net.SplitHostPort(u.Host) environment.IP = ip - cli, err = docker.CreateCompatibleDockerClient( + cli, err = createCompatibleDockerClient( func(specifiedApiVersion string) { log.Printf("[-] [INIT] [Using Docker API version: %s]", specifiedApiVersion) }, @@ -205,6 +204,57 @@ func init() { manager = &service.DefaultManager{Environment: &environment, Client: cli, Config: conf} } +func createCompatibleDockerClient(onVersionSpecified, onVersionDetermined, onUsingDefaultVersion func(string)) (*client.Client, error) { + const dockerApiVersion = "DOCKER_API_VERSION" + dockerApiVersionEnv := os.Getenv(dockerApiVersion) + if dockerApiVersionEnv != "" { + onVersionSpecified(dockerApiVersionEnv) + } else { + maxMajorVersion, maxMinorVersion := parseVersion(api.DefaultVersion) + minMajorVersion, minMinorVersion := parseVersion("1.24") + for majorVersion := maxMajorVersion; majorVersion >= minMajorVersion; majorVersion-- { + for minorVersion := maxMinorVersion; minorVersion >= minMinorVersion; minorVersion-- { + apiVersion := fmt.Sprintf("%d.%d", majorVersion, minorVersion) + _ = os.Setenv(dockerApiVersion, apiVersion) + docker, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return nil, err + } + if isDockerAPIVersionCorrect(docker) { + onVersionDetermined(apiVersion) + return docker, nil + } + _ = docker.Close() + } + } + onUsingDefaultVersion(api.DefaultVersion) + } + return client.NewClientWithOpts(client.FromEnv) +} + +func parseVersion(ver string) (int, int) { + const point = "." + pieces := strings.Split(ver, point) + major, err := strconv.Atoi(pieces[0]) + if err != nil { + return 0, 0 + } + minor, err := strconv.Atoi(pieces[1]) + if err != nil { + return 0, 0 + } + return major, minor +} + +func isDockerAPIVersionCorrect(docker *client.Client) bool { + ctx := context.Background() + apiInfo, err := docker.ServerVersion(ctx) + if err != nil { + return false + } + return apiInfo.APIVersion == docker.ClientVersion() +} + func parseGgrHost(s string) *ggr.Host { h, p, err := net.SplitHostPort(s) if err != nil { @@ -242,7 +292,7 @@ var seleniumPaths = struct { func selenium() http.Handler { mux := http.NewServeMux() - mux.HandleFunc(seleniumPaths.CreateSession, queue.Try(queue.Check(queue.Protect(post(create))))) + mux.HandleFunc(seleniumPaths.CreateSession, post(queue.Try(queue.Check(queue.Protect(create))))) mux.HandleFunc(seleniumPaths.ProxySession, proxy) mux.HandleFunc(paths.Status, status) mux.HandleFunc(paths.Welcome, welcome) @@ -261,7 +311,7 @@ func post(next http.HandlerFunc) http.HandlerFunc { func ping(w http.ResponseWriter, _ *http.Request) { w.Header().Add("Content-Type", "application/json") - json.NewEncoder(w).Encode(struct { + _ = json.NewEncoder(w).Encode(struct { Uptime string `json:"uptime"` LastReloadTime string `json:"lastReloadTime"` NumRequests uint64 `json:"numRequests"` @@ -275,7 +325,7 @@ func video(w http.ResponseWriter, r *http.Request) { deleteFileIfExists(requestId, w, r, videoOutputDir, paths.Video, "DELETED_VIDEO_FILE") return } - user, remote := util.RequestInfo(r) + user, remote := info.RequestInfo(r) if _, ok := r.URL.Query()[jsonParam]; ok { listFilesAsJson(requestId, w, videoOutputDir, "VIDEO_ERROR") return @@ -286,7 +336,7 @@ func video(w http.ResponseWriter, r *http.Request) { } func deleteFileIfExists(requestId uint64, w http.ResponseWriter, r *http.Request, dir string, prefix string, status string) { - user, remote := util.RequestInfo(r) + user, remote := info.RequestInfo(r) fileName := strings.TrimPrefix(r.URL.Path, prefix) filePath := filepath.Join(dir, fileName) _, err := os.Stat(filePath) @@ -329,11 +379,11 @@ func handler() http.Handler { selenium().ServeHTTP(w, r) }) root.HandleFunc(paths.Error, func(w http.ResponseWriter, r *http.Request) { - util.JsonError(w, "Session timed out or not found", http.StatusNotFound) + jsonerror.InvalidSessionID(errors.New("session timed out or not found")).Encode(w) }) root.HandleFunc(paths.Status, func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") - json.NewEncoder(w).Encode(conf.State(sessions, limit, queue.Queued(), queue.Pending())) + _ = json.NewEncoder(w).Encode(conf.State(sessions, limit, queue.Queued(), queue.Pending())) }) root.HandleFunc(paths.Ping, ping) root.Handle(paths.VNC, websocket.Handler(vnc)) @@ -384,7 +434,7 @@ func main() { sessions.Each(func(k string, s *session.Session) { if enableFileUpload { - os.RemoveAll(path.Join(os.TempDir(), k)) + _ = os.RemoveAll(path.Join(os.TempDir(), k)) } s.Cancel() }) diff --git a/metadata.go b/metadata.go index 2481a3e6..b8ed2768 100644 --- a/metadata.go +++ b/metadata.go @@ -1,15 +1,17 @@ +//go:build metadata // +build metadata package main import ( "encoding/json" - "github.com/aerokube/selenoid/event" - "github.com/aerokube/selenoid/session" - "io/ioutil" "log" + "os" "path/filepath" "time" + + "github.com/aerokube/selenoid/event" + "github.com/aerokube/selenoid/session" ) const metadataFileExtension = ".json" @@ -37,7 +39,7 @@ func (mp *MetadataProcessor) OnSessionStopped(stoppedSession event.StoppedSessio return } filename := filepath.Join(logOutputDir, stoppedSession.SessionId+metadataFileExtension) - err = ioutil.WriteFile(filename, data, 0644) + err = os.WriteFile(filename, data, 0644) if err != nil { log.Printf("[%d] [METADATA] [%s] [Failed to save to %s: %v]", stoppedSession.RequestId, stoppedSession.SessionId, filename, err) return diff --git a/protect/queue.go b/protect/queue.go index 6f16bf8a..53c75893 100644 --- a/protect/queue.go +++ b/protect/queue.go @@ -1,12 +1,14 @@ package protect import ( + "errors" + "github.com/aerokube/selenoid/info" "log" "math" "net/http" "time" - "github.com/aerokube/util" + "github.com/aerokube/selenoid/jsonerror" ) // Queue - struct to hold a number of sessions @@ -28,7 +30,8 @@ func (q *Queue) Try(next http.HandlerFunc) http.HandlerFunc { <-q.limit default: if noWait { - util.JsonError(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests) + err := errors.New(http.StatusText(http.StatusTooManyRequests)) + jsonerror.UnknownError(err).Encode(w) return } } @@ -44,9 +47,10 @@ func (q *Queue) Check(next http.HandlerFunc) http.HandlerFunc { <-q.limit default: if q.disabled { - user, remote := util.RequestInfo(r) + user, remote := info.RequestInfo(r) log.Printf("[-] [QUEUE_IS_FULL] [%s] [%s]", user, remote) - util.JsonError(w, "Queue Is Full", http.StatusTooManyRequests) + err := errors.New("queue is full") + jsonerror.UnknownError(err).Encode(w) return } } @@ -57,7 +61,7 @@ func (q *Queue) Check(next http.HandlerFunc) http.HandlerFunc { // Protect - handler to control limit of sessions func (q *Queue) Protect(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - user, remote := util.RequestInfo(r) + user, remote := info.RequestInfo(r) log.Printf("[-] [NEW_REQUEST] [%s] [%s]", user, remote) s := time.Now() go func() { diff --git a/s3_test.go b/s3_test.go index c6e62a54..3ade1170 100644 --- a/s3_test.go +++ b/s3_test.go @@ -1,20 +1,22 @@ +//go:build s3 // +build s3 package main import ( "context" - . "github.com/aandryashin/matchers" - "github.com/aerokube/selenoid/event" - "github.com/aerokube/selenoid/session" - "github.com/aerokube/selenoid/upload" - "io/ioutil" "net" "net/http" "net/http/httptest" + "os" "strings" "testing" "time" + + "github.com/aerokube/selenoid/event" + "github.com/aerokube/selenoid/session" + "github.com/aerokube/selenoid/upload" + assert "github.com/stretchr/testify/require" ) var ( @@ -62,7 +64,7 @@ func TestS3Uploader(t *testing.T) { ReducedRedundancy: true, } uploader.Init() - f, _ := ioutil.TempFile("", "some-file") + f, _ := os.CreateTemp("", "some-file") input := event.CreatedFile{ Event: event.Event{ RequestId: 4342, @@ -73,8 +75,8 @@ func TestS3Uploader(t *testing.T) { Type: "log", } uploaded, err := uploader.Upload(input) - AssertThat(t, err, Is{nil}) - AssertThat(t, uploaded, Is{true}) + assert.NoError(t, err) + assert.True(t, uploaded) } func TestGetKey(t *testing.T) { @@ -91,48 +93,48 @@ func TestGetKey(t *testing.T) { } key := upload.GetS3Key(testPattern, input) - AssertThat(t, key, EqualTo{"some-user/some-Session-id_internet-explorer_11_windows/log.txt"}) + assert.Equal(t, key, "some-user/some-Session-id_internet-explorer_11_windows/log.txt") input.Session.Caps.Name = "" input.Session.Caps.DeviceName = "internet explorer" key = upload.GetS3Key(testPattern, input) - AssertThat(t, key, EqualTo{"some-user/some-Session-id_internet-explorer_11_windows/log.txt"}) + assert.Equal(t, key, "some-user/some-Session-id_internet-explorer_11_windows/log.txt") input.Session.Caps.S3KeyPattern = "$quota/$fileType$fileExtension" key = upload.GetS3Key(testPattern, input) - AssertThat(t, key, EqualTo{"some-user/log.txt"}) + assert.Equal(t, key, "some-user/log.txt") input.Session.Caps.S3KeyPattern = "$fileName" key = upload.GetS3Key(testPattern, input) - AssertThat(t, key, EqualTo{"Some-File.txt"}) + assert.Equal(t, key, "Some-File.txt") } func TestFileMatches(t *testing.T) { matches, err := upload.FileMatches("", "", "any-file-name") - AssertThat(t, err, Is{nil}) - AssertThat(t, matches, Is{true}) + assert.NoError(t, err) + assert.True(t, matches) matches, err = upload.FileMatches("[", "", "/path/to/file.mp4") - AssertThat(t, err, Not{nil}) - AssertThat(t, matches, Is{false}) + assert.Error(t, err) + assert.False(t, matches) matches, err = upload.FileMatches("", "[", "/path/to/file.mp4") - AssertThat(t, err, Not{nil}) - AssertThat(t, matches, Is{false}) + assert.Error(t, err) + assert.False(t, matches) matches, err = upload.FileMatches("*.mp4", "", "/path/to/file.mp4") - AssertThat(t, err, Is{nil}) - AssertThat(t, matches, Is{true}) + assert.NoError(t, err) + assert.True(t, matches) matches, err = upload.FileMatches("*.mp4", "", "/path/to/file.log") - AssertThat(t, err, Is{nil}) - AssertThat(t, matches, Is{false}) + assert.NoError(t, err) + assert.False(t, matches) matches, err = upload.FileMatches("*.mp4", "", "/path/to/file.log") - AssertThat(t, err, Is{nil}) - AssertThat(t, matches, Is{false}) + assert.NoError(t, err) + assert.False(t, matches) matches, err = upload.FileMatches("", "*.log", "/path/to/file.log") - AssertThat(t, err, Is{nil}) - AssertThat(t, matches, Is{false}) + assert.NoError(t, err) + assert.False(t, matches) } diff --git a/selenoid.go b/selenoid.go index d84a7f04..16ce253f 100644 --- a/selenoid.go +++ b/selenoid.go @@ -7,12 +7,9 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" + "errors" "fmt" - "github.com/aerokube/selenoid/event" - "github.com/aerokube/selenoid/service" - "github.com/imdario/mergo" "io" - "io/ioutil" "log" "net" "net/http" @@ -26,10 +23,15 @@ import ( "sync" "time" + "github.com/aerokube/selenoid/info" + + "github.com/aerokube/selenoid/event" + "github.com/aerokube/selenoid/jsonerror" + "github.com/aerokube/selenoid/service" "github.com/aerokube/selenoid/session" - "github.com/aerokube/util" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "github.com/docker/docker/pkg/stdcopy" + "github.com/imdario/mergo" "golang.org/x/net/websocket" ) @@ -109,12 +111,12 @@ func getSerial() uint64 { func create(w http.ResponseWriter, r *http.Request) { sessionStartTime := time.Now() requestId := serial() - user, remote := util.RequestInfo(r) - body, err := ioutil.ReadAll(r.Body) - r.Body.Close() + user, remote := info.RequestInfo(r) + body, err := io.ReadAll(r.Body) + _ = r.Body.Close() if err != nil { log.Printf("[%d] [ERROR_READING_REQUEST] [%v]", requestId, err) - util.JsonError(w, err.Error(), http.StatusBadRequest) + jsonerror.InvalidArgument(err).Encode(w) queue.Drop() return } @@ -128,7 +130,7 @@ func create(w http.ResponseWriter, r *http.Request) { err = json.Unmarshal(body, &browser) if err != nil { log.Printf("[%d] [BAD_JSON_FORMAT] [%v]", requestId, err) - util.JsonError(w, err.Error(), http.StatusBadRequest) + jsonerror.InvalidArgument(err).Encode(w) queue.Drop() return } @@ -146,19 +148,19 @@ func create(w http.ResponseWriter, r *http.Request) { var finalVideoName, finalLogName string for _, fmc := range firstMatchCaps { caps = browser.Caps - mergo.Merge(&caps, *fmc) + _ = mergo.Merge(&caps, *fmc) caps.ProcessExtensionCapabilities() sessionTimeout, err = getSessionTimeout(caps.SessionTimeout, maxTimeout, timeout) if err != nil { log.Printf("[%d] [BAD_SESSION_TIMEOUT] [%s]", requestId, caps.SessionTimeout) - util.JsonError(w, err.Error(), http.StatusBadRequest) + jsonerror.InvalidArgument(err).Encode(w) queue.Drop() return } resolution, err := getScreenResolution(caps.ScreenResolution) if err != nil { log.Printf("[%d] [BAD_SCREEN_RESOLUTION] [%s]", requestId, caps.ScreenResolution) - util.JsonError(w, err.Error(), http.StatusBadRequest) + jsonerror.InvalidArgument(err).Encode(w) queue.Drop() return } @@ -166,7 +168,7 @@ func create(w http.ResponseWriter, r *http.Request) { videoScreenSize, err := getVideoScreenSize(caps.VideoScreenSize, resolution) if err != nil { log.Printf("[%d] [BAD_VIDEO_SCREEN_SIZE] [%s]", requestId, caps.VideoScreenSize) - util.JsonError(w, err.Error(), http.StatusBadRequest) + jsonerror.InvalidArgument(err).Encode(w) queue.Drop() return } @@ -186,24 +188,35 @@ func create(w http.ResponseWriter, r *http.Request) { } if !ok { log.Printf("[%d] [ENVIRONMENT_NOT_AVAILABLE] [%s] [%s]", requestId, caps.BrowserName(), caps.Version) - util.JsonError(w, "Requested environment is not available", http.StatusBadRequest) + jsonerror.InvalidArgument(errors.New("Requested environment is not available")).Encode(w) queue.Drop() return } startedService, err := starter.StartWithCancel() if err != nil { log.Printf("[%d] [SERVICE_STARTUP_FAILED] [%v]", requestId, err) - util.JsonError(w, err.Error(), http.StatusInternalServerError) + jsonerror.SessionNotCreated(err).Encode(w) queue.Drop() return } u := startedService.Url cancel := startedService.Cancel + host := "localhost" + if startedService.Origin != "" { + host = startedService.Origin + } + var resp *http.Response i := 1 for ; ; i++ { r.URL.Host, r.URL.Path = u.Host, path.Join(u.Path, r.URL.Path) - req, _ := http.NewRequest(http.MethodPost, r.URL.String(), bytes.NewReader(body)) + newBody := removeSelenoidOptions(body) + req, _ := http.NewRequest(http.MethodPost, r.URL.String(), bytes.NewReader(newBody)) + contentType := r.Header.Get("Content-Type") + if len(contentType) > 0 { + req.Header.Set("Content-Type", contentType) + } + req.Host = host ctx, done := context.WithTimeout(r.Context(), newSessionAttemptTimeout) defer done() log.Printf("[%d] [SESSION_ATTEMPTED] [%s] [%d]", requestId, u.String(), i) @@ -211,7 +224,7 @@ func create(w http.ResponseWriter, r *http.Request) { select { case <-ctx.Done(): if rsp != nil { - rsp.Body.Close() + _ = rsp.Body.Close() } switch ctx.Err() { case context.DeadlineExceeded: @@ -221,9 +234,9 @@ func create(w http.ResponseWriter, r *http.Request) { } err := fmt.Errorf("New session attempts retry count exceeded") log.Printf("[%d] [SESSION_FAILED] [%s] [%s]", requestId, u.String(), err) - util.JsonError(w, err.Error(), http.StatusInternalServerError) + jsonerror.UnknownError(err).Encode(w) case context.Canceled: - log.Printf("[%d] [CLIENT_DISCONNECTED] [%s] [%s] [%.2fs]", requestId, user, remote, util.SecondsSince(sessionStartTime)) + log.Printf("[%d] [CLIENT_DISCONNECTED] [%s] [%s] [%.2fs]", requestId, user, remote, info.SecondsSince(sessionStartTime)) } queue.Drop() cancel() @@ -232,10 +245,10 @@ func create(w http.ResponseWriter, r *http.Request) { } if err != nil { if rsp != nil { - rsp.Body.Close() + _ = rsp.Body.Close() } log.Printf("[%d] [SESSION_FAILED] [%s] [%s]", requestId, u.String(), err) - util.JsonError(w, err.Error(), http.StatusInternalServerError) + jsonerror.SessionNotCreated(err).Encode(w) queue.Drop() cancel() return @@ -269,12 +282,27 @@ 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) - if s.ID == "" { - s.ID = s.Value.ID + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("[%d] [ERROR_READING_RESPONSE] [%v]", requestId, err) + queue.Drop() + cancel() + w.WriteHeader(resp.StatusCode) + return } + newBody, sessionId, err := processBody(body, r.Host) + if err != nil { + log.Printf("[%d] [ERROR_PROCESSING_RESPONSE] [%v]", requestId, err) + queue.Drop() + cancel() + w.WriteHeader(resp.StatusCode) + return + } + resp.Body = io.NopCloser(bytes.NewReader(newBody)) + resp.ContentLength = int64(len(newBody)) + w.WriteHeader(resp.StatusCode) + _, _ = w.Write(newBody) + s.ID = sessionId } if s.ID == "" { log.Printf("[%d] [SESSION_FAILED] [%s] [%s]", requestId, u.String(), resp.Status) @@ -288,6 +316,7 @@ func create(w http.ResponseWriter, r *http.Request) { URL: u, Container: startedService.Container, HostPort: startedService.HostPort, + Origin: startedService.Origin, Timeout: sessionTimeout, TimeoutCh: onTimeout(sessionTimeout, func() { request{r}.session(s.ID).Delete(requestId) @@ -346,7 +375,74 @@ func create(w http.ResponseWriter, r *http.Request) { sess.Cancel = cancelAndRenameFiles sessions.Put(s.ID, sess) queue.Create() - log.Printf("[%d] [SESSION_CREATED] [%s] [%d] [%.2fs]", requestId, s.ID, i, util.SecondsSince(sessionStartTime)) + log.Printf("[%d] [SESSION_CREATED] [%s] [%d] [%.2fs]", requestId, s.ID, i, info.SecondsSince(sessionStartTime)) +} + +func removeSelenoidOptions(input []byte) []byte { + body := make(map[string]interface{}) + _ = json.Unmarshal(input, &body) + const selenoidOptions = "selenoid:options" + if raw, ok := body["desiredCapabilities"]; ok { + if dc, ok := raw.(map[string]interface{}); ok { + delete(dc, selenoidOptions) + } + } + if raw, ok := body["capabilities"]; ok { + if c, ok := raw.(map[string]interface{}); ok { + if raw, ok := c["alwaysMatch"]; ok { + if am, ok := raw.(map[string]interface{}); ok { + delete(am, selenoidOptions) + } + } + if raw, ok := c["firstMatch"]; ok { + if fm, ok := raw.([]interface{}); ok { + for _, raw := range fm { + if c, ok := raw.(map[string]interface{}); ok { + delete(c, selenoidOptions) + } + } + } + } + } + } + ret, _ := json.Marshal(body) + return ret +} + +func processBody(input []byte, host string) ([]byte, string, error) { + body := make(map[string]interface{}) + sessionId := "" + err := json.Unmarshal(input, &body) + if err != nil { + return nil, sessionId, fmt.Errorf("parse body response: %v", err) + } + // handle jsonwp response from older browsers (chrome < 75) + if rawId, ok := body["sessionId"]; ok { + if si, ok := rawId.(string); ok { + sessionId = si + } + } else { + if raw, ok := body["value"]; ok { + if v, ok := raw.(map[string]interface{}); ok { + if raw, ok := v["capabilities"]; ok { + if c, ok := raw.(map[string]interface{}); ok { + sessionId = v["sessionId"].(string) + c["se:cdp"] = fmt.Sprintf("ws://%s/devtools/%s/", host, sessionId) + if rbv, ok := c["browserVersion"]; ok { + if bv, ok := rbv.(string); ok { + c["se:cdpVersion"] = bv + } + } + } + } + } + } + } + ret, err := json.Marshal(body) + if err != nil { + return nil, sessionId, fmt.Errorf("marshal response: %v", err) + } + return ret, sessionId, nil } func preprocessSessionId(sid string) string { @@ -377,7 +473,7 @@ func getScreenResolution(input string) (string, error) { return fmt.Sprintf("%sx24", input), nil } return "", fmt.Errorf( - "Malformed screenResolution capability: %s. Correct format is WxH (1920x1080) or WxHxD (1920x1080x24).", + "malformed screenResolution capability: %s, correct format is WxH (1920x1080) or WxHxD (1920x1080x24)", input, ) } @@ -392,7 +488,7 @@ func getVideoScreenSize(videoScreenSize string, screenResolution string) (string return videoScreenSize, nil } return "", fmt.Errorf( - "Malformed videoScreenSize capability: %s. Correct format is WxH (1920x1080).", + "malformed videoScreenSize capability: %s, correct format is WxH (1920x1080)", videoScreenSize, ) } @@ -403,7 +499,7 @@ func getSessionTimeout(sessionTimeout string, maxTimeout time.Duration, defaultT if sessionTimeout != "" { st, err := time.ParseDuration(sessionTimeout) if err != nil { - return 0, fmt.Errorf("Invalid sessionTimeout capability: %v", err) + return 0, fmt.Errorf("invalid sessionTimeout capability: %v", err) } if st <= maxTimeout { return st, nil @@ -427,10 +523,12 @@ func getTemporaryFileName(dir string, extension string) string { func generateRandomFileName(extension string) string { randBytes := make([]byte, 16) - rand.Read(randBytes) + _, _ = rand.Read(randBytes) return "selenoid" + hex.EncodeToString(randBytes) + extension } +const vendorPrefix = "aerokube" + func proxy(w http.ResponseWriter, r *http.Request) { done := make(chan func()) go func() { @@ -447,6 +545,15 @@ func proxy(w http.ResponseWriter, r *http.Request) { id := fragments[2] sess, ok := sessions.Get(id) if ok { + if len(fragments) >= 4 && fragments[3] == vendorPrefix { + newFragments := []string{"", fragments[4], id} + if len(fragments) >= 5 { + newFragments = append(newFragments, fragments[5:]...) + } + r.URL.Host = (&request{r}).localaddr() + r.URL.Path = path.Clean(strings.Join(newFragments, slash)) + return + } sess.Lock.Lock() defer sess.Lock.Unlock() select { @@ -456,7 +563,7 @@ func proxy(w http.ResponseWriter, r *http.Request) { } if r.Method == http.MethodDelete && len(fragments) == 3 { if enableFileUpload { - os.RemoveAll(filepath.Join(os.TempDir(), id)) + _ = os.RemoveAll(filepath.Join(os.TempDir(), id)) } cancel = sess.Cancel sessions.Remove(id) @@ -472,7 +579,15 @@ func proxy(w http.ResponseWriter, r *http.Request) { return } } + seUploadPath, uploadPath := "/se/file", "/file" + if strings.HasSuffix(r.URL.Path, seUploadPath) { + r.URL.Path = strings.TrimSuffix(r.URL.Path, seUploadPath) + uploadPath + } r.URL.Host, r.URL.Path = sess.URL.Host, path.Clean(sess.URL.Path+r.URL.Path) + r.Host = "localhost" + if sess.Origin != "" { + r.Host = sess.Origin + } return } r.URL.Path = paths.Error @@ -483,7 +598,7 @@ func proxy(w http.ResponseWriter, r *http.Request) { func defaultErrorHandler(requestId uint64) func(http.ResponseWriter, *http.Request, error) { return func(w http.ResponseWriter, r *http.Request, err error) { - user, remote := util.RequestInfo(r) + user, remote := info.RequestInfo(r) log.Printf("[%d] [CLIENT_DISCONNECTED] [%s] [%s] [Error: %v]", requestId, user, remote, err) w.WriteHeader(http.StatusBadGateway) } @@ -495,6 +610,14 @@ func reverseProxy(hostFn func(sess *session.Session) string, status string) func sid, remainingPath := splitRequestPath(r.URL.Path) sess, ok := sessions.Get(sid) if ok { + select { + case <-sess.TimeoutCh: + default: + close(sess.TimeoutCh) + } + sess.TimeoutCh = onTimeout(sess.Timeout, func() { + request{r}.session(sid).Delete(requestId) + }) (&httputil.ReverseProxy{ Director: func(r *http.Request) { r.URL.Scheme = "http" @@ -505,7 +628,7 @@ func reverseProxy(hostFn func(sess *session.Session) string, status string) func ErrorHandler: defaultErrorHandler(requestId), }).ServeHTTP(w, r) } else { - util.JsonError(w, fmt.Sprintf("Unknown session %s", sid), http.StatusNotFound) + jsonerror.InvalidSessionID(fmt.Errorf("unknown session %s", sid)).Encode(w) log.Printf("[%d] [SESSION_NOT_FOUND] [%s]", requestId, sid) } } @@ -522,41 +645,42 @@ func fileUpload(w http.ResponseWriter, r *http.Request) { } err := json.NewDecoder(r.Body).Decode(&jsonRequest) if err != nil { - util.JsonError(w, err.Error(), http.StatusBadRequest) + jsonerror.InvalidArgument(err).Encode(w) return } z, err := zip.NewReader(bytes.NewReader(jsonRequest.File), int64(len(jsonRequest.File))) if err != nil { - util.JsonError(w, err.Error(), http.StatusBadRequest) + jsonerror.InvalidArgument(err).Encode(w) return } if len(z.File) != 1 { - util.JsonError(w, fmt.Sprintf("Expected there to be only 1 file. There were: %d", len(z.File)), http.StatusBadRequest) + err := fmt.Errorf("expected there to be only 1 file. There were: %d", len(z.File)) + jsonerror.InvalidArgument(err).Encode(w) return } file := z.File[0] src, err := file.Open() if err != nil { - util.JsonError(w, err.Error(), http.StatusBadRequest) + jsonerror.InvalidArgument(err).Encode(w) return } defer src.Close() dir := r.Header.Get("X-Selenoid-File") err = os.MkdirAll(dir, 0755) if err != nil { - util.JsonError(w, err.Error(), http.StatusInternalServerError) + jsonerror.UnknownError(err).Encode(w) return } fileName := filepath.Join(dir, file.Name) dst, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0644) if err != nil { - util.JsonError(w, err.Error(), http.StatusInternalServerError) + jsonerror.UnknownError(err).Encode(w) return } defer dst.Close() _, err = io.Copy(dst, src) if err != nil { - util.JsonError(w, err.Error(), http.StatusInternalServerError) + jsonerror.UnknownError(err).Encode(w) return } @@ -565,7 +689,7 @@ func fileUpload(w http.ResponseWriter, r *http.Request) { }{ V: fileName, } - json.NewEncoder(w).Encode(reply) + _ = json.NewEncoder(w).Encode(reply) } func vnc(wsconn *websocket.Conn) { @@ -586,11 +710,11 @@ func vnc(wsconn *websocket.Conn) { defer conn.Close() wsconn.PayloadType = websocket.BinaryFrame go func() { - io.Copy(wsconn, conn) - wsconn.Close() + _, _ = io.Copy(wsconn, conn) + _ = wsconn.Close() log.Printf("[%d] [VNC_SESSION_CLOSED] [%s]", requestId, sid) }() - io.Copy(conn, wsconn) + _, _ = io.Copy(conn, wsconn) log.Printf("[%d] [VNC_CLIENT_DISCONNECTED] [%s]", requestId, sid) } else { log.Printf("[%d] [VNC_NOT_ENABLED] [%s]", requestId, sid) @@ -612,7 +736,7 @@ func logs(w http.ResponseWriter, r *http.Request) { deleteFileIfExists(requestId, w, r, logOutputDir, paths.Logs, "DELETED_LOG_FILE") return } - user, remote := util.RequestInfo(r) + user, remote := info.RequestInfo(r) if _, ok := r.URL.Query()[jsonParam]; ok { listFilesAsJson(requestId, w, logOutputDir, "LOG_ERROR") return @@ -626,7 +750,7 @@ func logs(w http.ResponseWriter, r *http.Request) { } func listFilesAsJson(requestId uint64, w http.ResponseWriter, dir string, errStatus string) { - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) if err != nil { log.Printf("[%d] [%s] [%s]", requestId, errStatus, fmt.Sprintf("Failed to list directory %s: %v", logOutputDir, err)) w.WriteHeader(http.StatusInternalServerError) @@ -637,7 +761,7 @@ func listFilesAsJson(requestId uint64, w http.ResponseWriter, dir string, errSta ret = append(ret, f.Name()) } w.Header().Add("Content-Type", "application/json") - json.NewEncoder(w).Encode(ret) + _ = json.NewEncoder(w).Encode(ret) } func streamLogs(wsconn *websocket.Conn) { @@ -647,7 +771,7 @@ func streamLogs(wsconn *websocket.Conn) { sess, ok := sessions.Get(sid) if ok && sess.Container != nil { log.Printf("[%d] [CONTAINER_LOGS] [%s]", requestId, sess.Container.ID) - r, err := cli.ContainerLogs(wsconn.Request().Context(), sess.Container.ID, types.ContainerLogsOptions{ + r, err := cli.ContainerLogs(wsconn.Request().Context(), sess.Container.ID, container.LogsOptions{ ShowStdout: true, ShowStderr: true, Follow: true, @@ -658,7 +782,7 @@ func streamLogs(wsconn *websocket.Conn) { } defer r.Close() wsconn.PayloadType = websocket.BinaryFrame - stdcopy.StdCopy(wsconn, wsconn, r) + _, _ = stdcopy.StdCopy(wsconn, wsconn, r) log.Printf("[%d] [CONTAINER_LOGS_DISCONNECTED] [%s]", requestId, sid) } else { log.Printf("[%d] [SESSION_NOT_FOUND] [%s]", requestId, sid) @@ -668,7 +792,7 @@ func streamLogs(wsconn *websocket.Conn) { func status(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") ready := limit > sessions.Len() - json.NewEncoder(w).Encode( + _ = json.NewEncoder(w).Encode( map[string]interface{}{ "value": map[string]interface{}{ "message": fmt.Sprintf("Selenoid %s built at %s", gitRevision, buildStamp), @@ -679,7 +803,7 @@ func status(w http.ResponseWriter, _ *http.Request) { func welcome(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte(fmt.Sprintf("You are using Selenoid %s!", gitRevision))) + _, _ = w.Write([]byte(fmt.Sprintf("You are using Selenoid %s!", gitRevision))) } func onTimeout(t time.Duration, f func()) chan struct{} { diff --git a/selenoid_test.go b/selenoid_test.go index c1836cfc..b6f568d0 100644 --- a/selenoid_test.go +++ b/selenoid_test.go @@ -3,26 +3,23 @@ package main import ( "bytes" "context" + "encoding/json" "fmt" - "github.com/mafredri/cdp" - "github.com/mafredri/cdp/rpcc" - "io/ioutil" + "io" "log" "net/http" "net/http/httptest" "os" + "path/filepath" "strings" "testing" "time" - "github.com/aerokube/selenoid/config" - - "encoding/json" - "path/filepath" - - . "github.com/aandryashin/matchers" - . "github.com/aandryashin/matchers/httpresp" ggr "github.com/aerokube/ggr/config" + "github.com/aerokube/selenoid/config" + "github.com/mafredri/cdp" + "github.com/mafredri/cdp/rpcc" + assert "github.com/stretchr/testify/require" ) var _ = func() bool { @@ -36,8 +33,8 @@ var ( func init() { enableFileUpload = true - videoOutputDir, _ = ioutil.TempDir("", "selenoid-test") - logOutputDir, _ = ioutil.TempDir("", "selenoid-test") + videoOutputDir, _ = os.MkdirTemp("", "selenoid-test") + logOutputDir, _ = os.MkdirTemp("", "selenoid-test") saveAllLogs = true gitRevision = "test-revision" ggrHost = &ggr.Host{ @@ -51,56 +48,52 @@ func TestNewSessionWithGet(t *testing.T) { manager = &HTTPTest{Handler: Selenium()} rsp, err := http.Get(With(srv.URL).Path("/wd/hub/session")) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusMethodNotAllowed}) - - AssertThat(t, queue.Used(), EqualTo{0}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusMethodNotAllowed) + assert.Equal(t, queue.Used(), 0) } func TestBadJsonFormat(t *testing.T) { rsp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", nil) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusBadRequest}) - - AssertThat(t, queue.Used(), EqualTo{0}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusBadRequest) + assert.Equal(t, queue.Used(), 0) } func TestServiceStartupFailure(t *testing.T) { manager = &StartupError{} rsp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusInternalServerError}) - - AssertThat(t, queue.Used(), EqualTo{0}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusInternalServerError) + assert.Equal(t, queue.Used(), 0) } func TestBrowserNotFound(t *testing.T) { manager = &BrowserNotFound{} rsp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusBadRequest}) - - AssertThat(t, queue.Used(), EqualTo{0}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusBadRequest) + assert.Equal(t, queue.Used(), 0) } func TestGetDefaultScreenResolution(t *testing.T) { res, err := getScreenResolution("") - AssertThat(t, err, Is{nil}) - AssertThat(t, res, EqualTo{"1920x1080x24"}) + assert.NoError(t, err) + assert.Equal(t, res, "1920x1080x24") } func TestGetFullScreenResolution(t *testing.T) { res, err := getScreenResolution("1024x768x24") - AssertThat(t, err, Is{nil}) - AssertThat(t, res, EqualTo{"1024x768x24"}) + assert.NoError(t, err) + assert.Equal(t, res, "1024x768x24") } func TestGetShortScreenResolution(t *testing.T) { res, err := getScreenResolution("1024x768") - AssertThat(t, err, Is{nil}) - AssertThat(t, res, EqualTo{"1024x768x24"}) + assert.NoError(t, err) + assert.Equal(t, res, "1024x768x24") } func TestTooBigSessionTimeoutCapability(t *testing.T) { @@ -115,52 +108,48 @@ func testBadSessionTimeoutCapability(t *testing.T, timeoutValue string) { manager = &BrowserNotFound{} rsp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte(fmt.Sprintf(`{"desiredCapabilities":{"sessionTimeout":"%s"}}`, timeoutValue)))) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusBadRequest}) - - AssertThat(t, queue.Used(), EqualTo{0}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusBadRequest) + assert.Equal(t, queue.Used(), 0) } func TestMalformedScreenResolutionCapability(t *testing.T) { manager = &BrowserNotFound{} rsp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte(`{"desiredCapabilities":{"screenResolution":"bad-resolution"}}`))) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusBadRequest}) - - AssertThat(t, queue.Used(), EqualTo{0}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusBadRequest) + assert.Equal(t, queue.Used(), 0) } func TestGetVideoScreenSizeFromCapability(t *testing.T) { res, err := getVideoScreenSize("1024x768", "anything") - AssertThat(t, err, Is{nil}) - AssertThat(t, res, EqualTo{"1024x768"}) + assert.NoError(t, err) + assert.Equal(t, res, "1024x768") } func TestDetermineVideoScreenSizeFromScreenResolution(t *testing.T) { res, err := getVideoScreenSize("", "1024x768x24") - AssertThat(t, err, Is{nil}) - AssertThat(t, res, EqualTo{"1024x768"}) + assert.NoError(t, err) + assert.Equal(t, res, "1024x768") } func TestMalformedVideoScreenSizeCapability(t *testing.T) { manager = &BrowserNotFound{} rsp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte(`{"desiredCapabilities":{"videoScreenSize":"bad-size"}}`))) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusBadRequest}) - - AssertThat(t, queue.Used(), EqualTo{0}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusBadRequest) + assert.Equal(t, queue.Used(), 0) } func TestNewSessionNotFound(t *testing.T) { manager = &HTTPTest{Handler: Selenium()} rsp, err := http.Get(With(srv.URL).Path("/wd/hub/session/123")) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusNotFound}) - - AssertThat(t, queue.Used(), EqualTo{0}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusNotFound) + assert.Equal(t, queue.Used(), 0) } func TestNewSessionHostDown(t *testing.T) { @@ -177,13 +166,13 @@ func TestNewSessionHostDown(t *testing.T) { } rsp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusInternalServerError}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusInternalServerError) canceled = <-ch - AssertThat(t, canceled, Is{true}) + assert.True(t, canceled) - AssertThat(t, queue.Used(), EqualTo{0}) + assert.Equal(t, queue.Used(), 0) } func TestNewSessionBadHostResponse(t *testing.T) { @@ -195,13 +184,12 @@ func TestNewSessionBadHostResponse(t *testing.T) { } rsp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusBadRequest}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusBadRequest) canceled = <-ch - AssertThat(t, canceled, Is{true}) - - AssertThat(t, queue.Used(), EqualTo{0}) + assert.True(t, canceled) + assert.Equal(t, queue.Used(), 0) } func TestSessionCreated(t *testing.T) { @@ -209,16 +197,18 @@ func TestSessionCreated(t *testing.T) { timeout = 5 * time.Second resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte(`{"desiredCapabilities": {"enableVideo": true, "enableVNC": true, "sessionTimeout": "3s"}}`))) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) resp, err = http.Get(With(srv.URL).Path("/status")) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var state config.State - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&state}}) - AssertThat(t, state.Used, EqualTo{1}) - AssertThat(t, queue.Used(), EqualTo{1}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&state)) + assert.Equal(t, state.Used, 1) + assert.Equal(t, queue.Used(), 1) sessions.Remove(sess["sessionId"]) queue.Release() } @@ -227,26 +217,28 @@ func TestSessionCreatedW3C(t *testing.T) { manager = &HTTPTest{Handler: Selenium()} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte(`{"capabilities":{"alwaysMatch":{"acceptInsecureCerts":true, "browserName":"firefox", "browserVersion":"latest", "selenoid:options":{"enableVNC": true}}}}`))) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) resp, err = http.Get(With(srv.URL).Path("/status")) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var state config.State - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&state}}) - AssertThat(t, state.Used, EqualTo{1}) - AssertThat(t, queue.Used(), EqualTo{1}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&state)) + assert.Equal(t, state.Used, 1) + assert.Equal(t, queue.Used(), 1) versions, firefoxPresent := state.Browsers["firefox"] - AssertThat(t, firefoxPresent, Is{true}) + assert.True(t, firefoxPresent) users, versionPresent := versions["latest"] - AssertThat(t, versionPresent, Is{true}) + assert.True(t, versionPresent) userInfo, userPresent := users["unknown"] - AssertThat(t, userPresent, Is{true}) - AssertThat(t, userInfo, Not{nil}) - AssertThat(t, len(userInfo.Sessions), EqualTo{1}) - AssertThat(t, userInfo.Sessions[0].VNC, EqualTo{true}) + assert.True(t, userPresent) + assert.NotNil(t, userInfo) + assert.Len(t, userInfo.Sessions, 1) + assert.True(t, userInfo.Sessions[0].VNC) sessions.Remove(sess["sessionId"]) queue.Release() @@ -256,26 +248,28 @@ func TestSessionCreatedFirstMatchOnly(t *testing.T) { manager = &HTTPTest{Handler: Selenium()} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte(`{"capabilities":{"firstMatch":[{"browserName":"firefox", "browserVersion":"latest", "selenoid:options":{"enableVNC": true}}]}}`))) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) resp, err = http.Get(With(srv.URL).Path("/status")) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var state config.State - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&state}}) - AssertThat(t, state.Used, EqualTo{1}) - AssertThat(t, queue.Used(), EqualTo{1}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&state)) + assert.Equal(t, state.Used, 1) + assert.Equal(t, queue.Used(), 1) versions, firefoxPresent := state.Browsers["firefox"] - AssertThat(t, firefoxPresent, Is{true}) + assert.True(t, firefoxPresent) users, versionPresent := versions["latest"] - AssertThat(t, versionPresent, Is{true}) + assert.True(t, versionPresent) userInfo, userPresent := users["unknown"] - AssertThat(t, userPresent, Is{true}) - AssertThat(t, userInfo, Not{nil}) - AssertThat(t, len(userInfo.Sessions), EqualTo{1}) - AssertThat(t, userInfo.Sessions[0].VNC, EqualTo{true}) + assert.True(t, userPresent) + assert.NotNil(t, userInfo) + assert.Len(t, userInfo.Sessions, 1) + assert.True(t, userInfo.Sessions[0].VNC) sessions.Remove(sess["sessionId"]) queue.Release() @@ -290,16 +284,44 @@ func TestSessionCreatedWdHub(t *testing.T) { manager = &HTTPTest{Handler: root} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) resp, err = http.Get(With(srv.URL).Path("/status")) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var state config.State - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&state}}) - AssertThat(t, state.Used, EqualTo{1}) - AssertThat(t, queue.Used(), EqualTo{1}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&state)) + assert.Equal(t, state.Used, 1) + assert.Equal(t, queue.Used(), 1) + sessions.Remove(sess["sessionId"]) + queue.Release() +} + +func TestSessionWithContentTypeCreatedWdHub(t *testing.T) { + root := http.NewServeMux() + root.Handle("/wd/hub/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.URL.Path = strings.TrimPrefix(r.URL.Path, "/wd/hub") + assert.Equal(t, r.Header.Get("Content-Type"), "application/json; charset=utf-8") + Selenium().ServeHTTP(w, r) + })) + manager = &HTTPTest{Handler: root} + + resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "application/json; charset=utf-8", bytes.NewReader([]byte("{}"))) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) + var sess map[string]string + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) + + resp, err = http.Get(With(srv.URL).Path("/status")) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) + var state config.State + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&state)) + assert.Equal(t, state.Used, 1) + assert.Equal(t, queue.Used(), 1) sessions.Remove(sess["sessionId"]) queue.Release() } @@ -311,15 +333,16 @@ func TestSessionFailedAfterTimeout(t *testing.T) { })} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - AssertThat(t, resp, AllOf{Code{http.StatusInternalServerError}}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusInternalServerError) resp, err = http.Get(With(srv.URL).Path("/status")) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var state config.State - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&state}}) - AssertThat(t, state.Used, EqualTo{0}) - AssertThat(t, queue.Used(), EqualTo{0}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&state)) + assert.Equal(t, state.Used, 0) + assert.Equal(t, queue.Used(), 0) } func TestClientDisconnected(t *testing.T) { @@ -334,11 +357,12 @@ func TestClientDisconnected(t *testing.T) { cancel() resp, err := http.Get(With(srv.URL).Path("/status")) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var state config.State - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&state}}) - AssertThat(t, state.Used, EqualTo{0}) - AssertThat(t, queue.Used(), EqualTo{0}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&state)) + assert.Equal(t, state.Used, 0) + assert.Equal(t, queue.Used(), 0) } func TestSessionFailedAfterTwoTimeout(t *testing.T) { @@ -349,15 +373,16 @@ func TestSessionFailedAfterTwoTimeout(t *testing.T) { })} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - AssertThat(t, resp, AllOf{Code{http.StatusInternalServerError}}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusInternalServerError) resp, err = http.Get(With(srv.URL).Path("/status")) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var state config.State - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&state}}) - AssertThat(t, state.Used, EqualTo{0}) - AssertThat(t, queue.Used(), EqualTo{0}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&state)) + assert.Equal(t, state.Used, 0) + assert.Equal(t, queue.Used(), 0) } func TestSessionCreatedRedirect(t *testing.T) { @@ -373,36 +398,72 @@ func TestSessionCreatedRedirect(t *testing.T) { manager = &HTTPTest{Handler: root} resp, err := httpClient.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - AssertThat(t, resp.StatusCode, Is{http.StatusFound}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusFound) location := resp.Header.Get("Location") - AssertThat(t, resp.StatusCode, Is{Not{""}}) fragments := strings.Split(location, "/") sid := fragments[len(fragments)-1] resp, err = http.Get(With(srv.URL).Path("/status")) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var state config.State - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&state}}) - AssertThat(t, state.Used, EqualTo{1}) - AssertThat(t, queue.Used(), EqualTo{1}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&state)) + assert.Equal(t, state.Used, 1) + assert.Equal(t, queue.Used(), 1) sessions.Remove(sid) queue.Release() } +func TestSessionCreatedRemoveExtensionCapabilities(t *testing.T) { + desiredCapabilitiesPresent := true + alwaysMatchPresent := true + firstMatchPresent := true + chromeOptionsPresent := true + + var browser struct { + Caps map[string]interface{} `json:"desiredCapabilities"` + W3CCaps struct { + AlwaysMatch map[string]interface{} `json:"alwaysMatch"` + FirstMatch []map[string]interface{} `json:"firstMatch"` + } `json:"capabilities"` + } + + root := http.NewServeMux() + root.Handle("/wd/hub/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := json.NewDecoder(r.Body).Decode(&browser) + assert.NoError(t, err) + _, desiredCapabilitiesPresent = browser.Caps["selenoid:options"] + _, alwaysMatchPresent = browser.W3CCaps.AlwaysMatch["selenoid:options"] + _, chromeOptionsPresent = browser.W3CCaps.AlwaysMatch["goog:chromeOptions"] + assert.Len(t, browser.W3CCaps.FirstMatch, 1) + _, firstMatchPresent = browser.W3CCaps.FirstMatch[0]["selenoid:options"] + })) + manager = &HTTPTest{Handler: root} + + resp, err := httpClient.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte(`{"desiredCapabilities": {"browserName": "chrome", "selenoid:options": {"enableVNC": true}}, "capabilities":{"alwaysMatch":{"browserName": "chrome", "goog:chromeOptions": {"args": ["headless"]}, "selenoid:options":{"enableVNC": true}}, "firstMatch": [{"platform": "linux", "selenoid:options": {"enableVideo": true}}]}}`))) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) + assert.False(t, desiredCapabilitiesPresent) + assert.False(t, alwaysMatchPresent) + assert.True(t, chromeOptionsPresent) + assert.False(t, firstMatchPresent) +} + func TestProxySession(t *testing.T) { manager = &HTTPTest{Handler: Selenium()} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) resp, err = http.Get(With(srv.URL).Path(fmt.Sprintf("/wd/hub/session/%s/url", sess["sessionId"]))) - AssertThat(t, err, Is{nil}) - AssertThat(t, resp, Code{http.StatusOK}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) - AssertThat(t, queue.Used(), EqualTo{1}) + assert.Equal(t, queue.Used(), 1) sessions.Remove(sess["sessionId"]) queue.Release() } @@ -412,13 +473,14 @@ func TestProxySessionPanicOnAbortHandler(t *testing.T) { manager = &HTTPTest{Handler: Selenium()} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) req, _ := http.NewRequest(http.MethodGet, With(srv.URL).Path(fmt.Sprintf("/wd/hub/session/%s/url?abort-handler=true", sess["sessionId"])), nil) resp, err = http.DefaultClient.Do(req) - AssertThat(t, err, Not{nil}) + assert.Error(t, err) sessions.Remove(sess["sessionId"]) queue.Release() @@ -433,39 +495,43 @@ func TestSessionDeleted(t *testing.T) { } resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte(`{"desiredCapabilities": {"enableVideo": true}}`))) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) req, _ := http.NewRequest(http.MethodDelete, With(srv.URL).Path(fmt.Sprintf("/wd/hub/session/%s", sess["sessionId"])), nil) - http.DefaultClient.Do(req) + _, err = http.DefaultClient.Do(req) + assert.NoError(t, err) resp, err = http.Get(With(srv.URL).Path("/status")) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var state config.State - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&state}}) - AssertThat(t, state.Used, EqualTo{0}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&state)) + assert.Equal(t, state.Used, 0) canceled = <-ch - AssertThat(t, canceled, Is{true}) + assert.True(t, canceled) - AssertThat(t, queue.Used(), EqualTo{0}) + assert.Equal(t, queue.Used(), 0) } func TestSessionOnClose(t *testing.T) { manager = &HTTPTest{Handler: Selenium()} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) req, _ := http.NewRequest(http.MethodDelete, With(srv.URL).Path(fmt.Sprintf("/wd/hub/session/%s/window", sess["sessionId"])), nil) - http.DefaultClient.Do(req) + _, _ = http.DefaultClient.Do(req) - AssertThat(t, queue.Used(), EqualTo{1}) + assert.Equal(t, queue.Used(), 1) sessions.Remove(sess["sessionId"]) queue.Release() } @@ -480,29 +546,30 @@ func TestProxySessionCanceled(t *testing.T) { timeout = 100 * time.Millisecond resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) _, ok := sessions.Get(sess["sessionId"]) - AssertThat(t, ok, Is{true}) + assert.True(t, ok) req, _ := http.NewRequest(http.MethodGet, With(srv.URL).Path(fmt.Sprintf("/wd/hub/session/%s/url?timeout=1s", sess["sessionId"])), nil) ctx, cancel := context.WithCancel(context.Background()) req = req.WithContext(ctx) go func() { - http.DefaultClient.Do(req) + _, _ = http.DefaultClient.Do(req) }() <-time.After(50 * time.Millisecond) cancel() <-time.After(100 * time.Millisecond) _, ok = sessions.Get(sess["sessionId"]) - AssertThat(t, ok, Is{false}) + assert.False(t, ok) canceled = <-ch - AssertThat(t, canceled, Is{true}) + assert.True(t, canceled) - AssertThat(t, queue.Used(), EqualTo{0}) + assert.Equal(t, queue.Used(), 0) } func TestNewSessionTimeout(t *testing.T) { @@ -515,21 +582,22 @@ func TestNewSessionTimeout(t *testing.T) { timeout = 30 * time.Millisecond resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) _, ok := sessions.Get(sess["sessionId"]) - AssertThat(t, ok, Is{true}) + assert.True(t, ok) <-time.After(50 * time.Millisecond) _, ok = sessions.Get(sess["sessionId"]) - AssertThat(t, ok, Is{false}) + assert.False(t, ok) canceled = <-ch - AssertThat(t, canceled, Is{true}) + assert.True(t, canceled) - AssertThat(t, queue.Used(), EqualTo{0}) + assert.Equal(t, queue.Used(), 0) } func TestProxySessionTimeout(t *testing.T) { @@ -542,63 +610,63 @@ func TestProxySessionTimeout(t *testing.T) { timeout = 30 * time.Millisecond resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) _, ok := sessions.Get(sess["sessionId"]) - AssertThat(t, ok, Is{true}) + assert.True(t, ok) <-time.After(20 * time.Millisecond) _, ok = sessions.Get(sess["sessionId"]) - AssertThat(t, ok, Is{true}) - http.Get(With(srv.URL).Path(fmt.Sprintf("/wd/hub/session/%s/url", sess["sessionId"]))) + assert.True(t, ok) + _, _ = http.Get(With(srv.URL).Path(fmt.Sprintf("/wd/hub/session/%s/url", sess["sessionId"]))) <-time.After(20 * time.Millisecond) _, ok = sessions.Get(sess["sessionId"]) - AssertThat(t, ok, Is{true}) + assert.True(t, ok) <-time.After(50 * time.Millisecond) _, ok = sessions.Get(sess["sessionId"]) - AssertThat(t, ok, Is{false}) + assert.False(t, ok) canceled = <-ch - AssertThat(t, canceled, Is{true}) + assert.True(t, canceled) - AssertThat(t, queue.Used(), EqualTo{0}) + assert.Equal(t, queue.Used(), 0) } func TestFileUpload(t *testing.T) { manager = &HTTPTest{Handler: Selenium()} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) fileContents := []byte(`{"file":"UEsDBBQACAgIAJiC4koAAAAAAAAAAAAAAAAJAAAAaGVsbG8udHh080jNyclXCM8vyklRBABQSwcIoxwpHA4AAAAMAAAAUEsBAhQAFAAICAgAmILiSqMcKRwOAAAADAAAAAkAAAAAAAAAAAAAAAAAAAAAAGhlbGxvLnR4dFBLBQYAAAAAAQABADcAAABFAAAAAAA="}`) //Doing two times to test sequential upload resp, err = http.Post(With(srv.URL).Path(fmt.Sprintf("/wd/hub/session/%s/file", sess["sessionId"])), "", bytes.NewReader(fileContents)) - AssertThat(t, err, Is{nil}) - AssertThat(t, resp, Code{http.StatusOK}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) resp, err = http.Post(With(srv.URL).Path(fmt.Sprintf("/wd/hub/session/%s/file", sess["sessionId"])), "", bytes.NewReader(fileContents)) - AssertThat(t, err, Is{nil}) - AssertThat(t, resp, Code{http.StatusOK}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var jsonResponse map[string]string - - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&jsonResponse}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&jsonResponse)) f, err := os.Open(jsonResponse["value"]) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) - content, err := ioutil.ReadAll(f) - AssertThat(t, err, Is{nil}) + content, err := io.ReadAll(f) + assert.NoError(t, err) - AssertThat(t, string(content), EqualTo{"Hello World!"}) + assert.Equal(t, string(content), "Hello World!") sessions.Remove(sess["sessionId"]) queue.Release() @@ -608,14 +676,14 @@ func TestFileUploadBadJson(t *testing.T) { manager = &HTTPTest{Handler: Selenium()} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) resp, err = http.Post(With(srv.URL).Path(fmt.Sprintf("/wd/hub/session/%s/file", sess["sessionId"])), "", bytes.NewReader([]byte(`malformed json`))) - AssertThat(t, err, Is{nil}) - AssertThat(t, resp, Code{http.StatusBadRequest}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusBadRequest) sessions.Remove(sess["sessionId"]) queue.Release() @@ -625,14 +693,14 @@ func TestFileUploadNoFile(t *testing.T) { manager = &HTTPTest{Handler: Selenium()} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) resp, err = http.Post(With(srv.URL).Path(fmt.Sprintf("/wd/hub/session/%s/file", sess["sessionId"])), "", bytes.NewReader([]byte(`{}`))) - AssertThat(t, err, Is{nil}) - AssertThat(t, resp, Code{http.StatusBadRequest}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusBadRequest) sessions.Remove(sess["sessionId"]) queue.Release() @@ -642,14 +710,14 @@ func TestFileUploadTwoFiles(t *testing.T) { manager = &HTTPTest{Handler: Selenium()} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) resp, err = http.Post(With(srv.URL).Path(fmt.Sprintf("/wd/hub/session/%s/file", sess["sessionId"])), "", bytes.NewReader([]byte(`{"file":"UEsDBAoAAAAAAKGJ4koAAAAAAAAAAAAAAAAHABwAb25lLnR4dFVUCQADbv9YWZT/WFl1eAsAAQT1AQAABBQAAABQSwMECgAAAAAApIniSgAAAAAAAAAAAAAAAAcAHAB0d28udHh0VVQJAANz/1hZc/9YWXV4CwABBPUBAAAEFAAAAFBLAQIeAwoAAAAAAKGJ4koAAAAAAAAAAAAAAAAHABgAAAAAAAAAAACkgQAAAABvbmUudHh0VVQFAANu/1hZdXgLAAEE9QEAAAQUAAAAUEsBAh4DCgAAAAAApIniSgAAAAAAAAAAAAAAAAcAGAAAAAAAAAAAAKSBQQAAAHR3by50eHRVVAUAA3P/WFl1eAsAAQT1AQAABBQAAABQSwUGAAAAAAIAAgCaAAAAggAAAAAA"}`))) - AssertThat(t, err, Is{nil}) - AssertThat(t, resp, Code{http.StatusBadRequest}) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusBadRequest) sessions.Remove(sess["sessionId"]) queue.Release() @@ -658,116 +726,128 @@ func TestFileUploadTwoFiles(t *testing.T) { func TestPing(t *testing.T) { rsp, err := http.Get(With(srv.URL).Path("/ping")) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusOK}) - AssertThat(t, rsp.Body, Is{Not{nil}}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusOK) + assert.NotNil(t, rsp.Body) var data map[string]interface{} - bt, readErr := ioutil.ReadAll(rsp.Body) - AssertThat(t, readErr, Is{nil}) + bt, readErr := io.ReadAll(rsp.Body) + assert.NoError(t, readErr) jsonErr := json.Unmarshal(bt, &data) - AssertThat(t, jsonErr, Is{nil}) + assert.NoError(t, jsonErr) _, hasUptime := data["uptime"] - AssertThat(t, hasUptime, Is{true}) + assert.True(t, hasUptime) _, hasLastReloadTime := data["lastReloadTime"] - AssertThat(t, hasLastReloadTime, Is{true}) + assert.True(t, hasLastReloadTime) _, hasNumRequests := data["numRequests"] - AssertThat(t, hasNumRequests, Is{true}) + assert.True(t, hasNumRequests) version, hasVersion := data["version"] - AssertThat(t, hasVersion, Is{true}) - AssertThat(t, version, EqualTo{"test-revision"}) + assert.True(t, hasVersion) + assert.Equal(t, version, "test-revision") } func TestStatus(t *testing.T) { rsp, err := http.Get(With(srv.URL).Path("/wd/hub/status")) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusOK}) - AssertThat(t, rsp.Body, Is{Not{nil}}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusOK) + assert.NotNil(t, rsp.Body) var data map[string]interface{} - bt, readErr := ioutil.ReadAll(rsp.Body) - AssertThat(t, readErr, Is{nil}) + bt, readErr := io.ReadAll(rsp.Body) + assert.NoError(t, readErr) jsonErr := json.Unmarshal(bt, &data) - AssertThat(t, jsonErr, Is{nil}) + assert.NoError(t, jsonErr) value, hasValue := data["value"] - AssertThat(t, hasValue, Is{true}) + assert.True(t, hasValue) valueMap := value.(map[string]interface{}) ready, hasReady := valueMap["ready"] - AssertThat(t, hasReady, Is{true}) - AssertThat(t, ready, Is{true}) + assert.True(t, hasReady) + assert.Equal(t, ready, true) _, hasMessage := valueMap["message"] - AssertThat(t, hasMessage, Is{true}) + assert.True(t, hasMessage) } func TestServeAndDeleteVideoFile(t *testing.T) { fileName := "testfile" filePath := filepath.Join(videoOutputDir, fileName) - ioutil.WriteFile(filePath, []byte("test-data"), 0644) + _ = os.WriteFile(filePath, []byte("test-data"), 0644) rsp, err := http.Get(With(srv.URL).Path("/video/testfile")) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusOK}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusOK) rsp, err = http.Get(With(srv.URL).Path("/video/?json")) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusOK}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusOK) var files []string - AssertThat(t, rsp, IsJson{&files}) - AssertThat(t, files, EqualTo{[]string{"testfile"}}) + assert.NoError(t, json.NewDecoder(rsp.Body).Decode(&files)) + assert.Equal(t, files, []string{"testfile"}) deleteReq, _ := http.NewRequest(http.MethodDelete, With(srv.URL).Path("/video/testfile"), nil) rsp, err = http.DefaultClient.Do(deleteReq) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusOK}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusOK) //Deleting already deleted file rsp, err = http.DefaultClient.Do(deleteReq) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusNotFound}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusNotFound) } func TestServeAndDeleteLogFile(t *testing.T) { fileName := "logfile.log" filePath := filepath.Join(logOutputDir, fileName) - ioutil.WriteFile(filePath, []byte("test-data"), 0644) + _ = os.WriteFile(filePath, []byte("test-data"), 0644) rsp, err := http.Get(With(srv.URL).Path("/logs/logfile.log")) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusOK}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusOK) rsp, err = http.Get(With(srv.URL).Path("/logs/?json")) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusOK}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusOK) var files []string - AssertThat(t, rsp, IsJson{&files}) - AssertThat(t, len(files) > 0, Is{true}) + assert.NoError(t, json.NewDecoder(rsp.Body).Decode(&files)) + assert.True(t, len(files) > 0) deleteReq, _ := http.NewRequest(http.MethodDelete, With(srv.URL).Path("/logs/logfile.log"), nil) rsp, err = http.DefaultClient.Do(deleteReq) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusOK}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusOK) rsp, err = http.DefaultClient.Do(deleteReq) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusNotFound}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusNotFound) } func TestFileDownload(t *testing.T) { + testFileDownload(t, func(sessionId string) string { + return fmt.Sprintf("/download/%s/testfile", sessionId) + }) +} + +func TestFileDownloadProtocolExtension(t *testing.T) { + testFileDownload(t, func(sessionId string) string { + return fmt.Sprintf("/wd/hub/session/%s/aerokube/download/testfile", sessionId) + }) +} + +func testFileDownload(t *testing.T, path func(string) string) { manager = &HTTPTest{Handler: Selenium()} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) - rsp, err := http.Get(With(srv.URL).Path(fmt.Sprintf("/download/%s/testfile", sess["sessionId"]))) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusOK}) - data, err := ioutil.ReadAll(rsp.Body) - AssertThat(t, err, Is{nil}) - AssertThat(t, string(data), EqualTo{"test-data"}) + rsp, err := http.Get(With(srv.URL).Path(path(sess["sessionId"]))) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) + data, err := io.ReadAll(rsp.Body) + assert.NoError(t, err) + assert.Equal(t, string(data), "test-data") sessions.Remove(sess["sessionId"]) queue.Release() @@ -775,29 +855,41 @@ func TestFileDownload(t *testing.T) { func TestFileDownloadMissingSession(t *testing.T) { rsp, err := http.Get(With(srv.URL).Path("/download/missing-session/testfile")) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusNotFound}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusNotFound) } func TestClipboard(t *testing.T) { + testClipboard(t, func(sessionId string) string { + return fmt.Sprintf("/clipboard/%s", sessionId) + }) +} + +func TestClipboardProtocolExtension(t *testing.T) { + testClipboard(t, func(sessionId string) string { + return fmt.Sprintf("/wd/hub/session/%s/aerokube/clipboard", sessionId) + }) +} + +func testClipboard(t *testing.T, path func(string) string) { manager = &HTTPTest{Handler: Selenium()} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) - rsp, err := http.Get(With(srv.URL).Path(fmt.Sprintf("/clipboard/%s", sess["sessionId"]))) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusOK}) - data, err := ioutil.ReadAll(rsp.Body) - AssertThat(t, err, Is{nil}) - AssertThat(t, string(data), EqualTo{"test-clipboard-value"}) + rsp, err := http.Get(With(srv.URL).Path(path(sess["sessionId"]))) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) + data, err := io.ReadAll(rsp.Body) + assert.NoError(t, err) + assert.Equal(t, string(data), "test-clipboard-value") - rsp, err = http.Post(With(srv.URL).Path(fmt.Sprintf("/clipboard/%s", sess["sessionId"])), "text/plain", bytes.NewReader([]byte("any-data"))) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusOK}) + rsp, err = http.Post(With(srv.URL).Path(path(sess["sessionId"])), "text/plain", bytes.NewReader([]byte("any-data"))) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) sessions.Remove(sess["sessionId"]) queue.Release() @@ -805,18 +897,18 @@ func TestClipboard(t *testing.T) { func TestClipboardMissingSession(t *testing.T) { rsp, err := http.Get(With(srv.URL).Path("/clipboard/missing-session")) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusNotFound}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusNotFound) } func TestDevtools(t *testing.T) { manager = &HTTPTest{Handler: Selenium()} resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) - AssertThat(t, err, Is{nil}) - + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) var sess map[string]string - AssertThat(t, resp, AllOf{Code{http.StatusOK}, IsJson{&sess}}) + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) u := fmt.Sprintf("ws://%s/devtools/%s", srv.Listener.Addr().String(), sess["sessionId"]) @@ -824,29 +916,82 @@ func TestDevtools(t *testing.T) { defer cancel() conn, err := rpcc.DialContext(ctx, u) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) defer conn.Close() c := cdp.NewClient(conn) err = c.Page.Enable(ctx) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) sessions.Remove(sess["sessionId"]) queue.Release() } +func TestAddedSeCdpCapability(t *testing.T) { + fn := func(input map[string]interface{}) { + input["value"] = map[string]interface{}{ + "sessionId": input["sessionId"], + "capabilities": map[string]interface{}{"browserVersion": "some-version"}, + } + delete(input, "sessionId") + } + manager = &HTTPTest{Handler: Selenium(fn)} + + resp, err := http.Post(With(srv.URL).Path("/wd/hub/session"), "", bytes.NewReader([]byte("{}"))) + assert.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) + var sess map[string]interface{} + assert.NoError(t, json.NewDecoder(resp.Body).Decode(&sess)) + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + rv, ok := sess["value"] + assert.True(t, ok) + value, ok := rv.(map[string]interface{}) + assert.True(t, ok) + rc, ok := value["capabilities"] + assert.True(t, ok) + rs, ok := value["sessionId"] + assert.True(t, ok) + sessionId, ok := rs.(string) + assert.True(t, ok) + capabilities, ok := rc.(map[string]interface{}) + assert.True(t, ok) + rcv, ok := capabilities["se:cdpVersion"] + assert.True(t, ok) + cv, ok := rcv.(string) + assert.True(t, ok) + assert.NotEmpty(t, cv) + rws, ok := capabilities["se:cdp"] + assert.True(t, ok) + ws, ok := rws.(string) + assert.True(t, ok) + assert.NotEmpty(t, ws) + conn, err := rpcc.DialContext(ctx, ws) + assert.NoError(t, err) + defer conn.Close() + + c := cdp.NewClient(conn) + err = c.Page.Enable(ctx) + assert.NoError(t, err) + + sessions.Remove(sessionId) + queue.Release() +} + func TestParseGgrHost(t *testing.T) { h := parseGgrHost("some-host.example.com:4444") - AssertThat(t, h.Name, EqualTo{"some-host.example.com"}) - AssertThat(t, h.Port, EqualTo{4444}) + assert.Equal(t, h.Name, "some-host.example.com") + assert.Equal(t, h.Port, 4444) } func TestWelcomeScreen(t *testing.T) { rsp, err := http.Get(With(srv.URL).Path("/")) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusOK}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusOK) rsp, err = http.Get(With(srv.URL).Path("/wd/hub")) - AssertThat(t, err, Is{nil}) - AssertThat(t, rsp, Code{http.StatusOK}) + assert.NoError(t, err) + assert.Equal(t, rsp.StatusCode, http.StatusOK) } diff --git a/service/docker.go b/service/docker.go index a102f9cf..805e46bc 100644 --- a/service/docker.go +++ b/service/docker.go @@ -3,26 +3,26 @@ package service import ( "context" "fmt" - "github.com/docker/go-units" + "github.com/aerokube/selenoid/info" + "github.com/docker/docker/api/types" "log" "net" "net/url" + "os" + "path/filepath" "strconv" + "strings" "time" "github.com/aerokube/selenoid/config" "github.com/aerokube/selenoid/session" - "github.com/aerokube/util" - "github.com/docker/docker/api/types" ctr "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/strslice" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "github.com/docker/go-connections/nat" - "os" - "path/filepath" - "strings" + "github.com/docker/go-units" ) const ( @@ -143,16 +143,20 @@ func (d *Docker) StartWithCancel() (*StartedService, error) { } cl := d.Client env := getEnv(d.ServiceBase, d.Caps) + cfg := &ctr.Config{ + Image: image.(string), + Env: env, + ExposedPorts: portConfig.ExposedPorts, + Labels: getLabels(d.Service, d.Caps), + } + hn := getContainerHostname(d.Caps) + if hn != "" { + cfg.Hostname = hn + } container, err := cl.ContainerCreate(ctx, - &ctr.Config{ - Hostname: getContainerHostname(d.Caps), - Image: image.(string), - Env: env, - ExposedPorts: portConfig.ExposedPorts, - Labels: getLabels(d.Service, d.Caps), - }, + cfg, &hostConfig, - &network.NetworkingConfig{}, "") + &network.NetworkingConfig{}, nil, "") if err != nil { return nil, fmt.Errorf("create container: %v", err) } @@ -160,12 +164,12 @@ func (d *Docker) StartWithCancel() (*StartedService, error) { browserContainerId := container.ID videoContainerId := "" log.Printf("[%d] [STARTING_CONTAINER] [%s] [%s]", requestId, image, browserContainerId) - err = cl.ContainerStart(ctx, browserContainerId, types.ContainerStartOptions{}) + err = cl.ContainerStart(ctx, browserContainerId, ctr.StartOptions{}) if err != nil { removeContainer(ctx, cl, requestId, browserContainerId) return nil, fmt.Errorf("start container: %v", err) } - log.Printf("[%d] [CONTAINER_STARTED] [%s] [%s] [%.2fs]", requestId, image, browserContainerId, util.SecondsSince(browserContainerStartTime)) + log.Printf("[%d] [CONTAINER_STARTED] [%s] [%s] [%.2fs]", requestId, image, browserContainerId, info.SecondsSince(browserContainerStartTime)) if len(d.AdditionalNetworks) > 0 { for _, networkName := range d.AdditionalNetworks { @@ -213,7 +217,7 @@ func (d *Docker) StartWithCancel() (*StartedService, error) { removeContainer(ctx, cl, requestId, browserContainerId) return nil, fmt.Errorf("wait: %v", err) } - log.Printf("[%d] [SERVICE_STARTED] [%s] [%s] [%.2fs]", requestId, image, browserContainerId, util.SecondsSince(serviceStartTime)) + log.Printf("[%d] [SERVICE_STARTED] [%s] [%s] [%.2fs]", requestId, image, browserContainerId, info.SecondsSince(serviceStartTime)) log.Printf("[%d] [PROXY_TO] [%s] [%s]", requestId, browserContainerId, u.String()) var publishedPortsInfo map[string]string @@ -221,6 +225,11 @@ func (d *Docker) StartWithCancel() (*StartedService, error) { publishedPortsInfo = getContainerPorts(stat) } + var origin string + if stat.Config != nil { + origin = net.JoinHostPort(stat.Config.Hostname, d.Service.Port) + } + s := StartedService{ Url: u, Container: &session.Container{ @@ -229,13 +238,14 @@ func (d *Docker) StartWithCancel() (*StartedService, error) { Ports: publishedPortsInfo, }, HostPort: hostPort, + Origin: origin, Cancel: func() { if videoContainerId != "" { stopVideoContainer(ctx, cl, requestId, videoContainerId, d.Environment) } defer removeContainer(ctx, cl, requestId, browserContainerId) if d.LogOutputDir != "" && (d.SaveAllLogs || d.Log) { - r, err := d.Client.ContainerLogs(ctx, browserContainerId, types.ContainerLogsOptions{ + r, err := d.Client.ContainerLogs(ctx, browserContainerId, ctr.LogsOptions{ Timestamps: true, ShowStdout: true, ShowStderr: true, @@ -399,7 +409,7 @@ func getContainerHostname(caps session.Caps) string { if caps.ContainerHostname != "" { return caps.ContainerHostname } - return "localhost" + return "" } func getExtraHosts(service *config.Browser, caps session.Caps) []string { @@ -527,7 +537,7 @@ func startVideoContainer(ctx context.Context, cl *client.Client, requestId uint6 Env: env, }, hostConfig, - &network.NetworkingConfig{}, "") + &network.NetworkingConfig{}, nil, "") if err != nil { removeContainer(ctx, cl, requestId, browserContainer.ID) return "", fmt.Errorf("create video container: %v", err) @@ -535,13 +545,13 @@ func startVideoContainer(ctx context.Context, cl *client.Client, requestId uint6 videoContainerId := videoContainer.ID log.Printf("[%d] [STARTING_VIDEO_CONTAINER] [%s] [%s]", requestId, videoContainerImage, videoContainerId) - err = cl.ContainerStart(ctx, videoContainerId, types.ContainerStartOptions{}) + err = cl.ContainerStart(ctx, videoContainerId, ctr.StartOptions{}) if err != nil { removeContainer(ctx, cl, requestId, browserContainer.ID) removeContainer(ctx, cl, requestId, videoContainerId) return "", fmt.Errorf("start video container: %v", err) } - log.Printf("[%d] [VIDEO_CONTAINER_STARTED] [%s] [%s] [%.2fs]", requestId, videoContainerImage, videoContainerId, util.SecondsSince(videoContainerStartTime)) + log.Printf("[%d] [VIDEO_CONTAINER_STARTED] [%s] [%s] [%.2fs]", requestId, videoContainerImage, videoContainerId, info.SecondsSince(videoContainerStartTime)) return videoContainerId, nil } @@ -575,7 +585,7 @@ func stopVideoContainer(ctx context.Context, cli *client.Client, requestId uint6 func removeContainer(ctx context.Context, cli *client.Client, requestId uint64, id string) { log.Printf("[%d] [REMOVING_CONTAINER] [%s]", requestId, id) - err := cli.ContainerRemove(ctx, id, types.ContainerRemoveOptions{Force: true, RemoveVolumes: true}) + err := cli.ContainerRemove(ctx, id, ctr.RemoveOptions{Force: true, RemoveVolumes: true}) if err != nil { log.Printf("[%d] [FAILED_TO_REMOVE_CONTAINER] [%s] [%v]", requestId, id, err) return diff --git a/service/driver.go b/service/driver.go index 342b2de6..72b8a39e 100644 --- a/service/driver.go +++ b/service/driver.go @@ -1,18 +1,18 @@ package service import ( + "errors" "fmt" + "github.com/aerokube/selenoid/info" "log" "net" "net/url" + "os" "os/exec" + "path/filepath" "time" - "errors" "github.com/aerokube/selenoid/session" - "github.com/aerokube/util" - "os" - "path/filepath" ) // Driver - driver processes manager @@ -63,7 +63,7 @@ func (d *Driver) StartWithCancel() (*StartedService, error) { cmd.Stdout = f cmd.Stderr = f } - l.Close() + _ = l.Close() log.Printf("[%d] [STARTING_PROCESS] [%s]", requestId, cmdLine) s := time.Now() err = cmd.Start() @@ -75,13 +75,13 @@ func (d *Driver) StartWithCancel() (*StartedService, error) { d.stopProcess(cmd) return nil, err } - log.Printf("[%d] [PROCESS_STARTED] [%d] [%.2fs]", requestId, cmd.Process.Pid, util.SecondsSince(s)) + log.Printf("[%d] [PROCESS_STARTED] [%d] [%.2fs]", requestId, cmd.Process.Pid, info.SecondsSince(s)) log.Printf("[%d] [PROXY_TO] [%s]", requestId, u.String()) - hp := session.HostPort{} - if d.Caps.VNC { - hp.VNC = "127.0.0.1:5900" - } - return &StartedService{Url: u, HostPort: hp, Cancel: func() { d.stopProcess(cmd) }}, nil + hp := session.HostPort{} + if d.Caps.VNC { + hp.VNC = "127.0.0.1:5900" + } + return &StartedService{Url: u, HostPort: hp, Origin: fmt.Sprintf("localhost:%s", port), Cancel: func() { d.stopProcess(cmd) }}, nil } func (d *Driver) stopProcess(cmd *exec.Cmd) { @@ -93,7 +93,7 @@ func (d *Driver) stopProcess(cmd *exec.Cmd) { return } if stdout, ok := cmd.Stdout.(*os.File); ok && !d.CaptureDriverLogs && d.LogOutputDir != "" { - stdout.Close() + _ = stdout.Close() } - log.Printf("[%d] [TERMINATED_PROCESS] [%d] [%.2fs]", d.RequestId, cmd.Process.Pid, util.SecondsSince(s)) + log.Printf("[%d] [TERMINATED_PROCESS] [%d] [%.2fs]", d.RequestId, cmd.Process.Pid, info.SecondsSince(s)) } diff --git a/service/driver_unix.go b/service/driver_unix.go index e0ff87f2..edcf0fcd 100644 --- a/service/driver_unix.go +++ b/service/driver_unix.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package service @@ -8,7 +9,7 @@ import ( ) func stopProc(cmd *exec.Cmd) error { - exitCode := cmd.Process.Signal(syscall.SIGINT) - cmd.Wait() - return exitCode + exitCode := cmd.Process.Signal(syscall.SIGINT) + _ = cmd.Wait() + return exitCode } diff --git a/service/driver_windows.go b/service/driver_windows.go index cff64adb..3166722b 100644 --- a/service/driver_windows.go +++ b/service/driver_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package service @@ -8,6 +9,6 @@ import ( func stopProc(cmd *exec.Cmd) error { error := cmd.Process.Kill() - cmd.Wait() + _ = cmd.Wait() return error } diff --git a/service/service.go b/service/service.go index c46890e8..79571db7 100644 --- a/service/service.go +++ b/service/service.go @@ -45,6 +45,7 @@ type StartedService struct { Url *url.URL Container *session.Container HostPort session.HostPort + Origin string Cancel func() } @@ -108,7 +109,7 @@ func wait(u string, t time.Duration) error { req.Close = true resp, err := http.DefaultClient.Do(req) if resp != nil { - resp.Body.Close() + _ = resp.Body.Close() } if err != nil { <-time.After(50 * time.Millisecond) diff --git a/service_test.go b/service_test.go index 9d34cd68..0503d391 100644 --- a/service_test.go +++ b/service_test.go @@ -3,16 +3,7 @@ package main import ( "bytes" "fmt" - . "github.com/aandryashin/matchers" - "github.com/aerokube/selenoid/config" - "github.com/aerokube/selenoid/service" - "github.com/aerokube/selenoid/session" - "github.com/aerokube/util" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/client" - "golang.org/x/net/websocket" "io" - "io/ioutil" "net" "net/http" "net/http/httptest" @@ -21,6 +12,14 @@ import ( "sync" "testing" "time" + + "github.com/aerokube/selenoid/config" + "github.com/aerokube/selenoid/service" + "github.com/aerokube/selenoid/session" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" + assert "github.com/stretchr/testify/require" + "golang.org/x/net/websocket" ) var ( @@ -40,8 +39,8 @@ func updateMux(mux http.Handler) { lock.Lock() defer lock.Unlock() mockServer = httptest.NewServer(mux) - os.Setenv("DOCKER_HOST", "tcp://"+hostPort(mockServer.URL)) - os.Setenv("DOCKER_API_VERSION", "1.29") + _ = os.Setenv("DOCKER_HOST", "tcp://"+hostPort(mockServer.URL)) + _ = os.Setenv("DOCKER_API_VERSION", "1.29") cli, _ = client.NewClientWithOpts(client.FromEnv) } @@ -60,7 +59,7 @@ func testMux() http.Handler { func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) output := `{"id": "e90e34656806", "warnings": []}` - w.Write([]byte(output)) + _, _ = w.Write([]byte(output)) }, )) mux.HandleFunc("/v1.29/containers/e90e34656806/start", http.HandlerFunc( @@ -80,9 +79,9 @@ func testMux() http.Handler { w.WriteHeader(http.StatusOK) const streamTypeStderr = 2 header := []byte{streamTypeStderr, 0, 0, 0, 0, 0, 0, 9} - w.Write(header) + _, _ = w.Write(header) data := []byte("test-data") - w.Write(data) + _, _ = w.Write(data) }, )) mux.HandleFunc("/v%s/containers/e90e34656806", http.HandlerFunc( @@ -160,7 +159,7 @@ func testMux() http.Handler { "Mounts": [] } `, p, p, p, p, p, p) - w.Write([]byte(output)) + _, _ = w.Write([]byte(output)) }, )) mux.HandleFunc("/v1.29/networks/net-1/connect", http.HandlerFunc( @@ -222,7 +221,7 @@ func testConfig(env *service.Environment) *config.Config { } func testEnvironment() *service.Environment { - logOutputDir, _ = ioutil.TempDir("", "selenoid-test") + logOutputDir, _ = os.MkdirTemp("", "selenoid-test") return &service.Environment{ CPU: int64(0), Memory: int64(0), @@ -263,18 +262,18 @@ func TestFindDockerIPSpecified(t *testing.T) { func testDocker(t *testing.T, env *service.Environment, cfg *config.Config) { starter := createDockerStarter(t, env, cfg) startedService, err := starter.StartWithCancel() - AssertThat(t, err, Is{nil}) - AssertThat(t, startedService.Url, Not{nil}) - AssertThat(t, startedService.Container, Not{nil}) - AssertThat(t, startedService.Container.ID, EqualTo{"e90e34656806"}) - AssertThat(t, startedService.HostPort.VNC, EqualTo{"127.0.0.1:5900"}) - AssertThat(t, startedService.Cancel, Not{nil}) + assert.NoError(t, err) + assert.NotNil(t, startedService.Url) + assert.NotNil(t, startedService.Container) + assert.Equal(t, startedService.Container.ID, "e90e34656806") + assert.Equal(t, startedService.HostPort.VNC, "127.0.0.1:5900") + assert.NotNil(t, startedService.Cancel) startedService.Cancel() } func createDockerStarter(t *testing.T, env *service.Environment, cfg *config.Config) service.Starter { cli, err := client.NewClientWithOpts(client.FromEnv) - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) manager := service.DefaultManager{Environment: env, Client: cli, Config: cfg} caps := session.Caps{ DeviceName: "firefox", @@ -299,8 +298,8 @@ func createDockerStarter(t *testing.T, env *service.Environment, cfg *config.Con TestName: "my-cool-test", } starter, success := manager.Find(caps, 42) - AssertThat(t, success, Is{true}) - AssertThat(t, starter, Not{nil}) + assert.True(t, success) + assert.NotNil(t, starter) return starter } @@ -310,7 +309,7 @@ func failingMux(numDeleteRequests *int) http.Handler { func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) output := `{"id": "e90e34656806", "warnings": []}` - w.Write([]byte(output)) + _, _ = w.Write([]byte(output)) }, )) mux.HandleFunc("/v1.29/containers/e90e34656806/start", http.HandlerFunc( @@ -334,8 +333,8 @@ func TestDeleteContainerOnStartupError(t *testing.T) { env := testEnvironment() starter := createDockerStarter(t, env, testConfig(env)) _, err := starter.StartWithCancel() - AssertThat(t, err, Not{nil}) - AssertThat(t, numDeleteRequests, EqualTo{1}) + assert.Error(t, err) + assert.Equal(t, numDeleteRequests, 1) } func TestFindDriver(t *testing.T) { @@ -347,8 +346,8 @@ func TestFindDriver(t *testing.T) { VNC: true, } starter, success := manager.Find(caps, 42) - AssertThat(t, success, Is{true}) - AssertThat(t, starter, Not{nil}) + assert.True(t, success) + assert.NotNil(t, starter) } func TestGetVNC(t *testing.T) { @@ -364,8 +363,8 @@ func TestGetVNC(t *testing.T) { }) defer sessions.Remove("test-session") - u := fmt.Sprintf("ws://%s/vnc/test-session", util.HostPort(srv.URL)) - AssertThat(t, readDataFromWebSocket(t, u), EqualTo{"test-data"}) + u := fmt.Sprintf("ws://%s/vnc/test-session", hostPort(srv.URL)) + assert.Equal(t, readDataFromWebSocket(t, u), "test-data") } func testTCPServer(data string) net.Listener { @@ -376,8 +375,8 @@ func testTCPServer(data string) net.Listener { if err != nil { continue } - io.WriteString(conn, data) - conn.Close() + _, _ = io.WriteString(conn, data) + _ = conn.Close() return } }() @@ -386,12 +385,12 @@ func testTCPServer(data string) net.Listener { func readDataFromWebSocket(t *testing.T, wsURL string) string { ws, err := websocket.Dial(wsURL, "", "http://localhost") - AssertThat(t, err, Is{nil}) + assert.NoError(t, err) var msg = make([]byte, 512) _, err = ws.Read(msg) msg = bytes.Trim(msg, "\x00") - //AssertThat(t, err, Is{nil}) + //assert.NoError(t, err) return string(msg) } @@ -408,6 +407,6 @@ func TestGetLogs(t *testing.T) { }) defer sessions.Remove("test-session") - u := fmt.Sprintf("ws://%s/logs/test-session", util.HostPort(srv.URL)) - AssertThat(t, readDataFromWebSocket(t, u), EqualTo{"test-data"}) + u := fmt.Sprintf("ws://%s/logs/test-session", hostPort(srv.URL)) + assert.Equal(t, readDataFromWebSocket(t, u), "test-data") } diff --git a/session/session.go b/session/session.go index ca530585..3b730625 100644 --- a/session/session.go +++ b/session/session.go @@ -1,10 +1,11 @@ package session import ( - "github.com/imdario/mergo" "net/url" "sync" "time" + + "github.com/imdario/mergo" ) // Caps - user capabilities @@ -15,6 +16,7 @@ type Caps struct { W3CVersion string `json:"browserVersion,omitempty"` Platform string `json:"platform,omitempty"` W3CPlatform string `json:"platformName,omitempty"` + W3CDeviceName string `json:"appium:deviceName,omitempty"` ScreenResolution string `json:"screenResolution,omitempty"` Skin string `json:"skin,omitempty"` VNC bool `json:"enableVNC,omitempty"` @@ -46,13 +48,12 @@ func (c *Caps) ProcessExtensionCapabilities() { if c.W3CPlatform != "" { c.Platform = c.W3CPlatform } + if c.W3CDeviceName != "" { + c.DeviceName = c.W3CDeviceName + } if c.ExtensionCapabilities != nil { mergo.Merge(c, *c.ExtensionCapabilities, mergo.WithOverride) //We probably need to handle returned error - - //According to Selenium standard vendor-specific capabilities for - //intermediary node should not be proxied to endpoint node - c.ExtensionCapabilities = nil } } @@ -61,7 +62,10 @@ func (c *Caps) BrowserName() string { if browserName != "" { return browserName } - return c.DeviceName + if c.DeviceName != "" { + return c.DeviceName + } + return c.W3CDeviceName } // Container - container information @@ -78,6 +82,7 @@ type Session struct { URL *url.URL Container *Container HostPort HostPort + Origin string Cancel func() Timeout time.Duration TimeoutCh chan struct{} diff --git a/upload/s3.go b/upload/s3.go index 01ed6dd8..b54dfbf1 100644 --- a/upload/s3.go +++ b/upload/s3.go @@ -1,3 +1,4 @@ +//go:build s3 // +build s3 package upload @@ -5,18 +6,19 @@ package upload import ( "flag" "fmt" - "github.com/aerokube/selenoid/event" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - awssession "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3/s3manager" - "github.com/pkg/errors" "log" "mime" "os" "path/filepath" "strings" "time" + + "github.com/aerokube/selenoid/event" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + awssession "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "github.com/pkg/errors" ) func init() { diff --git a/upload/uploader.go b/upload/uploader.go index 7ce3d949..4a48ded8 100644 --- a/upload/uploader.go +++ b/upload/uploader.go @@ -1,10 +1,11 @@ package upload import ( - "github.com/aerokube/selenoid/event" - "github.com/aerokube/util" + "github.com/aerokube/selenoid/info" "log" "time" + + "github.com/aerokube/selenoid/event" ) var ( @@ -38,7 +39,7 @@ func AddUploader(u Uploader) { func (ul *Upload) OnFileCreated(createdFile event.CreatedFile) { if len(ul.uploaders) > 0 { for _, uploader := range ul.uploaders { - go func() { + go func(uploader Uploader) { s := time.Now() uploaded, err := uploader.Upload(createdFile) if err != nil { @@ -46,9 +47,9 @@ func (ul *Upload) OnFileCreated(createdFile event.CreatedFile) { return } if uploaded { - log.Printf("[%d] [UPLOADED_FILE] [%s] [%.2fs]", createdFile.RequestId, createdFile.Name, util.SecondsSince(s)) + log.Printf("[%d] [UPLOADED_FILE] [%s] [%.2fs]", createdFile.RequestId, createdFile.Name, info.SecondsSince(s)) } - }() + }(uploader) } } } diff --git a/utils_test.go b/utils_test.go index d40a0457..eb0f44ff 100644 --- a/utils_test.go +++ b/utils_test.go @@ -5,8 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/aerokube/selenoid/protect" - "github.com/gorilla/websocket" "log" "net/http" "net/http/httptest" @@ -14,15 +12,15 @@ import ( "strconv" "strings" "sync" - - "time" - "testing" + "time" - . "github.com/aandryashin/matchers" + "github.com/aerokube/selenoid/protect" "github.com/aerokube/selenoid/service" "github.com/aerokube/selenoid/session" - "github.com/pborman/uuid" + "github.com/google/uuid" + "github.com/gorilla/websocket" + assert "github.com/stretchr/testify/require" ) type HTTPTest struct { @@ -80,7 +78,7 @@ type StartupError struct{} func (m *StartupError) StartWithCancel() (*service.StartedService, error) { log.Println("Starting StartupError Service...") log.Println("Failed to start StartupError Service...") - return nil, errors.New("Failed to start Service") + return nil, errors.New("failed to start Service") } func (m *StartupError) Find(caps session.Caps, requestId uint64) (service.Starter, bool) { @@ -99,7 +97,7 @@ func (r With) Path(p string) string { return fmt.Sprintf("%s%s", r, p) } -func Selenium() http.Handler { +func Selenium(nsp ...func(map[string]interface{})) http.Handler { var lock sync.RWMutex sessions := make(map[string]struct{}) mux := http.NewServeMux() @@ -108,13 +106,17 @@ func Selenium() http.Handler { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - u := uuid.New() + u := uuid.NewString() lock.Lock() sessions[u] = struct{}{} lock.Unlock() - json.NewEncoder(w).Encode(struct { - S string `json:"sessionId"` - }{u}) + ret := map[string]interface{}{ + "sessionId": u, + } + for _, n := range nsp { + n(ret) + } + _ = json.NewEncoder(w).Encode(&ret) }) mux.HandleFunc("/session/", func(w http.ResponseWriter, r *http.Request) { u := strings.Split(r.URL.Path, "/")[2] @@ -129,7 +131,7 @@ func Selenium() http.Handler { out := "this call was relayed by the reverse proxy" // Setting wrong Content-Length leads to abort handler error w.Header().Add("Content-Length", strconv.Itoa(2*len(out))) - fmt.Fprintln(w, out) + _, _ = fmt.Fprintln(w, out) return } d, _ := time.ParseDuration(r.FormValue("timeout")) @@ -143,7 +145,7 @@ func Selenium() http.Handler { }) mux.HandleFunc("/testfile", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte("test-data")) + _, _ = w.Write([]byte("test-data")) }) upgrader := websocket.Upgrader{ CheckOrigin: func(_ *http.Request) bool { @@ -181,7 +183,7 @@ func Selenium() http.Handler { } } w.WriteHeader(http.StatusOK) - w.Write([]byte("test-clipboard-value")) + _, _ = w.Write([]byte("test-clipboard-value")) }) return mux } @@ -190,6 +192,7 @@ func TestProcessExtensionCapabilities(t *testing.T) { capsJson := `{ "version": "57.0", "browserName": "firefox", + "appium:deviceName": "android", "selenoid:options": { "name": "ExampleTestName", "enableVNC": true, @@ -200,19 +203,20 @@ func TestProcessExtensionCapabilities(t *testing.T) { }` var caps session.Caps err := json.Unmarshal([]byte(capsJson), &caps) - AssertThat(t, err, Is{nil}) - AssertThat(t, caps.Name, EqualTo{"firefox"}) - AssertThat(t, caps.Version, EqualTo{"57.0"}) - AssertThat(t, caps.TestName, EqualTo{""}) + assert.NoError(t, err) + assert.Equal(t, caps.Name, "firefox") + assert.Equal(t, caps.Version, "57.0") + assert.Equal(t, caps.TestName, "") caps.ProcessExtensionCapabilities() - AssertThat(t, caps.Name, EqualTo{"firefox"}) - AssertThat(t, caps.Version, EqualTo{"57.0"}) - AssertThat(t, caps.TestName, EqualTo{"ExampleTestName"}) - AssertThat(t, caps.VNC, EqualTo{true}) - AssertThat(t, caps.VideoFrameRate, EqualTo{uint16(24)}) - AssertThat(t, caps.Env, EqualTo{[]string{"LANG=de_DE.UTF-8"}}) - AssertThat(t, caps.Labels, EqualTo{map[string]string{"key": "value"}}) + assert.Equal(t, caps.Name, "firefox") + assert.Equal(t, caps.Version, "57.0") + assert.Equal(t, caps.DeviceName, "android") + assert.Equal(t, caps.TestName, "ExampleTestName") + assert.True(t, caps.VNC) + assert.Equal(t, caps.VideoFrameRate, uint16(24)) + assert.Equal(t, caps.Env, []string{"LANG=de_DE.UTF-8"}) + assert.Equal(t, caps.Labels, map[string]string{"key": "value"}) } func TestSumUsedTotalGreaterThanPending(t *testing.T) { @@ -230,25 +234,53 @@ func TestSumUsedTotalGreaterThanPending(t *testing.T) { u := srv.URL + "/" _, err := http.Get(u) - AssertThat(t, err, Is{nil}) - AssertThat(t, queue.Pending(), EqualTo{1}) + assert.NoError(t, err) + assert.Equal(t, queue.Pending(), 1) queue.Create() - AssertThat(t, queue.Pending(), EqualTo{0}) - AssertThat(t, queue.Used(), EqualTo{1}) + assert.Equal(t, queue.Pending(), 0) + assert.Equal(t, queue.Used(), 1) _, err = http.Get(u) - AssertThat(t, err, Is{nil}) - AssertThat(t, queue.Pending(), EqualTo{1}) + assert.NoError(t, err) + assert.Equal(t, queue.Pending(), 1) queue.Create() - AssertThat(t, queue.Pending(), EqualTo{0}) - AssertThat(t, queue.Used(), EqualTo{2}) + assert.Equal(t, queue.Pending(), 0) + assert.Equal(t, queue.Used(), 2) req, _ := http.NewRequest(http.MethodGet, u, nil) - ctx, _ := context.WithTimeout(context.Background(), 100*time.Millisecond) + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() req = req.WithContext(ctx) _, err = http.DefaultClient.Do(req) - AssertThat(t, err, Not{nil}) - AssertThat(t, queue.Pending(), EqualTo{0}) - AssertThat(t, queue.Used(), EqualTo{2}) + assert.Error(t, err) + assert.Equal(t, queue.Pending(), 0) + assert.Equal(t, queue.Used(), 2) +} + +func TestBrowserName(t *testing.T) { + var caps session.Caps + + var capsJson = `{ + "appium:deviceName": "iPhone 7" + }` + err := json.Unmarshal([]byte(capsJson), &caps) + assert.NoError(t, err) + assert.Equal(t, caps.BrowserName(), "iPhone 7") + + capsJson = `{ + "deviceName": "android 11" + }` + err = json.Unmarshal([]byte(capsJson), &caps) + assert.NoError(t, err) + assert.Equal(t, caps.BrowserName(), "android 11") + + capsJson = `{ + "deviceName": "android 11", + "appium:deviceName": "iPhone 7", + "browserName": "firefox" + }` + err = json.Unmarshal([]byte(capsJson), &caps) + assert.NoError(t, err) + assert.Equal(t, caps.BrowserName(), "firefox") }