├── examples ├── autobahn │ ├── reports │ │ └── .gitkeep │ ├── config │ │ └── fuzzingserver.json │ ├── README.md │ └── script.js ├── test-api.k6.io.js └── test-local-echo.js ├── CODEOWNERS ├── websockets ├── main_test.go ├── events │ └── events.go ├── helpers.go ├── params.go ├── listeners.go ├── blob.go ├── blob_test.go ├── websockets.go └── websockets_test.go ├── .gitignore ├── .github ├── workflows │ ├── issue-auto-assign.yml │ └── all.yml └── pull_request_template.md ├── register.go ├── modtools_frozen.yml ├── Makefile ├── README.md ├── go.mod ├── go.sum └── LICENSE /examples/autobahn/reports/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @grafana/k6-core 2 | -------------------------------------------------------------------------------- /websockets/main_test.go: -------------------------------------------------------------------------------- 1 | package websockets 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.uber.org/goleak" 7 | ) 8 | 9 | func TestMain(m *testing.M) { 10 | goleak.VerifyTestMain(m) 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | examples/autobahn/k6 2 | examples/autobahn/reports 3 | vendor 4 | k6 5 | 6 | # we use the config from the main k6's repository 7 | # https://github.com/grafana/k6/blob/master/.golangci.yml 8 | .golangci.yml -------------------------------------------------------------------------------- /.github/workflows/issue-auto-assign.yml: -------------------------------------------------------------------------------- 1 | name: "Auto assign maintainer to issue" 2 | on: 3 | issues: 4 | types: [opened] 5 | 6 | permissions: 7 | issues: write 8 | 9 | jobs: 10 | assign-maintainer: 11 | uses: grafana/k6/.github/workflows/issue-auto-assign.yml@master 12 | -------------------------------------------------------------------------------- /examples/autobahn/config/fuzzingserver.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "ws://127.0.0.1:9001", 3 | "outdir": "./reports/clients", 4 | "cases": ["*"], 5 | "exclude-cases": [ 6 | "9.*", 7 | "12.*", 8 | "13.*" 9 | ], 10 | "exclude-agent-cases": {} 11 | } 12 | -------------------------------------------------------------------------------- /register.go: -------------------------------------------------------------------------------- 1 | // Package websockets exist just to register the websockets extension 2 | package websockets 3 | 4 | import ( 5 | "github.com/grafana/xk6-websockets/websockets" 6 | "go.k6.io/k6/js/modules" 7 | ) 8 | 9 | func init() { 10 | modules.Register("k6/x/websockets", new(websockets.RootModule)) 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/all.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | # Enable manually triggering this workflow via the API or web UI 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | tags: 9 | - v* 10 | pull_request: 11 | 12 | jobs: 13 | checks: 14 | uses: grafana/k6-ci/.github/workflows/all.yml@main -------------------------------------------------------------------------------- /examples/autobahn/README.md: -------------------------------------------------------------------------------- 1 | # Autobahn test suite 2 | 3 | Docs: https://github.com/crossbario/autobahn-testsuite 4 | 5 | ## Usage 6 | 7 | Run the WebSocket server. 8 | 9 | ```sh 10 | $ docker run -it --rm \ 11 | -v ${PWD}/config:/config \ 12 | -v ${PWD}/reports:/reports \ 13 | -p 9001:9001 \ 14 | -p 8080:8080 \ 15 | --name fuzzingserver \ 16 | crossbario/autobahn-testsuite 17 | ``` 18 | 19 | Run the autobahn client test with k6. 20 | 21 | ```sh 22 | ./k6 run ./script.js 23 | ``` 24 | 25 | Open the browser to `http://localhost:8080` for checking the report. 26 | -------------------------------------------------------------------------------- /websockets/events/events.go: -------------------------------------------------------------------------------- 1 | // Package events represent the events that can be sent to the client 2 | // https://dom.spec.whatwg.org/#event 3 | package events 4 | 5 | const ( 6 | // OPEN is the event name for the open event 7 | OPEN = "open" 8 | // CLOSE is the event name for the close event 9 | CLOSE = "close" 10 | // MESSAGE is the event name for the message event 11 | MESSAGE = "message" 12 | // ERROR is the event name for the error event 13 | ERROR = "error" 14 | // PING is the event name for the ping event 15 | PING = "ping" 16 | // PONG is the event name for the pong event 17 | PONG = "pong" 18 | ) 19 | -------------------------------------------------------------------------------- /modtools_frozen.yml: -------------------------------------------------------------------------------- 1 | - path: github.com/spf13/afero 2 | minVersion: v1.1.2 3 | validUntil: 2029-08-04T16:29:18+03:00 4 | - path: gopkg.in/guregu/null.v3 5 | minVersion: v3.3.0 6 | validUntil: 2029-08-04T16:32:44+03:00 7 | - path: gopkg.in/yaml.v3 8 | minVersion: v3.0.0-20200313102051-9f266ea9e77c 9 | validUntil: 2029-08-04T16:32:56+03:00 10 | - path: github.com/spf13/cobra 11 | minVersion: v1.4.0 12 | validUntil: 2049-07-26T17:21:52+03:00 13 | - path: github.com/spf13/pflag 14 | minVersion: v1.0.5 15 | validUntil: 2049-07-26T17:21:57+03:00 16 | - path: github.com/andybalholm/cascadia 17 | minVersion: v1.1.0 18 | validUntil: 2049-07-26T17:24:57+03:00 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What? 2 | 3 | 4 | 5 | ## Why? 6 | 7 | 8 | 9 | ## Checklist 10 | 11 | 15 | 16 | - [ ] I have performed a self-review of my code. 17 | - [ ] I have added tests for my changes. 18 | - [ ] I have run linter locally (`make lint`) and all checks pass. 19 | - [ ] I have run tests locally (`make test`) and all tests pass. 20 | - [ ] I have commented on my code, particularly in hard-to-understand areas. 21 | 22 | 23 | ## Related PR(s)/Issue(s) 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/autobahn/script.js: -------------------------------------------------------------------------------- 1 | import { WebSocket } from "k6/x/websockets" 2 | import { sleep, check } from "k6" 3 | import exec from 'k6/execution' 4 | 5 | export let options = { 6 | iterations: 247, // get this value from the Autobahn server 7 | vus: 3, 8 | } 9 | 10 | const base = `ws://127.0.0.1:9001` 11 | const agent = "k6v383" 12 | 13 | export default function() { 14 | let testCase = exec.scenario.iterationInTest+1 15 | let url = `${base}/runCase?case=${testCase}&agent=${agent}`; 16 | let ws = new WebSocket(url); 17 | 18 | ws.addEventListener("open", () => { 19 | console.log(`Testing case #${testCase}`) 20 | }); 21 | 22 | ws.addEventListener("message", (e) => { 23 | if (e.event === 'ERROR') { 24 | console.log(`VU ${__VU}: test: #${testCase} error:`, e.data, `and message:`, e.message) 25 | return 26 | } 27 | ws.send(e.data) 28 | }) 29 | 30 | ws.addEventListener("error", (e) => { 31 | console.error(`test: #${testCase} error:`, e) 32 | ws.close() 33 | }) 34 | } 35 | 36 | export function teardown() { 37 | let ws = new WebSocket(`${base}/updateReports?agent=${agent}`) 38 | ws.addEventListener("open", (e) => { 39 | console.log("Updating the report") 40 | }); 41 | 42 | ws.addEventListener("error", (e) => { 43 | console.error("Updating the report failed:", e) 44 | ws.close() 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /websockets/helpers.go: -------------------------------------------------------------------------------- 1 | package websockets 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/grafana/sobek" 9 | "go.k6.io/k6/js/common" 10 | ) 11 | 12 | // must is a small helper that will panic if err is not nil. 13 | func must(rt *sobek.Runtime, err error) { 14 | if err != nil { 15 | common.Throw(rt, err) 16 | } 17 | } 18 | 19 | func isString(o *sobek.Object, rt *sobek.Runtime) bool { 20 | return o.Prototype().Get("constructor") == rt.GlobalObject().Get("String") 21 | } 22 | 23 | func isArray(o *sobek.Object, rt *sobek.Runtime) bool { 24 | return o.Prototype().Get("constructor") == rt.GlobalObject().Get("Array") 25 | } 26 | 27 | func isUint8Array(o *sobek.Object, rt *sobek.Runtime) bool { 28 | return o.Prototype().Get("constructor") == rt.GlobalObject().Get("Uint8Array") 29 | } 30 | 31 | func isDataView(o *sobek.Object, rt *sobek.Runtime) bool { 32 | return o.Prototype().Get("constructor") == rt.GlobalObject().Get("DataView") 33 | } 34 | 35 | func isBlob(o *sobek.Object, blobConstructor sobek.Value) bool { 36 | return o.Prototype().Get("constructor") == blobConstructor 37 | } 38 | 39 | func isObject(val sobek.Value) bool { 40 | return val != nil && val.ExportType() != nil && val.ExportType().Kind() == reflect.Map 41 | } 42 | 43 | func extractBytes(o *sobek.Object, rt *sobek.Runtime) []byte { 44 | arrayBuffer, ok := sobek.AssertFunction(o.Get("arrayBuffer")) 45 | if !ok { 46 | common.Throw(rt, errors.New("Blob.[arrayBuffer] is not a function")) 47 | } 48 | 49 | buffer, err := arrayBuffer(sobek.Undefined()) 50 | if err != nil { 51 | common.Throw(rt, fmt.Errorf("call to Blob.[arrayBuffer] failed: %w", err)) 52 | } 53 | 54 | p, ok := buffer.Export().(*sobek.Promise) 55 | if !ok { 56 | common.Throw(rt, errors.New("Blob.[arrayBuffer] return is not a Promise")) 57 | } 58 | 59 | ab, ok := p.Result().Export().(sobek.ArrayBuffer) 60 | if !ok { 61 | common.Throw(rt, errors.New("Blob.[arrayBuffer] promise's return is not an ArrayBuffer")) 62 | } 63 | 64 | return ab.Bytes() 65 | } 66 | -------------------------------------------------------------------------------- /examples/test-api.k6.io.js: -------------------------------------------------------------------------------- 1 | import { randomString, randomIntBetween } from "https://jslib.k6.io/k6-utils/1.1.0/index.js"; 2 | import { WebSocket } from "k6/x/websockets" 3 | import { setTimeout, clearTimeout, setInterval, clearInterval } from "k6/x/timers" 4 | 5 | let chatRoomName = 'publicRoom'; // choose your chat room name 6 | let sessionDuration = randomIntBetween(5000, 60000); // user session between 5s and 1m 7 | 8 | 9 | export default function() { 10 | for (let i = 0; i < 4; i++) { 11 | startWSWorker(i) 12 | } 13 | } 14 | 15 | function startWSWorker(id) { 16 | let url = `wss://test-api.k6.io/ws/crocochat/${chatRoomName}/`; 17 | let ws = new WebSocket(url); 18 | ws.binaryType = "arraybuffer"; 19 | ws.addEventListener("open", () => { 20 | ws.send(JSON.stringify({ 'event': 'SET_NAME', 'new_name': `Croc ${__VU}:${id}` })); 21 | 22 | ws.addEventListener("message", (e) => { 23 | let msg = JSON.parse(e.data); 24 | if (msg.event === 'CHAT_MSG') { 25 | console.log(`VU ${__VU}:${id} received: ${msg.user} says: ${msg.message}`) 26 | } 27 | else if (msg.event === 'ERROR') { 28 | console.error(`VU ${__VU}:${id} received:: ${msg.message}`) 29 | } 30 | else { 31 | console.log(`VU ${__VU}:${id} received unhandled message: ${msg.message}`) 32 | } 33 | }) 34 | 35 | 36 | let intervalId = setInterval(() => { 37 | ws.send(JSON.stringify({ 'event': 'SAY', 'message': `I'm saying ${randomString(5)}` })); 38 | }, randomIntBetween(2000, 8000)); // say something every 2-8seconds 39 | 40 | 41 | let timeout1id = setTimeout(function() { 42 | clearInterval(intervalId) 43 | console.log(`VU ${__VU}:${id}: ${sessionDuration}ms passed, leaving the chat`); 44 | ws.send(JSON.stringify({ 'event': 'LEAVE' })); 45 | }, sessionDuration); 46 | 47 | let timeout2id = setTimeout(function() { 48 | console.log(`Closing the socket forcefully 3s after graceful LEAVE`); 49 | ws.close(); 50 | }, sessionDuration + 3000); 51 | 52 | ws.addEventListener("close", () => { 53 | clearTimeout(timeout1id); 54 | clearTimeout(timeout2id); 55 | console.log(`VU ${__VU}:${id}: disconnected`); 56 | }) 57 | }); 58 | } 59 | 60 | -------------------------------------------------------------------------------- /examples/test-local-echo.js: -------------------------------------------------------------------------------- 1 | import { WebSocket } from "k6/x/websockets" 2 | import { setTimeout, clearTimeout, setInterval, clearInterval } from "k6/x/timers" 3 | 4 | const CLOSED_STATE = 3 5 | 6 | export default function() { 7 | // local echo server should be launched with `make ws-echo-server-run` 8 | var url = "ws://localhost:10000"; 9 | var params = { "tags": { "my_tag": "hello" } }; 10 | 11 | let ws = new WebSocket(url, null, params) 12 | ws.binaryType = "arraybuffer"; 13 | ws.onopen = () => { 14 | console.log('connected') 15 | ws.send(Date.now().toString()) 16 | } 17 | 18 | let intervalId = setInterval(() => { 19 | ws.ping(); 20 | console.log("Pinging every 1 sec (setInterval test)") 21 | }, 1000); 22 | 23 | let timeout1id = setTimeout(function() { 24 | console.log('2 seconds passed, closing the socket') 25 | clearInterval(intervalId) 26 | ws.close() 27 | 28 | }, 2000); 29 | 30 | ws.onclose = () => { 31 | clearTimeout(timeout1id); 32 | 33 | console.log('disconnected') 34 | } 35 | 36 | 37 | ws.onping = () => { 38 | console.log("PING!") 39 | } 40 | 41 | ws.onpong = () => { 42 | console.log("PONG!") 43 | } 44 | 45 | // Multiple event handlers on the same event 46 | ws.addEventListener("pong", () => { 47 | console.log("OTHER PONG!") 48 | }) 49 | 50 | ws.onmessage = (m) => { 51 | let parsed = parseInt(m.data, 10) 52 | if (Number.isNaN(parsed)) { 53 | console.log('Not a number received: ', m.data) 54 | 55 | return 56 | } 57 | 58 | console.log(`Roundtrip time: ${Date.now() - parsed} ms`); 59 | 60 | let timeoutId = setTimeout(function() { 61 | if (ws.readyState == CLOSED_STATE) { 62 | console.log("Socket closed, not sending anything"); 63 | 64 | clearTimeout(timeoutId); 65 | return; 66 | } 67 | 68 | ws.send(Date.now().toString()) 69 | }, 500); 70 | } 71 | 72 | ws.onerror = (e) => { 73 | if (e.error != "websocket: close sent") { 74 | console.log('An unexpected error occurred: ', e.error); 75 | } 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS += --silent 2 | GOLANGCI_CONFIG ?= .golangci.yml 3 | GOLANGCI_LINT_VERSION = $(shell "$(head -n 1 .golangci.yml | tr -d '\# ')") 4 | 5 | all: clean lint test build 6 | 7 | ## help: Prints a list of available build targets. 8 | help: 9 | echo "Usage: make ... " 10 | echo "" 11 | echo "Available targets are:" 12 | echo '' 13 | sed -n 's/^##//p' ${PWD}/Makefile | column -t -s ':' | sed -e 's/^/ /' 14 | echo 15 | echo "Targets run by default are: `sed -n 's/^all: //p' ./Makefile | sed -e 's/ /, /g' | sed -e 's/\(.*\), /\1, and /'`" 16 | 17 | ## build: Builds a custom 'k6' with the local extension. 18 | build: 19 | xk6 build --with $(shell go list -m)=. --with github.com/grafana/xk6-timers 20 | 21 | ## ws-echo-server-run: Runs the ws-echo-server 22 | ws-echo-server-run: 23 | docker run --detach --rm --name ws-echo-server -p 10000:8080 jmalloc/echo-server 24 | 25 | ## ws-echo-server-stop: Stops the ws-echo-server 26 | ws-echo-server-stop: 27 | docker stop ws-echo-server 28 | 29 | ## linter-config: Checks if the linter config exists, if not, downloads it from the main k6 repository. 30 | linter-config: 31 | test -s "${GOLANGCI_CONFIG}" || (echo "No linter config, downloading from main k6 repository..." && curl --silent --show-error --fail --no-location https://raw.githubusercontent.com/grafana/k6/master/.golangci.yml --output "${GOLANGCI_CONFIG}") 32 | 33 | ## check-linter-version: Checks if the linter version is the same as the one specified in the linter config. 34 | check-linter-version: 35 | (golangci-lint version | grep "version $(shell head -n 1 .golangci.yml | tr -d '\# ')") || echo "Your installation of golangci-lint is different from the one that is specified in k6's linter config (there it's $(shell head -n 1 .golangci.yml | tr -d '\# ')). Results could be different in the CI." 36 | 37 | ## test: Executes any tests. 38 | test: 39 | go test -race -timeout 30s ./... 40 | 41 | ## lint: Runs the linters. 42 | lint: linter-config check-linter-version 43 | echo "Running linters..." 44 | golangci-lint run --out-format=tab ./... 45 | 46 | ## check: Runs the linters and tests. 47 | check: lint test 48 | 49 | ## clean: Removes any previously created artifacts/downloads. 50 | clean: 51 | echo "Cleaning up..." 52 | rm -f ./k6 53 | rm .golangci.yml 54 | rm -rf vendor 55 | 56 | .PHONY: test lint check ws-echo-server-run ws-echo-server-stop build clean linter-config check-linter-version -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xk6-websockets 2 | 3 | > [!WARNING] 4 | > The `xk6-websockets` extension [has been merged](https://github.com/grafana/k6/pull/4131) to the [main k6 repository](https://github.com/grafana/k6). Please contribute and [open issues there](https://github.com/grafana/k6/issues). This repository is no longer maintained. 5 | 6 | This extension adds a PoC [Websockets API](https://websockets.spec.whatwg.org) implementation to [k6](https://www.k6.io). 7 | 8 | This is meant to try to implement the specification as close as possible without doing stuff that don't make sense in k6 like: 9 | 10 | 1. not reporting errors 11 | 2. not allowing some ports and other security workarounds 12 | 13 | It supports additional k6 specific features such as: 14 | 15 | * Custom metrics tags 16 | * Cookie jar 17 | * Headers customization 18 | * Support for ping/pong which isn't part of the specification 19 | * Compression Support (The only supported algorithm currently is `deflate`) 20 | 21 | It is implemented using the [xk6](https://k6.io/blog/extending-k6-with-xk6/) system. 22 | 23 | ## Requirements 24 | 25 | * [Golang 1.19+](https://go.dev/) 26 | * [Git](https://git-scm.com/) 27 | * [xk6](https://github.com/grafana/xk6) (`go install go.k6.io/xk6/cmd/xk6@latest`) 28 | * [curl](https://curl.se/) (downloading the k6 core's linter rule-set) 29 | 30 | ## Getting started 31 | 32 | 1. Build the k6's binary: 33 | 34 | ```shell 35 | $ make build 36 | ``` 37 | 38 | 2. Run an example: 39 | 40 | ```shell 41 | $ ./k6 run ./examples/test-api.k6.io.js 42 | ``` 43 | 44 | ## Discrepancies with the specifications 45 | 46 | * `binaryType` does not have a default value (in contrast to the spec, [which suggests `"blob"` as default](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType)), 47 | so in order to successfully receive binary messages a `binaryType` must be explicitly set either to `"arraybuffer"` (for `ArrayBuffer`) 48 | or `"blob"` (for `Blob`). 49 | 50 | ## Contributing 51 | 52 | Contributing to this repository is following general k6's [contribution guidelines](https://github.com/grafana/k6/blob/master/CONTRIBUTING.md) since the long-term goal is to merge this extension into the main k6 repository. 53 | 54 | ### Testing 55 | 56 | To run the test you can use the `make test` target. 57 | 58 | ### Linting 59 | 60 | To run the linter you can use the `make lint` target. 61 | 62 | > [!IMPORTANT] 63 | > By default there is golangci-lint config presented. Since the long-term goal is to merge the module back to the grafana/k6 we use the k6's linter rules. The rule set will be downloaded automatically while the first run of the `make lint` or you could do that manually by running `make linter-config`. 64 | -------------------------------------------------------------------------------- /websockets/params.go: -------------------------------------------------------------------------------- 1 | package websockets 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/cookiejar" 7 | "strings" 8 | 9 | "github.com/grafana/sobek" 10 | 11 | "go.k6.io/k6/js/common" 12 | httpModule "go.k6.io/k6/js/modules/k6/http" 13 | "go.k6.io/k6/lib" 14 | "go.k6.io/k6/metrics" 15 | ) 16 | 17 | // wsParams represent the parameters bag for websocket 18 | type wsParams struct { 19 | headers http.Header 20 | cookieJar *cookiejar.Jar 21 | tagsAndMeta *metrics.TagsAndMeta 22 | enableCompression bool 23 | subprocotols []string 24 | } 25 | 26 | // buildParams builds WebSocket params and configure some of them 27 | func buildParams(state *lib.State, rt *sobek.Runtime, raw sobek.Value) (*wsParams, error) { 28 | tagsAndMeta := state.Tags.GetCurrentValues() 29 | 30 | parsed := &wsParams{ 31 | headers: make(http.Header), 32 | cookieJar: state.CookieJar, 33 | tagsAndMeta: &tagsAndMeta, 34 | } 35 | 36 | parsed.headers.Set("User-Agent", state.Options.UserAgent.String) 37 | 38 | if raw == nil || sobek.IsUndefined(raw) { 39 | return parsed, nil 40 | } 41 | 42 | params := raw.ToObject(rt) 43 | for _, k := range params.Keys() { 44 | switch k { 45 | case "headers": 46 | headersV := params.Get(k) 47 | if sobek.IsUndefined(headersV) || sobek.IsNull(headersV) { 48 | continue 49 | } 50 | headersObj := headersV.ToObject(rt) 51 | if headersObj == nil { 52 | continue 53 | } 54 | for _, key := range headersObj.Keys() { 55 | parsed.headers.Set(key, headersObj.Get(key).String()) 56 | } 57 | case "tags": 58 | if err := common.ApplyCustomUserTags(rt, parsed.tagsAndMeta, params.Get(k)); err != nil { 59 | return nil, fmt.Errorf("invalid WebSocket tags option: %w", err) 60 | } 61 | case "jar": 62 | jarV := params.Get(k) 63 | if sobek.IsUndefined(jarV) || sobek.IsNull(jarV) { 64 | continue 65 | } 66 | if v, ok := jarV.Export().(*httpModule.CookieJar); ok { 67 | parsed.cookieJar = v.Jar 68 | } 69 | case "compression": 70 | // deflate compression algorithm is supported - as defined in RFC7692 71 | // compression here relies on the implementation in gorilla/websocket package, usage is 72 | // experimental and may result in decreased performance. package supports 73 | // only "no context takeover" scenario 74 | 75 | algoString := strings.TrimSpace(params.Get(k).ToString().String()) 76 | if algoString != "deflate" { 77 | return nil, fmt.Errorf("unsupported compression algorithm '%s', supported algorithm is 'deflate'", algoString) 78 | } 79 | 80 | parsed.enableCompression = true 81 | default: 82 | return nil, fmt.Errorf("unknown WebSocket's option %s", k) 83 | } 84 | } 85 | 86 | return parsed, nil 87 | } 88 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grafana/xk6-websockets 2 | 3 | go 1.21 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/gorilla/websocket v1.5.3 9 | github.com/grafana/sobek v0.0.0-20241024150027-d91f02b05e9b 10 | github.com/mstoykov/k6-taskqueue-lib v0.1.0 11 | github.com/sirupsen/logrus v1.9.3 12 | github.com/stretchr/testify v1.9.0 13 | go.k6.io/k6 v0.54.1-0.20241025083358-192a49e1c20d 14 | go.uber.org/goleak v1.3.0 15 | gopkg.in/guregu/null.v3 v3.3.0 16 | ) 17 | 18 | require ( 19 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect 20 | github.com/PuerkitoBio/goquery v1.9.2 // indirect 21 | github.com/Soontao/goHttpDigestClient v0.0.0-20170320082612-6d28bb1415c5 // indirect 22 | github.com/andybalholm/brotli v1.1.1 // indirect 23 | github.com/andybalholm/cascadia v1.3.2 // indirect 24 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/dlclark/regexp2 v1.11.4 // indirect 27 | github.com/dop251/goja v0.0.0-20240516125602-ccbae20bcec2 // indirect 28 | github.com/evanw/esbuild v0.21.2 // indirect 29 | github.com/fatih/color v1.17.0 // indirect 30 | github.com/go-logr/logr v1.4.2 // indirect 31 | github.com/go-logr/stdr v1.2.2 // indirect 32 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect 33 | github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6 // indirect 34 | github.com/google/uuid v1.6.0 // indirect 35 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect 36 | github.com/josharian/intern v1.0.0 // indirect 37 | github.com/klauspost/compress v1.17.11 // indirect 38 | github.com/mailru/easyjson v0.7.7 // indirect 39 | github.com/mattn/go-colorable v0.1.13 // indirect 40 | github.com/mattn/go-isatty v0.0.20 // indirect 41 | github.com/mccutchen/go-httpbin v1.1.2-0.20190116014521-c5cb2f4802fa // indirect 42 | github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd // indirect 43 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect 44 | github.com/onsi/ginkgo v1.16.5 // indirect 45 | github.com/onsi/gomega v1.20.2 // indirect 46 | github.com/pmezard/go-difflib v1.0.0 // indirect 47 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect 48 | github.com/spf13/afero v1.1.2 // indirect 49 | github.com/tidwall/gjson v1.17.1 // indirect 50 | github.com/tidwall/match v1.1.1 // indirect 51 | github.com/tidwall/pretty v1.2.1 // indirect 52 | go.opentelemetry.io/otel v1.29.0 // indirect 53 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 // indirect 54 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 // indirect 55 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 // indirect 56 | go.opentelemetry.io/otel/metric v1.29.0 // indirect 57 | go.opentelemetry.io/otel/sdk v1.29.0 // indirect 58 | go.opentelemetry.io/otel/trace v1.29.0 // indirect 59 | go.opentelemetry.io/proto/otlp v1.3.1 // indirect 60 | golang.org/x/crypto v0.28.0 // indirect 61 | golang.org/x/net v0.30.0 // indirect 62 | golang.org/x/sys v0.26.0 // indirect 63 | golang.org/x/text v0.19.0 // indirect 64 | golang.org/x/time v0.7.0 // indirect 65 | google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd // indirect 66 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect 67 | google.golang.org/grpc v1.65.0 // indirect 68 | google.golang.org/protobuf v1.34.2 // indirect 69 | gopkg.in/yaml.v3 v3.0.1 // indirect 70 | ) 71 | -------------------------------------------------------------------------------- /websockets/listeners.go: -------------------------------------------------------------------------------- 1 | package websockets 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/grafana/sobek" 7 | "github.com/grafana/xk6-websockets/websockets/events" 8 | ) 9 | 10 | // eventListeners keeps track of the eventListeners for each event type 11 | type eventListeners struct { 12 | open *eventListener 13 | message *eventListener 14 | error *eventListener 15 | close *eventListener 16 | ping *eventListener 17 | pong *eventListener 18 | } 19 | 20 | func newEventListeners() *eventListeners { 21 | return &eventListeners{ 22 | open: newListener(events.OPEN), 23 | message: newListener(events.MESSAGE), 24 | error: newListener(events.ERROR), 25 | close: newListener(events.CLOSE), 26 | ping: newListener(events.PING), 27 | pong: newListener(events.PONG), 28 | } 29 | } 30 | 31 | // eventListener represents a tuple of listeners of a certain type 32 | // property on represents the eventListener that serves for the on* properties, like onopen, onmessage, etc. 33 | // property list keeps any other listeners that were added with addEventListener 34 | type eventListener struct { 35 | eventType string 36 | 37 | // this return sobek.value *and* error in order to return error on exception instead of panic 38 | // https://pkg.go.dev/github.com/dop251/goja#hdr-Functions 39 | on func(sobek.Value) (sobek.Value, error) 40 | list []func(sobek.Value) (sobek.Value, error) 41 | } 42 | 43 | // newListener creates a new listener of a certain type 44 | func newListener(eventType string) *eventListener { 45 | return &eventListener{ 46 | eventType: eventType, 47 | } 48 | } 49 | 50 | // add adds a listener to the listener list 51 | func (l *eventListener) add(fn func(sobek.Value) (sobek.Value, error)) { 52 | l.list = append(l.list, fn) 53 | } 54 | 55 | // setOn sets a listener for the on* properties, like onopen, onmessage, etc. 56 | func (l *eventListener) setOn(fn func(sobek.Value) (sobek.Value, error)) { 57 | l.on = fn 58 | } 59 | 60 | // getOn returns the on* property for a certain event type 61 | func (l *eventListener) getOn() func(sobek.Value) (sobek.Value, error) { 62 | return l.on 63 | } 64 | 65 | // return all possible listeners for a certain event type 66 | func (l *eventListener) all() []func(sobek.Value) (sobek.Value, error) { 67 | if l.on == nil { 68 | return l.list 69 | } 70 | 71 | return append([]func(sobek.Value) (sobek.Value, error){l.on}, l.list...) 72 | } 73 | 74 | // getTypes return event listener of a certain type 75 | func (l *eventListeners) getType(t string) *eventListener { 76 | switch t { 77 | case events.OPEN: 78 | return l.open 79 | case events.MESSAGE: 80 | return l.message 81 | case events.ERROR: 82 | return l.error 83 | case events.CLOSE: 84 | return l.close 85 | case events.PING: 86 | return l.ping 87 | case events.PONG: 88 | return l.pong 89 | default: 90 | return nil 91 | } 92 | } 93 | 94 | // add adds a listener to the listeners 95 | func (l *eventListeners) add(t string, f func(sobek.Value) (sobek.Value, error)) error { 96 | list := l.getType(t) 97 | 98 | if list == nil { 99 | return fmt.Errorf("unknown event type: %s", t) 100 | } 101 | 102 | list.add(f) 103 | 104 | return nil 105 | } 106 | 107 | // all returns all possible listeners for a certain event type or an empty array 108 | func (l *eventListeners) all(t string) []func(sobek.Value) (sobek.Value, error) { 109 | list := l.getType(t) 110 | 111 | if list == nil { 112 | return []func(sobek.Value) (sobek.Value, error){} 113 | } 114 | 115 | return list.all() 116 | } 117 | -------------------------------------------------------------------------------- /websockets/blob.go: -------------------------------------------------------------------------------- 1 | package websockets 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "unsafe" 9 | 10 | "github.com/grafana/sobek" 11 | 12 | "go.k6.io/k6/js/common" 13 | "go.k6.io/k6/js/modules/k6/experimental/streams" 14 | ) 15 | 16 | type blob struct { 17 | typ string 18 | data bytes.Buffer 19 | } 20 | 21 | func (b *blob) text() string { 22 | return b.data.String() 23 | } 24 | 25 | func (r *WebSocketsAPI) blob(call sobek.ConstructorCall) *sobek.Object { 26 | rt := r.vu.Runtime() 27 | 28 | b := &blob{} 29 | var blobParts []interface{} 30 | if len(call.Arguments) > 0 { 31 | if err := rt.ExportTo(call.Arguments[0], &blobParts); err != nil { 32 | common.Throw(rt, fmt.Errorf("failed to process [blobParts]: %w", err)) 33 | } 34 | } 35 | 36 | if len(blobParts) > 0 { 37 | r.fillData(b, blobParts, call) 38 | } 39 | 40 | if len(call.Arguments) > 1 && !sobek.IsUndefined(call.Arguments[1]) { 41 | opts := call.Arguments[1] 42 | if !isObject(opts) { 43 | common.Throw(rt, errors.New("[options] must be an object")) 44 | } 45 | 46 | typeOpt := opts.ToObject(rt).Get("type") 47 | if !sobek.IsUndefined(typeOpt) { 48 | b.typ = typeOpt.String() 49 | } 50 | } 51 | 52 | obj := rt.NewObject() 53 | must(rt, obj.DefineAccessorProperty("size", rt.ToValue(func() sobek.Value { 54 | return rt.ToValue(b.data.Len()) 55 | }), nil, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 56 | must(rt, obj.DefineAccessorProperty("type", rt.ToValue(func() sobek.Value { 57 | return rt.ToValue(b.typ) 58 | }), nil, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 59 | 60 | must(rt, obj.Set("arrayBuffer", func(_ sobek.FunctionCall) sobek.Value { 61 | promise, resolve, _ := rt.NewPromise() 62 | err := resolve(rt.NewArrayBuffer(b.data.Bytes())) 63 | if err != nil { 64 | panic(err) 65 | } 66 | return rt.ToValue(promise) 67 | })) 68 | must(rt, obj.Set("bytes", func(_ sobek.FunctionCall) sobek.Value { 69 | promise, resolve, reject := rt.NewPromise() 70 | data, err := rt.New(rt.Get("Uint8Array"), rt.ToValue(b.data.Bytes())) 71 | if err == nil { 72 | err = resolve(data) 73 | } else { 74 | err = reject(fmt.Errorf("failed to create Uint8Array: %w", err)) 75 | } 76 | if err != nil { 77 | panic(err) 78 | } 79 | return rt.ToValue(promise) 80 | })) 81 | must(rt, obj.Set("slice", func(call sobek.FunctionCall) sobek.Value { 82 | return r.slice(call, b, rt) 83 | })) 84 | must(rt, obj.Set("text", func(_ sobek.FunctionCall) sobek.Value { 85 | promise, resolve, _ := rt.NewPromise() 86 | err := resolve(b.text()) 87 | if err != nil { 88 | panic(err) 89 | } 90 | return rt.ToValue(promise) 91 | })) 92 | must(rt, obj.Set("stream", func(_ sobek.FunctionCall) sobek.Value { 93 | return rt.ToValue(streams.NewReadableStreamFromReader(r.vu, &b.data)) 94 | })) 95 | 96 | proto := call.This.Prototype() 97 | must(rt, proto.Set("toString", func(_ sobek.FunctionCall) sobek.Value { 98 | return rt.ToValue("[object Blob]") 99 | })) 100 | must(rt, obj.SetPrototype(proto)) 101 | 102 | return obj 103 | } 104 | 105 | func (r *WebSocketsAPI) fillData(b *blob, blobParts []interface{}, call sobek.ConstructorCall) { 106 | rt := r.vu.Runtime() 107 | 108 | if len(blobParts) > 0 { 109 | for n, part := range blobParts { 110 | var err error 111 | switch v := part.(type) { 112 | case []uint8: 113 | _, err = b.data.Write(v) 114 | case []int8, []int16, []int32, []int64, []uint16, []uint32, []uint64, []float32, []float64: 115 | _, err = b.data.Write(toByteSlice(v)) 116 | case sobek.ArrayBuffer: 117 | _, err = b.data.Write(v.Bytes()) 118 | case *sobek.ArrayBuffer: 119 | _, err = b.data.Write(v.Bytes()) 120 | case string: 121 | _, err = b.data.WriteString(v) 122 | case map[string]interface{}: 123 | obj := call.Arguments[0].ToObject(rt).Get(strconv.FormatInt(int64(n), 10)).ToObject(rt) 124 | switch { 125 | case isDataView(obj, rt): 126 | _, err = b.data.Write(obj.Get("buffer").Export().(sobek.ArrayBuffer).Bytes()) 127 | case isBlob(obj, r.blobConstructor): 128 | _, err = b.data.Write(extractBytes(obj, rt)) 129 | default: 130 | err = fmt.Errorf("unsupported type: %T", part) 131 | } 132 | default: 133 | err = fmt.Errorf("unsupported type: %T", part) 134 | } 135 | if err != nil { 136 | common.Throw(rt, fmt.Errorf("failed to process [blobParts]: %w", err)) 137 | } 138 | } 139 | } 140 | } 141 | 142 | func (r *WebSocketsAPI) slice(call sobek.FunctionCall, b *blob, rt *sobek.Runtime) sobek.Value { 143 | var ( 144 | from int 145 | to = b.data.Len() 146 | ct = "" 147 | ) 148 | 149 | if len(call.Arguments) > 0 { 150 | from = int(call.Arguments[0].ToInteger()) 151 | } 152 | 153 | if len(call.Arguments) > 1 { 154 | to = int(call.Arguments[1].ToInteger()) 155 | if to < 0 { 156 | to = b.data.Len() + to 157 | } 158 | } 159 | 160 | if len(call.Arguments) > 2 { 161 | ct = call.Arguments[2].String() 162 | } 163 | 164 | opts := rt.NewObject() 165 | must(rt, opts.Set("type", ct)) 166 | 167 | sliced, err := rt.New(r.blobConstructor, rt.ToValue([]interface{}{b.data.Bytes()[from:to]}), opts) 168 | must(rt, err) 169 | 170 | return sliced 171 | } 172 | 173 | // toByteSlice converts a slice of numbers to a slice of bytes. 174 | // 175 | //nolint:gosec 176 | func toByteSlice(data interface{}) []byte { 177 | switch v := data.(type) { 178 | case []int8: 179 | return unsafe.Slice((*byte)(unsafe.Pointer(&v[0])), len(v)) 180 | case []uint16: 181 | return unsafe.Slice((*byte)(unsafe.Pointer(&v[0])), len(v)*2) 182 | case []int16: 183 | return unsafe.Slice((*byte)(unsafe.Pointer(&v[0])), len(v)*2) 184 | case []uint32: 185 | return unsafe.Slice((*byte)(unsafe.Pointer(&v[0])), len(v)*4) 186 | case []int32: 187 | return unsafe.Slice((*byte)(unsafe.Pointer(&v[0])), len(v)*4) 188 | case []uint64: 189 | return unsafe.Slice((*byte)(unsafe.Pointer(&v[0])), len(v)*8) 190 | case []int64: 191 | return unsafe.Slice((*byte)(unsafe.Pointer(&v[0])), len(v)*8) 192 | case []float32: 193 | return unsafe.Slice((*byte)(unsafe.Pointer(&v[0])), len(v)*4) 194 | case []float64: 195 | return unsafe.Slice((*byte)(unsafe.Pointer(&v[0])), len(v)*8) 196 | default: 197 | // this should never happen 198 | common.Throw(nil, fmt.Errorf("unsupported type: %T", data)) 199 | return nil 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /websockets/blob_test.go: -------------------------------------------------------------------------------- 1 | package websockets 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/grafana/sobek" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestBlob(t *testing.T) { 12 | t.Parallel() 13 | 14 | tcs := map[string]struct { 15 | blobPartsDef string 16 | bytesExpected []byte 17 | }{ 18 | "String": { 19 | blobPartsDef: `["PASS"]`, 20 | bytesExpected: []byte("PASS"), 21 | }, 22 | "MultipleStrings": { 23 | blobPartsDef: `["P", "A", "SS"]`, 24 | bytesExpected: []byte("PASS"), 25 | }, 26 | "ArrayBuffer": { 27 | blobPartsDef: `[new Uint8Array([0x50, 0x41, 0x53, 0x53]).buffer]`, 28 | bytesExpected: []byte("PASS"), 29 | }, 30 | "Int8Array": { 31 | blobPartsDef: `[new Int8Array([0x50, 0x41, 0x53, 0x53])]`, 32 | bytesExpected: []byte("PASS"), 33 | }, 34 | "Uint8Array": { 35 | blobPartsDef: `[new Uint8Array([0x50, 0x41, 0x53, 0x53])]`, 36 | bytesExpected: []byte("PASS"), 37 | }, 38 | "Uint8ClampedArray": { 39 | blobPartsDef: `[new Uint8ClampedArray([0x50, 0x41, 0x53, 0x53])]`, 40 | bytesExpected: []byte("PASS"), 41 | }, 42 | "Int16Array": { 43 | blobPartsDef: `[new Int16Array([0x4150, 0x5353])]`, 44 | bytesExpected: []byte("PASS"), 45 | }, 46 | "Uint16Array": { 47 | blobPartsDef: `[new Uint16Array([0x4150, 0x5353])]`, 48 | bytesExpected: []byte("PASS"), 49 | }, 50 | "Int32Array": { 51 | blobPartsDef: `[new Int32Array([0x53534150])]`, 52 | bytesExpected: []byte("PASS"), 53 | }, 54 | "Uint32Array": { 55 | blobPartsDef: `[new Uint32Array([0x53534150])]`, 56 | bytesExpected: []byte("PASS"), 57 | }, 58 | "Float32Array": { 59 | blobPartsDef: `[new Float32Array(new Uint8Array([0x50, 0x41, 0x53, 0x53]).buffer)]`, 60 | bytesExpected: []byte("PASS"), 61 | }, 62 | "Float64Array": { 63 | // Byte length of Float64Array should be a multiple of 8 64 | blobPartsDef: `[new Float64Array(new Uint8Array([0x50, 0x41, 0x53, 0x53, 0x00, 0x00, 0x00, 0x00]).buffer)]`, 65 | bytesExpected: append([]byte("PASS"), 0x0, 0x0, 0x0, 0x0), 66 | }, 67 | "DataView": { 68 | blobPartsDef: `[new DataView(new Int8Array([0x50, 0x41, 0x53, 0x53]).buffer)]`, 69 | bytesExpected: []byte("PASS"), 70 | }, 71 | "Blob": { 72 | blobPartsDef: `[new Blob(["PASS"])]`, 73 | bytesExpected: []byte("PASS"), 74 | }, 75 | } 76 | 77 | for name, tc := range tcs { 78 | tc := tc 79 | t.Run(name, func(t *testing.T) { 80 | t.Parallel() 81 | ts := newTestState(t) 82 | val, err := ts.runtime.RunOnEventLoop(fmt.Sprintf(` 83 | (async () => { 84 | const blobParts = %s; 85 | const blob = new Blob(blobParts); 86 | return blob.arrayBuffer(); 87 | })() 88 | `, tc.blobPartsDef)) 89 | 90 | require.NoError(t, err) 91 | 92 | p, ok := val.Export().(*sobek.Promise) 93 | require.True(t, ok) 94 | 95 | ab, ok := p.Result().Export().(sobek.ArrayBuffer) 96 | require.True(t, ok) 97 | require.Equal(t, tc.bytesExpected, ab.Bytes()) 98 | }) 99 | } 100 | } 101 | 102 | func TestBlob_type(t *testing.T) { 103 | t.Parallel() 104 | ts := newTestState(t) 105 | rt := ts.runtime.VU.Runtime() 106 | val, err := ts.runtime.RunOnEventLoop(` 107 | new Blob(["PASS"], { type: "text/example" }); 108 | `) 109 | require.NoError(t, err) 110 | require.True(t, isBlob(val.ToObject(rt), ts.module.blobConstructor)) 111 | require.Equal(t, "text/example", val.ToObject(rt).Get("type").String()) 112 | } 113 | 114 | func TestBlob_size(t *testing.T) { 115 | t.Parallel() 116 | ts := newTestState(t) 117 | rt := ts.runtime.VU.Runtime() 118 | val, err := ts.runtime.RunOnEventLoop(` 119 | new Blob(["PASS"]); 120 | `) 121 | require.NoError(t, err) 122 | require.True(t, isBlob(val.ToObject(rt), ts.module.blobConstructor)) 123 | require.Equal(t, int64(4), val.ToObject(rt).Get("size").ToInteger()) 124 | } 125 | 126 | func TestBlob_arrayBuffer(t *testing.T) { 127 | t.Parallel() 128 | ts := newTestState(t) 129 | val, err := ts.runtime.RunOnEventLoop(` 130 | (async () => { 131 | const blob = new Blob(["P", "A", "SS"]); 132 | return blob.arrayBuffer(); 133 | })() 134 | `) 135 | require.NoError(t, err) 136 | 137 | p, ok := val.Export().(*sobek.Promise) 138 | require.True(t, ok) 139 | 140 | ab, ok := p.Result().Export().(sobek.ArrayBuffer) 141 | require.True(t, ok) 142 | require.Equal(t, []byte("PASS"), ab.Bytes()) 143 | } 144 | 145 | func TestBlob_bytes(t *testing.T) { 146 | t.Parallel() 147 | ts := newTestState(t) 148 | val, err := ts.runtime.RunOnEventLoop(` 149 | (async () => { 150 | const blob = new Blob(["P", "A", "SS"]); 151 | return blob.bytes(); 152 | })() 153 | `) 154 | require.NoError(t, err) 155 | 156 | p, ok := val.Export().(*sobek.Promise) 157 | require.True(t, ok) 158 | 159 | rt := ts.runtime.VU.Runtime() 160 | res := p.Result().ToObject(rt) 161 | require.True(t, isUint8Array(res, rt)) 162 | 163 | var resBytes []byte 164 | require.NoError(t, rt.ExportTo(res.Get("buffer"), &resBytes)) 165 | require.Equal(t, []byte("PASS"), resBytes) 166 | } 167 | 168 | func TestBlob_slice(t *testing.T) { 169 | t.Parallel() 170 | 171 | tcs := map[string]struct { 172 | call string 173 | bytesExpected []byte 174 | ctExpected string 175 | }{ 176 | "slice()": { 177 | call: `slice()`, 178 | bytesExpected: []byte("PASS"), 179 | }, 180 | "slice(start)": { 181 | call: `slice(1)`, 182 | bytesExpected: []byte("ASS"), 183 | }, 184 | "slice(start, end)": { 185 | call: `slice(0,1)`, 186 | bytesExpected: []byte("P"), 187 | }, 188 | "slice(start, end, contentType)": { 189 | call: `slice(0,1,"text/example")`, 190 | bytesExpected: []byte("P"), 191 | ctExpected: "text/example", 192 | }, 193 | } 194 | 195 | for name, tc := range tcs { 196 | tc := tc 197 | t.Run(name, func(t *testing.T) { 198 | t.Parallel() 199 | ts := newTestState(t) 200 | val, err := ts.runtime.RunOnEventLoop(fmt.Sprintf(` 201 | (async () => { 202 | const blob = new Blob(["PASS"]); 203 | return blob.%s; 204 | })() 205 | `, tc.call)) 206 | 207 | require.NoError(t, err) 208 | 209 | p, ok := val.Export().(*sobek.Promise) 210 | require.True(t, ok) 211 | 212 | rt := ts.runtime.VU.Runtime() 213 | assertBlobTypeAndContents(t, ts, p.Result().ToObject(rt), tc.ctExpected, tc.bytesExpected) 214 | }) 215 | } 216 | } 217 | 218 | func assertBlobTypeAndContents(t *testing.T, ts testState, blob *sobek.Object, expType string, expContents []byte) { 219 | t.Helper() 220 | 221 | // First, we assert the given object is 'instanceof' Blob. 222 | require.True(t, isBlob(blob, ts.module.blobConstructor)) 223 | 224 | // Then, we assert the type of the blob. 225 | require.Equal(t, expType, blob.Get("type").String()) 226 | 227 | // Finally, we assert the contents of the blob, by calling `.arrayBuffer()` 228 | // and comparing the result with the expected contents. 229 | call, ok := sobek.AssertFunction(blob.Get("arrayBuffer")) 230 | require.True(t, ok) 231 | 232 | ret, err := call(sobek.Undefined()) 233 | require.NoError(t, err) 234 | p, ok := ret.Export().(*sobek.Promise) 235 | require.True(t, ok) 236 | 237 | ab, ok := p.Result().Export().(sobek.ArrayBuffer) 238 | require.True(t, ok) 239 | require.Equal(t, expContents, ab.Bytes()) 240 | } 241 | 242 | func TestBlob_text(t *testing.T) { 243 | t.Parallel() 244 | ts := newTestState(t) 245 | val, err := ts.runtime.RunOnEventLoop(` 246 | (async () => { 247 | const blob = new Blob(["P", "A", "SS"]); 248 | return blob.text(); 249 | })() 250 | `) 251 | require.NoError(t, err) 252 | 253 | p, ok := val.Export().(*sobek.Promise) 254 | require.True(t, ok) 255 | 256 | require.Equal(t, "PASS", p.Result().String()) 257 | } 258 | 259 | func TestBlob_stream(t *testing.T) { 260 | t.Parallel() 261 | ts := newTestState(t) 262 | val, err := ts.runtime.RunOnEventLoop(` 263 | (async () => { 264 | const blob = new Blob(["P", "A", "SS"]); 265 | const reader = blob.stream().getReader(); 266 | const {value} = await reader.read(); 267 | return value; 268 | })() 269 | `) 270 | require.NoError(t, err) 271 | 272 | p, ok := val.Export().(*sobek.Promise) 273 | require.True(t, ok) 274 | require.Equal(t, "PASS", p.Result().String()) 275 | } 276 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= 2 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= 3 | github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= 4 | github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 5 | github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= 6 | github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= 7 | github.com/Soontao/goHttpDigestClient v0.0.0-20170320082612-6d28bb1415c5 h1:k+1+doEm31k0rRjCjLnGG3YRkuO9ljaEyS2ajZd6GK8= 8 | github.com/Soontao/goHttpDigestClient v0.0.0-20170320082612-6d28bb1415c5/go.mod h1:5Q4+CyR7+Q3VMG8f78ou+QSX/BNUNUx5W48eFRat8DQ= 9 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 10 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 11 | github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= 12 | github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= 13 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 14 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= 19 | github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 20 | github.com/dop251/goja v0.0.0-20240516125602-ccbae20bcec2 h1:OFTHt+yJDo/uaIKMGjEKzc3DGhrpQZoqvMUIloZv6ZY= 21 | github.com/dop251/goja v0.0.0-20240516125602-ccbae20bcec2/go.mod h1:o31y53rb/qiIAONF7w3FHJZRqqP3fzHUr1HqanthByw= 22 | github.com/evanw/esbuild v0.21.2 h1:CLplcGi794CfHLVmUbvVfTMKkykm+nyIHU8SU60KUTA= 23 | github.com/evanw/esbuild v0.21.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= 24 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 25 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 26 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 27 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 28 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= 29 | github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= 30 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 31 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 32 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 33 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 34 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 35 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= 36 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 37 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 38 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 40 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 41 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 42 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 43 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 44 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 45 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 46 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 47 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 48 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 49 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 50 | github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6 h1:ZgoomqkdjGbQ3+qQXCkvYMCDvGDNg2k5JJDjjdTB6jY= 51 | github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= 52 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 53 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 54 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 55 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 56 | github.com/grafana/sobek v0.0.0-20241024150027-d91f02b05e9b h1:hzfIt1lf19Zx1jIYdeHvuWS266W+jL+7dxbpvH2PZMQ= 57 | github.com/grafana/sobek v0.0.0-20241024150027-d91f02b05e9b/go.mod h1:FmcutBFPLiGgroH42I4/HBahv7GxVjODcVWFTw1ISes= 58 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= 59 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= 60 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 61 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 62 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 63 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 64 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 65 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 66 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 67 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 68 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 69 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 70 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 71 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 72 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 73 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 74 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 75 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 76 | github.com/mccutchen/go-httpbin v1.1.2-0.20190116014521-c5cb2f4802fa h1:lx8ZnNPwjkXSzOROz0cg69RlErRXs+L3eDkggASWKLo= 77 | github.com/mccutchen/go-httpbin v1.1.2-0.20190116014521-c5cb2f4802fa/go.mod h1:fhpOYavp5g2K74XDl/ao2y4KvhqVtKlkg1e+0UaQv7I= 78 | github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd h1:AC3N94irbx2kWGA8f/2Ks7EQl2LxKIRQYuT9IJDwgiI= 79 | github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd/go.mod h1:9vRHVuLCjoFfE3GT06X0spdOAO+Zzo4AMjdIwUHBvAk= 80 | github.com/mstoykov/envconfig v1.5.0 h1:E2FgWf73BQt0ddgn7aoITkQHmgwAcHup1s//MsS5/f8= 81 | github.com/mstoykov/envconfig v1.5.0/go.mod h1:vk/d9jpexY2Z9Bb0uB4Ndesss1Sr0Z9ZiGUrg5o9VGk= 82 | github.com/mstoykov/k6-taskqueue-lib v0.1.0 h1:M3eww1HSOLEN6rIkbNOJHhOVhlqnqkhYj7GTieiMBz4= 83 | github.com/mstoykov/k6-taskqueue-lib v0.1.0/go.mod h1:PXdINulapvmzF545Auw++SCD69942FeNvUztaa9dVe4= 84 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= 85 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= 86 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 87 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 88 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 89 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 90 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 91 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 92 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 93 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 94 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 95 | github.com/onsi/gomega v1.20.2 h1:8uQq0zMgLEfa0vRrrBgaJF2gyW9Da9BmfGV+OyUzfkY= 96 | github.com/onsi/gomega v1.20.2/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= 97 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 98 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 99 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 100 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 101 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e h1:zWKUYT07mGmVBH+9UgnHXd/ekCK99C8EbDSAt5qsjXE= 102 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= 103 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 104 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 105 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 106 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 107 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 108 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 109 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 110 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 111 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 112 | github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= 113 | github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 114 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 115 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 116 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 117 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 118 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 119 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 120 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 121 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 122 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 123 | go.k6.io/k6 v0.54.1-0.20241025083358-192a49e1c20d h1:g5NRFV7CH8J+6ymnKVHN0C+udJA0n0sQ5Bc4PcCOW8Y= 124 | go.k6.io/k6 v0.54.1-0.20241025083358-192a49e1c20d/go.mod h1:D1iT0FttS9fRlLjiBSUGWOYbu6J3w/8h+axduZyt5Zk= 125 | go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 126 | go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 127 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= 128 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= 129 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0 h1:nSiV3s7wiCam610XcLbYOmMfJxB9gO4uK3Xgv5gmTgg= 130 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.29.0/go.mod h1:hKn/e/Nmd19/x1gvIHwtOwVWM+VhuITSWip3JUDghj0= 131 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 h1:JAv0Jwtl01UFiyWZEMiJZBiTlv5A50zNs8lsthXqIio= 132 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0/go.mod h1:QNKLmUEAq2QUbPQUfvw4fmv0bgbK7UlOSFCnXyfvSNc= 133 | go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 134 | go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 135 | go.opentelemetry.io/otel/sdk v1.29.0 h1:vkqKjk7gwhS8VaWb0POZKmIEDimRCMsopNYnriHyryo= 136 | go.opentelemetry.io/otel/sdk v1.29.0/go.mod h1:pM8Dx5WKnvxLCb+8lG1PRNIDxu9g9b9g59Qr7hfAAok= 137 | go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 138 | go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 139 | go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= 140 | go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 141 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 142 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 143 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 144 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 145 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 146 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 147 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 148 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 149 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 150 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 151 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 152 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 153 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 154 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 155 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 156 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 157 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 158 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 159 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 160 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 161 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 162 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 163 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 164 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 165 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 166 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 167 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 168 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 169 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 170 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 171 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 172 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 173 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 174 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 177 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 178 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 179 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 180 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 181 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 182 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 183 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 184 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 185 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 186 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 187 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 188 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 189 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 190 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 191 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 192 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 193 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 194 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 195 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 196 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 197 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 198 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 199 | golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= 200 | golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 201 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 202 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 203 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 204 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 205 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 206 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 207 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 208 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 209 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 210 | google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd h1:BBOTEWLuuEGQy9n1y9MhVJ9Qt0BDu21X8qZs71/uPZo= 211 | google.golang.org/genproto/googleapis/api v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:fO8wJzT2zbQbAjbIoos1285VfEIYKDDY+Dt+WpTkh6g= 212 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd h1:6TEm2ZxXoQmFWFlt1vNxvVOa1Q0dXFQD1m/rYjXmS0E= 213 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 214 | google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= 215 | google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 216 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 217 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 218 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 219 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 220 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 221 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 222 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 223 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 224 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 225 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 226 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 227 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 228 | gopkg.in/guregu/null.v3 v3.3.0 h1:8j3ggqq+NgKt/O7mbFVUFKUMWN+l1AmT5jQmJ6nPh2c= 229 | gopkg.in/guregu/null.v3 v3.3.0/go.mod h1:E4tX2Qe3h7QdL+uZ3a0vqvYwKQsRSQKM5V4YltdgH9Y= 230 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 231 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 232 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 233 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 234 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 235 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 236 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 237 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 238 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 239 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 240 | -------------------------------------------------------------------------------- /websockets/websockets.go: -------------------------------------------------------------------------------- 1 | // Package websockets implements to some extend WebSockets API https://websockets.spec.whatwg.org 2 | package websockets 3 | 4 | import ( 5 | "context" 6 | "crypto/tls" 7 | "errors" 8 | "fmt" 9 | "net" 10 | "net/http" 11 | "net/url" 12 | "strconv" 13 | "sync" 14 | "time" 15 | 16 | "github.com/gorilla/websocket" 17 | "github.com/grafana/sobek" 18 | "github.com/grafana/xk6-websockets/websockets/events" 19 | "github.com/mstoykov/k6-taskqueue-lib/taskqueue" 20 | 21 | "go.k6.io/k6/js/common" 22 | "go.k6.io/k6/js/modules" 23 | "go.k6.io/k6/metrics" 24 | ) 25 | 26 | // RootModule is the root module for the websockets API 27 | type RootModule struct{} 28 | 29 | // WebSocketsAPI is the k6 extension implementing the websocket API as defined in https://websockets.spec.whatwg.org 30 | type WebSocketsAPI struct { //nolint:revive 31 | vu modules.VU 32 | blobConstructor sobek.Value 33 | } 34 | 35 | var _ modules.Module = &RootModule{} 36 | 37 | // NewModuleInstance returns a new instance of the module 38 | func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance { 39 | return &WebSocketsAPI{ 40 | vu: vu, 41 | } 42 | } 43 | 44 | // Exports implements the modules.Instance interface's Exports 45 | func (r *WebSocketsAPI) Exports() modules.Exports { 46 | r.blobConstructor = r.vu.Runtime().ToValue(r.blob) 47 | return modules.Exports{ 48 | Named: map[string]interface{}{ 49 | "WebSocket": r.websocket, 50 | "Blob": r.blobConstructor, 51 | }, 52 | } 53 | } 54 | 55 | // ReadyState is websocket specification's readystate 56 | type ReadyState uint8 57 | 58 | const ( 59 | // CONNECTING is the state while the web socket is connecting 60 | CONNECTING ReadyState = iota 61 | // OPEN is the state after the websocket is established and before it starts closing 62 | OPEN 63 | // CLOSING is while the websocket is closing but is *not* closed yet 64 | CLOSING 65 | // CLOSED is when the websocket is finally closed 66 | CLOSED 67 | ) 68 | 69 | type webSocket struct { 70 | vu modules.VU 71 | blobConstructor sobek.Value 72 | 73 | url *url.URL 74 | conn *websocket.Conn 75 | tagsAndMeta *metrics.TagsAndMeta 76 | tq *taskqueue.TaskQueue 77 | builtinMetrics *metrics.BuiltinMetrics 78 | obj *sobek.Object // the object that is given to js to interact with the WebSocket 79 | started time.Time 80 | 81 | done chan struct{} 82 | writeQueueCh chan message 83 | 84 | eventListeners *eventListeners 85 | 86 | sendPings ping 87 | 88 | // fields that should be seen by js only be updated on the event loop 89 | readyState ReadyState 90 | bufferedAmount int 91 | binaryType string 92 | protocol string 93 | extensions []string 94 | } 95 | 96 | type ping struct { 97 | counter int 98 | timestamps map[string]time.Time 99 | } 100 | 101 | func (r *WebSocketsAPI) websocket(c sobek.ConstructorCall) *sobek.Object { 102 | rt := r.vu.Runtime() 103 | 104 | url, err := parseURL(c.Argument(0)) 105 | if err != nil { 106 | common.Throw(rt, err) 107 | } 108 | 109 | params, err := buildParams(r.vu.State(), rt, c.Argument(2)) 110 | if err != nil { 111 | common.Throw(rt, err) 112 | } 113 | 114 | subprocotolsArg := c.Argument(1) 115 | if !common.IsNullish(subprocotolsArg) { 116 | subprocotolsObj := subprocotolsArg.ToObject(rt) 117 | switch { 118 | case isString(subprocotolsObj, rt): 119 | params.subprocotols = append(params.subprocotols, subprocotolsObj.String()) 120 | case isArray(subprocotolsObj, rt): 121 | for _, key := range subprocotolsObj.Keys() { 122 | params.subprocotols = append(params.subprocotols, subprocotolsObj.Get(key).String()) 123 | } 124 | } 125 | } 126 | 127 | w := &webSocket{ 128 | vu: r.vu, 129 | blobConstructor: r.blobConstructor, 130 | url: url, 131 | tq: taskqueue.New(r.vu.RegisterCallback), 132 | readyState: CONNECTING, 133 | builtinMetrics: r.vu.State().BuiltinMetrics, 134 | done: make(chan struct{}), 135 | writeQueueCh: make(chan message), 136 | eventListeners: newEventListeners(), 137 | obj: rt.NewObject(), 138 | tagsAndMeta: params.tagsAndMeta, 139 | sendPings: ping{timestamps: make(map[string]time.Time)}, 140 | binaryType: blobBinaryType, 141 | } 142 | 143 | // Maybe have this after the goroutine below ?!? 144 | defineWebsocket(rt, w) 145 | 146 | go w.establishConnection(params) 147 | return w.obj 148 | } 149 | 150 | // parseURL parses the url from the first constructor calls argument or returns an error 151 | func parseURL(urlValue sobek.Value) (*url.URL, error) { 152 | if urlValue == nil || sobek.IsUndefined(urlValue) { 153 | return nil, errors.New("WebSocket requires a url") 154 | } 155 | 156 | // TODO: throw the SyntaxError (https://websockets.spec.whatwg.org/#dom-websocket-websocket) 157 | urlString := urlValue.String() 158 | url, err := url.Parse(urlString) 159 | if err != nil { 160 | return nil, fmt.Errorf("WebSocket requires valid url, but got %q which resulted in %w", urlString, err) 161 | } 162 | if url.Scheme != "ws" && url.Scheme != "wss" { 163 | return nil, fmt.Errorf("WebSocket requires url with scheme ws or wss, but got %q", url.Scheme) 164 | } 165 | if url.Fragment != "" { 166 | return nil, fmt.Errorf("WebSocket requires no url fragment, but got %q", url.Fragment) 167 | } 168 | 169 | return url, nil 170 | } 171 | 172 | const ( 173 | arraybufferBinaryType = "arraybuffer" 174 | blobBinaryType = "blob" 175 | ) 176 | 177 | // defineWebsocket defines all properties and methods for the WebSocket 178 | func defineWebsocket(rt *sobek.Runtime, w *webSocket) { 179 | must(rt, w.obj.DefineDataProperty( 180 | "addEventListener", rt.ToValue(w.addEventListener), sobek.FLAG_FALSE, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 181 | must(rt, w.obj.DefineDataProperty( 182 | "send", rt.ToValue(w.send), sobek.FLAG_FALSE, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 183 | must(rt, w.obj.DefineDataProperty( 184 | "ping", rt.ToValue(w.ping), sobek.FLAG_FALSE, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 185 | must(rt, w.obj.DefineDataProperty( 186 | "close", rt.ToValue(w.close), sobek.FLAG_FALSE, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 187 | must(rt, w.obj.DefineDataProperty( 188 | "url", rt.ToValue(w.url.String()), sobek.FLAG_FALSE, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 189 | must(rt, w.obj.DefineAccessorProperty( // this needs to be with an accessor as we change the value 190 | "readyState", rt.ToValue(func() sobek.Value { 191 | return rt.ToValue((uint)(w.readyState)) 192 | }), nil, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 193 | must(rt, w.obj.DefineAccessorProperty( 194 | "bufferedAmount", rt.ToValue(func() sobek.Value { return rt.ToValue(w.bufferedAmount) }), nil, 195 | sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 196 | must(rt, w.obj.DefineAccessorProperty("extensions", 197 | rt.ToValue(func() sobek.Value { return rt.ToValue(w.extensions) }), nil, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 198 | must(rt, w.obj.DefineAccessorProperty( 199 | "protocol", rt.ToValue(func() sobek.Value { return rt.ToValue(w.protocol) }), nil, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 200 | must(rt, w.obj.DefineAccessorProperty( 201 | "binaryType", rt.ToValue(func() sobek.Value { 202 | return rt.ToValue(w.binaryType) 203 | }), rt.ToValue(func(s string) error { 204 | switch s { 205 | case blobBinaryType, arraybufferBinaryType: 206 | w.binaryType = s 207 | return nil 208 | default: 209 | return fmt.Errorf(`unknown binaryType %s, the supported ones are "blob" and "arraybuffer"`, s) 210 | } 211 | }), sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 212 | 213 | setOn := func(property string, el *eventListener) { 214 | if el == nil { 215 | // this is generally should not happen, but we're being defensive 216 | common.Throw(rt, fmt.Errorf("not supported on-handler '%s'", property)) 217 | } 218 | 219 | must(rt, w.obj.DefineAccessorProperty( 220 | property, rt.ToValue(func() sobek.Value { 221 | return rt.ToValue(el.getOn) 222 | }), rt.ToValue(func(call sobek.FunctionCall) sobek.Value { 223 | arg := call.Argument(0) 224 | 225 | // it's possible to unset handlers by setting them to null 226 | if arg == nil || sobek.IsUndefined(arg) || sobek.IsNull(arg) { 227 | el.setOn(nil) 228 | 229 | return nil 230 | } 231 | 232 | fn, isFunc := sobek.AssertFunction(arg) 233 | if !isFunc { 234 | common.Throw(rt, fmt.Errorf("a value for '%s' should be callable", property)) 235 | } 236 | 237 | el.setOn(func(v sobek.Value) (sobek.Value, error) { return fn(sobek.Undefined(), v) }) 238 | 239 | return nil 240 | }), sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 241 | } 242 | 243 | setOn("onmessage", w.eventListeners.getType(events.MESSAGE)) 244 | setOn("onerror", w.eventListeners.getType(events.ERROR)) 245 | setOn("onopen", w.eventListeners.getType(events.OPEN)) 246 | setOn("onclose", w.eventListeners.getType(events.CLOSE)) 247 | setOn("onping", w.eventListeners.getType(events.PING)) 248 | setOn("onpong", w.eventListeners.getType(events.PONG)) 249 | } 250 | 251 | type message struct { 252 | mtype int // message type consts as defined in gorilla/websocket/conn.go 253 | data []byte 254 | t time.Time 255 | } 256 | 257 | // documented https://websockets.spec.whatwg.org/#concept-websocket-establish 258 | func (w *webSocket) establishConnection(params *wsParams) { 259 | state := w.vu.State() 260 | w.started = time.Now() 261 | var tlsConfig *tls.Config 262 | if state.TLSConfig != nil { 263 | tlsConfig = state.TLSConfig.Clone() 264 | tlsConfig.NextProtos = []string{"http/1.1"} 265 | } 266 | // technically we have to do a fetch request here, so ... uh do normal one ;) 267 | wsd := websocket.Dialer{ 268 | HandshakeTimeout: time.Second * 60, // TODO configurable 269 | // Pass a custom net.DialContext function to websocket.Dialer that will substitute 270 | // the underlying net.Conn with our own tracked netext.Conn 271 | NetDialContext: state.Dialer.DialContext, 272 | Proxy: http.ProxyFromEnvironment, 273 | TLSClientConfig: tlsConfig, 274 | EnableCompression: params.enableCompression, 275 | Subprotocols: params.subprocotols, 276 | } 277 | 278 | // this is needed because of how interfaces work and that wsd.Jar is http.Cookiejar 279 | if params.cookieJar != nil { 280 | wsd.Jar = params.cookieJar 281 | } 282 | 283 | ctx := w.vu.Context() 284 | start := time.Now() 285 | conn, httpResponse, connErr := wsd.DialContext(ctx, w.url.String(), params.headers) 286 | connectionEnd := time.Now() 287 | connectionDuration := metrics.D(connectionEnd.Sub(start)) 288 | 289 | systemTags := state.Options.SystemTags 290 | 291 | if conn != nil && conn.RemoteAddr() != nil { 292 | if ip, _, err := net.SplitHostPort(conn.RemoteAddr().String()); err == nil { 293 | w.tagsAndMeta.SetSystemTagOrMetaIfEnabled(systemTags, metrics.TagIP, ip) 294 | } 295 | } 296 | 297 | if httpResponse != nil { 298 | defer func() { 299 | _ = httpResponse.Body.Close() 300 | }() 301 | 302 | w.tagsAndMeta.SetSystemTagOrMetaIfEnabled(systemTags, metrics.TagStatus, strconv.Itoa(httpResponse.StatusCode)) 303 | if conn != nil { 304 | w.protocol = conn.Subprotocol() 305 | } 306 | w.extensions = httpResponse.Header.Values("Sec-WebSocket-Extensions") 307 | w.tagsAndMeta.SetSystemTagOrMetaIfEnabled(systemTags, metrics.TagSubproto, w.protocol) 308 | } 309 | w.conn = conn 310 | 311 | nameTagValue, nameTagManuallySet := params.tagsAndMeta.Tags.Get(metrics.TagName.String()) 312 | // After k6 v0.41.0, the `name` and `url` tags have the exact same values: 313 | if nameTagManuallySet { 314 | w.tagsAndMeta.SetSystemTagOrMetaIfEnabled(systemTags, metrics.TagURL, nameTagValue) 315 | } else { 316 | w.tagsAndMeta.SetSystemTagOrMetaIfEnabled(systemTags, metrics.TagURL, w.url.String()) 317 | w.tagsAndMeta.SetSystemTagOrMetaIfEnabled(systemTags, metrics.TagName, w.url.String()) 318 | } 319 | 320 | w.emitConnectionMetrics(ctx, start, connectionDuration) 321 | if connErr != nil { 322 | // Pass the error to the user script before exiting immediately 323 | w.tq.Queue(func() error { 324 | return w.connectionClosedWithError(connErr) 325 | }) 326 | w.tq.Close() 327 | return 328 | } 329 | go w.loop() 330 | w.tq.Queue(func() error { 331 | return w.connectionConnected() 332 | }) 333 | } 334 | 335 | // emitConnectionMetrics emits the metrics for a websocket connection. 336 | func (w *webSocket) emitConnectionMetrics(ctx context.Context, start time.Time, duration float64) { 337 | state := w.vu.State() 338 | 339 | metrics.PushIfNotDone(ctx, state.Samples, metrics.ConnectedSamples{ 340 | Samples: []metrics.Sample{ 341 | { 342 | TimeSeries: metrics.TimeSeries{Metric: state.BuiltinMetrics.WSSessions, Tags: w.tagsAndMeta.Tags}, 343 | Time: start, 344 | Metadata: w.tagsAndMeta.Metadata, 345 | Value: 1, 346 | }, 347 | { 348 | TimeSeries: metrics.TimeSeries{Metric: state.BuiltinMetrics.WSConnecting, Tags: w.tagsAndMeta.Tags}, 349 | Time: start, 350 | Metadata: w.tagsAndMeta.Metadata, 351 | Value: duration, 352 | }, 353 | }, 354 | Tags: w.tagsAndMeta.Tags, 355 | Time: start, 356 | }) 357 | } 358 | 359 | const writeWait = 10 * time.Second 360 | 361 | func (w *webSocket) loop() { 362 | // Pass ping/pong events through the main control loop 363 | pingChan := make(chan string) 364 | pongChan := make(chan string) 365 | w.conn.SetPingHandler(func(msg string) error { pingChan <- msg; return nil }) 366 | w.conn.SetPongHandler(func(pingID string) error { pongChan <- pingID; return nil }) 367 | 368 | ctx := w.vu.Context() 369 | wg := new(sync.WaitGroup) 370 | 371 | defer func() { 372 | metrics.PushIfNotDone(ctx, w.vu.State().Samples, metrics.Sample{ 373 | TimeSeries: metrics.TimeSeries{ 374 | Metric: w.builtinMetrics.WSSessionDuration, 375 | Tags: w.tagsAndMeta.Tags, 376 | }, 377 | Time: time.Now(), 378 | Metadata: w.tagsAndMeta.Metadata, 379 | Value: metrics.D(time.Since(w.started)), 380 | }) 381 | _ = w.conn.Close() 382 | wg.Wait() 383 | w.tq.Close() 384 | }() 385 | wg.Add(2) 386 | go w.readPump(wg) 387 | go w.writePump(wg) 388 | 389 | ctxDone := ctx.Done() 390 | for { 391 | select { 392 | case <-ctxDone: 393 | // VU is shutting down during an interrupt 394 | // socket events will not be forwarded to the VU 395 | w.queueClose() 396 | ctxDone = nil // this is to block this branch and get through w.done 397 | case <-w.done: 398 | return 399 | case pingData := <-pingChan: 400 | 401 | // Handle pings received from the server 402 | // - trigger the `ping` event 403 | // - reply with pong (needed when `SetPingHandler` is overwritten) 404 | // WriteControl is okay to be concurrent so we don't need to gsend this over writeChannel 405 | err := w.conn.WriteControl(websocket.PongMessage, []byte(pingData), time.Now().Add(writeWait)) 406 | w.tq.Queue(func() error { 407 | if err != nil { 408 | return w.callErrorListeners(err) 409 | } 410 | 411 | return w.callEventListeners(events.PING) 412 | }) 413 | 414 | case pingID := <-pongChan: 415 | w.tq.Queue(func() error { 416 | // Handle pong responses to our pings 417 | w.trackPong(pingID) 418 | 419 | return w.callEventListeners(events.PONG) 420 | }) 421 | } 422 | } 423 | } 424 | 425 | func (w *webSocket) queueMessage(msg *message) { 426 | w.tq.Queue(func() error { 427 | if w.readyState != OPEN { 428 | return nil // TODO maybe still emit 429 | } 430 | // TODO maybe emit after all the listeners have fired and skip it if defaultPrevent was called?!? 431 | metrics.PushIfNotDone(w.vu.Context(), w.vu.State().Samples, metrics.Sample{ 432 | TimeSeries: metrics.TimeSeries{ 433 | Metric: w.builtinMetrics.WSMessagesReceived, 434 | Tags: w.tagsAndMeta.Tags, 435 | }, 436 | Time: msg.t, 437 | Metadata: w.tagsAndMeta.Metadata, 438 | Value: 1, 439 | }) 440 | 441 | rt := w.vu.Runtime() 442 | ev := w.newEvent(events.MESSAGE, msg.t) 443 | 444 | if msg.mtype == websocket.BinaryMessage { 445 | var data any 446 | switch w.binaryType { 447 | case blobBinaryType: 448 | var err error 449 | data, err = rt.New(w.blobConstructor, rt.ToValue([]interface{}{msg.data})) 450 | if err != nil { 451 | return fmt.Errorf("failed to create Blob: %w", err) 452 | } 453 | case arraybufferBinaryType: 454 | data = rt.NewArrayBuffer(msg.data) 455 | default: 456 | return fmt.Errorf(`unknown binaryType %q, the supported ones are "blob" and "arraybuffer"`, w.binaryType) 457 | } 458 | must(rt, ev.DefineDataProperty("data", rt.ToValue(data), sobek.FLAG_FALSE, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 459 | } else { 460 | must( 461 | rt, 462 | ev.DefineDataProperty("data", rt.ToValue(string(msg.data)), sobek.FLAG_FALSE, sobek.FLAG_FALSE, sobek.FLAG_TRUE), 463 | ) 464 | } 465 | must( 466 | rt, 467 | ev.DefineDataProperty("origin", rt.ToValue(w.url.String()), sobek.FLAG_FALSE, sobek.FLAG_FALSE, sobek.FLAG_TRUE), 468 | ) 469 | 470 | for _, messageListener := range w.eventListeners.all(events.MESSAGE) { 471 | if _, err := messageListener(ev); err != nil { 472 | _ = w.conn.Close() // TODO log it? 473 | _ = w.connectionClosedWithError(err) // TODO log it? 474 | return err 475 | } 476 | } 477 | return nil 478 | }) 479 | } 480 | 481 | func (w *webSocket) readPump(wg *sync.WaitGroup) { 482 | defer wg.Done() 483 | for { 484 | messageType, data, err := w.conn.ReadMessage() 485 | if err == nil { 486 | w.queueMessage(&message{ 487 | mtype: messageType, 488 | data: data, 489 | t: time.Now(), 490 | }) 491 | 492 | continue 493 | } 494 | 495 | if !websocket.IsUnexpectedCloseError( 496 | err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { 497 | // maybe still log it with debug level? 498 | err = nil 499 | } 500 | 501 | if err != nil { 502 | w.tq.Queue(func() error { 503 | _ = w.conn.Close() // TODO fix this 504 | return nil 505 | }) 506 | } 507 | 508 | w.tq.Queue(func() error { 509 | return w.connectionClosedWithError(err) 510 | }) 511 | 512 | return 513 | } 514 | } 515 | 516 | func (w *webSocket) writePump(wg *sync.WaitGroup) { 517 | defer wg.Done() 518 | wg.Add(1) 519 | samplesOutput := w.vu.State().Samples 520 | ctx := w.vu.Context() 521 | writeChannel := make(chan message) 522 | go func() { 523 | defer wg.Done() 524 | for { 525 | select { 526 | case msg, ok := <-writeChannel: 527 | if !ok { 528 | return 529 | } 530 | size := len(msg.data) 531 | 532 | err := func() error { 533 | if msg.mtype != websocket.PingMessage { 534 | return w.conn.WriteMessage(msg.mtype, msg.data) 535 | } 536 | 537 | // WriteControl is concurrently okay 538 | return w.conn.WriteControl(msg.mtype, msg.data, msg.t.Add(writeWait)) 539 | }() 540 | if err != nil { 541 | w.tq.Queue(func() error { 542 | _ = w.conn.Close() // TODO fix 543 | closeErr := w.connectionClosedWithError(err) 544 | return closeErr 545 | }) 546 | return 547 | } 548 | // This from the specification needs to happen like that instead of with 549 | // atomics or locks outside of the event loop 550 | w.tq.Queue(func() error { 551 | w.bufferedAmount -= size 552 | return nil 553 | }) 554 | 555 | metrics.PushIfNotDone(ctx, samplesOutput, metrics.Sample{ 556 | TimeSeries: metrics.TimeSeries{ 557 | Metric: w.builtinMetrics.WSMessagesSent, 558 | Tags: w.tagsAndMeta.Tags, 559 | }, 560 | Time: time.Now(), 561 | Metadata: w.tagsAndMeta.Metadata, 562 | Value: 1, 563 | }) 564 | case <-w.done: 565 | return 566 | } 567 | } 568 | }() 569 | { 570 | defer close(writeChannel) 571 | queue := make([]message, 0) 572 | var wch chan message 573 | var msg message 574 | for { 575 | wch = nil // this way if nothing to read it will just block 576 | if len(queue) > 0 { 577 | msg = queue[0] 578 | wch = writeChannel 579 | } 580 | select { 581 | case msg = <-w.writeQueueCh: 582 | queue = append(queue, msg) 583 | case wch <- msg: 584 | queue = queue[:copy(queue, queue[1:])] 585 | case <-w.done: 586 | return 587 | } 588 | } 589 | } 590 | } 591 | 592 | func (w *webSocket) send(msg sobek.Value) { 593 | w.assertStateOpen() 594 | 595 | switch o := msg.Export().(type) { 596 | case string: 597 | w.bufferedAmount += len(o) 598 | w.writeQueueCh <- message{ 599 | mtype: websocket.TextMessage, 600 | data: []byte(o), 601 | t: time.Now(), 602 | } 603 | case *sobek.ArrayBuffer: 604 | w.sendArrayBuffer(*o) 605 | case sobek.ArrayBuffer: 606 | w.sendArrayBuffer(o) 607 | case map[string]interface{}: 608 | rt := w.vu.Runtime() 609 | obj := msg.ToObject(rt) 610 | if !isBlob(obj, w.blobConstructor) { 611 | common.Throw(rt, fmt.Errorf("unsupported send type %T", o)) 612 | } 613 | 614 | b := extractBytes(obj, rt) 615 | w.bufferedAmount += len(b) 616 | w.writeQueueCh <- message{ 617 | mtype: websocket.BinaryMessage, 618 | data: b, 619 | t: time.Now(), 620 | } 621 | default: 622 | rt := w.vu.Runtime() 623 | isView, err := isArrayBufferView(rt, msg) 624 | if err != nil { 625 | common.Throw(rt, 626 | fmt.Errorf("got error while trying to check if argument is ArrayBufferView: %w", err)) 627 | } 628 | if !isView { 629 | common.Throw(rt, fmt.Errorf("unsupported send type %T", o)) 630 | } 631 | 632 | buffer := msg.ToObject(rt).Get("buffer") 633 | ab, ok := buffer.Export().(sobek.ArrayBuffer) 634 | if !ok { 635 | common.Throw(rt, 636 | fmt.Errorf("buffer of an ArrayBufferView was not an ArrayBuffer but %T", buffer.Export())) 637 | } 638 | w.sendArrayBuffer(ab) 639 | } 640 | } 641 | 642 | func (w *webSocket) sendArrayBuffer(o sobek.ArrayBuffer) { 643 | b := o.Bytes() 644 | w.bufferedAmount += len(b) 645 | w.writeQueueCh <- message{ 646 | mtype: websocket.BinaryMessage, 647 | data: b, 648 | t: time.Now(), 649 | } 650 | } 651 | 652 | func isArrayBufferView(rt *sobek.Runtime, v sobek.Value) (bool, error) { 653 | var isView sobek.Callable 654 | var ok bool 655 | exc := rt.Try(func() { 656 | isView, ok = sobek.AssertFunction( 657 | rt.Get("ArrayBuffer").ToObject(rt).Get("isView")) 658 | }) 659 | if exc != nil { 660 | return false, exc 661 | } 662 | 663 | if !ok { 664 | return false, fmt.Errorf("couldn't get ArrayBuffer.isView as it isn't a function") 665 | } 666 | 667 | boolValue, err := isView(nil, v) 668 | if err != nil { 669 | return false, err 670 | } 671 | return boolValue.ToBoolean(), nil 672 | } 673 | 674 | // Ping sends a ping message over the websocket. 675 | func (w *webSocket) ping() { 676 | w.assertStateOpen() 677 | 678 | pingID := strconv.Itoa(w.sendPings.counter) 679 | 680 | w.writeQueueCh <- message{ 681 | mtype: websocket.PingMessage, 682 | data: []byte(pingID), 683 | t: time.Now(), 684 | } 685 | 686 | w.sendPings.timestamps[pingID] = time.Now() 687 | w.sendPings.counter++ 688 | } 689 | 690 | func (w *webSocket) trackPong(pingID string) { 691 | pongTimestamp := time.Now() 692 | 693 | pingTimestamp, ok := w.sendPings.timestamps[pingID] 694 | if !ok { 695 | // We received a pong for a ping we didn't send; ignore 696 | // (this shouldn't happen with a compliant server) 697 | w.vu.State().Logger.Warnf("received pong for unknown ping ID %s", pingID) 698 | 699 | return 700 | } 701 | 702 | metrics.PushIfNotDone(w.vu.Context(), w.vu.State().Samples, metrics.Sample{ 703 | TimeSeries: metrics.TimeSeries{ 704 | Metric: w.builtinMetrics.WSPing, 705 | Tags: w.tagsAndMeta.Tags, 706 | }, 707 | Time: pongTimestamp, 708 | Metadata: w.tagsAndMeta.Metadata, 709 | Value: metrics.D(pongTimestamp.Sub(pingTimestamp)), 710 | }) 711 | } 712 | 713 | // assertStateOpen checks if the websocket is in the OPEN state 714 | // otherwise it throws an error (panic) 715 | func (w *webSocket) assertStateOpen() { 716 | if w.readyState == OPEN { 717 | return 718 | } 719 | 720 | // TODO figure out if we should give different error while being closed/closed/connecting 721 | common.Throw(w.vu.Runtime(), errors.New("InvalidStateError")) 722 | } 723 | 724 | // TODO support code and reason 725 | func (w *webSocket) close(code int, reason string) { 726 | if w.readyState == CLOSED || w.readyState == CLOSING { 727 | return 728 | } 729 | w.readyState = CLOSING 730 | if code == 0 { 731 | code = websocket.CloseNormalClosure 732 | } 733 | w.writeQueueCh <- message{ 734 | mtype: websocket.CloseMessage, 735 | data: websocket.FormatCloseMessage(code, reason), 736 | t: time.Now(), 737 | } 738 | } 739 | 740 | func (w *webSocket) queueClose() { 741 | w.tq.Queue(func() error { 742 | w.close(websocket.CloseNormalClosure, "") 743 | return nil 744 | }) 745 | } 746 | 747 | // to be run only on the eventloop 748 | // from https://websockets.spec.whatwg.org/#feedback-from-the-protocol 749 | func (w *webSocket) connectionConnected() error { 750 | if w.readyState != CONNECTING { 751 | return nil 752 | } 753 | w.readyState = OPEN 754 | return w.callOpenListeners(time.Now()) // TODO fix time 755 | } 756 | 757 | // to be run only on the eventloop 758 | func (w *webSocket) connectionClosedWithError(err error) error { 759 | if w.readyState == CLOSED { 760 | return nil 761 | } 762 | w.readyState = CLOSED 763 | close(w.done) 764 | 765 | if err != nil { 766 | if errList := w.callErrorListeners(err); errList != nil { 767 | return errList // TODO ... still call the close listeners ?!? 768 | } 769 | } 770 | return w.callEventListeners(events.CLOSE) 771 | } 772 | 773 | // newEvent return an event implementing "implements" https://dom.spec.whatwg.org/#event 774 | // needs to be called on the event loop 775 | // TODO: move to events 776 | func (w *webSocket) newEvent(eventType string, t time.Time) *sobek.Object { 777 | rt := w.vu.Runtime() 778 | o := rt.NewObject() 779 | 780 | must(rt, o.DefineAccessorProperty("type", rt.ToValue(func() string { 781 | return eventType 782 | }), nil, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 783 | must(rt, o.DefineAccessorProperty("target", rt.ToValue(func() interface{} { 784 | return w.obj 785 | }), nil, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 786 | // skip srcElement 787 | // skip currentTarget ??!! 788 | // skip eventPhase ??!! 789 | // skip stopPropagation 790 | // skip cancelBubble 791 | // skip stopImmediatePropagation 792 | // skip a bunch more 793 | 794 | must(rt, o.DefineAccessorProperty("timestamp", rt.ToValue(func() float64 { 795 | return float64(t.UnixNano()) / 1_000_000 // milliseconds as double as per the spec 796 | // https://w3c.github.io/hr-time/#dom-domhighrestimestamp 797 | }), nil, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 798 | 799 | return o 800 | } 801 | 802 | func (w *webSocket) callOpenListeners(timestamp time.Time) error { 803 | for _, openListener := range w.eventListeners.all(events.OPEN) { 804 | if _, err := openListener(w.newEvent(events.OPEN, timestamp)); err != nil { 805 | _ = w.conn.Close() // TODO log it? 806 | _ = w.connectionClosedWithError(err) // TODO log it? 807 | return err 808 | } 809 | } 810 | return nil 811 | } 812 | 813 | func (w *webSocket) callErrorListeners(e error) error { // TODO use the error even thought it is not by the spec 814 | rt := w.vu.Runtime() 815 | 816 | ev := w.newEvent(events.ERROR, time.Now()) 817 | must(rt, ev.DefineDataProperty("error", 818 | rt.ToValue(e.Error()), 819 | sobek.FLAG_FALSE, sobek.FLAG_FALSE, sobek.FLAG_TRUE)) 820 | for _, errorListener := range w.eventListeners.all(events.ERROR) { 821 | if _, err := errorListener(ev); err != nil { // TODO fix timestamp 822 | return err 823 | } 824 | } 825 | return nil 826 | } 827 | 828 | func (w *webSocket) callEventListeners(eventType string) error { 829 | for _, listener := range w.eventListeners.all(eventType) { 830 | // TODO the event here needs to be different and have an error (figure out it was for the close listeners) 831 | if _, err := listener(w.newEvent(eventType, time.Now())); err != nil { // TODO fix timestamp 832 | return err 833 | } 834 | } 835 | return nil 836 | } 837 | 838 | func (w *webSocket) addEventListener(event string, handler func(sobek.Value) (sobek.Value, error)) { 839 | // TODO support options https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#parameters 840 | 841 | if handler == nil { 842 | common.Throw(w.vu.Runtime(), fmt.Errorf("handler for event type %q isn't a callable function", event)) 843 | } 844 | 845 | if err := w.eventListeners.add(event, handler); err != nil { 846 | w.vu.State().Logger.Warnf("can't add event handler: %s", err) 847 | } 848 | } 849 | 850 | // TODO add remove listeners 851 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ### GNU AFFERO GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 19 November 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | ### Preamble 12 | 13 | The GNU Affero General Public License is a free, copyleft license for 14 | software and other kinds of works, specifically designed to ensure 15 | cooperation with the community in the case of network server software. 16 | 17 | The licenses for most software and other practical works are designed 18 | to take away your freedom to share and change the works. By contrast, 19 | our General Public Licenses are intended to guarantee your freedom to 20 | share and change all versions of a program--to make sure it remains 21 | free software for all its users. 22 | 23 | When we speak of free software, we are referring to freedom, not 24 | price. Our General Public Licenses are designed to make sure that you 25 | have the freedom to distribute copies of free software (and charge for 26 | them if you wish), that you receive source code or can get it if you 27 | want it, that you can change the software or use pieces of it in new 28 | free programs, and that you know you can do these things. 29 | 30 | Developers that use our General Public Licenses protect your rights 31 | with two steps: (1) assert copyright on the software, and (2) offer 32 | you this License which gives you legal permission to copy, distribute 33 | and/or modify the software. 34 | 35 | A secondary benefit of defending all users' freedom is that 36 | improvements made in alternate versions of the program, if they 37 | receive widespread use, become available for other developers to 38 | incorporate. Many developers of free software are heartened and 39 | encouraged by the resulting cooperation. However, in the case of 40 | software used on network servers, this result may fail to come about. 41 | The GNU General Public License permits making a modified version and 42 | letting the public access it on a server without ever releasing its 43 | source code to the public. 44 | 45 | The GNU Affero General Public License is designed specifically to 46 | ensure that, in such cases, the modified source code becomes available 47 | to the community. It requires the operator of a network server to 48 | provide the source code of the modified version running there to the 49 | users of that server. Therefore, public use of a modified version, on 50 | a publicly accessible server, gives the public access to the source 51 | code of the modified version. 52 | 53 | An older license, called the Affero General Public License and 54 | published by Affero, was designed to accomplish similar goals. This is 55 | a different license, not a version of the Affero GPL, but Affero has 56 | released a new version of the Affero GPL which permits relicensing 57 | under this license. 58 | 59 | The precise terms and conditions for copying, distribution and 60 | modification follow. 61 | 62 | ### TERMS AND CONDITIONS 63 | 64 | #### 0. Definitions. 65 | 66 | "This License" refers to version 3 of the GNU Affero General Public 67 | License. 68 | 69 | "Copyright" also means copyright-like laws that apply to other kinds 70 | of works, such as semiconductor masks. 71 | 72 | "The Program" refers to any copyrightable work licensed under this 73 | License. Each licensee is addressed as "you". "Licensees" and 74 | "recipients" may be individuals or organizations. 75 | 76 | To "modify" a work means to copy from or adapt all or part of the work 77 | in a fashion requiring copyright permission, other than the making of 78 | an exact copy. The resulting work is called a "modified version" of 79 | the earlier work or a work "based on" the earlier work. 80 | 81 | A "covered work" means either the unmodified Program or a work based 82 | on the Program. 83 | 84 | To "propagate" a work means to do anything with it that, without 85 | permission, would make you directly or secondarily liable for 86 | infringement under applicable copyright law, except executing it on a 87 | computer or modifying a private copy. Propagation includes copying, 88 | distribution (with or without modification), making available to the 89 | public, and in some countries other activities as well. 90 | 91 | To "convey" a work means any kind of propagation that enables other 92 | parties to make or receive copies. Mere interaction with a user 93 | through a computer network, with no transfer of a copy, is not 94 | conveying. 95 | 96 | An interactive user interface displays "Appropriate Legal Notices" to 97 | the extent that it includes a convenient and prominently visible 98 | feature that (1) displays an appropriate copyright notice, and (2) 99 | tells the user that there is no warranty for the work (except to the 100 | extent that warranties are provided), that licensees may convey the 101 | work under this License, and how to view a copy of this License. If 102 | the interface presents a list of user commands or options, such as a 103 | menu, a prominent item in the list meets this criterion. 104 | 105 | #### 1. Source Code. 106 | 107 | The "source code" for a work means the preferred form of the work for 108 | making modifications to it. "Object code" means any non-source form of 109 | a work. 110 | 111 | A "Standard Interface" means an interface that either is an official 112 | standard defined by a recognized standards body, or, in the case of 113 | interfaces specified for a particular programming language, one that 114 | is widely used among developers working in that language. 115 | 116 | The "System Libraries" of an executable work include anything, other 117 | than the work as a whole, that (a) is included in the normal form of 118 | packaging a Major Component, but which is not part of that Major 119 | Component, and (b) serves only to enable use of the work with that 120 | Major Component, or to implement a Standard Interface for which an 121 | implementation is available to the public in source code form. A 122 | "Major Component", in this context, means a major essential component 123 | (kernel, window system, and so on) of the specific operating system 124 | (if any) on which the executable work runs, or a compiler used to 125 | produce the work, or an object code interpreter used to run it. 126 | 127 | The "Corresponding Source" for a work in object code form means all 128 | the source code needed to generate, install, and (for an executable 129 | work) run the object code and to modify the work, including scripts to 130 | control those activities. However, it does not include the work's 131 | System Libraries, or general-purpose tools or generally available free 132 | programs which are used unmodified in performing those activities but 133 | which are not part of the work. For example, Corresponding Source 134 | includes interface definition files associated with source files for 135 | the work, and the source code for shared libraries and dynamically 136 | linked subprograms that the work is specifically designed to require, 137 | such as by intimate data communication or control flow between those 138 | subprograms and other parts of the work. 139 | 140 | The Corresponding Source need not include anything that users can 141 | regenerate automatically from other parts of the Corresponding Source. 142 | 143 | The Corresponding Source for a work in source code form is that same 144 | work. 145 | 146 | #### 2. Basic Permissions. 147 | 148 | All rights granted under this License are granted for the term of 149 | copyright on the Program, and are irrevocable provided the stated 150 | conditions are met. This License explicitly affirms your unlimited 151 | permission to run the unmodified Program. The output from running a 152 | covered work is covered by this License only if the output, given its 153 | content, constitutes a covered work. This License acknowledges your 154 | rights of fair use or other equivalent, as provided by copyright law. 155 | 156 | You may make, run and propagate covered works that you do not convey, 157 | without conditions so long as your license otherwise remains in force. 158 | You may convey covered works to others for the sole purpose of having 159 | them make modifications exclusively for you, or provide you with 160 | facilities for running those works, provided that you comply with the 161 | terms of this License in conveying all material for which you do not 162 | control copyright. Those thus making or running the covered works for 163 | you must do so exclusively on your behalf, under your direction and 164 | control, on terms that prohibit them from making any copies of your 165 | copyrighted material outside their relationship with you. 166 | 167 | Conveying under any other circumstances is permitted solely under the 168 | conditions stated below. Sublicensing is not allowed; section 10 makes 169 | it unnecessary. 170 | 171 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 172 | 173 | No covered work shall be deemed part of an effective technological 174 | measure under any applicable law fulfilling obligations under article 175 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 176 | similar laws prohibiting or restricting circumvention of such 177 | measures. 178 | 179 | When you convey a covered work, you waive any legal power to forbid 180 | circumvention of technological measures to the extent such 181 | circumvention is effected by exercising rights under this License with 182 | respect to the covered work, and you disclaim any intention to limit 183 | operation or modification of the work as a means of enforcing, against 184 | the work's users, your or third parties' legal rights to forbid 185 | circumvention of technological measures. 186 | 187 | #### 4. Conveying Verbatim Copies. 188 | 189 | You may convey verbatim copies of the Program's source code as you 190 | receive it, in any medium, provided that you conspicuously and 191 | appropriately publish on each copy an appropriate copyright notice; 192 | keep intact all notices stating that this License and any 193 | non-permissive terms added in accord with section 7 apply to the code; 194 | keep intact all notices of the absence of any warranty; and give all 195 | recipients a copy of this License along with the Program. 196 | 197 | You may charge any price or no price for each copy that you convey, 198 | and you may offer support or warranty protection for a fee. 199 | 200 | #### 5. Conveying Modified Source Versions. 201 | 202 | You may convey a work based on the Program, or the modifications to 203 | produce it from the Program, in the form of source code under the 204 | terms of section 4, provided that you also meet all of these 205 | conditions: 206 | 207 | - a) The work must carry prominent notices stating that you modified 208 | it, and giving a relevant date. 209 | - b) The work must carry prominent notices stating that it is 210 | released under this License and any conditions added under 211 | section 7. This requirement modifies the requirement in section 4 212 | to "keep intact all notices". 213 | - c) You must license the entire work, as a whole, under this 214 | License to anyone who comes into possession of a copy. This 215 | License will therefore apply, along with any applicable section 7 216 | additional terms, to the whole of the work, and all its parts, 217 | regardless of how they are packaged. This License gives no 218 | permission to license the work in any other way, but it does not 219 | invalidate such permission if you have separately received it. 220 | - d) If the work has interactive user interfaces, each must display 221 | Appropriate Legal Notices; however, if the Program has interactive 222 | interfaces that do not display Appropriate Legal Notices, your 223 | work need not make them do so. 224 | 225 | A compilation of a covered work with other separate and independent 226 | works, which are not by their nature extensions of the covered work, 227 | and which are not combined with it such as to form a larger program, 228 | in or on a volume of a storage or distribution medium, is called an 229 | "aggregate" if the compilation and its resulting copyright are not 230 | used to limit the access or legal rights of the compilation's users 231 | beyond what the individual works permit. Inclusion of a covered work 232 | in an aggregate does not cause this License to apply to the other 233 | parts of the aggregate. 234 | 235 | #### 6. Conveying Non-Source Forms. 236 | 237 | You may convey a covered work in object code form under the terms of 238 | sections 4 and 5, provided that you also convey the machine-readable 239 | Corresponding Source under the terms of this License, in one of these 240 | ways: 241 | 242 | - a) Convey the object code in, or embodied in, a physical product 243 | (including a physical distribution medium), accompanied by the 244 | Corresponding Source fixed on a durable physical medium 245 | customarily used for software interchange. 246 | - b) Convey the object code in, or embodied in, a physical product 247 | (including a physical distribution medium), accompanied by a 248 | written offer, valid for at least three years and valid for as 249 | long as you offer spare parts or customer support for that product 250 | model, to give anyone who possesses the object code either (1) a 251 | copy of the Corresponding Source for all the software in the 252 | product that is covered by this License, on a durable physical 253 | medium customarily used for software interchange, for a price no 254 | more than your reasonable cost of physically performing this 255 | conveying of source, or (2) access to copy the Corresponding 256 | Source from a network server at no charge. 257 | - c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | - d) Convey the object code by offering access from a designated 263 | place (gratis or for a charge), and offer equivalent access to the 264 | Corresponding Source in the same way through the same place at no 265 | further charge. You need not require recipients to copy the 266 | Corresponding Source along with the object code. If the place to 267 | copy the object code is a network server, the Corresponding Source 268 | may be on a different server (operated by you or a third party) 269 | that supports equivalent copying facilities, provided you maintain 270 | clear directions next to the object code saying where to find the 271 | Corresponding Source. Regardless of what server hosts the 272 | Corresponding Source, you remain obligated to ensure that it is 273 | available for as long as needed to satisfy these requirements. 274 | - e) Convey the object code using peer-to-peer transmission, 275 | provided you inform other peers where the object code and 276 | Corresponding Source of the work are being offered to the general 277 | public at no charge under subsection 6d. 278 | 279 | A separable portion of the object code, whose source code is excluded 280 | from the Corresponding Source as a System Library, need not be 281 | included in conveying the object code work. 282 | 283 | A "User Product" is either (1) a "consumer product", which means any 284 | tangible personal property which is normally used for personal, 285 | family, or household purposes, or (2) anything designed or sold for 286 | incorporation into a dwelling. In determining whether a product is a 287 | consumer product, doubtful cases shall be resolved in favor of 288 | coverage. For a particular product received by a particular user, 289 | "normally used" refers to a typical or common use of that class of 290 | product, regardless of the status of the particular user or of the way 291 | in which the particular user actually uses, or expects or is expected 292 | to use, the product. A product is a consumer product regardless of 293 | whether the product has substantial commercial, industrial or 294 | non-consumer uses, unless such uses represent the only significant 295 | mode of use of the product. 296 | 297 | "Installation Information" for a User Product means any methods, 298 | procedures, authorization keys, or other information required to 299 | install and execute modified versions of a covered work in that User 300 | Product from a modified version of its Corresponding Source. The 301 | information must suffice to ensure that the continued functioning of 302 | the modified object code is in no case prevented or interfered with 303 | solely because modification has been made. 304 | 305 | If you convey an object code work under this section in, or with, or 306 | specifically for use in, a User Product, and the conveying occurs as 307 | part of a transaction in which the right of possession and use of the 308 | User Product is transferred to the recipient in perpetuity or for a 309 | fixed term (regardless of how the transaction is characterized), the 310 | Corresponding Source conveyed under this section must be accompanied 311 | by the Installation Information. But this requirement does not apply 312 | if neither you nor any third party retains the ability to install 313 | modified object code on the User Product (for example, the work has 314 | been installed in ROM). 315 | 316 | The requirement to provide Installation Information does not include a 317 | requirement to continue to provide support service, warranty, or 318 | updates for a work that has been modified or installed by the 319 | recipient, or for the User Product in which it has been modified or 320 | installed. Access to a network may be denied when the modification 321 | itself materially and adversely affects the operation of the network 322 | or violates the rules and protocols for communication across the 323 | network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | #### 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders 351 | of that material) supplement the terms of this License with terms: 352 | 353 | - a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | - b) Requiring preservation of specified reasonable legal notices or 356 | author attributions in that material or in the Appropriate Legal 357 | Notices displayed by works containing it; or 358 | - c) Prohibiting misrepresentation of the origin of that material, 359 | or requiring that modified versions of such material be marked in 360 | reasonable ways as different from the original version; or 361 | - d) Limiting the use for publicity purposes of names of licensors 362 | or authors of the material; or 363 | - e) Declining to grant rights under trademark law for use of some 364 | trade names, trademarks, or service marks; or 365 | - f) Requiring indemnification of licensors and authors of that 366 | material by anyone who conveys the material (or modified versions 367 | of it) with contractual assumptions of liability to the recipient, 368 | for any liability that these contractual assumptions directly 369 | impose on those licensors and authors. 370 | 371 | All other non-permissive additional terms are considered "further 372 | restrictions" within the meaning of section 10. If the Program as you 373 | received it, or any part of it, contains a notice stating that it is 374 | governed by this License along with a term that is a further 375 | restriction, you may remove that term. If a license document contains 376 | a further restriction but permits relicensing or conveying under this 377 | License, you may add to a covered work material governed by the terms 378 | of that license document, provided that the further restriction does 379 | not survive such relicensing or conveying. 380 | 381 | If you add terms to a covered work in accord with this section, you 382 | must place, in the relevant source files, a statement of the 383 | additional terms that apply to those files, or a notice indicating 384 | where to find the applicable terms. 385 | 386 | Additional terms, permissive or non-permissive, may be stated in the 387 | form of a separately written license, or stated as exceptions; the 388 | above requirements apply either way. 389 | 390 | #### 8. Termination. 391 | 392 | You may not propagate or modify a covered work except as expressly 393 | provided under this License. Any attempt otherwise to propagate or 394 | modify it is void, and will automatically terminate your rights under 395 | this License (including any patent licenses granted under the third 396 | paragraph of section 11). 397 | 398 | However, if you cease all violation of this License, then your license 399 | from a particular copyright holder is reinstated (a) provisionally, 400 | unless and until the copyright holder explicitly and finally 401 | terminates your license, and (b) permanently, if the copyright holder 402 | fails to notify you of the violation by some reasonable means prior to 403 | 60 days after the cessation. 404 | 405 | Moreover, your license from a particular copyright holder is 406 | reinstated permanently if the copyright holder notifies you of the 407 | violation by some reasonable means, this is the first time you have 408 | received notice of violation of this License (for any work) from that 409 | copyright holder, and you cure the violation prior to 30 days after 410 | your receipt of the notice. 411 | 412 | Termination of your rights under this section does not terminate the 413 | licenses of parties who have received copies or rights from you under 414 | this License. If your rights have been terminated and not permanently 415 | reinstated, you do not qualify to receive new licenses for the same 416 | material under section 10. 417 | 418 | #### 9. Acceptance Not Required for Having Copies. 419 | 420 | You are not required to accept this License in order to receive or run 421 | a copy of the Program. Ancillary propagation of a covered work 422 | occurring solely as a consequence of using peer-to-peer transmission 423 | to receive a copy likewise does not require acceptance. However, 424 | nothing other than this License grants you permission to propagate or 425 | modify any covered work. These actions infringe copyright if you do 426 | not accept this License. Therefore, by modifying or propagating a 427 | covered work, you indicate your acceptance of this License to do so. 428 | 429 | #### 10. Automatic Licensing of Downstream Recipients. 430 | 431 | Each time you convey a covered work, the recipient automatically 432 | receives a license from the original licensors, to run, modify and 433 | propagate that work, subject to this License. You are not responsible 434 | for enforcing compliance by third parties with this License. 435 | 436 | An "entity transaction" is a transaction transferring control of an 437 | organization, or substantially all assets of one, or subdividing an 438 | organization, or merging organizations. If propagation of a covered 439 | work results from an entity transaction, each party to that 440 | transaction who receives a copy of the work also receives whatever 441 | licenses to the work the party's predecessor in interest had or could 442 | give under the previous paragraph, plus a right to possession of the 443 | Corresponding Source of the work from the predecessor in interest, if 444 | the predecessor has it or can get it with reasonable efforts. 445 | 446 | You may not impose any further restrictions on the exercise of the 447 | rights granted or affirmed under this License. For example, you may 448 | not impose a license fee, royalty, or other charge for exercise of 449 | rights granted under this License, and you may not initiate litigation 450 | (including a cross-claim or counterclaim in a lawsuit) alleging that 451 | any patent claim is infringed by making, using, selling, offering for 452 | sale, or importing the Program or any portion of it. 453 | 454 | #### 11. Patents. 455 | 456 | A "contributor" is a copyright holder who authorizes use under this 457 | License of the Program or a work on which the Program is based. The 458 | work thus licensed is called the contributor's "contributor version". 459 | 460 | A contributor's "essential patent claims" are all patent claims owned 461 | or controlled by the contributor, whether already acquired or 462 | hereafter acquired, that would be infringed by some manner, permitted 463 | by this License, of making, using, or selling its contributor version, 464 | but do not include claims that would be infringed only as a 465 | consequence of further modification of the contributor version. For 466 | purposes of this definition, "control" includes the right to grant 467 | patent sublicenses in a manner consistent with the requirements of 468 | this License. 469 | 470 | Each contributor grants you a non-exclusive, worldwide, royalty-free 471 | patent license under the contributor's essential patent claims, to 472 | make, use, sell, offer for sale, import and otherwise run, modify and 473 | propagate the contents of its contributor version. 474 | 475 | In the following three paragraphs, a "patent license" is any express 476 | agreement or commitment, however denominated, not to enforce a patent 477 | (such as an express permission to practice a patent or covenant not to 478 | sue for patent infringement). To "grant" such a patent license to a 479 | party means to make such an agreement or commitment not to enforce a 480 | patent against the party. 481 | 482 | If you convey a covered work, knowingly relying on a patent license, 483 | and the Corresponding Source of the work is not available for anyone 484 | to copy, free of charge and under the terms of this License, through a 485 | publicly available network server or other readily accessible means, 486 | then you must either (1) cause the Corresponding Source to be so 487 | available, or (2) arrange to deprive yourself of the benefit of the 488 | patent license for this particular work, or (3) arrange, in a manner 489 | consistent with the requirements of this License, to extend the patent 490 | license to downstream recipients. "Knowingly relying" means you have 491 | actual knowledge that, but for the patent license, your conveying the 492 | covered work in a country, or your recipient's use of the covered work 493 | in a country, would infringe one or more identifiable patents in that 494 | country that you have reason to believe are valid. 495 | 496 | If, pursuant to or in connection with a single transaction or 497 | arrangement, you convey, or propagate by procuring conveyance of, a 498 | covered work, and grant a patent license to some of the parties 499 | receiving the covered work authorizing them to use, propagate, modify 500 | or convey a specific copy of the covered work, then the patent license 501 | you grant is automatically extended to all recipients of the covered 502 | work and works based on it. 503 | 504 | A patent license is "discriminatory" if it does not include within the 505 | scope of its coverage, prohibits the exercise of, or is conditioned on 506 | the non-exercise of one or more of the rights that are specifically 507 | granted under this License. You may not convey a covered work if you 508 | are a party to an arrangement with a third party that is in the 509 | business of distributing software, under which you make payment to the 510 | third party based on the extent of your activity of conveying the 511 | work, and under which the third party grants, to any of the parties 512 | who would receive the covered work from you, a discriminatory patent 513 | license (a) in connection with copies of the covered work conveyed by 514 | you (or copies made from those copies), or (b) primarily for and in 515 | connection with specific products or compilations that contain the 516 | covered work, unless you entered into that arrangement, or that patent 517 | license was granted, prior to 28 March 2007. 518 | 519 | Nothing in this License shall be construed as excluding or limiting 520 | any implied license or other defenses to infringement that may 521 | otherwise be available to you under applicable patent law. 522 | 523 | #### 12. No Surrender of Others' Freedom. 524 | 525 | If conditions are imposed on you (whether by court order, agreement or 526 | otherwise) that contradict the conditions of this License, they do not 527 | excuse you from the conditions of this License. If you cannot convey a 528 | covered work so as to satisfy simultaneously your obligations under 529 | this License and any other pertinent obligations, then as a 530 | consequence you may not convey it at all. For example, if you agree to 531 | terms that obligate you to collect a royalty for further conveying 532 | from those to whom you convey the Program, the only way you could 533 | satisfy both those terms and this License would be to refrain entirely 534 | from conveying the Program. 535 | 536 | #### 13. Remote Network Interaction; Use with the GNU General Public License. 537 | 538 | Notwithstanding any other provision of this License, if you modify the 539 | Program, your modified version must prominently offer all users 540 | interacting with it remotely through a computer network (if your 541 | version supports such interaction) an opportunity to receive the 542 | Corresponding Source of your version by providing access to the 543 | Corresponding Source from a network server at no charge, through some 544 | standard or customary means of facilitating copying of software. This 545 | Corresponding Source shall include the Corresponding Source for any 546 | work covered by version 3 of the GNU General Public License that is 547 | incorporated pursuant to the following paragraph. 548 | 549 | Notwithstanding any other provision of this License, you have 550 | permission to link or combine any covered work with a work licensed 551 | under version 3 of the GNU General Public License into a single 552 | combined work, and to convey the resulting work. The terms of this 553 | License will continue to apply to the part which is the covered work, 554 | but the work with which it is combined will remain governed by version 555 | 3 of the GNU General Public License. 556 | 557 | #### 14. Revised Versions of this License. 558 | 559 | The Free Software Foundation may publish revised and/or new versions 560 | of the GNU Affero General Public License from time to time. Such new 561 | versions will be similar in spirit to the present version, but may 562 | differ in detail to address new problems or concerns. 563 | 564 | Each version is given a distinguishing version number. If the Program 565 | specifies that a certain numbered version of the GNU Affero General 566 | Public License "or any later version" applies to it, you have the 567 | option of following the terms and conditions either of that numbered 568 | version or of any later version published by the Free Software 569 | Foundation. If the Program does not specify a version number of the 570 | GNU Affero General Public License, you may choose any version ever 571 | published by the Free Software Foundation. 572 | 573 | If the Program specifies that a proxy can decide which future versions 574 | of the GNU Affero General Public License can be used, that proxy's 575 | public statement of acceptance of a version permanently authorizes you 576 | to choose that version for the Program. 577 | 578 | Later license versions may give you additional or different 579 | permissions. However, no additional obligations are imposed on any 580 | author or copyright holder as a result of your choosing to follow a 581 | later version. 582 | 583 | #### 15. Disclaimer of Warranty. 584 | 585 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 586 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 587 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 588 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 589 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 590 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 591 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 592 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 593 | CORRECTION. 594 | 595 | #### 16. Limitation of Liability. 596 | 597 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 598 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 599 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 600 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 601 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 602 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 603 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 604 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 605 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 606 | 607 | #### 17. Interpretation of Sections 15 and 16. 608 | 609 | If the disclaimer of warranty and limitation of liability provided 610 | above cannot be given local legal effect according to their terms, 611 | reviewing courts shall apply local law that most closely approximates 612 | an absolute waiver of all civil liability in connection with the 613 | Program, unless a warranty or assumption of liability accompanies a 614 | copy of the Program in return for a fee. 615 | 616 | END OF TERMS AND CONDITIONS 617 | 618 | ### How to Apply These Terms to Your New Programs 619 | 620 | If you develop a new program, and you want it to be of the greatest 621 | possible use to the public, the best way to achieve this is to make it 622 | free software which everyone can redistribute and change under these 623 | terms. 624 | 625 | To do so, attach the following notices to the program. It is safest to 626 | attach them to the start of each source file to most effectively state 627 | the exclusion of warranty; and each file should have at least the 628 | "copyright" line and a pointer to where the full notice is found. 629 | 630 | 631 | Copyright (C) 632 | 633 | This program is free software: you can redistribute it and/or modify 634 | it under the terms of the GNU Affero General Public License as 635 | published by the Free Software Foundation, either version 3 of the 636 | License, or (at your option) any later version. 637 | 638 | This program is distributed in the hope that it will be useful, 639 | but WITHOUT ANY WARRANTY; without even the implied warranty of 640 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 641 | GNU Affero General Public License for more details. 642 | 643 | You should have received a copy of the GNU Affero General Public License 644 | along with this program. If not, see . 645 | 646 | Also add information on how to contact you by electronic and paper 647 | mail. 648 | 649 | If your software can interact with users remotely through a computer 650 | network, you should also make sure that it provides a way for users to 651 | get its source. For example, if your program is a web application, its 652 | interface could display a "Source" link that leads users to an archive 653 | of the code. There are many ways you could offer source, and different 654 | solutions will be better for different programs; see section 13 for 655 | the specific requirements. 656 | 657 | You should also get your employer (if you work as a programmer) or 658 | school, if any, to sign a "copyright disclaimer" for the program, if 659 | necessary. For more information on this, and how to apply and follow 660 | the GNU AGPL, see . 661 | -------------------------------------------------------------------------------- /websockets/websockets_test.go: -------------------------------------------------------------------------------- 1 | package websockets 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/cookiejar" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | "testing" 12 | 13 | "github.com/gorilla/websocket" 14 | "github.com/sirupsen/logrus" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/require" 17 | "gopkg.in/guregu/null.v3" 18 | 19 | httpModule "go.k6.io/k6/js/modules/k6/http" 20 | "go.k6.io/k6/js/modulestest" 21 | "go.k6.io/k6/lib" 22 | "go.k6.io/k6/lib/testutils" 23 | "go.k6.io/k6/lib/testutils/httpmultibin" 24 | "go.k6.io/k6/metrics" 25 | ) 26 | 27 | // copied from k6/ws 28 | func assertSessionMetricsEmitted( 29 | t *testing.T, 30 | sampleContainers []metrics.SampleContainer, 31 | subprotocol, 32 | url string, 33 | status int, //nolint:unparam // TODO: check why it always same in tests 34 | group string, //nolint:unparam // TODO: check why it always same in tests 35 | ) { 36 | t.Helper() 37 | seenSessions := false 38 | seenSessionDuration := false 39 | seenConnecting := false 40 | 41 | require.NotEmpty(t, sampleContainers) 42 | for _, sampleContainer := range sampleContainers { 43 | require.NotEmpty(t, sampleContainer.GetSamples()) 44 | for _, sample := range sampleContainer.GetSamples() { 45 | tags := sample.Tags.Map() 46 | if tags["url"] == url { 47 | switch sample.Metric.Name { 48 | case metrics.WSConnectingName: 49 | seenConnecting = true 50 | case metrics.WSSessionDurationName: 51 | seenSessionDuration = true 52 | case metrics.WSSessionsName: 53 | seenSessions = true 54 | } 55 | 56 | assert.Equal(t, strconv.Itoa(status), tags["status"]) 57 | assert.Equal(t, subprotocol, tags["subproto"]) 58 | assert.Equal(t, group, tags["group"]) 59 | } 60 | } 61 | } 62 | assert.True(t, seenConnecting, "url %s didn't emit Connecting", url) 63 | assert.True(t, seenSessions, "url %s didn't emit Sessions", url) 64 | assert.True(t, seenSessionDuration, "url %s didn't emit SessionDuration", url) 65 | } 66 | 67 | // also copied from k6/ws 68 | func assertMetricEmittedCount(t *testing.T, metricName string, sampleContainers []metrics.SampleContainer, url string, count int) { 69 | t.Helper() 70 | actualCount := 0 71 | 72 | for _, sampleContainer := range sampleContainers { 73 | for _, sample := range sampleContainer.GetSamples() { 74 | surl, ok := sample.Tags.Get("url") 75 | assert.True(t, ok) 76 | if surl == url && sample.Metric.Name == metricName { 77 | actualCount++ 78 | } 79 | } 80 | } 81 | assert.Equal(t, count, actualCount, "url %s emitted %s %d times, expected was %d times", url, metricName, actualCount, count) 82 | } 83 | 84 | type testState struct { 85 | tb *httpmultibin.HTTPMultiBin 86 | runtime *modulestest.Runtime 87 | samples chan metrics.SampleContainer 88 | t testing.TB 89 | 90 | callRecorder *callRecorder 91 | errors chan error 92 | 93 | module *WebSocketsAPI 94 | } 95 | 96 | // callRecorder a helper type that records all calls 97 | type callRecorder struct { 98 | sync.Mutex 99 | calls []string 100 | } 101 | 102 | // Call records a call 103 | func (r *callRecorder) Call(text string) { 104 | r.Lock() 105 | defer r.Unlock() 106 | 107 | r.calls = append(r.calls, text) 108 | } 109 | 110 | // Len just returns the length of the calls 111 | func (r *callRecorder) Len() int { 112 | r.Lock() 113 | defer r.Unlock() 114 | 115 | return len(r.calls) 116 | } 117 | 118 | // Len just returns the length of the calls 119 | func (r *callRecorder) Recorded() []string { 120 | r.Lock() 121 | defer r.Unlock() 122 | 123 | result := []string{} 124 | result = append(result, r.calls...) 125 | 126 | return result 127 | } 128 | 129 | func newTestState(t testing.TB) testState { 130 | runtime := modulestest.NewRuntime(t) 131 | tb := httpmultibin.NewHTTPMultiBin(t) 132 | 133 | samples := make(chan metrics.SampleContainer, 1000) 134 | state := &lib.State{ 135 | Dialer: tb.Dialer, 136 | Options: lib.Options{ 137 | SystemTags: metrics.NewSystemTagSet( 138 | metrics.TagURL, 139 | metrics.TagProto, 140 | metrics.TagStatus, 141 | metrics.TagSubproto, 142 | ), 143 | UserAgent: null.StringFrom("TestUserAgent"), 144 | }, 145 | Samples: samples, 146 | TLSConfig: tb.TLSClientConfig, 147 | BuiltinMetrics: runtime.BuiltinMetrics, 148 | Tags: lib.NewVUStateTags(runtime.VU.InitEnvField.Registry.RootTagSet()), 149 | } 150 | 151 | recorder := &callRecorder{ 152 | calls: make([]string, 0), 153 | } 154 | 155 | m := new(RootModule).NewModuleInstance(runtime.VU) 156 | require.NoError(t, runtime.VU.RuntimeField.Set("WebSocket", m.Exports().Named["WebSocket"])) 157 | require.NoError(t, runtime.VU.RuntimeField.Set("Blob", m.Exports().Named["Blob"])) 158 | require.NoError(t, runtime.VU.RuntimeField.Set("call", recorder.Call)) 159 | 160 | runtime.MoveToVUContext(state) 161 | return testState{ 162 | runtime: runtime, 163 | tb: tb, 164 | samples: samples, 165 | callRecorder: recorder, 166 | errors: make(chan error, 50), 167 | t: t, 168 | module: m.(*WebSocketsAPI), 169 | } 170 | } 171 | 172 | func (ts *testState) addHandler(uri string, upgrader *websocket.Upgrader, message *testMessage) { 173 | ts.tb.Mux.HandleFunc(uri, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 174 | // when upgrader not passed we should use default one 175 | if upgrader == nil { 176 | upgrader = &websocket.Upgrader{} 177 | } 178 | 179 | conn, err := upgrader.Upgrade(w, req, w.Header()) 180 | if err != nil { 181 | ts.errors <- fmt.Errorf("%s cannot upgrade request: %w", uri, err) 182 | return 183 | } 184 | 185 | defer func() { 186 | err = conn.Close() 187 | if err != nil { 188 | ts.t.Logf("error while closing connection in %s: %v", uri, err) 189 | return 190 | } 191 | }() 192 | 193 | if message == nil { 194 | return 195 | } 196 | 197 | if err = conn.WriteMessage(message.kind, message.data); err != nil { 198 | ts.errors <- fmt.Errorf("%s cannot write message: %w", uri, err) 199 | return 200 | } 201 | })) 202 | } 203 | 204 | type testMessage struct { 205 | kind int 206 | data []byte 207 | } 208 | 209 | func TestBasic(t *testing.T) { 210 | t.Parallel() 211 | ts := newTestState(t) 212 | sr := ts.tb.Replacer.Replace 213 | _, err := ts.runtime.RunOnEventLoop(sr(` 214 | var ws = new WebSocket("WSBIN_URL/ws-echo") 215 | ws.addEventListener("open", () => { 216 | ws.send("something") 217 | ws.close() 218 | }) 219 | `)) 220 | require.NoError(t, err) 221 | samples := metrics.GetBufferedSamples(ts.samples) 222 | assertSessionMetricsEmitted(t, samples, "", sr("WSBIN_URL/ws-echo"), http.StatusSwitchingProtocols, "") 223 | } 224 | 225 | func TestBasicSendBlob(t *testing.T) { 226 | t.Parallel() 227 | ts := newTestState(t) 228 | sr := ts.tb.Replacer.Replace 229 | _, err := ts.runtime.RunOnEventLoop(sr(` 230 | var ws = new WebSocket("WSBIN_URL/ws-echo") 231 | ws.addEventListener("open", () => { 232 | ws.send(new Blob(["something"])) 233 | ws.close() 234 | }) 235 | `)) 236 | require.NoError(t, err) 237 | samples := metrics.GetBufferedSamples(ts.samples) 238 | assertSessionMetricsEmitted(t, samples, "", sr("WSBIN_URL/ws-echo"), http.StatusSwitchingProtocols, "") 239 | } 240 | 241 | func TestAddUndefinedHandler(t *testing.T) { 242 | t.Parallel() 243 | ts := newTestState(t) 244 | sr := ts.tb.Replacer.Replace 245 | _, err := ts.runtime.RunOnEventLoop(sr(` 246 | var ws = new WebSocket("WSBIN_URL/ws-echo") 247 | ws.addEventListener("open", () => { 248 | ws.close() 249 | }) 250 | ws.addEventListener("open", undefined) 251 | `)) 252 | require.ErrorContains(t, err, "handler for event type \"open\" isn't a callable function") 253 | } 254 | 255 | func TestBasicWithOn(t *testing.T) { 256 | t.Parallel() 257 | 258 | ts := newTestState(t) 259 | sr := ts.tb.Replacer.Replace 260 | _, err := ts.runtime.RunOnEventLoop(sr(` 261 | var ws = new WebSocket("WSBIN_URL/ws-echo") 262 | ws.onopen = () => { 263 | ws.send("something") 264 | ws.close() 265 | } 266 | `)) 267 | require.NoError(t, err) 268 | samples := metrics.GetBufferedSamples(ts.samples) 269 | assertSessionMetricsEmitted(t, samples, "", sr("WSBIN_URL/ws-echo"), http.StatusSwitchingProtocols, "") 270 | } 271 | 272 | func TestReadyState(t *testing.T) { 273 | t.Parallel() 274 | ts := newTestState(t) 275 | _, err := ts.runtime.RunOnEventLoop(ts.tb.Replacer.Replace(` 276 | var ws = new WebSocket("WSBIN_URL/ws-echo") 277 | ws.addEventListener("open", () => { 278 | if (ws.readyState != 1){ 279 | throw new Error("Expected ready state 1 got "+ ws.readyState) 280 | } 281 | ws.addEventListener("close", () => { 282 | if (ws.readyState != 3){ 283 | throw new Error("Expected ready state 3 got "+ ws.readyState) 284 | } 285 | 286 | }) 287 | ws.send("something") 288 | ws.close() 289 | }) 290 | if (ws.readyState != 0){ 291 | throw new Error("Expected ready state 0 got "+ ws.readyState) 292 | } 293 | `)) 294 | require.NoError(t, err) 295 | } 296 | 297 | func TestBinaryState(t *testing.T) { 298 | t.Parallel() 299 | ts := newTestState(t) 300 | logger, hook := testutils.NewLoggerWithHook(t, logrus.WarnLevel) 301 | ts.runtime.VU.StateField.Logger = logger 302 | _, err := ts.runtime.RunOnEventLoop(ts.tb.Replacer.Replace(` 303 | var ws = new WebSocket("WSBIN_URL/ws-echo-invalid") 304 | ws.addEventListener("open", () => { 305 | ws.send(new Uint8Array([164,41]).buffer) 306 | if (ws.bufferedAmount != 2) { 307 | throw "Expected 2 bufferedAmount got "+ ws.bufferedAmount 308 | } 309 | ws.send("k6") 310 | if (ws.bufferedAmount != 4) { 311 | throw "Expected 4 bufferedAmount got "+ ws.bufferedAmount 312 | } 313 | ws.onmessage = (e) => { 314 | if (ws.bufferedAmount != 0 && ws.bufferedAmount != 2) { // it is possible one or both were flushed 315 | throw "Expected 0 or 2 bufferedAmount, but got "+ ws.bufferedAmount 316 | } 317 | ws.close() 318 | call(JSON.stringify(e)) 319 | } 320 | }) 321 | 322 | if (ws.binaryType != "blob") { 323 | throw new Error("Wrong binaryType value, expected to be blob got "+ ws.binaryType) 324 | } 325 | 326 | var thrown = false; 327 | try { 328 | ws.binaryType = "something" 329 | } catch(e) { 330 | thrown = true 331 | } 332 | if (!thrown) { 333 | throw new Error("Expects ws.binaryType to be writable only with valid values") 334 | } 335 | `)) 336 | require.NoError(t, err) 337 | logs := hook.Drain() 338 | require.Len(t, logs, 0) 339 | } 340 | 341 | func TestBinaryType_Default(t *testing.T) { 342 | t.Parallel() 343 | ts := newTestState(t) 344 | logger, hook := testutils.NewLoggerWithHook(t, logrus.WarnLevel) 345 | ts.runtime.VU.StateField.Logger = logger 346 | _, err := ts.runtime.RunOnEventLoop(ts.tb.Replacer.Replace(` 347 | var ws = new WebSocket("WSBIN_URL/ws-echo-invalid") 348 | ws.addEventListener("open", () => { 349 | const sent = new Uint8Array([164,41]).buffer 350 | ws.send(sent) 351 | ws.onmessage = async (e) => { 352 | if (!(e.data instanceof Blob)) { 353 | throw new Error("Wrong event.data type; expected: Blob, got: "+ typeof e.data) 354 | } 355 | const received = await e.data.arrayBuffer(); 356 | 357 | if (sent.byteLength !== received.byteLength) { 358 | throw new Error("The data received " + received.byteLength +" isn't equal to the data sent "+ sent.byteLength) 359 | } 360 | 361 | ws.close() 362 | } 363 | }) 364 | `)) 365 | require.NoError(t, err) 366 | logs := hook.Drain() 367 | require.Len(t, logs, 0) 368 | } 369 | 370 | func TestBinaryType_Blob(t *testing.T) { 371 | t.Parallel() 372 | ts := newTestState(t) 373 | logger, hook := testutils.NewLoggerWithHook(t, logrus.WarnLevel) 374 | ts.runtime.VU.StateField.Logger = logger 375 | _, err := ts.runtime.RunOnEventLoop(ts.tb.Replacer.Replace(` 376 | var ws = new WebSocket("WSBIN_URL/ws-echo") 377 | ws.binaryType = "blob" 378 | ws.addEventListener("open", () => { 379 | const sent = new Uint8Array([164,41]).buffer 380 | ws.send(sent) 381 | ws.onmessage = (e) => { 382 | if (!(e.data instanceof Blob)) { 383 | throw new Error("Wrong event.data type; expected: Blob, got: "+ typeof e.data) 384 | } 385 | 386 | e.data.arrayBuffer().then((ab) => { 387 | if (sent.byteLength !== ab.byteLength) { 388 | throw new Error("The data received isn't equal to the data sent") 389 | } 390 | }) 391 | 392 | ws.close() 393 | } 394 | }) 395 | `)) 396 | require.NoError(t, err) 397 | logs := hook.Drain() 398 | require.Len(t, logs, 0) 399 | } 400 | 401 | func TestBinaryType_ArrayBuffer(t *testing.T) { 402 | t.Parallel() 403 | ts := newTestState(t) 404 | logger, hook := testutils.NewLoggerWithHook(t, logrus.WarnLevel) 405 | ts.runtime.VU.StateField.Logger = logger 406 | _, err := ts.runtime.RunOnEventLoop(ts.tb.Replacer.Replace(` 407 | var ws = new WebSocket("WSBIN_URL/ws-echo") 408 | ws.binaryType = "arraybuffer" 409 | ws.addEventListener("open", () => { 410 | const sent = new Uint8Array([164,41]).buffer 411 | ws.send(sent) 412 | ws.onmessage = (e) => { 413 | if (!(e.data instanceof ArrayBuffer)) { 414 | throw new Error("Wrong event.data type; expected: ArrayBuffer, got: "+ typeof e.data) 415 | } 416 | 417 | if (sent.byteLength !== e.data.byteLength) { 418 | throw new Error("The data received isn't equal to the data sent") 419 | } 420 | 421 | ws.close() 422 | } 423 | }) 424 | `)) 425 | require.NoError(t, err) 426 | logs := hook.Drain() 427 | require.Len(t, logs, 0) 428 | } 429 | 430 | func TestExceptionDontPanic(t *testing.T) { 431 | t.Parallel() 432 | cases := map[string]struct { 433 | script, expectedError string 434 | }{ 435 | "open": { 436 | script: ` 437 | var ws = new WebSocket("WSBIN_URL/ws/echo") 438 | ws.addEventListener("open", () => { 439 | oops 440 | })`, 441 | expectedError: "oops is not defined at :4:4", 442 | }, 443 | "onopen": { 444 | script: ` 445 | var ws = new WebSocket("WSBIN_URL/ws/echo") 446 | ws.onopen = () => { 447 | oops 448 | }`, 449 | expectedError: "oops is not defined at :4:4", 450 | }, 451 | "error": { 452 | script: ` 453 | var ws = new WebSocket("WSBIN_URL/badurl") 454 | ws.addEventListener("error", () =>{ 455 | inerroridf 456 | }) 457 | `, 458 | expectedError: "inerroridf is not defined at :4:4", 459 | }, 460 | "onerror": { 461 | script: ` 462 | var ws = new WebSocket("WSBIN_URL/badurl") 463 | ws.onerror = () => { 464 | inerroridf 465 | } 466 | `, 467 | expectedError: "inerroridf is not defined at :4:4", 468 | }, 469 | "close": { 470 | script: ` 471 | var ws = new WebSocket("WSBIN_URL/ws/echo") 472 | ws.addEventListener("open", () => { 473 | ws.close() 474 | }) 475 | ws.addEventListener("close", ()=>{ 476 | incloseidf 477 | })`, 478 | expectedError: "incloseidf is not defined at :7:4", 479 | }, 480 | "onclose": { 481 | script: ` 482 | var ws = new WebSocket("WSBIN_URL/ws/echo") 483 | ws.onopen = () => { 484 | ws.close() 485 | } 486 | ws.onclose = () =>{ 487 | incloseidf 488 | }`, 489 | expectedError: "incloseidf is not defined at :7:4", 490 | }, 491 | "message": { 492 | script: ` 493 | var ws = new WebSocket("WSBIN_URL/ws/echo") 494 | ws.addEventListener("open", () => { 495 | ws.send("something") 496 | }) 497 | ws.addEventListener("message", ()=>{ 498 | inmessageidf 499 | })`, 500 | expectedError: "inmessageidf is not defined at :7:4", 501 | }, 502 | "onmessage": { 503 | script: ` 504 | var ws = new WebSocket("WSBIN_URL/ws/echo") 505 | ws.onopen = () => { 506 | ws.send("something") 507 | } 508 | ws.onmessage = () =>{ 509 | inmessageidf 510 | }`, 511 | expectedError: "inmessageidf is not defined at :7:4", 512 | }, 513 | } 514 | for name, testcase := range cases { 515 | testcase := testcase 516 | t.Run(name, func(t *testing.T) { 517 | t.Parallel() 518 | ts := newTestState(t) 519 | // This is here as the on in k6 echos and closes, which races to whether we will get the message or not. And that seems like the correct thing to happen either way. 520 | ts.tb.Mux.HandleFunc("/ws/echo", func(w http.ResponseWriter, req *http.Request) { 521 | conn, err := (&websocket.Upgrader{}).Upgrade(w, req, w.Header()) 522 | if err != nil { 523 | return 524 | } 525 | defer func() { 526 | _ = conn.Close() 527 | }() 528 | for { 529 | msgt, msg, err := conn.ReadMessage() 530 | if err != nil { 531 | return 532 | } 533 | err = conn.WriteMessage(msgt, msg) 534 | if err != nil { 535 | return 536 | } 537 | } 538 | }) 539 | 540 | sr := ts.tb.Replacer.Replace 541 | _, err := ts.runtime.RunOnEventLoop(sr(testcase.script)) 542 | require.Error(t, err) 543 | require.ErrorContains(t, err, testcase.expectedError) 544 | }) 545 | } 546 | } 547 | 548 | func TestTwoTalking(t *testing.T) { 549 | t.Parallel() 550 | ts := newTestState(t) 551 | sr := ts.tb.Replacer.Replace 552 | 553 | ch1 := make(chan message) 554 | ch2 := make(chan message) 555 | 556 | ts.tb.Mux.HandleFunc("/ws/couple/", func(w http.ResponseWriter, req *http.Request) { 557 | path := strings.TrimPrefix(req.URL.Path, "/ws/couple/") 558 | var wch chan message 559 | var rch chan message 560 | 561 | switch path { 562 | case "1": 563 | wch = ch1 564 | rch = ch2 565 | case "2": 566 | wch = ch2 567 | rch = ch1 568 | default: 569 | w.WriteHeader(http.StatusTeapot) 570 | } 571 | 572 | conn, err := (&websocket.Upgrader{}).Upgrade(w, req, w.Header()) 573 | if err != nil { 574 | return 575 | } 576 | defer func() { 577 | _ = conn.Close() 578 | }() 579 | 580 | go func() { 581 | defer close(wch) 582 | for { 583 | msgT, msg, err := conn.ReadMessage() 584 | if err != nil { 585 | return 586 | } 587 | wch <- message{ 588 | data: msg, 589 | mtype: msgT, 590 | } 591 | } 592 | }() 593 | for msg := range rch { 594 | err := conn.WriteMessage(msg.mtype, msg.data) 595 | if err != nil { 596 | return 597 | } 598 | } 599 | }) 600 | 601 | _, err := ts.runtime.RunOnEventLoop(sr(` 602 | var count = 0; 603 | var ws1 = new WebSocket("WSBIN_URL/ws/couple/1"); 604 | ws1.addEventListener("open", () => { 605 | ws1.send("I am 1"); 606 | }) 607 | ws1.addEventListener("message", (e)=>{ 608 | if (e.data != "I am 2") { 609 | throw "oops"; 610 | } 611 | count++; 612 | if (count == 2) { 613 | ws1.close(); 614 | } 615 | }) 616 | var ws2 = new WebSocket("WSBIN_URL/ws/couple/2"); 617 | ws2.addEventListener("open", () => { 618 | ws2.send("I am 2"); 619 | }) 620 | ws2.addEventListener("message", (e)=>{ 621 | if (e.data != "I am 1") { 622 | throw "oops"; 623 | } 624 | count++; 625 | if (count == 2) { 626 | ws2.close(); 627 | } 628 | }) 629 | `)) 630 | require.NoError(t, err) 631 | samples := metrics.GetBufferedSamples(ts.samples) 632 | assertSessionMetricsEmitted(t, samples, "", sr("WSBIN_URL/ws/couple/1"), http.StatusSwitchingProtocols, "") 633 | assertSessionMetricsEmitted(t, samples, "", sr("WSBIN_URL/ws/couple/2"), http.StatusSwitchingProtocols, "") 634 | } 635 | 636 | func TestTwoTalkingUsingOn(t *testing.T) { 637 | t.Parallel() 638 | ts := newTestState(t) 639 | sr := ts.tb.Replacer.Replace 640 | 641 | ch1 := make(chan message) 642 | ch2 := make(chan message) 643 | 644 | ts.tb.Mux.HandleFunc("/ws/couple/", func(w http.ResponseWriter, req *http.Request) { 645 | path := strings.TrimPrefix(req.URL.Path, "/ws/couple/") 646 | var wch chan message 647 | var rch chan message 648 | 649 | switch path { 650 | case "1": 651 | wch = ch1 652 | rch = ch2 653 | case "2": 654 | wch = ch2 655 | rch = ch1 656 | default: 657 | w.WriteHeader(http.StatusTeapot) 658 | } 659 | 660 | conn, err := (&websocket.Upgrader{}).Upgrade(w, req, w.Header()) 661 | if err != nil { 662 | return 663 | } 664 | defer func() { 665 | _ = conn.Close() 666 | }() 667 | 668 | go func() { 669 | defer close(wch) 670 | for { 671 | msgT, msg, err := conn.ReadMessage() 672 | if err != nil { 673 | return 674 | } 675 | wch <- message{ 676 | data: msg, 677 | mtype: msgT, 678 | } 679 | } 680 | }() 681 | for msg := range rch { 682 | err := conn.WriteMessage(msg.mtype, msg.data) 683 | if err != nil { 684 | return 685 | } 686 | } 687 | }) 688 | 689 | _, err := ts.runtime.RunOnEventLoop(sr(` 690 | var count = 0; 691 | var ws1 = new WebSocket("WSBIN_URL/ws/couple/1"); 692 | ws1.onopen = () => { 693 | ws1.send("I am 1"); 694 | } 695 | 696 | ws1.onmessage = (e) => { 697 | if (e.data != "I am 2") { 698 | throw "oops"; 699 | } 700 | count++; 701 | if (count == 2) { 702 | ws1.close(); 703 | } 704 | } 705 | 706 | var ws2 = new WebSocket("WSBIN_URL/ws/couple/2"); 707 | ws2.onopen = () => { 708 | ws2.send("I am 2"); 709 | } 710 | ws2.onmessage = (e) => { 711 | if (e.data != "I am 1") { 712 | throw "oops"; 713 | } 714 | count++; 715 | if (count == 2) { 716 | ws2.close(); 717 | } 718 | } 719 | `)) 720 | require.NoError(t, err) 721 | samples := metrics.GetBufferedSamples(ts.samples) 722 | assertSessionMetricsEmitted(t, samples, "", sr("WSBIN_URL/ws/couple/1"), http.StatusSwitchingProtocols, "") 723 | assertSessionMetricsEmitted(t, samples, "", sr("WSBIN_URL/ws/couple/2"), http.StatusSwitchingProtocols, "") 724 | } 725 | 726 | func TestSubProtocols(t *testing.T) { 727 | t.Parallel() 728 | ts := newTestState(t) 729 | sr := ts.tb.Replacer.Replace 730 | 731 | ts.tb.Mux.HandleFunc("/ws/protocols", func(w http.ResponseWriter, req *http.Request) { 732 | conn, err := (&websocket.Upgrader{Subprotocols: []string{"unsupported", "supported"}}).Upgrade(w, req, w.Header()) 733 | if conn.Subprotocol() != "supported" { 734 | _ = conn.WriteMessage(websocket.TextMessage, []byte(`bad subprotocol on server `+conn.Subprotocol())) 735 | return 736 | } 737 | ch := make(chan message) 738 | if err != nil { 739 | return 740 | } 741 | defer func() { 742 | _ = conn.Close() 743 | }() 744 | 745 | go func() { 746 | defer close(ch) 747 | for { 748 | msgT, msg, err := conn.ReadMessage() 749 | if err != nil { 750 | return 751 | } 752 | ch <- message{ 753 | data: msg, 754 | mtype: msgT, 755 | } 756 | } 757 | }() 758 | for msg := range ch { 759 | err := conn.WriteMessage(msg.mtype, msg.data) 760 | if err != nil { 761 | return 762 | } 763 | } 764 | }) 765 | 766 | _, err := ts.runtime.RunOnEventLoop(sr(` 767 | const ws = new WebSocket("WSBIN_URL/ws/protocols", ["one", "supported"]); 768 | ws.onopen = () => { 769 | if (ws.protocol != "supported") { 770 | throw "bad protocol " + ws.protocol; 771 | } 772 | ws.send("hello"); 773 | } 774 | 775 | ws.onmessage = (e) => { 776 | if (e.data != "hello") { 777 | throw "oops"; 778 | } 779 | ws.close(); 780 | } 781 | ws.onerror = (e) => { throw e.error; ws.close();} 782 | `)) 783 | require.NoError(t, err) 784 | samples := metrics.GetBufferedSamples(ts.samples) 785 | assertSessionMetricsEmitted(t, samples, "supported", sr("WSBIN_URL/ws/protocols"), http.StatusSwitchingProtocols, "") 786 | } 787 | 788 | func TestDialError(t *testing.T) { 789 | t.Parallel() 790 | ts := newTestState(t) 791 | sr := ts.tb.Replacer.Replace 792 | 793 | // without listeners 794 | _, err := ts.runtime.RunOnEventLoop(sr(` 795 | var ws = new WebSocket("ws://127.0.0.2"); 796 | `)) 797 | require.NoError(t, err) 798 | 799 | _, err = ts.runtime.RunOnEventLoop(sr(` 800 | var ws = new WebSocket("ws://127.0.0.2"); 801 | ws.addEventListener("error", (e) =>{ 802 | ws.close(); 803 | throw new Error("The provided url is an invalid endpoint") 804 | }) 805 | `)) 806 | assert.Error(t, err) 807 | } 808 | 809 | func TestOnError(t *testing.T) { 810 | t.Parallel() 811 | ts := newTestState(t) 812 | sr := ts.tb.Replacer.Replace 813 | 814 | _, err := ts.runtime.RunOnEventLoop(sr(` 815 | var ws = new WebSocket("ws://127.0.0.2"); 816 | ws.onerror = (e) => { 817 | ws.close(); 818 | throw new Error("lorem ipsum") 819 | } 820 | `)) 821 | assert.Error(t, err) 822 | assert.Equal(t, "Error: lorem ipsum at :5:10(7)", err.Error()) 823 | } 824 | 825 | func TestOnClose(t *testing.T) { 826 | t.Parallel() 827 | ts := newTestState(t) 828 | sr := ts.tb.Replacer.Replace 829 | 830 | _, err := ts.runtime.RunOnEventLoop(sr(` 831 | var ws = new WebSocket("WSBIN_URL/ws/echo") 832 | ws.onopen = () => { 833 | ws.close() 834 | } 835 | ws.onclose = () =>{ 836 | call("from close") 837 | } 838 | `)) 839 | assert.NoError(t, err) 840 | assert.Equal(t, []string{"from close"}, ts.callRecorder.Recorded()) 841 | } 842 | 843 | func TestMixingOnAndAddHandlers(t *testing.T) { 844 | t.Parallel() 845 | ts := newTestState(t) 846 | sr := ts.tb.Replacer.Replace 847 | 848 | _, err := ts.runtime.RunOnEventLoop(sr(` 849 | var ws = new WebSocket("WSBIN_URL/ws/echo") 850 | ws.onopen = () => { 851 | ws.close() 852 | } 853 | ws.addEventListener("close", () => { 854 | call("from addEventListener") 855 | }) 856 | ws.onclose = () =>{ 857 | call("from onclose") 858 | } 859 | `)) 860 | assert.NoError(t, err) 861 | assert.Equal(t, 2, ts.callRecorder.Len()) 862 | assert.Contains(t, ts.callRecorder.Recorded(), "from addEventListener") 863 | assert.Contains(t, ts.callRecorder.Recorded(), "from onclose") 864 | } 865 | 866 | func TestOncloseRedefineListener(t *testing.T) { 867 | t.Parallel() 868 | 869 | ts := newTestState(t) 870 | sr := ts.tb.Replacer.Replace 871 | 872 | _, err := ts.runtime.RunOnEventLoop(sr(` 873 | var ws = new WebSocket("WSBIN_URL/ws/echo") 874 | ws.onopen = () => { 875 | ws.close() 876 | } 877 | ws.onclose = () =>{ 878 | call("from onclose") 879 | } 880 | ws.onclose = () =>{ 881 | call("from onclose 2") 882 | } 883 | `)) 884 | assert.NoError(t, err) 885 | assert.Equal(t, []string{"from onclose 2"}, ts.callRecorder.Recorded()) 886 | } 887 | 888 | func TestOncloseRedefineWithNull(t *testing.T) { 889 | t.Parallel() 890 | 891 | ts := newTestState(t) 892 | sr := ts.tb.Replacer.Replace 893 | 894 | _, err := ts.runtime.RunOnEventLoop(sr(` 895 | var ws = new WebSocket("WSBIN_URL/ws/echo") 896 | ws.onopen = () => { 897 | ws.close() 898 | } 899 | ws.onclose = () =>{ 900 | call("from onclose") 901 | } 902 | ws.onclose = null 903 | `)) 904 | assert.NoError(t, err) 905 | assert.Equal(t, 0, ts.callRecorder.Len()) 906 | } 907 | 908 | func TestOncloseDefineWithInvalidValue(t *testing.T) { 909 | t.Parallel() 910 | 911 | ts := newTestState(t) 912 | sr := ts.tb.Replacer.Replace 913 | 914 | _, err := ts.runtime.RunOnEventLoop(sr(` 915 | var ws = new WebSocket("WSBIN_URL/ws/echo") 916 | ws.onclose = 1 917 | `)) 918 | assert.Error(t, err) 919 | assert.Contains(t, err.Error(), "a value for 'onclose' should be callable") 920 | } 921 | 922 | func TestCustomHeaders(t *testing.T) { 923 | t.Parallel() 924 | ts := newTestState(t) 925 | sr := ts.tb.Replacer.Replace 926 | 927 | mu := &sync.Mutex{} 928 | collected := make(http.Header) 929 | 930 | ts.tb.Mux.HandleFunc("/ws-echo-someheader", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 931 | responseHeaders := w.Header().Clone() 932 | conn, err := (&websocket.Upgrader{}).Upgrade(w, req, responseHeaders) 933 | if err != nil { 934 | ts.errors <- fmt.Errorf("/ws-echo-someheader cannot upgrade request: %w", err) 935 | return 936 | } 937 | 938 | mu.Lock() 939 | collected = req.Header.Clone() 940 | mu.Unlock() 941 | 942 | err = conn.Close() 943 | if err != nil { 944 | t.Logf("error while closing connection in /ws-echo-someheader: %v", err) 945 | } 946 | })) 947 | 948 | _, err := ts.runtime.RunOnEventLoop(sr(` 949 | var ws = new WebSocket("WSBIN_URL/ws-echo-someheader", null, {headers: {"x-lorem": "ipsum"}}) 950 | ws.onopen = () => { 951 | ws.close() 952 | } 953 | `)) 954 | assert.NoError(t, err) 955 | 956 | samples := metrics.GetBufferedSamples(ts.samples) 957 | assertSessionMetricsEmitted(t, samples, "", sr("WSBIN_URL/ws-echo-someheader"), http.StatusSwitchingProtocols, "") 958 | 959 | mu.Lock() 960 | assert.True(t, len(collected) > 0) 961 | assert.Equal(t, "ipsum", collected.Get("x-lorem")) 962 | assert.Equal(t, "TestUserAgent", collected.Get("User-Agent")) 963 | mu.Unlock() 964 | assert.Len(t, ts.errors, 0) 965 | } 966 | 967 | func TestCookies(t *testing.T) { 968 | t.Parallel() 969 | ts := newTestState(t) 970 | sr := ts.tb.Replacer.Replace 971 | 972 | mu := &sync.Mutex{} 973 | collected := make(map[string]string) 974 | 975 | ts.tb.Mux.HandleFunc("/ws-echo-someheader", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 976 | responseHeaders := w.Header().Clone() 977 | conn, err := (&websocket.Upgrader{}).Upgrade(w, req, responseHeaders) 978 | if err != nil { 979 | ts.errors <- fmt.Errorf("/ws-echo-someheader cannot upgrade request: %w", err) 980 | return 981 | } 982 | 983 | mu.Lock() 984 | for _, v := range req.Cookies() { 985 | collected[v.Name] = v.Value 986 | } 987 | mu.Unlock() 988 | 989 | err = conn.Close() 990 | if err != nil { 991 | t.Logf("error while closing connection in /ws-echo-someheader: %v", err) 992 | } 993 | })) 994 | 995 | err := ts.runtime.VU.RuntimeField.Set("http", httpModule.New().NewModuleInstance(ts.runtime.VU).Exports().Default) 996 | require.NoError(t, err) 997 | 998 | ts.runtime.VU.StateField.CookieJar, _ = cookiejar.New(nil) 999 | _, err = ts.runtime.RunOnEventLoop(sr(` 1000 | var jar = new http.CookieJar(); 1001 | jar.set("HTTPBIN_URL/ws-echo-someheader", "someheader", "customjar") 1002 | 1003 | var ws = new WebSocket("WSBIN_URL/ws-echo-someheader", null, {jar: jar}) 1004 | ws.onopen = () => { 1005 | ws.close() 1006 | } 1007 | `)) 1008 | assert.NoError(t, err) 1009 | 1010 | samples := metrics.GetBufferedSamples(ts.samples) 1011 | assertSessionMetricsEmitted(t, samples, "", sr("WSBIN_URL/ws-echo-someheader"), http.StatusSwitchingProtocols, "") 1012 | 1013 | mu.Lock() 1014 | assert.True(t, len(collected) > 0) 1015 | assert.Equal(t, map[string]string{"someheader": "customjar"}, collected) 1016 | mu.Unlock() 1017 | 1018 | assert.Len(t, ts.errors, 0) 1019 | } 1020 | 1021 | func TestCookiesDefaultJar(t *testing.T) { 1022 | t.Parallel() 1023 | ts := newTestState(t) 1024 | sr := ts.tb.Replacer.Replace 1025 | 1026 | mu := &sync.Mutex{} 1027 | collected := make(map[string]string) 1028 | 1029 | ts.tb.Mux.HandleFunc("/ws-echo-someheader", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 1030 | responseHeaders := w.Header().Clone() 1031 | conn, err := (&websocket.Upgrader{}).Upgrade(w, req, responseHeaders) 1032 | if err != nil { 1033 | ts.errors <- fmt.Errorf("/ws-echo-someheader cannot upgrade request: %w", err) 1034 | return 1035 | } 1036 | 1037 | mu.Lock() 1038 | for _, v := range req.Cookies() { 1039 | collected[v.Name] = v.Value 1040 | } 1041 | mu.Unlock() 1042 | 1043 | err = conn.Close() 1044 | if err != nil { 1045 | t.Logf("error while closing connection in /ws-echo-someheader: %v", err) 1046 | } 1047 | })) 1048 | 1049 | err := ts.runtime.VU.RuntimeField.Set("http", httpModule.New().NewModuleInstance(ts.runtime.VU).Exports().Default) 1050 | require.NoError(t, err) 1051 | 1052 | ts.runtime.VU.StateField.CookieJar, _ = cookiejar.New(nil) 1053 | _, err = ts.runtime.RunOnEventLoop(sr(` 1054 | http.cookieJar().set("HTTPBIN_URL/ws-echo-someheader", "someheader", "defaultjar") 1055 | 1056 | var ws = new WebSocket("WSBIN_URL/ws-echo-someheader", null) 1057 | ws.onopen = () => { 1058 | ws.close() 1059 | } 1060 | `)) 1061 | assert.NoError(t, err) 1062 | 1063 | samples := metrics.GetBufferedSamples(ts.samples) 1064 | assertSessionMetricsEmitted(t, samples, "", sr("WSBIN_URL/ws-echo-someheader"), http.StatusSwitchingProtocols, "") 1065 | 1066 | mu.Lock() 1067 | assert.True(t, len(collected) > 0) 1068 | assert.Equal(t, map[string]string{"someheader": "defaultjar"}, collected) 1069 | mu.Unlock() 1070 | 1071 | assert.Len(t, ts.errors, 0) 1072 | } 1073 | 1074 | func TestManualNameTag(t *testing.T) { 1075 | t.Parallel() 1076 | ts := newTestState(t) 1077 | sr := ts.tb.Replacer.Replace 1078 | 1079 | ts.runtime.VU.StateField.Options.SystemTags = metrics.ToSystemTagSet([]string{"url", "name"}) 1080 | 1081 | _, err := ts.runtime.RunOnEventLoop(sr(` 1082 | var ws = new WebSocket("WSBIN_URL/ws-echo", null, { tags: { name: "custom" } } ) 1083 | ws.onopen = () => { 1084 | ws.send("test") 1085 | } 1086 | ws.onmessage = (event) => { 1087 | if (event.data != "test") { 1088 | throw new Error ("echo'd data doesn't match our message!"); 1089 | } 1090 | ws.close() 1091 | } 1092 | ws.onerror = (e) => { throw JSON.stringify(e) } 1093 | `)) 1094 | require.NoError(t, err) 1095 | 1096 | containers := metrics.GetBufferedSamples(ts.samples) 1097 | require.NotEmpty(t, containers) 1098 | 1099 | for _, sampleContainer := range containers { 1100 | require.NotEmpty(t, sampleContainer.GetSamples()) 1101 | for _, sample := range sampleContainer.GetSamples() { 1102 | dataToCheck := sample.Tags.Map() 1103 | require.NotEmpty(t, dataToCheck) 1104 | 1105 | assert.Equal(t, "custom", dataToCheck["url"]) 1106 | assert.Equal(t, "custom", dataToCheck["name"]) 1107 | } 1108 | } 1109 | } 1110 | 1111 | func TestSystemTags(t *testing.T) { 1112 | t.Parallel() 1113 | 1114 | testedSystemTags := []string{"status", "subproto", "url", "ip"} 1115 | for _, expectedTagStr := range testedSystemTags { 1116 | expectedTagStr := expectedTagStr 1117 | t.Run("only "+expectedTagStr, func(t *testing.T) { 1118 | t.Parallel() 1119 | expectedTag, err := metrics.SystemTagString(expectedTagStr) 1120 | require.NoError(t, err) 1121 | 1122 | ts := newTestState(t) 1123 | sr := ts.tb.Replacer.Replace 1124 | ts.runtime.VU.StateField.Options.SystemTags = metrics.ToSystemTagSet([]string{expectedTagStr}) 1125 | 1126 | _, err = ts.runtime.RunOnEventLoop(sr(` 1127 | var ws = new WebSocket("WSBIN_URL/ws-echo") 1128 | ws.onopen = () => { 1129 | ws.send("test") 1130 | } 1131 | ws.onmessage = (event) => { 1132 | if (event.data != "test") { 1133 | throw new Error ("echo'd data doesn't match our message!"); 1134 | } 1135 | ws.close() 1136 | } 1137 | ws.onerror = (e) => { throw JSON.stringify(e) } 1138 | `)) 1139 | require.NoError(t, err) 1140 | 1141 | containers := metrics.GetBufferedSamples(ts.samples) 1142 | require.NotEmpty(t, containers) 1143 | for _, sampleContainer := range containers { 1144 | require.NotEmpty(t, sampleContainer.GetSamples()) 1145 | for _, sample := range sampleContainer.GetSamples() { 1146 | var dataToCheck map[string]string 1147 | if metrics.NonIndexableSystemTags.Has(expectedTag) { 1148 | dataToCheck = sample.Metadata 1149 | } else { 1150 | dataToCheck = sample.Tags.Map() 1151 | } 1152 | 1153 | require.NotEmpty(t, dataToCheck) 1154 | for emittedTag := range dataToCheck { 1155 | assert.Equal(t, expectedTagStr, emittedTag) 1156 | } 1157 | } 1158 | } 1159 | }) 1160 | } 1161 | } 1162 | 1163 | func TestCustomTags(t *testing.T) { 1164 | t.Parallel() 1165 | 1166 | ts := newTestState(t) 1167 | sr := ts.tb.Replacer.Replace 1168 | _, err := ts.runtime.RunOnEventLoop(sr(` 1169 | var ws = new WebSocket("WSBIN_URL/ws-echo", null, {tags: {lorem: "ipsum", version: 13}}) 1170 | ws.onopen = () => { 1171 | ws.send("something") 1172 | ws.close() 1173 | } 1174 | ws.onerror = (e) => { throw JSON.stringify(e) } 1175 | `)) 1176 | require.NoError(t, err) 1177 | samples := metrics.GetBufferedSamples(ts.samples) 1178 | assertSessionMetricsEmitted(t, samples, "", sr("WSBIN_URL/ws-echo"), http.StatusSwitchingProtocols, "") 1179 | 1180 | for _, sampleContainer := range samples { 1181 | require.NotEmpty(t, sampleContainer.GetSamples()) 1182 | for _, sample := range sampleContainer.GetSamples() { 1183 | dataToCheck := sample.Tags.Map() 1184 | 1185 | require.NotEmpty(t, dataToCheck) 1186 | 1187 | assert.Equal(t, "ipsum", dataToCheck["lorem"]) 1188 | assert.Equal(t, "13", dataToCheck["version"]) 1189 | assert.NotEmpty(t, dataToCheck["url"]) 1190 | } 1191 | } 1192 | } 1193 | 1194 | func TestCompressionSession(t *testing.T) { 1195 | t.Parallel() 1196 | const text string = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sed pharetra sapien. Nunc laoreet molestie ante ac gravida. Etiam interdum dui viverra posuere egestas. Pellentesque at dolor tristique, mattis turpis eget, commodo purus. Nunc orci aliquam.` 1197 | 1198 | ts := newTestState(t) 1199 | sr := ts.tb.Replacer.Replace 1200 | 1201 | ts.addHandler("/ws-compression", &websocket.Upgrader{ 1202 | EnableCompression: true, 1203 | ReadBufferSize: 1024, 1204 | WriteBufferSize: 1024, 1205 | }, &testMessage{websocket.TextMessage, []byte(text)}) 1206 | 1207 | _, err := ts.runtime.RunOnEventLoop(sr(` 1208 | var params = { 1209 | "compression": "deflate" 1210 | } 1211 | var ws = new WebSocket("WSBIN_URL/ws-compression", null, params) 1212 | 1213 | ws.onmessage = (event) => { 1214 | if (event.data != "` + text + `"){ 1215 | throw new Error("wrong message received from server: ", event.data) 1216 | } 1217 | 1218 | const expectedExtension = "permessage-deflate; server_no_context_takeover; client_no_context_takeover" 1219 | if (!(ws.extensions.includes(expectedExtension))) { 1220 | throw "expected value '" + expectedExtension + "' missing in " + JSON.stringify(ws.extensions); 1221 | } 1222 | 1223 | ws.close() 1224 | } 1225 | 1226 | `)) 1227 | 1228 | require.NoError(t, err) 1229 | 1230 | samples := metrics.GetBufferedSamples(ts.samples) 1231 | url := sr("WSBIN_URL/ws-compression") 1232 | assertSessionMetricsEmitted(t, samples, "", url, http.StatusSwitchingProtocols, "") 1233 | assertMetricEmittedCount(t, metrics.WSMessagesReceivedName, samples, url, 1) 1234 | 1235 | assert.Len(t, ts.errors, 0) 1236 | } 1237 | 1238 | func TestServerWithoutCompression(t *testing.T) { 1239 | t.Parallel() 1240 | const text string = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sed pharetra sapien. Nunc laoreet molestie ante ac gravida. Etiam interdum dui viverra posuere egestas. Pellentesque at dolor tristique, mattis turpis eget, commodo purus. Nunc orci aliquam.` 1241 | 1242 | ts := newTestState(t) 1243 | sr := ts.tb.Replacer.Replace 1244 | 1245 | ts.addHandler("/ws-compression", &websocket.Upgrader{}, &testMessage{websocket.TextMessage, []byte(text)}) 1246 | 1247 | _, err := ts.runtime.RunOnEventLoop(sr(` 1248 | var params = { 1249 | "compression": "deflate" 1250 | } 1251 | var ws = new WebSocket("WSBIN_URL/ws-compression", null, params) 1252 | ws.onmessage = (event) => { 1253 | if (event.data != "` + text + `"){ 1254 | throw new Error("wrong message received from server: ", event.data) 1255 | } 1256 | 1257 | ws.close() 1258 | } 1259 | `)) 1260 | 1261 | require.NoError(t, err) 1262 | 1263 | samples := metrics.GetBufferedSamples(ts.samples) 1264 | url := sr("WSBIN_URL/ws-compression") 1265 | assertSessionMetricsEmitted(t, samples, "", url, http.StatusSwitchingProtocols, "") 1266 | assertMetricEmittedCount(t, metrics.WSMessagesReceivedName, samples, url, 1) 1267 | 1268 | assert.Len(t, ts.errors, 0) 1269 | } 1270 | 1271 | func TestCompressionParams(t *testing.T) { 1272 | t.Parallel() 1273 | testCases := []struct { 1274 | compression string 1275 | expectedError string 1276 | }{ 1277 | { 1278 | compression: `""`, 1279 | expectedError: `unsupported compression algorithm '', supported algorithm is 'deflate'`, 1280 | }, 1281 | { 1282 | compression: `null`, 1283 | expectedError: `unsupported compression algorithm 'null', supported algorithm is 'deflate'`, 1284 | }, 1285 | { 1286 | compression: `undefined`, 1287 | expectedError: `unsupported compression algorithm 'undefined', supported algorithm is 'deflate'`, 1288 | }, 1289 | { 1290 | compression: `" "`, 1291 | expectedError: `unsupported compression algorithm '', supported algorithm is 'deflate'`, 1292 | }, 1293 | {compression: `"deflate"`}, 1294 | {compression: `"deflate "`}, 1295 | { 1296 | compression: `"gzip"`, 1297 | expectedError: `unsupported compression algorithm 'gzip', supported algorithm is 'deflate'`, 1298 | }, 1299 | { 1300 | compression: `"deflate, gzip"`, 1301 | expectedError: `unsupported compression algorithm 'deflate, gzip', supported algorithm is 'deflate'`, 1302 | }, 1303 | { 1304 | compression: `"deflate, deflate"`, 1305 | expectedError: `unsupported compression algorithm 'deflate, deflate', supported algorithm is 'deflate'`, 1306 | }, 1307 | { 1308 | compression: `"deflate, "`, 1309 | expectedError: `unsupported compression algorithm 'deflate,', supported algorithm is 'deflate'`, 1310 | }, 1311 | } 1312 | 1313 | for _, testCase := range testCases { 1314 | testCase := testCase 1315 | t.Run(testCase.compression, func(t *testing.T) { 1316 | t.Parallel() 1317 | ts := newTestState(t) 1318 | sr := ts.tb.Replacer.Replace 1319 | 1320 | ts.addHandler("/ws-compression-param", &websocket.Upgrader{ 1321 | EnableCompression: true, 1322 | ReadBufferSize: 1024, 1323 | WriteBufferSize: 1024, 1324 | }, nil) 1325 | 1326 | _, err := ts.runtime.RunOnEventLoop(sr(` 1327 | var ws = new WebSocket("WSBIN_URL/ws-compression-param", null, {"compression":` + testCase.compression + `}) 1328 | ws.onopen = () => { 1329 | ws.close() 1330 | } 1331 | `)) 1332 | 1333 | if testCase.expectedError == "" { 1334 | require.NoError(t, err) 1335 | } else { 1336 | require.Error(t, err) 1337 | require.Contains(t, err.Error(), testCase.expectedError) 1338 | } 1339 | }) 1340 | } 1341 | } 1342 | 1343 | func TestSessionPing(t *testing.T) { 1344 | t.Parallel() 1345 | tb := httpmultibin.NewHTTPMultiBin(t) 1346 | sr := tb.Replacer.Replace 1347 | 1348 | ts := newTestState(t) 1349 | 1350 | _, err := ts.runtime.RunOnEventLoop(sr(` 1351 | var ws = new WebSocket("WSBIN_URL/ws-echo") 1352 | ws.onopen = () => { 1353 | ws.ping() 1354 | } 1355 | 1356 | ws.onpong = () => { 1357 | call("from onpong") 1358 | ws.close() 1359 | } 1360 | ws.onerror = (e) => { throw JSON.stringify(e) } 1361 | `)) 1362 | 1363 | require.NoError(t, err) 1364 | 1365 | samplesBuf := metrics.GetBufferedSamples(ts.samples) 1366 | assertSessionMetricsEmitted(t, samplesBuf, "", sr("WSBIN_URL/ws-echo"), http.StatusSwitchingProtocols, "") 1367 | assert.Equal(t, []string{"from onpong"}, ts.callRecorder.Recorded()) 1368 | } 1369 | 1370 | func TestSessionPingAdd(t *testing.T) { 1371 | t.Parallel() 1372 | tb := httpmultibin.NewHTTPMultiBin(t) 1373 | sr := tb.Replacer.Replace 1374 | 1375 | ts := newTestState(t) 1376 | 1377 | _, err := ts.runtime.RunOnEventLoop(sr(` 1378 | var ws = new WebSocket("WSBIN_URL/ws-echo") 1379 | ws.addEventListener("open", () => { 1380 | ws.ping() 1381 | }) 1382 | 1383 | ws.onerror = (e) => { throw JSON.stringify(e) } 1384 | ws.addEventListener("pong", () => { 1385 | call("from onpong") 1386 | ws.close() 1387 | }) 1388 | `)) 1389 | 1390 | require.NoError(t, err) 1391 | 1392 | samplesBuf := metrics.GetBufferedSamples(ts.samples) 1393 | assertSessionMetricsEmitted(t, samplesBuf, "", sr("WSBIN_URL/ws-echo"), http.StatusSwitchingProtocols, "") 1394 | assert.Equal(t, []string{"from onpong"}, ts.callRecorder.Recorded()) 1395 | } 1396 | 1397 | func TestLockingUpWithAThrow(t *testing.T) { 1398 | t.Parallel() 1399 | tb := httpmultibin.NewHTTPMultiBin(t) 1400 | ctx, cancel := context.WithCancel(context.Background()) 1401 | defer cancel() 1402 | sr := tb.Replacer.Replace 1403 | 1404 | ts := newTestState(t) 1405 | go destroySamples(ctx, ts.samples) 1406 | ts.runtime.VU.CtxField = ctx 1407 | err := ts.runtime.EventLoop.Start(func() error { 1408 | _, runErr := ts.runtime.VU.Runtime().RunString(sr(` 1409 | let a = 0; 1410 | const connections = 200; 1411 | async function s() { 1412 | let ws = new WebSocket("WSBIN_URL/ws-echo") 1413 | ws.addEventListener("open", () => { 1414 | ws.ping() 1415 | a++ 1416 | }) 1417 | 1418 | ws.addEventListener("pong", () => { 1419 | ws.ping() 1420 | if (a == connections){ 1421 | a++ 1422 | ws.close() 1423 | throw "s"; 1424 | } 1425 | }) 1426 | } 1427 | [...Array(connections)].forEach(_ => s()) 1428 | `)) 1429 | return runErr 1430 | }) 1431 | 1432 | cancel() 1433 | assert.ErrorContains(t, err, "s at ") 1434 | ts.runtime.EventLoop.WaitOnRegistered() 1435 | } 1436 | 1437 | func TestLockingUpWithAJustGeneralCancel(t *testing.T) { 1438 | t.Parallel() 1439 | tb := httpmultibin.NewHTTPMultiBin(t) 1440 | ctx, cancel := context.WithCancel(context.Background()) 1441 | defer cancel() 1442 | sr := tb.Replacer.Replace 1443 | 1444 | ts := newTestState(t) 1445 | defer func() { 1446 | close(ts.samples) 1447 | }() 1448 | go destroySamples(ctx, ts.samples) 1449 | ts.runtime.VU.CtxField = ctx 1450 | require.NoError(t, ts.runtime.VU.RuntimeField.Set("cancel", cancel)) 1451 | _, err := ts.runtime.RunOnEventLoop(sr(` 1452 | let a = 0; 1453 | const connections = 1000; 1454 | async function s() { 1455 | var ws = new WebSocket("WSBIN_URL/ws-echo") 1456 | ws.addEventListener("open", () => { 1457 | ws.ping() 1458 | }) 1459 | 1460 | ws.addEventListener("pong", () => { 1461 | try{ 1462 | ws.ping() // this will 1463 | } catch(e) {} 1464 | a++ 1465 | if (a == connections){ 1466 | cancel() 1467 | } 1468 | }) 1469 | } 1470 | [...Array(connections)].forEach(_ => s()) 1471 | `)) 1472 | 1473 | cancel() 1474 | assert.NoError(t, err) 1475 | ts.runtime.EventLoop.WaitOnRegistered() 1476 | } 1477 | 1478 | func destroySamples(ctx context.Context, c <-chan metrics.SampleContainer) { 1479 | for { 1480 | select { 1481 | case <-c: 1482 | case <-ctx.Done(): 1483 | return 1484 | } 1485 | } 1486 | } 1487 | 1488 | func TestArrayBufferViewSupport(t *testing.T) { 1489 | t.Parallel() 1490 | for _, name := range []string{ // Commented ones aren't support by Sobek 1491 | "Int8Array", "Int16Array", "Int32Array", 1492 | "Uint8Array", "Uint16Array", "Uint32Array", "Uint8ClampedArray", 1493 | // "BigInt64Array", "BigUint64Arrays", 1494 | /*"Float16Array", */ "Float32Array", "Float64Array", 1495 | } { 1496 | name := name 1497 | t.Run(name, func(t *testing.T) { 1498 | t.Parallel() 1499 | 1500 | testArrayBufferViewSupport(t, name) 1501 | }) 1502 | } 1503 | } 1504 | 1505 | func testArrayBufferViewSupport(t *testing.T, viewName string) { 1506 | t.Helper() 1507 | ts := newTestState(t) 1508 | logger, hook := testutils.NewLoggerWithHook(t, logrus.WarnLevel) 1509 | ts.runtime.VU.StateField.Logger = logger 1510 | _, err := ts.runtime.RunOnEventLoop(ts.tb.Replacer.Replace(fmt.Sprintf(` 1511 | var ws = new WebSocket("WSBIN_URL/ws-echo") 1512 | ws.addEventListener("open", () => { 1513 | const sent = new %[1]s([164, 41]) 1514 | ws.send(sent) 1515 | ws.onmessage = async (e) => { 1516 | const received = new %[1]s(await e.data.arrayBuffer()); 1517 | for (let i = 0; i < sent.length; i++) { 1518 | if (sent.at(i) != received.at(i)) { 1519 | throw "Values at " + i + " were different " + sent.at(i) + " vs " + received.at(i); 1520 | } 1521 | } 1522 | ws.close() 1523 | } 1524 | }) 1525 | `, viewName))) 1526 | require.NoError(t, err) 1527 | logs := hook.Drain() 1528 | require.Len(t, logs, 0) 1529 | } 1530 | 1531 | func TestReadyStateSwitch(t *testing.T) { 1532 | t.Parallel() 1533 | ts := newTestState(t) 1534 | logger, hook := testutils.NewLoggerWithHook(t, logrus.WarnLevel) 1535 | ts.runtime.VU.StateField.Logger = logger 1536 | _, err := ts.runtime.RunOnEventLoop(ts.tb.Replacer.Replace(` 1537 | var ws = new WebSocket("WSBIN_URL/ws-echo") 1538 | try { 1539 | switch (ws.readyState) { 1540 | case 0: 1541 | break; 1542 | default: 1543 | throw "ws.readyState doesn't get correct value in switch" 1544 | } 1545 | } finally { 1546 | ws.close() 1547 | } 1548 | `)) 1549 | require.NoError(t, err) 1550 | logs := hook.Drain() 1551 | require.Len(t, logs, 0) 1552 | } 1553 | --------------------------------------------------------------------------------