Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autobahn suite #3251

Merged
merged 9 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/autobahn.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Autobahn
on:
workflow_dispatch:

pull_request:
paths:
- '.github/workflows/autobahn.yml'
- 'lib/web/websocket/**'
- 'test/autobahn/**'

permissions:
contents: read
pull-requests: write

jobs:
autobahn:
name: Autobahn Test Suite
runs-on: ubuntu-latest
container: node:22
services:
fuzzingserver:
image: crossbario/autobahn-testsuite:latest
ports:
- '9001:9001'
options: --name fuzzingserver
volumes:
- ${{ github.workspace }}/test/autobahn/config:/config
- ${{ github.workspace }}/test/autobahn/reports:/reports
steps:
- name: Checkout Code
uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2
with:
persist-credentials: false
clean: false

- name: Restart Autobahn Server
# Restart service after volumes have been checked out
uses: docker://docker
with:
args: docker restart --time 0 --signal=SIGKILL fuzzingserver

- name: Setup Node
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
node-version: 22

- name: Run Autobahn Test Suite
run: npm run test:websocket:autobahn
env:
FUZZING_SERVER_URL: ws://fuzzingserver:9001

- name: Report into CI
id: report-ci
run: npm run test:websocket:autobahn:report

- name: Generate Report for PR Comment
if: github.event_name == 'pull_request'
id: report-markdown
run: |
echo "comment<<nEOFn" >> $GITHUB_OUTPUT
node test/autobahn/report.js >> $GITHUB_OUTPUT
echo "nEOFn" >> $GITHUB_OUTPUT
env:
REPORTER: markdown

- name: Comment PR
if: github.event_name == 'pull_request'
uses: thollander/actions-comment-pull-request@v2
with:
message: ${{ steps.report-markdown.outputs.comment }}
comment_tag: autobahn
8 changes: 2 additions & 6 deletions lib/web/websocket/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,13 +261,9 @@ function closeWebSocketConnection (ws, code, reason, reasonByteLength) {
/** @type {import('stream').Duplex} */
const socket = ws[kResponse].socket

socket.write(frame.createFrame(opcodes.CLOSE), (err) => {
if (!err) {
ws[kSentClose] = sentCloseFrameState.SENT
}
})
socket.write(frame.createFrame(opcodes.CLOSE))

ws[kSentClose] = sentCloseFrameState.PROCESSING
ws[kSentClose] = sentCloseFrameState.SENT

// Upon either sending or receiving a Close control frame, it is said
// that _The WebSocket Closing Handshake is Started_ and that the
Expand Down
43 changes: 39 additions & 4 deletions lib/web/websocket/receiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ const assert = require('node:assert')
const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols')
const { channels } = require('../../core/diagnostics')
const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived, utf8Decode, isControlFrame, isContinuationFrame } = require('./util')
const {
isValidStatusCode,
isValidOpcode,
failWebsocketConnection,
websocketMessageReceived,
utf8Decode,
isControlFrame,
isContinuationFrame,
isTextBinaryFrame
} = require('./util')
const { WebsocketFrameSend } = require('./frame')
const { CloseEvent } = require('./events')

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

if (!isValidOpcode(opcode)) {
failWebsocketConnection(this.ws, 'Invalid opcode received')
return callback()
}

if (masked) {
failWebsocketConnection(this.ws, 'Frame cannot be masked')
return callback()
}

const rsv1 = (buffer[0] & 0x40) !== 0
const rsv2 = (buffer[0] & 0x20) !== 0
const rsv3 = (buffer[0] & 0x10) !== 0

// MUST be 0 unless an extension is negotiated that defines meanings
// for non-zero values. If a nonzero value is received and none of
// the negotiated extensions defines the meaning of such a nonzero
// value, the receiving endpoint MUST _Fail the WebSocket
// Connection_.
if (rsv1 || rsv2 || rsv3) {
failWebsocketConnection(this.ws, 'RSV1, RSV2, RSV3 must be clear')
return
}

const fragmented = !fin && opcode !== opcodes.CONTINUATION

