Skip to content

Commit

Permalink
Autobahn suite (#3251)
Browse files Browse the repository at this point in the history
* 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 <aras.abbasi@googlemail.com>
  • Loading branch information
KhafraDev and Uzlopak committed May 13, 2024
1 parent 48b356c commit 388fe56
Show file tree
Hide file tree
Showing 11 changed files with 341 additions and 14 deletions.
71 changes: 71 additions & 0 deletions .github/workflows/autobahn.yml
@@ -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
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
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
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
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
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
@@ -0,0 +1 @@
reports/clients
47 changes: 47 additions & 0 deletions test/autobahn/client.js
@@ -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
@@ -0,0 +1,7 @@
{
"url": "ws://127.0.0.1:9001",
"outdir": "./reports/clients",
"cases": ["*"],
"exclude-cases": [],
"exclude-agent-cases": {}
}

0 comments on commit 388fe56

Please sign in to comment.