Skip to content

Commit 388fe56

Browse files
KhafraDevUzlopak
andauthored
Autobahn suite (#3251)
* fix error message in websocket * add autobahn suite * fix opcode tests * fix: rsv bits must be clear * add autobahn workflow * run autobahn on pull_request event * fix case 5.18 * add commenting of status into PR * add permission --------- Co-authored-by: uzlopak <[email protected]>
1 parent 48b356c commit 388fe56

File tree

11 files changed

+341
-14
lines changed

11 files changed

+341
-14
lines changed

.github/workflows/autobahn.yml

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: Autobahn
2+
on:
3+
workflow_dispatch:
4+
5+
pull_request:
6+
paths:
7+
- '.github/workflows/autobahn.yml'
8+
- 'lib/web/websocket/**'
9+
- 'test/autobahn/**'
10+
11+
permissions:
12+
contents: read
13+
pull-requests: write
14+
15+
jobs:
16+
autobahn:
17+
name: Autobahn Test Suite
18+
runs-on: ubuntu-latest
19+
container: node:22
20+
services:
21+
fuzzingserver:
22+
image: crossbario/autobahn-testsuite:latest
23+
ports:
24+
- '9001:9001'
25+
options: --name fuzzingserver
26+
volumes:
27+
- ${{ github.workspace }}/test/autobahn/config:/config
28+
- ${{ github.workspace }}/test/autobahn/reports:/reports
29+
steps:
30+
- name: Checkout Code
31+
uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
32+
with:
33+
persist-credentials: false
34+
clean: false
35+
36+
- name: Restart Autobahn Server
37+
# Restart service after volumes have been checked out
38+
uses: docker://docker
39+
with:
40+
args: docker restart --time 0 --signal=SIGKILL fuzzingserver
41+
42+
- name: Setup Node
43+
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
44+
with:
45+
node-version: 22
46+
47+
- name: Run Autobahn Test Suite
48+
run: npm run test:websocket:autobahn
49+
env:
50+
FUZZING_SERVER_URL: ws://fuzzingserver:9001
51+
52+
- name: Report into CI
53+
id: report-ci
54+
run: npm run test:websocket:autobahn:report
55+
56+
- name: Generate Report for PR Comment
57+
if: github.event_name == 'pull_request'
58+
id: report-markdown
59+
run: |
60+
echo "comment<<nEOFn" >> $GITHUB_OUTPUT
61+
node test/autobahn/report.js >> $GITHUB_OUTPUT
62+
echo "nEOFn" >> $GITHUB_OUTPUT
63+
env:
64+
REPORTER: markdown
65+
66+
- name: Comment PR
67+
if: github.event_name == 'pull_request'
68+
uses: thollander/actions-comment-pull-request@v2
69+
with:
70+
message: ${{ steps.report-markdown.outputs.comment }}
71+
comment_tag: autobahn

lib/web/websocket/connection.js

+2-6
Original file line numberDiff line numberDiff line change
@@ -261,13 +261,9 @@ function closeWebSocketConnection (ws, code, reason, reasonByteLength) {
261261
/** @type {import('stream').Duplex} */
262262
const socket = ws[kResponse].socket
263263

264-
socket.write(frame.createFrame(opcodes.CLOSE), (err) => {
265-
if (!err) {
266-
ws[kSentClose] = sentCloseFrameState.SENT
267-
}
268-
})
264+
socket.write(frame.createFrame(opcodes.CLOSE))
269265

270-
ws[kSentClose] = sentCloseFrameState.PROCESSING
266+
ws[kSentClose] = sentCloseFrameState.SENT
271267

272268
// Upon either sending or receiving a Close control frame, it is said
273269
// that _The WebSocket Closing Handshake is Started_ and that the

lib/web/websocket/receiver.js

+39-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ const assert = require('node:assert')
55
const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
66
const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols')
77
const { channels } = require('../../core/diagnostics')
8-
const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived, utf8Decode, isControlFrame, isContinuationFrame } = require('./util')
8+
const {
9+
isValidStatusCode,
10+
isValidOpcode,
11+
failWebsocketConnection,
12+
websocketMessageReceived,
13+
utf8Decode,
14+
isControlFrame,
15+
isContinuationFrame,
16+
isTextBinaryFrame
17+
} = require('./util')
918
const { WebsocketFrameSend } = require('./frame')
1019
const { CloseEvent } = require('./events')
1120

@@ -58,19 +67,45 @@ class ByteParser extends Writable {
5867
const opcode = buffer[0] & 0x0F
5968
const masked = (buffer[1] & 0x80) === 0x80
6069

70+
if (!isValidOpcode(opcode)) {
71+
failWebsocketConnection(this.ws, 'Invalid opcode received')
72+
return callback()
73+
}
74+
6175
if (masked) {
6276
failWebsocketConnection(this.ws, 'Frame cannot be masked')
6377
return callback()
6478
}
6579

80+
const rsv1 = (buffer[0] & 0x40) !== 0
81+
const rsv2 = (buffer[0] & 0x20) !== 0
82+
const rsv3 = (buffer[0] & 0x10) !== 0
83+
84+
// MUST be 0 unless an extension is negotiated that defines meanings
85+
// for non-zero values. If a nonzero value is received and none of
86+
// the negotiated extensions defines the meaning of such a nonzero
87+
// value, the receiving endpoint MUST _Fail the WebSocket
88+
// Connection_.
89+
if (rsv1 || rsv2 || rsv3) {
90+
failWebsocketConnection(this.ws, 'RSV1, RSV2, RSV3 must be clear')
91+
return
92+
}
93+
6694
const fragmented = !fin && opcode !== opcodes.CONTINUATION
6795

68-
if (fragmented && opcode !== opcodes.BINARY && opcode !== opcodes.TEXT) {
96+
if (fragmented && !isTextBinaryFrame(opcode)) {
6997
// Only text and binary frames can be fragmented
7098
failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.')
7199
return
72100
}
73101

102+
// If we are already parsing a text/binary frame and do not receive either
103+
// a continuation frame or close frame, fail the connection.
104+
if (isTextBinaryFrame(opcode) && this.#info.opcode !== undefined) {
105+
failWebsocketConnection(this.ws, 'Expected continuation frame')
106+
return
107+
}
108+
74109
const payloadLength = buffer[1] & 0x7F
75110

76111
if (isControlFrame(opcode)) {
@@ -269,7 +304,7 @@ class ByteParser extends Writable {
269304

270305
if (info.payloadLength > 125) {
271306
// Control frames can have a payload length of 125 bytes MAX
272-
callback(new Error('Payload length for control frame exceeded 125 bytes.'))
307+
failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.')
273308
return false
274309
} else if (this.#byteOffset < info.payloadLength) {
275310
callback()
@@ -375,7 +410,7 @@ class ByteParser extends Writable {
375410
parseContinuationFrame (callback, info) {
376411
// If we received a continuation frame before we started parsing another frame.
377412
if (this.#info.opcode === undefined) {
378-
callback(new Error('Received unexpected continuation frame.'))
413+
failWebsocketConnection(this.ws, 'Received unexpected continuation frame.')
379414
return false
380415
} else if (this.#byteOffset < info.payloadLength) {
381416
callback()

lib/web/websocket/util.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,14 @@ function isContinuationFrame (opcode) {
226226
return opcode === opcodes.CONTINUATION
227227
}
228228

229+
function isTextBinaryFrame (opcode) {
230+
return opcode === opcodes.TEXT || opcode === opcodes.BINARY
231+
}
232+
233+
function isValidOpcode (opcode) {
234+
return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
235+
}
236+
229237
// https://nodejs.org/api/intl.html#detecting-internationalization-support
230238
const hasIntl = typeof process.versions.icu === 'string'
231239
const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undefined
@@ -255,5 +263,7 @@ module.exports = {
255263
websocketMessageReceived,
256264
utf8Decode,
257265
isControlFrame,
258-
isContinuationFrame
266+
isContinuationFrame,
267+
isTextBinaryFrame,
268+
isValidOpcode
259269
}

lib/web/websocket/websocket.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const { ByteParser } = require('./receiver')
2626
const { kEnumerableProperty, isBlobLike } = require('../../core/util')
2727
const { getGlobalDispatcher } = require('../../global')
2828
const { types } = require('node:util')
29-
const { ErrorEvent } = require('./events')
29+
const { ErrorEvent, CloseEvent } = require('./events')
3030

3131
let experimentalWarned = false
3232

@@ -594,9 +594,19 @@ function onParserDrain () {
594594
}
595595

596596
function onParserError (err) {
597-
fireEvent('error', this, () => new ErrorEvent('error', { error: err, message: err.reason }))
597+
let message
598+
let code
599+
600+
if (err instanceof CloseEvent) {
601+
message = err.reason
602+
code = err.code
603+
} else {
604+
message = err.message
605+
}
606+
607+
fireEvent('error', this, () => new ErrorEvent('error', { error: err, message }))
598608

599-
closeWebSocketConnection(this, err.code)
609+
closeWebSocketConnection(this, code)
600610
}
601611

602612
module.exports = {

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@
8888
"test:typescript": "tsd && tsc --skipLibCheck test/imports/undici-import.ts",
8989
"test:webidl": "borp -p \"test/webidl/*.js\"",
9090
"test:websocket": "borp -p \"test/websocket/*.js\"",
91+
"test:websocket:autobahn": "node test/autobahn/client.js",
92+
"test:websocket:autobahn:report": "node test/autobahn/report.js",
9193
"test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
9294
"test:wpt:withoutintl": "node test/wpt/start-fetch.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
9395
"coverage": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report",

test/autobahn/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
reports/clients

test/autobahn/client.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict'
2+
3+
const { WebSocket } = require('../..')
4+
5+
let currentTest = 1
6+
let testCount
7+
8+
const autobahnFuzzingserverUrl = process.env.FUZZING_SERVER_URL || 'ws://localhost:9001'
9+
10+
function nextTest () {
11+
let ws
12+
13+
if (currentTest > testCount) {
14+
ws = new WebSocket(`${autobahnFuzzingserverUrl}/updateReports?agent=undici`)
15+
return
16+
}
17+
18+
console.log(`Running test case ${currentTest}/${testCount}`)
19+
20+
ws = new WebSocket(
21+
`${autobahnFuzzingserverUrl}/runCase?case=${currentTest}&agent=undici`
22+
)
23+
ws.addEventListener('message', (data) => {
24+
ws.send(data.data)
25+
})
26+
ws.addEventListener('close', () => {
27+
currentTest++
28+
process.nextTick(nextTest)
29+
})
30+
ws.addEventListener('error', (e) => {
31+
console.error(e.error)
32+
})
33+
}
34+
35+
const ws = new WebSocket(`${autobahnFuzzingserverUrl}/getCaseCount`)
36+
ws.addEventListener('message', (data) => {
37+
testCount = parseInt(data.data)
38+
})
39+
ws.addEventListener('close', () => {
40+
if (testCount > 0) {
41+
nextTest()
42+
}
43+
})
44+
ws.addEventListener('error', (e) => {
45+
console.error(e.error)
46+
process.exit(1)
47+
})
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"url": "ws://127.0.0.1:9001",
3+
"outdir": "./reports/clients",
4+
"cases": ["*"],
5+
"exclude-cases": [],
6+
"exclude-agent-cases": {}
7+
}

0 commit comments

Comments
 (0)