if (fragmented && opcode !== opcodes.BINARY && opcode !== opcodes.TEXT) {
if (fragmented && !isTextBinaryFrame(opcode)) {
// Only text and binary frames can be fragmented
failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.')
return
}

// If we are already parsing a text/binary frame and do not receive either
// a continuation frame or close frame, fail the connection.
if (isTextBinaryFrame(opcode) && this.#info.opcode !== undefined) {
failWebsocketConnection(this.ws, 'Expected continuation frame')
return
}

const payloadLength = buffer[1] & 0x7F

if (isControlFrame(opcode)) {
Expand Down Expand Up @@ -269,7 +304,7 @@ class ByteParser extends Writable {

if (info.payloadLength > 125) {
// Control frames can have a payload length of 125 bytes MAX
callback(new Error('Payload length for control frame exceeded 125 bytes.'))
failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.')
return false
} else if (this.#byteOffset < info.payloadLength) {
callback()
Expand Down Expand Up @@ -375,7 +410,7 @@ class ByteParser extends Writable {
parseContinuationFrame (callback, info) {
// If we received a continuation frame before we started parsing another frame.
if (this.#info.opcode === undefined) {
callback(new Error('Received unexpected continuation frame.'))
failWebsocketConnection(this.ws, 'Received unexpected continuation frame.')
return false
} else if (this.#byteOffset < info.payloadLength) {
callback()
Expand Down
12 changes: 11 additions & 1 deletion lib/web/websocket/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,14 @@ function isContinuationFrame (opcode) {
return opcode === opcodes.CONTINUATION
}

function isTextBinaryFrame (opcode) {
return opcode === opcodes.TEXT || opcode === opcodes.BINARY
}

function isValidOpcode (opcode) {
return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
}

// https://nodejs.org/api/intl.html#detecting-internationalization-support
const hasIntl = typeof process.versions.icu === 'string'
const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undefined
Expand Down Expand Up @@ -255,5 +263,7 @@ module.exports = {
websocketMessageReceived,
utf8Decode,
isControlFrame,
isContinuationFrame
isContinuationFrame,
isTextBinaryFrame,
isValidOpcode
}
16 changes: 13 additions & 3 deletions lib/web/websocket/websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const { ByteParser } = require('./receiver')
const { kEnumerableProperty, isBlobLike } = require('../../core/util')
const { getGlobalDispatcher } = require('../../global')
const { types } = require('node:util')
const { ErrorEvent } = require('./events')
const { ErrorEvent, CloseEvent } = require('./events')

let experimentalWarned = false

Expand Down Expand Up @@ -594,9 +594,19 @@ function onParserDrain () {
}

function onParserError (err) {
fireEvent('error', this, () => new ErrorEvent('error', { error: err, message: err.reason }))
let message
let code

if (err instanceof CloseEvent) {
message = err.reason
code = err.code
} else {
message = err.message
}

fireEvent('error', this, () => new ErrorEvent('error', { error: err, message }))

closeWebSocketConnection(this, err.code)
closeWebSocketConnection(this, code)
}

module.exports = {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@
"test:typescript": "tsd && tsc --skipLibCheck test/imports/undici-import.ts",
"test:webidl": "borp -p \"test/webidl/*.js\"",
"test:websocket": "borp -p \"test/websocket/*.js\"",
"test:websocket:autobahn": "node test/autobahn/client.js",
"test:websocket:autobahn:report": "node test/autobahn/report.js",
"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",
"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",
"coverage": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report",
Expand Down
1 change: 1 addition & 0 deletions test/autobahn/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
reports/clients
47 changes: 47 additions & 0 deletions test/autobahn/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict'

const { WebSocket } = require('../..')

let currentTest = 1
let testCount

const autobahnFuzzingserverUrl = process.env.FUZZING_SERVER_URL || 'ws://localhost:9001'

function nextTest () {
let ws

if (currentTest > testCount) {
ws = new WebSocket(`${autobahnFuzzingserverUrl}/updateReports?agent=undici`)
return
}

console.log(`Running test case ${currentTest}/${testCount}`)

ws = new WebSocket(
`${autobahnFuzzingserverUrl}/runCase?case=${currentTest}&agent=undici`
)
ws.addEventListener('message', (data) => {
ws.send(data.data)
})
ws.addEventListener('close', () => {
currentTest++
process.nextTick(nextTest)
})
ws.addEventListener('error', (e) => {
console.error(e.error)
})
}

const ws = new WebSocket(`${autobahnFuzzingserverUrl}/getCaseCount`)
ws.addEventListener('message', (data) => {
testCount = parseInt(data.data)
})
ws.addEventListener('close', () => {
if (testCount > 0) {
nextTest()
}
})
ws.addEventListener('error', (e) => {
console.error(e.error)
process.exit(1)
})
7 changes: 7 additions & 0 deletions test/autobahn/config/fuzzingserver.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"url": "ws://127.0.0.1:9001",
"outdir": "./reports/clients",
"cases": ["*"],
"exclude-cases": [],
"exclude-agent-cases": {}
}
Loading
Loading