diff --git a/docs/api/DiagnosticsChannel.md b/docs/api/DiagnosticsChannel.md index 09a7f9a06c0..1104e7ddb53 100644 --- a/docs/api/DiagnosticsChannel.md +++ b/docs/api/DiagnosticsChannel.md @@ -135,3 +135,44 @@ diagnosticsChannel.channel('undici:client:connectError').subscribe(({ error, soc // connector is a function that creates the socket console.log(`Connect failed with ${error.message}`) }) +``` + +## `undici:websocket:open` + +This message is published after the client has successfully connected to a server. + +```js +import diagnosticsChannel from 'diagnostics_channel' + +diagnosticsChannel.channel('undici:websocket:open').subscribe(({ address, protocol, extensions }) => { + console.log(address) // address, family, and port + console.log(protocol) // negotiated subprotocols + console.log(extensions) // negotiated extensions +}) +``` + +## `undici:websocket:close` + +This message is published after the connection has closed. + +```js +import diagnosticsChannel from 'diagnostics_channel' + +diagnosticsChannel.channel('undici:websocket:close').subscribe(({ websocket, code, reason }) => { + console.log(websocket) // the WebSocket object + console.log(code) // the closing status code + console.log(reason) // the closing reason +}) +``` + +## `undici:websocket:socket_error` + +This message is published if the socket experiences an error. + +```js +import diagnosticsChannel from 'diagnostics_channel' + +diagnosticsChannel.channel('undici:websocket:socket_error').subscribe((error) => { + console.log(error) +}) +``` diff --git a/docs/api/WebSocket.md b/docs/api/WebSocket.md new file mode 100644 index 00000000000..639a5333a1c --- /dev/null +++ b/docs/api/WebSocket.md @@ -0,0 +1,20 @@ +# Class: WebSocket + +> ⚠️ Warning: the WebSocket API is experimental and has known bugs. + +Extends: [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) + +The WebSocket object provides a way to manage a WebSocket connection to a server, allowing bidirectional communication. The API follows the [WebSocket spec](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). + +## `new WebSocket(url[, protocol])` + +Arguments: + +* **url** `URL | string` - The url's protocol *must* be `ws` or `wss`. +* **protocol** `string | string[]` (optional) - Subprotocol(s) to request the server use. + +## Read More + +- [MDN - WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) +- [The WebSocket Specification](https://www.rfc-editor.org/rfc/rfc6455) +- [The WHATWG WebSocket Specification](https://websockets.spec.whatwg.org/) diff --git a/docsify/sidebar.md b/docsify/sidebar.md index ec275063930..e0821478e56 100644 --- a/docsify/sidebar.md +++ b/docsify/sidebar.md @@ -16,6 +16,7 @@ * [MockErrors](/docs/api/MockErrors.md "Undici API - MockErrors") * [API Lifecycle](/docs/api/api-lifecycle.md "Undici API - Lifecycle") * [Diagnostics Channel Support](/docs/api/DiagnosticsChannel.md "Diagnostics Channel Support") + * [WebSocket](/docs/api/WebSocket.md "Undici API - WebSocket") * Best Practices * [Proxy](/docs/best-practices/proxy.md "Connecting through a proxy") * [Client Certificate](/docs/best-practices/client-certificate.md "Connect using a client certificate") diff --git a/index.d.ts b/index.d.ts index d663178d410..527c524c1f2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -21,6 +21,7 @@ export * from './types/file' export * from './types/filereader' export * from './types/formdata' export * from './types/diagnostics-channel' +export * from './types/websocket' export { Interceptable } from './types/mock-interceptor' export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler } diff --git a/index.js b/index.js index ad01a46920d..2ed0d1e0d62 100644 --- a/index.js +++ b/index.js @@ -119,6 +119,12 @@ if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) { module.exports.getGlobalOrigin = getGlobalOrigin } +if (nodeMajor >= 18) { + const { WebSocket } = require('./lib/websocket/websocket') + + module.exports.WebSocket = WebSocket +} + module.exports.request = makeDispatcher(api.request) module.exports.stream = makeDispatcher(api.stream) module.exports.pipeline = makeDispatcher(api.pipeline) diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 8014d0fb3ba..55d3fa79f5c 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -58,6 +58,7 @@ const { dataURLProcessor, serializeAMimeType } = require('./dataURL') const { TransformStream } = require('stream/web') const { getGlobalDispatcher } = require('../../index') const { webidl } = require('./webidl') +const { STATUS_CODES } = require('http') /** @type {import('buffer').resolveObjectURL} */ let resolveObjectURL @@ -1743,12 +1744,17 @@ async function httpNetworkFetch ( } try { - const { body, status, statusText, headersList } = await dispatch({ body: requestBody }) + // socket is only provided for websockets + const { body, status, statusText, headersList, socket } = await dispatch({ body: requestBody }) - const iterator = body[Symbol.asyncIterator]() - fetchParams.controller.next = () => iterator.next() + if (socket) { + response = makeResponse({ status, statusText, headersList, socket }) + } else { + const iterator = body[Symbol.asyncIterator]() + fetchParams.controller.next = () => iterator.next() - response = makeResponse({ status, statusText, headersList }) + response = makeResponse({ status, statusText, headersList }) + } } catch (err) { // 10. If aborted, then: if (err.name === 'AbortError') { @@ -1932,7 +1938,10 @@ async function httpNetworkFetch ( async function dispatch ({ body }) { const url = requestCurrentURL(request) - return new Promise((resolve, reject) => fetchParams.controller.dispatcher.dispatch( + /** @type {import('../..').Agent} */ + const agent = fetchParams.controller.dispatcher + + return new Promise((resolve, reject) => agent.dispatch( { path: url.pathname + url.search, origin: url.origin, @@ -1941,7 +1950,8 @@ async function httpNetworkFetch ( headers: [...request.headersList].flat(), maxRedirections: 0, bodyTimeout: 300_000, - headersTimeout: 300_000 + headersTimeout: 300_000, + upgrade: request.mode === 'websocket' ? 'websocket' : undefined }, { body: null, @@ -2060,6 +2070,30 @@ async function httpNetworkFetch ( fetchParams.controller.terminate(error) reject(error) + }, + + onUpgrade (status, headersList, socket) { + if (status !== 101) { + return + } + + const headers = new Headers() + + for (let n = 0; n < headersList.length; n += 2) { + const key = headersList[n + 0].toString('latin1') + const val = headersList[n + 1].toString('latin1') + + headers.append(key, val) + } + + resolve({ + status, + statusText: STATUS_CODES[status], + headersList: headers[kHeadersList], + socket + }) + + return true } } )) diff --git a/lib/fetch/webidl.js b/lib/fetch/webidl.js index 32c1befab00..e6eaaa499f6 100644 --- a/lib/fetch/webidl.js +++ b/lib/fetch/webidl.js @@ -472,10 +472,20 @@ webidl.converters['unsigned long long'] = function (V) { return x } +// https://webidl.spec.whatwg.org/#es-unsigned-long +webidl.converters['unsigned long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 32, "unsigned"). + const x = webidl.util.ConvertToInt(V, 32, 'unsigned') + + // 2. Return the IDL unsigned long value that + // represents the same numeric value as x. + return x +} + // https://webidl.spec.whatwg.org/#es-unsigned-short -webidl.converters['unsigned short'] = function (V) { +webidl.converters['unsigned short'] = function (V, opts) { // 1. Let x be ? ConvertToInt(V, 16, "unsigned"). - const x = webidl.util.ConvertToInt(V, 16, 'unsigned') + const x = webidl.util.ConvertToInt(V, 16, 'unsigned', opts) // 2. Return the IDL unsigned short value that represents // the same numeric value as x. diff --git a/lib/websocket/connection.js b/lib/websocket/connection.js new file mode 100644 index 00000000000..4c743e2d333 --- /dev/null +++ b/lib/websocket/connection.js @@ -0,0 +1,326 @@ +'use strict' + +// TODO: crypto isn't available in all environments +const { randomBytes, createHash } = require('crypto') +const diagnosticsChannel = require('diagnostics_channel') +const { uid, states } = require('./constants') +const { + kReadyState, + kResponse, + kExtensions, + kProtocol, + kSentClose, + kByteParser, + kReceivedClose +} = require('./symbols') +const { fireEvent, failWebsocketConnection } = require('./util') +const { CloseEvent } = require('./events') +const { ByteParser } = require('./receiver') +const { makeRequest } = require('../fetch/request') +const { fetching } = require('../fetch/index') +const { getGlobalDispatcher } = require('../..') + +const channels = {} +channels.ping = diagnosticsChannel.channel('undici:websocket:ping') +channels.pong = diagnosticsChannel.channel('undici:websocket:pong') +channels.open = diagnosticsChannel.channel('undici:websocket:open') +channels.close = diagnosticsChannel.channel('undici:websocket:close') +channels.socketError = diagnosticsChannel.channel('undici:websocket:socket_error') + +/** + * @see https://websockets.spec.whatwg.org/#concept-websocket-establish + * @param {URL} url + * @param {string|string[]} protocols + * @param {import('./websocket').WebSocket} ws + */ +function establishWebSocketConnection (url, protocols, ws) { + // 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s + // scheme is "ws", and to "https" otherwise. + const requestURL = url + + requestURL.protocol = url.protocol === 'ws:' ? 'http:' : 'https:' + + // 2. Let request be a new request, whose URL is requestURL, client is client, + // service-workers mode is "none", referrer is "no-referrer", mode is + // "websocket", credentials mode is "include", cache mode is "no-store" , + // and redirect mode is "error". + const request = makeRequest({ + urlList: [requestURL], + serviceWorkers: 'none', + referrer: 'no-referrer', + mode: 'websocket', + credentials: 'include', + cache: 'no-store', + redirect: 'error' + }) + + // 3. Append (`Upgrade`, `websocket`) to request’s header list. + // 4. Append (`Connection`, `Upgrade`) to request’s header list. + // Note: both of these are handled by undici currently. + // https://github.com/nodejs/undici/blob/68c269c4144c446f3f1220951338daef4a6b5ec4/lib/client.js#L1397 + + // 5. Let keyValue be a nonce consisting of a randomly selected + // 16-byte value that has been forgiving-base64-encoded and + // isomorphic encoded. + const keyValue = randomBytes(16).toString('base64') + + // 6. Append (`Sec-WebSocket-Key`, keyValue) to request’s + // header list. + request.headersList.append('sec-websocket-key', keyValue) + + // 7. Append (`Sec-WebSocket-Version`, `13`) to request’s + // header list. + request.headersList.append('sec-websocket-version', '13') + + // 8. For each protocol in protocols, combine + // (`Sec-WebSocket-Protocol`, protocol) in request’s header + // list. + for (const protocol of protocols) { + request.headersList.append('sec-websocket-protocol', protocol) + } + + // 9. Let permessageDeflate be a user-agent defined + // "permessage-deflate" extension header value. + // https://github.com/mozilla/gecko-dev/blob/ce78234f5e653a5d3916813ff990f053510227bc/netwerk/protocol/websocket/WebSocketChannel.cpp#L2673 + // TODO: enable once permessage-deflate is supported + const permessageDeflate = '' // 'permessage-deflate; 15' + + // 10. Append (`Sec-WebSocket-Extensions`, permessageDeflate) to + // request’s header list. + // request.headersList.append('sec-websocket-extensions', permessageDeflate) + + // 11. Fetch request with useParallelQueue set to true, and + // processResponse given response being these steps: + const controller = fetching({ + request, + useParallelQueue: true, + dispatcher: getGlobalDispatcher(), + processResponse (response) { + // 1. If response is a network error or its status is not 101, + // fail the WebSocket connection. + if (response.type === 'error' || response.status !== 101) { + failWebsocketConnection(ws, 'Received network error or non-101 status code.') + return + } + + // 2. If protocols is not the empty list and extracting header + // list values given `Sec-WebSocket-Protocol` and response’s + // header list results in null, failure, or the empty byte + // sequence, then fail the WebSocket connection. + if (protocols.length !== 0 && !response.headersList.get('Sec-WebSocket-Protocol')) { + failWebsocketConnection(ws, 'Server did not respond with sent protocols.') + return + } + + // 3. Follow the requirements stated step 2 to step 6, inclusive, + // of the last set of steps in section 4.1 of The WebSocket + // Protocol to validate response. This either results in fail + // the WebSocket connection or the WebSocket connection is + // established. + + // 2. If the response lacks an |Upgrade| header field or the |Upgrade| + // header field contains a value that is not an ASCII case- + // insensitive match for the value "websocket", the client MUST + // _Fail the WebSocket Connection_. + if (response.headersList.get('Upgrade')?.toLowerCase() !== 'websocket') { + failWebsocketConnection(ws, 'Server did not set Upgrade header to "websocket".') + return + } + + // 3. If the response lacks a |Connection| header field or the + // |Connection| header field doesn't contain a token that is an + // ASCII case-insensitive match for the value "Upgrade", the client + // MUST _Fail the WebSocket Connection_. + if (response.headersList.get('Connection')?.toLowerCase() !== 'upgrade') { + failWebsocketConnection(ws, 'Server did not set Connection header to "upgrade".') + return + } + + // 4. If the response lacks a |Sec-WebSocket-Accept| header field or + // the |Sec-WebSocket-Accept| contains a value other than the + // base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket- + // Key| (as a string, not base64-decoded) with the string "258EAFA5- + // E914-47DA-95CA-C5AB0DC85B11" but ignoring any leading and + // trailing whitespace, the client MUST _Fail the WebSocket + // Connection_. + const secWSAccept = response.headersList.get('Sec-WebSocket-Accept') + const digest = createHash('sha1').update(keyValue + uid).digest('base64') + if (secWSAccept !== digest) { + failWebsocketConnection(ws, 'Incorrect hash received in Sec-WebSocket-Accept header.') + return + } + + // 5. If the response includes a |Sec-WebSocket-Extensions| header + // field and this header field indicates the use of an extension + // that was not present in the client's handshake (the server has + // indicated an extension not requested by the client), the client + // MUST _Fail the WebSocket Connection_. (The parsing of this + // header field to determine which extensions are requested is + // discussed in Section 9.1.) + const secExtension = response.headersList.get('Sec-WebSocket-Extensions') + + if (secExtension !== null && secExtension !== permessageDeflate) { + failWebsocketConnection(ws, 'Received different permessage-deflate than the one set.') + return + } + + // 6. If the response includes a |Sec-WebSocket-Protocol| header field + // and this header field indicates the use of a subprotocol that was + // not present in the client's handshake (the server has indicated a + // subprotocol not requested by the client), the client MUST _Fail + // the WebSocket Connection_. + const secProtocol = response.headersList.get('Sec-WebSocket-Protocol') + + if (secProtocol !== null && secProtocol !== request.headersList.get('Sec-WebSocket-Protocol')) { + failWebsocketConnection(ws, 'Protocol was not set in the opening handshake.') + return + } + + // processResponse is called when the "response’s header list has been received and initialized." + // once this happens, the connection is open + ws[kResponse] = response + + const parser = new ByteParser(ws) + response.socket.ws = ws // TODO: use symbol + ws[kByteParser] = parser + + whenConnectionEstablished(ws) + + response.socket.on('data', onSocketData) + response.socket.on('close', onSocketClose) + response.socket.on('error', onSocketError) + + parser.on('drain', onParserDrain) + } + }) + + return controller +} + +/** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + * @param {import('./websocket').WebSocket} ws + */ +function whenConnectionEstablished (ws) { + const { [kResponse]: response } = ws + + // 1. Change the ready state to OPEN (1). + ws[kReadyState] = states.OPEN + + // 2. Change the extensions attribute’s value to the extensions in use, if + // it is not the null value. + // https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 + const extensions = response.headersList.get('sec-websocket-extensions') + + if (extensions !== null) { + ws[kExtensions] = extensions + } + + // 3. Change the protocol attribute’s value to the subprotocol in use, if + // it is not the null value. + // https://datatracker.ietf.org/doc/html/rfc6455#section-1.9 + const protocol = response.headersList.get('sec-websocket-protocol') + + if (protocol !== null) { + ws[kProtocol] = protocol + } + + // 4. Fire an event named open at the WebSocket object. + fireEvent('open', ws) + + if (channels.open.hasSubscribers) { + channels.open.publish({ + address: response.socket.address(), + protocol, + extensions + }) + } +} + +/** + * @param {Buffer} chunk + */ +function onSocketData (chunk) { + if (!this.ws[kByteParser].write(chunk)) { + this.pause() + } +} + +function onParserDrain () { + this.ws[kResponse].socket.resume() +} + +/** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.4 + */ +function onSocketClose () { + const { ws } = this + + // If the TCP connection was closed after the + // WebSocket closing handshake was completed, the WebSocket connection + // is said to have been closed _cleanly_. + const wasClean = ws[kSentClose] && ws[kReceivedClose] + + let code = 1005 + let reason = '' + + const result = ws[kByteParser].closingInfo + + if (result) { + code = result.code ?? 1005 + reason = result.reason + } else if (!ws[kSentClose]) { + // If _The WebSocket + // Connection is Closed_ and no Close control frame was received by the + // endpoint (such as could occur if the underlying transport connection + // is lost), _The WebSocket Connection Close Code_ is considered to be + // 1006. + code = 1006 + } + + // 1. Change the ready state to CLOSED (3). + ws[kReadyState] = states.CLOSED + + // 2. If the user agent was required to fail the WebSocket + // connection, or if the WebSocket connection was closed + // after being flagged as full, fire an event named error + // at the WebSocket object. + // TODO + + // 3. Fire an event named close at the WebSocket object, + // using CloseEvent, with the wasClean attribute + // initialized to true if the connection closed cleanly + // and false otherwise, the code attribute initialized to + // the WebSocket connection close code, and the reason + // attribute initialized to the result of applying UTF-8 + // decode without BOM to the WebSocket connection close + // reason. + fireEvent('close', ws, CloseEvent, { + wasClean, code, reason + }) + + if (channels.close.hasSubscribers) { + channels.close.publish({ + websocket: ws, + code, + reason + }) + } +} + +function onSocketError (error) { + const { ws } = this + + ws[kReadyState] = states.CLOSING + + if (channels.socketError.hasSubscribers) { + channels.socketError.publish(error) + } + + this.destroy() +} + +module.exports = { + establishWebSocketConnection +} diff --git a/lib/websocket/constants.js b/lib/websocket/constants.js new file mode 100644 index 00000000000..27b5e27c2a5 --- /dev/null +++ b/lib/websocket/constants.js @@ -0,0 +1,48 @@ +'use strict' + +// This is a Globally Unique Identifier unique used +// to validate that the endpoint accepts websocket +// connections. +// See https://www.rfc-editor.org/rfc/rfc6455.html#section-1.3 +const uid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + +/** @type {PropertyDescriptor} */ +const staticPropertyDescriptors = { + enumerable: true, + writable: false, + configurable: false +} + +const states = { + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3 +} + +const opcodes = { + CONTINUATION: 0x0, + TEXT: 0x1, + BINARY: 0x2, + CLOSE: 0x8, + PING: 0x9, + PONG: 0xA +} + +const maxUnsigned16Bit = 2 ** 16 - 1 // 65535 + +const parserStates = { + INFO: 0, + PAYLOADLENGTH_16: 2, + PAYLOADLENGTH_64: 3, + READ_DATA: 4 +} + +module.exports = { + uid, + staticPropertyDescriptors, + states, + opcodes, + maxUnsigned16Bit, + parserStates +} diff --git a/lib/websocket/events.js b/lib/websocket/events.js new file mode 100644 index 00000000000..a1d3a676cd4 --- /dev/null +++ b/lib/websocket/events.js @@ -0,0 +1,301 @@ +'use strict' + +const { webidl } = require('../fetch/webidl') +const { kEnumerableProperty } = require('../core/util') +const { MessagePort } = require('worker_threads') + +/** + * @see https://html.spec.whatwg.org/multipage/comms.html#messageevent + */ +class MessageEvent extends Event { + #eventInit + + constructor (type, eventInitDict = {}) { + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.MessageEventInit(eventInitDict) + + super(type, eventInitDict) + + this.#eventInit = eventInitDict + } + + get data () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.data + } + + get origin () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.origin + } + + get lastEventId () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.lastEventId + } + + get source () { + webidl.brandCheck(this, MessageEvent) + + return this.#eventInit.source + } + + get ports () { + webidl.brandCheck(this, MessageEvent) + + if (!Object.isFrozen(this.#eventInit.ports)) { + Object.freeze(this.#eventInit.ports) + } + + return this.#eventInit.ports + } + + initMessageEvent ( + type, + bubbles = false, + cancelable = false, + data = null, + origin = '', + lastEventId = '', + source = null, + ports = [] + ) { + webidl.brandCheck(this, MessageEvent) + + webidl.argumentLengthCheck(arguments, 1, { header: 'MessageEvent.initMessageEvent' }) + + return new MessageEvent(type, { + bubbles, cancelable, data, origin, lastEventId, source, ports + }) + } +} + +/** + * @see https://websockets.spec.whatwg.org/#the-closeevent-interface + */ +class CloseEvent extends Event { + #eventInit + + constructor (type, eventInitDict = {}) { + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.CloseEventInit(eventInitDict) + + super(type, eventInitDict) + + this.#eventInit = eventInitDict + } + + get wasClean () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.wasClean + } + + get code () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.code + } + + get reason () { + webidl.brandCheck(this, CloseEvent) + + return this.#eventInit.reason + } +} + +// https://html.spec.whatwg.org/multipage/webappapis.html#the-errorevent-interface +class ErrorEvent extends Event { + #eventInit + + constructor (type, eventInitDict) { + webidl.argumentLengthCheck(arguments, 1, { header: 'ErrorEvent constructor' }) + + super(type, eventInitDict) + + type = webidl.converters.DOMString(type) + if (eventInitDict !== undefined) { + eventInitDict = webidl.converters.ErrorEventInit(eventInitDict) + } + + this.#eventInit = eventInitDict + } + + get message () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.message + } + + get filename () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.filename + } + + get lineno () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.lineno + } + + get colno () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.colno + } + + get error () { + webidl.brandCheck(this, ErrorEvent) + + return this.#eventInit.error + } +} + +Object.defineProperties(MessageEvent.prototype, { + [Symbol.toStringTag]: { + value: 'MessageEvent', + configurable: true + }, + data: kEnumerableProperty, + origin: kEnumerableProperty, + lastEventId: kEnumerableProperty, + source: kEnumerableProperty, + ports: kEnumerableProperty, + initMessageEvent: kEnumerableProperty +}) + +Object.defineProperties(CloseEvent.prototype, { + [Symbol.toStringTag]: { + value: 'CloseEvent', + configurable: true + }, + reason: kEnumerableProperty, + code: kEnumerableProperty, + wasClean: kEnumerableProperty +}) + +Object.defineProperties(ErrorEvent.prototype, { + [Symbol.toStringTag]: { + value: 'ErrorEvent', + configurable: true + }, + message: kEnumerableProperty, + filename: kEnumerableProperty, + lineno: kEnumerableProperty, + colno: kEnumerableProperty, + error: kEnumerableProperty +}) + +webidl.converters.MessagePort = webidl.interfaceConverter(MessagePort) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.MessagePort +) + +const eventInit = [ + { + key: 'bubbles', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'cancelable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'composed', + converter: webidl.converters.boolean, + defaultValue: false + } +] + +webidl.converters.MessageEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'data', + converter: webidl.converters.any, + defaultValue: null + }, + { + key: 'origin', + converter: webidl.converters.USVString, + defaultValue: '' + }, + { + key: 'lastEventId', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'source', + // Node doesn't implement WindowProxy or ServiceWorker, so the only + // valid value for source is a MessagePort. + converter: webidl.nullableConverter(webidl.converters.MessagePort), + defaultValue: null + }, + { + key: 'ports', + converter: webidl.converters['sequence'], + get defaultValue () { + return [] + } + } +]) + +webidl.converters.CloseEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'wasClean', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'code', + converter: webidl.converters['unsigned short'], + defaultValue: 0 + }, + { + key: 'reason', + converter: webidl.converters.USVString, + defaultValue: '' + } +]) + +webidl.converters.ErrorEventInit = webidl.dictionaryConverter([ + ...eventInit, + { + key: 'message', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'filename', + converter: webidl.converters.USVString, + defaultValue: '' + }, + { + key: 'lineno', + converter: webidl.converters['unsigned long'], + defaultValue: 0 + }, + { + key: 'colno', + converter: webidl.converters['unsigned long'], + defaultValue: 0 + }, + { + key: 'error', + converter: webidl.converters.any + } +]) + +module.exports = { + MessageEvent, + CloseEvent, + ErrorEvent +} diff --git a/lib/websocket/frame.js b/lib/websocket/frame.js new file mode 100644 index 00000000000..2508a28e891 --- /dev/null +++ b/lib/websocket/frame.js @@ -0,0 +1,64 @@ +'use strict' + +const { randomBytes } = require('crypto') +const { maxUnsigned16Bit } = require('./constants') + +class WebsocketFrameSend { + /** + * @param {Buffer|undefined} data + */ + constructor (data) { + this.frameData = data + this.maskKey = randomBytes(4) + } + + createFrame (opcode) { + const bodyLength = this.frameData?.byteLength ?? 0 + + /** @type {number} */ + let payloadLength = bodyLength // 0-125 + let offset = 6 + + if (bodyLength > maxUnsigned16Bit) { + offset += 8 // payload length is next 8 bytes + payloadLength = 127 + } else if (bodyLength > 125) { + offset += 2 // payload length is next 2 bytes + payloadLength = 126 + } + + // TODO: switch to Buffer.allocUnsafe + const buffer = Buffer.alloc(bodyLength + offset) + + buffer[0] |= 0x80 // FIN + buffer[0] = (buffer[0] & 0xF0) + opcode // opcode + + /*! ws. MIT License. Einar Otto Stangvik */ + buffer[offset - 4] = this.maskKey[0] + buffer[offset - 3] = this.maskKey[1] + buffer[offset - 2] = this.maskKey[2] + buffer[offset - 1] = this.maskKey[3] + + buffer[1] = payloadLength + + if (payloadLength === 126) { + new DataView(buffer.buffer).setUint16(2, bodyLength) + } else if (payloadLength === 127) { + // TODO: optimize this once tests are added for payloads >= 2^16 bytes + buffer.writeUIntBE(bodyLength, 4, 6) + } + + buffer[1] |= 0x80 // MASK + + // mask body + for (let i = 0; i < bodyLength; i++) { + buffer[offset + i] = this.frameData[i] ^ this.maskKey[i % 4] + } + + return buffer + } +} + +module.exports = { + WebsocketFrameSend +} diff --git a/lib/websocket/receiver.js b/lib/websocket/receiver.js new file mode 100644 index 00000000000..12f9884c42a --- /dev/null +++ b/lib/websocket/receiver.js @@ -0,0 +1,231 @@ +'use strict' + +const { Writable } = require('stream') +const { parserStates, opcodes, states } = require('./constants') +const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols') +const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = require('./util') +const { WebsocketFrameSend } = require('./frame') + +class ByteParser extends Writable { + #buffers = [] + #byteOffset = 0 + + #state = parserStates.INFO + + #info = {} + + constructor (ws) { + super() + + this.ws = ws + } + + /** + * @param {Buffer} chunk + * @param {() => void} callback + */ + _write (chunk, _, callback) { + this.#buffers.push(chunk) + this.#byteOffset += chunk.length + + this.run(callback) + } + + /** + * Runs whenever a new chunk is received. + * Callback is called whenever there are no more chunks buffering, + * or not enough bytes are buffered to parse. + */ + run (callback) { + if (this.#state === parserStates.INFO) { + // If there aren't enough bytes to parse the payload length, etc. + if (this.#byteOffset < 2) { + return callback() + } + + const buffer = Buffer.concat(this.#buffers, this.#byteOffset) + + this.#info.fin = (buffer[0] & 0x80) !== 0 + this.#info.opcode = buffer[0] & 0x0F + + const fragmented = !this.#info.fin && this.#info.opcode !== opcodes.CONTINUATION + + if (fragmented && this.#info.opcode !== opcodes.BINARY && this.#info.opcode !== opcodes.TEXT) { + // Only text and binary frames can be fragmented + failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.') + return + } + + const payloadLength = buffer[1] & 0x7F + + if (payloadLength <= 125) { + this.#info.payloadLength = payloadLength + this.#state = parserStates.READ_DATA + } else if (payloadLength === 126) { + this.#state = parserStates.PAYLOADLENGTH_16 + } else if (payloadLength === 127) { + this.#state = parserStates.PAYLOADLENGTH_64 + } + + if ( + (this.#info.opcode === opcodes.PING || + this.#info.opcode === opcodes.PONG || + this.#info.opcode === opcodes.CLOSE) && + payloadLength > 125 + ) { + // Control frames can have a payload length of 125 bytes MAX + failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.') + return + } else if (this.#info.opcode === opcodes.CLOSE) { + if (payloadLength === 1) { + failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.') + return + } + + const body = buffer.subarray(2, payloadLength + 2) + + this.#info.closeInfo = this.parseCloseBody(false, body) + + if (!this.ws[kSentClose]) { + // If an endpoint receives a Close frame and did not previously send a + // Close frame, the endpoint MUST send a Close frame in response. (When + // sending a Close frame in response, the endpoint typically echos the + // status code it received.) + const body = Buffer.allocUnsafe(2) + body.writeUInt16BE(this.#info.closeInfo.code, 0) + const closeFrame = new WebsocketFrameSend(body) + + this.ws[kResponse].socket.write( + closeFrame.createFrame(opcodes.CLOSE), + (err) => { + if (!err) { + this.ws[kSentClose] = true + } + } + ) + } + + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + this.ws[kReadyState] = states.CLOSING + + this.ws[kReceivedClose] = true + + return + } + + // TODO: handle control frames here. Since they are unfragmented, and can + // be sent in the middle of other frames, we shouldn't parse them as normal. + + this.#buffers = [buffer.subarray(2)] + this.#byteOffset -= 2 + } else if (this.#state === parserStates.PAYLOADLENGTH_16) { + if (this.#byteOffset < 2) { + return callback() + } + + const buffer = Buffer.concat(this.#buffers, this.#byteOffset) + + // TODO: optimize this + this.#info.payloadLength = buffer.subarray(0, 2).readUInt16BE(0) + this.#state = parserStates.READ_DATA + + this.#buffers = [buffer.subarray(2)] + this.#byteOffset -= 2 + } else if (this.#state === parserStates.PAYLOADLENGTH_64) { + if (this.#byteOffset < 8) { + return callback() + } + + const buffer = Buffer.concat(this.#buffers, this.#byteOffset) + + // TODO: optimize this + this.#info.payloadLength = buffer.subarray(0, 8).readBigUint64BE(0) + this.#state = parserStates.READ_DATA + + this.#buffers = [buffer.subarray(8)] + this.#byteOffset -= 8 + } else if (this.#state === parserStates.READ_DATA) { + if (this.#byteOffset < this.#info.payloadLength) { + // If there is still more data in this chunk that needs to be read + return callback() + } else if (this.#byteOffset >= this.#info.payloadLength) { + // If the server sent multiple frames in a single chunk + const buffer = Buffer.concat(this.#buffers, this.#byteOffset) + + this.#info.data = buffer.subarray(0, this.#info.payloadLength) + + if (this.#byteOffset > this.#info.payloadLength) { + this.#buffers = [buffer.subarray(this.#info.data.length)] + this.#byteOffset -= this.#info.data.length + } else { + this.#buffers.length = 0 + this.#byteOffset = 0 + } + + websocketMessageReceived(this.ws, this.#info.opcode, this.#info.data) + + this.#info = {} + this.#state = parserStates.INFO + } + } + + if (this.#byteOffset > 0) { + return this.run(callback) + } else { + callback() + } + } + + parseCloseBody (onlyCode, data) { + // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5 + /** @type {number|undefined} */ + let code + + if (data.length >= 2) { + // _The WebSocket Connection Close Code_ is + // defined as the status code (Section 7.4) contained in the first Close + // control frame received by the application + code = data.readUInt16BE(0) + } + + if (onlyCode) { + if (!isValidStatusCode(code)) { + return null + } + + return { code } + } + + // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.6 + /** @type {Buffer} */ + let reason = data.subarray(2) + + // Remove BOM + if (reason[0] === 0xEF && reason[1] === 0xBB && reason[2] === 0xBF) { + reason = reason.subarray(3) + } + + if (code !== undefined && !isValidStatusCode(code)) { + return null + } + + try { + // TODO: optimize this + reason = new TextDecoder('utf-8', { fatal: true }).decode(reason) + } catch { + return null + } + + return { code, reason } + } + + get closingInfo () { + return this.#info.closeInfo + } +} + +module.exports = { + ByteParser +} diff --git a/lib/websocket/symbols.js b/lib/websocket/symbols.js new file mode 100644 index 00000000000..5e135862f81 --- /dev/null +++ b/lib/websocket/symbols.js @@ -0,0 +1,15 @@ +'use strict' + +module.exports = { + kWebSocketURL: Symbol('url'), + kReadyState: Symbol('ready state'), + kController: Symbol('controller'), + kResponse: Symbol('response'), + kExtensions: Symbol('extensions'), + kProtocol: Symbol('protocol'), + kBinaryType: Symbol('binary type'), + kClosingFrame: Symbol('closing frame'), + kSentClose: Symbol('sent close'), + kReceivedClose: Symbol('received close'), + kByteParser: Symbol('byte parser') +} diff --git a/lib/websocket/util.js b/lib/websocket/util.js new file mode 100644 index 00000000000..6c59b2c2380 --- /dev/null +++ b/lib/websocket/util.js @@ -0,0 +1,200 @@ +'use strict' + +const { kReadyState, kController, kResponse, kBinaryType, kWebSocketURL } = require('./symbols') +const { states, opcodes } = require('./constants') +const { MessageEvent, ErrorEvent } = require('./events') + +/* globals Blob */ + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isEstablished (ws) { + // If the server's response is validated as provided for above, it is + // said that _The WebSocket Connection is Established_ and that the + // WebSocket Connection is in the OPEN state. + return ws[kReadyState] === states.OPEN +} + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isClosing (ws) { + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + return ws[kReadyState] === states.CLOSING +} + +/** + * @param {import('./websocket').WebSocket} ws + */ +function isClosed (ws) { + return ws[kReadyState] === states.CLOSED +} + +/** + * @see https://dom.spec.whatwg.org/#concept-event-fire + * @param {string} e + * @param {EventTarget} target + * @param {EventInit | undefined} eventInitDict + */ +function fireEvent (e, target, eventConstructor = Event, eventInitDict) { + // 1. If eventConstructor is not given, then let eventConstructor be Event. + + // 2. Let event be the result of creating an event given eventConstructor, + // in the relevant realm of target. + // 3. Initialize event’s type attribute to e. + const event = new eventConstructor(e, eventInitDict) // eslint-disable-line new-cap + + // 4. Initialize any other IDL attributes of event as described in the + // invocation of this algorithm. + + // 5. Return the result of dispatching event at target, with legacy target + // override flag set if set. + target.dispatchEvent(event) +} + +/** + * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol + * @param {import('./websocket').WebSocket} ws + * @param {number} type Opcode + * @param {Buffer} data application data + */ +function websocketMessageReceived (ws, type, data) { + // 1. If ready state is not OPEN (1), then return. + if (ws[kReadyState] !== states.OPEN) { + return + } + + // 2. Let dataForEvent be determined by switching on type and binary type: + let dataForEvent + + if (type === opcodes.TEXT) { + // -> type indicates that the data is Text + // a new DOMString containing data + try { + dataForEvent = new TextDecoder('utf-8', { fatal: true }).decode(data) + } catch { + failWebsocketConnection(ws, 'Received invalid UTF-8 in text frame.') + return + } + } else if (type === opcodes.BINARY) { + if (ws[kBinaryType] === 'blob') { + // -> type indicates that the data is Binary and binary type is "blob" + // a new Blob object, created in the relevant Realm of the WebSocket + // object, that represents data as its raw data + dataForEvent = new Blob([data]) + } else { + // -> type indicates that the data is Binary and binary type is "arraybuffer" + // a new ArrayBuffer object, created in the relevant Realm of the + // WebSocket object, whose contents are data + dataForEvent = new Uint8Array(data).buffer + } + } + + // 3. Fire an event named message at the WebSocket object, using MessageEvent, + // with the origin attribute initialized to the serialization of the WebSocket + // object’s url's origin, and the data attribute initialized to dataForEvent. + fireEvent('message', ws, MessageEvent, { + origin: ws[kWebSocketURL].origin, + data: dataForEvent + }) +} + +/** + * @see https://datatracker.ietf.org/doc/html/rfc6455 + * @see https://datatracker.ietf.org/doc/html/rfc2616 + * @see https://bugs.chromium.org/p/chromium/issues/detail?id=398407 + * @param {string} protocol + */ +function isValidSubprotocol (protocol) { + // If present, this value indicates one + // or more comma-separated subprotocol the client wishes to speak, + // ordered by preference. The elements that comprise this value + // MUST be non-empty strings with characters in the range U+0021 to + // U+007E not including separator characters as defined in + // [RFC2616] and MUST all be unique strings. + if (protocol.length === 0) { + return false + } + + for (const char of protocol) { + const code = char.charCodeAt(0) + + if ( + code < 0x21 || + code > 0x7E || + char === '(' || + char === ')' || + char === '<' || + char === '>' || + char === '@' || + char === ',' || + char === ';' || + char === ':' || + char === '\\' || + char === '"' || + char === '/' || + char === '[' || + char === ']' || + char === '?' || + char === '=' || + char === '{' || + char === '}' || + code === 32 || // SP + code === 9 // HT + ) { + return false + } + } + + return true +} + +/** + * @see https://datatracker.ietf.org/doc/html/rfc6455#section-7-4 + * @param {number} code + */ +function isValidStatusCode (code) { + if (code >= 1000 && code < 1015) { + return ( + code !== 1004 && // reserved + code !== 1005 && // "MUST NOT be set as a status code" + code !== 1006 // "MUST NOT be set as a status code" + ) + } + + return code >= 3000 && code <= 4999 +} + +/** + * @param {import('./websocket').WebSocket} ws + * @param {string|undefined} reason + */ +function failWebsocketConnection (ws, reason) { + const { [kController]: controller, [kResponse]: response } = ws + + controller.abort() + + if (response?.socket && !response.socket.destroyed) { + response.socket.destroy() + } + + if (reason) { + fireEvent('error', ws, ErrorEvent, { + error: new Error(reason) + }) + } +} + +module.exports = { + isEstablished, + isClosing, + isClosed, + fireEvent, + isValidSubprotocol, + isValidStatusCode, + failWebsocketConnection, + websocketMessageReceived +} diff --git a/lib/websocket/websocket.js b/lib/websocket/websocket.js new file mode 100644 index 00000000000..cac33d1eaab --- /dev/null +++ b/lib/websocket/websocket.js @@ -0,0 +1,556 @@ +'use strict' + +const { webidl } = require('../fetch/webidl') +const { DOMException } = require('../fetch/constants') +const { URLSerializer } = require('../fetch/dataURL') +const { staticPropertyDescriptors, states, opcodes } = require('./constants') +const { + kWebSocketURL, + kReadyState, + kController, + kExtensions, + kProtocol, + kBinaryType, + kResponse, + kSentClose +} = require('./symbols') +const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection } = require('./util') +const { establishWebSocketConnection } = require('./connection') +const { WebsocketFrameSend } = require('./frame') +const { kEnumerableProperty, isBlobLike } = require('../core/util') +const { types } = require('util') + +let experimentalWarned = false + +// https://websockets.spec.whatwg.org/#interface-definition +class WebSocket extends EventTarget { + #events = { + open: null, + error: null, + close: null, + message: null + } + + #bufferedAmount = 0 + + /** + * @param {string} url + * @param {string|string[]} protocols + */ + constructor (url, protocols = []) { + super() + + webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket constructor' }) + + if (!experimentalWarned) { + experimentalWarned = true + process.emitWarning('WebSockets are experimental, expect them to change at any time.', { + code: 'UNDICI-WS' + }) + } + + url = webidl.converters.USVString(url) + protocols = webidl.converters['DOMString or sequence'](protocols) + + // 1. Let urlRecord be the result of applying the URL parser to url. + let urlRecord + + try { + urlRecord = new URL(url) + } catch (e) { + // 2. If urlRecord is failure, then throw a "SyntaxError" DOMException. + throw new DOMException(e, 'SyntaxError') + } + + // 3. If urlRecord’s scheme is not "ws" or "wss", then throw a + // "SyntaxError" DOMException. + if (urlRecord.protocol !== 'ws:' && urlRecord.protocol !== 'wss:') { + throw new DOMException( + `Expected a ws: or wss: protocol, got ${urlRecord.protocol}`, + 'SyntaxError' + ) + } + + // 4. If urlRecord’s fragment is non-null, then throw a "SyntaxError" + // DOMException. + if (urlRecord.hash) { + throw new DOMException('Got fragment', 'SyntaxError') + } + + // 5. If protocols is a string, set protocols to a sequence consisting + // of just that string. + if (typeof protocols === 'string') { + if (protocols.length === 0) { + protocols = [] + } else { + protocols = [protocols] + } + } + + // 6. If any of the values in protocols occur more than once or otherwise + // fail to match the requirements for elements that comprise the value + // of `Sec-WebSocket-Protocol` fields as defined by The WebSocket + // protocol, then throw a "SyntaxError" DOMException. + if (protocols.length !== new Set(protocols.map(p => p.toLowerCase())).size) { + throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError') + } + + if (protocols.length > 0 && !protocols.every(p => isValidSubprotocol(p))) { + throw new DOMException('Invalid Sec-WebSocket-Protocol value', 'SyntaxError') + } + + // 7. Set this's url to urlRecord. + this[kWebSocketURL] = urlRecord + + // 8. Let client be this's relevant settings object. + + // 9. Run this step in parallel: + + // 1. Establish a WebSocket connection given urlRecord, protocols, + // and client. + this[kController] = establishWebSocketConnection(urlRecord, protocols, this) + + // Each WebSocket object has an associated ready state, which is a + // number representing the state of the connection. Initially it must + // be CONNECTING (0). + this[kReadyState] = WebSocket.CONNECTING + + // The extensions attribute must initially return the empty string. + this[kExtensions] = '' + + // The protocol attribute must initially return the empty string. + this[kProtocol] = '' + + // Each WebSocket object has an associated binary type, which is a + // BinaryType. Initially it must be "blob". + this[kBinaryType] = 'blob' + } + + /** + * @see https://websockets.spec.whatwg.org/#dom-websocket-close + * @param {number|undefined} code + * @param {string|undefined} reason + */ + close (code = undefined, reason = undefined) { + webidl.brandCheck(this, WebSocket) + + if (code !== undefined) { + code = webidl.converters['unsigned short'](code, { clamp: true }) + } + + if (reason !== undefined) { + reason = webidl.converters.USVString(reason) + } + + // 1. If code is present, but is neither an integer equal to 1000 nor an + // integer in the range 3000 to 4999, inclusive, throw an + // "InvalidAccessError" DOMException. + if (code !== undefined) { + if (code !== 1000 && (code < 3000 || code > 4999)) { + throw new DOMException('invalid code', 'InvalidAccessError') + } + } + + let reasonByteLength = 0 + + // 2. If reason is present, then run these substeps: + if (reason !== undefined) { + // 1. Let reasonBytes be the result of encoding reason. + // 2. If reasonBytes is longer than 123 bytes, then throw a + // "SyntaxError" DOMException. + reasonByteLength = Buffer.byteLength(reason) + + if (reasonByteLength > 123) { + throw new DOMException( + `Reason must be less than 123 bytes; received ${reasonByteLength}`, + 'SyntaxError' + ) + } + } + + // 3. Run the first matching steps from the following list: + if (this[kReadyState] === WebSocket.CLOSING || this[kReadyState] === WebSocket.CLOSED) { + // If this's ready state is CLOSING (2) or CLOSED (3) + // Do nothing. + } else if (!isEstablished(this)) { + // If the WebSocket connection is not yet established + // Fail the WebSocket connection and set this's ready state + // to CLOSING (2). + failWebsocketConnection(this, 'Connection was closed before it was established.') + this[kReadyState] = WebSocket.CLOSING + } else if (!isClosing(this)) { + // If the WebSocket closing handshake has not yet been started + // Start the WebSocket closing handshake and set this's ready + // state to CLOSING (2). + // - If neither code nor reason is present, the WebSocket Close + // message must not have a body. + // - If code is present, then the status code to use in the + // WebSocket Close message must be the integer given by code. + // - If reason is also present, then reasonBytes must be + // provided in the Close message after the status code. + + const frame = new WebsocketFrameSend() + + // If neither code nor reason is present, the WebSocket Close + // message must not have a body. + + // If code is present, then the status code to use in the + // WebSocket Close message must be the integer given by code. + if (code !== undefined && reason === undefined) { + frame.frameData = Buffer.allocUnsafe(2) + frame.frameData.writeUInt16BE(code, 0) + } else if (code !== undefined && reason !== undefined) { + // If reason is also present, then reasonBytes must be + // provided in the Close message after the status code. + frame.frameData = Buffer.allocUnsafe(2 + reasonByteLength) + frame.frameData.writeUInt16BE(code, 0) + // the body MAY contain UTF-8-encoded data with value /reason/ + frame.frameData.write(reason, 2, 'utf-8') + } else { + frame.frameData = Buffer.alloc(0) + } + + /** @type {import('stream').Duplex} */ + const socket = this[kResponse].socket + + socket.write(frame.createFrame(opcodes.CLOSE), (err) => { + if (!err) { + this[kSentClose] = true + } + }) + + // Upon either sending or receiving a Close control frame, it is said + // that _The WebSocket Closing Handshake is Started_ and that the + // WebSocket connection is in the CLOSING state. + this[kReadyState] = states.CLOSING + } else { + // Otherwise + // Set this's ready state to CLOSING (2). + this[kReadyState] = WebSocket.CLOSING + } + } + + /** + * @see https://websockets.spec.whatwg.org/#dom-websocket-send + * @param {NodeJS.TypedArray|ArrayBuffer|Blob|string} data + */ + send (data) { + webidl.brandCheck(this, WebSocket) + + webidl.argumentLengthCheck(arguments, 1, { header: 'WebSocket.send' }) + + data = webidl.converters.WebSocketSendData(data) + + // 1. If this's ready state is CONNECTING, then throw an + // "InvalidStateError" DOMException. + if (this[kReadyState] === WebSocket.CONNECTING) { + throw new DOMException('Sent before connected.', 'InvalidStateError') + } + + // 2. Run the appropriate set of steps from the following list: + // https://datatracker.ietf.org/doc/html/rfc6455#section-6.1 + // https://datatracker.ietf.org/doc/html/rfc6455#section-5.2 + + if (!isEstablished(this) || isClosing(this)) { + return + } + + /** @type {import('stream').Duplex} */ + const socket = this[kResponse].socket + + // If data is a string + if (typeof data === 'string') { + // If the WebSocket connection is established and the WebSocket + // closing handshake has not yet started, then the user agent + // must send a WebSocket Message comprised of the data argument + // using a text frame opcode; if the data cannot be sent, e.g. + // because it would need to be buffered but the buffer is full, + // the user agent must flag the WebSocket as full and then close + // the WebSocket connection. Any invocation of this method with a + // string argument that does not throw an exception must increase + // the bufferedAmount attribute by the number of bytes needed to + // express the argument as UTF-8. + + const value = Buffer.from(data) + const frame = new WebsocketFrameSend(value) + const buffer = frame.createFrame(opcodes.TEXT) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + } else if (types.isArrayBuffer(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need + // to be buffered but the buffer is full, the user agent must flag + // the WebSocket as full and then close the WebSocket connection. + // The data to be sent is the data stored in the buffer described + // by the ArrayBuffer object. Any invocation of this method with an + // ArrayBuffer argument that does not throw an exception must + // increase the bufferedAmount attribute by the length of the + // ArrayBuffer in bytes. + + const value = Buffer.from(data) + const frame = new WebsocketFrameSend(value) + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + } else if (ArrayBuffer.isView(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need to + // be buffered but the buffer is full, the user agent must flag the + // WebSocket as full and then close the WebSocket connection. The + // data to be sent is the data stored in the section of the buffer + // described by the ArrayBuffer object that data references. Any + // invocation of this method with this kind of argument that does + // not throw an exception must increase the bufferedAmount attribute + // by the length of data’s buffer in bytes. + + const ab = new ArrayBuffer(data.byteLength) + new data.constructor(ab).set(data) + const value = Buffer.from(ab) + + const frame = new WebsocketFrameSend(value) + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + } else if (isBlobLike(data)) { + // If the WebSocket connection is established, and the WebSocket + // closing handshake has not yet started, then the user agent must + // send a WebSocket Message comprised of data using a binary frame + // opcode; if the data cannot be sent, e.g. because it would need to + // be buffered but the buffer is full, the user agent must flag the + // WebSocket as full and then close the WebSocket connection. The data + // to be sent is the raw data represented by the Blob object. Any + // invocation of this method with a Blob argument that does not throw + // an exception must increase the bufferedAmount attribute by the size + // of the Blob object’s raw data, in bytes. + + const frame = new WebsocketFrameSend() + + data.arrayBuffer().then((ab) => { + const value = Buffer.from(ab) + frame.frameData = value + const buffer = frame.createFrame(opcodes.BINARY) + + this.#bufferedAmount += value.byteLength + socket.write(buffer, () => { + this.#bufferedAmount -= value.byteLength + }) + }) + } + } + + get readyState () { + webidl.brandCheck(this, WebSocket) + + // The readyState getter steps are to return this's ready state. + return this[kReadyState] + } + + get bufferedAmount () { + webidl.brandCheck(this, WebSocket) + + return this.#bufferedAmount + } + + get url () { + webidl.brandCheck(this, WebSocket) + + // The url getter steps are to return this's url, serialized. + return URLSerializer(this[kWebSocketURL]) + } + + get extensions () { + webidl.brandCheck(this, WebSocket) + + return this[kExtensions] + } + + get protocol () { + webidl.brandCheck(this, WebSocket) + + return this[kProtocol] + } + + get onopen () { + webidl.brandCheck(this, WebSocket) + + return this.#events.open + } + + set onopen (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.open) { + this.removeEventListener('open', this.#events.open) + } + + if (typeof fn === 'function') { + this.#events.open = fn + this.addEventListener('open', fn) + } else { + this.#events.open = null + } + } + + get onerror () { + webidl.brandCheck(this, WebSocket) + + return this.#events.error + } + + set onerror (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.error) { + this.removeEventListener('error', this.#events.error) + } + + if (typeof fn === 'function') { + this.#events.error = fn + this.addEventListener('error', fn) + } else { + this.#events.error = null + } + } + + get onclose () { + webidl.brandCheck(this, WebSocket) + + return this.#events.close + } + + set onclose (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.close) { + this.removeEventListener('close', this.#events.close) + } + + if (typeof fn === 'function') { + this.#events.close = fn + this.addEventListener('close', fn) + } else { + this.#events.close = null + } + } + + get onmessage () { + webidl.brandCheck(this, WebSocket) + + return this.#events.message + } + + set onmessage (fn) { + webidl.brandCheck(this, WebSocket) + + if (this.#events.message) { + this.removeEventListener('message', this.#events.message) + } + + if (typeof fn === 'function') { + this.#events.message = fn + this.addEventListener('message', fn) + } else { + this.#events.message = null + } + } + + get binaryType () { + webidl.brandCheck(this, WebSocket) + + return this[kBinaryType] + } + + set binaryType (type) { + webidl.brandCheck(this, WebSocket) + + if (type !== 'blob' && type !== 'arraybuffer') { + this[kBinaryType] = 'blob' + } else { + this[kBinaryType] = type + } + } +} + +// https://websockets.spec.whatwg.org/#dom-websocket-connecting +WebSocket.CONNECTING = WebSocket.prototype.CONNECTING = states.CONNECTING +// https://websockets.spec.whatwg.org/#dom-websocket-open +WebSocket.OPEN = WebSocket.prototype.OPEN = states.OPEN +// https://websockets.spec.whatwg.org/#dom-websocket-closing +WebSocket.CLOSING = WebSocket.prototype.CLOSING = states.CLOSING +// https://websockets.spec.whatwg.org/#dom-websocket-closed +WebSocket.CLOSED = WebSocket.prototype.CLOSED = states.CLOSED + +Object.defineProperties(WebSocket.prototype, { + CONNECTING: staticPropertyDescriptors, + OPEN: staticPropertyDescriptors, + CLOSING: staticPropertyDescriptors, + CLOSED: staticPropertyDescriptors, + url: kEnumerableProperty, + readyState: kEnumerableProperty, + bufferedAmount: kEnumerableProperty, + onopen: kEnumerableProperty, + onerror: kEnumerableProperty, + onclose: kEnumerableProperty, + close: kEnumerableProperty, + onmessage: kEnumerableProperty, + binaryType: kEnumerableProperty, + send: kEnumerableProperty, + extensions: kEnumerableProperty, + protocol: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'WebSocket', + writable: false, + enumerable: false, + configurable: true + } +}) + +Object.defineProperties(WebSocket, { + CONNECTING: staticPropertyDescriptors, + OPEN: staticPropertyDescriptors, + CLOSING: staticPropertyDescriptors, + CLOSED: staticPropertyDescriptors +}) + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.DOMString +) + +webidl.converters['DOMString or sequence'] = function (V) { + if (webidl.util.Type(V) === 'Object' && Symbol.iterator in V) { + return webidl.converters['sequence'](V) + } + + return webidl.converters.DOMString(V) +} + +webidl.converters.WebSocketSendData = function (V) { + if (webidl.util.Type(V) === 'Object') { + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if (ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)) { + return webidl.converters.BufferSource(V) + } + } + + return webidl.converters.USVString(V) +} + +module.exports = { + WebSocket +} diff --git a/package.json b/package.json index a192b2d674c..4cfb3af9e1b 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "test:tap": "tap test/*.js test/diagnostics-channel/*.js", "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w", "test:typescript": "tsd && tsc test/imports/undici-import.ts", - "test:wpt": "node scripts/verifyVersion 18 || (node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs)", + "test:wpt": "node scripts/verifyVersion 18 || (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)", "coverage": "nyc --reporter=text --reporter=html npm run test", "coverage:ci": "nyc --reporter=lcov npm run test", "bench": "PORT=3042 concurrently -k -s first npm:bench:server npm:bench:run", @@ -96,7 +96,8 @@ "tap": "^16.1.0", "tsd": "^0.24.1", "typescript": "^4.8.4", - "wait-on": "^6.0.0" + "wait-on": "^6.0.0", + "ws": "^8.11.0" }, "engines": { "node": ">=12.18" diff --git a/test/wpt/runner/runner/runner.mjs b/test/wpt/runner/runner/runner.mjs index 7b81a8674f2..8dfb21e8236 100644 --- a/test/wpt/runner/runner/runner.mjs +++ b/test/wpt/runner/runner/runner.mjs @@ -36,7 +36,8 @@ export class WPTRunner extends EventEmitter { completed: 0, failed: 0, success: 0, - expectedFailures: 0 + expectedFailures: 0, + skipped: 0 } constructor (folder, url) { @@ -92,6 +93,11 @@ export class WPTRunner extends EventEmitter { : readFileSync(test, 'utf-8') const meta = this.resolveMeta(code, test) + if (this.#status[basename(test)]?.skip) { + this.#stats.skipped += 1 + continue + } + const worker = new Worker(workerPath, { workerData: { // Code to load before the test harness and tests. @@ -129,7 +135,7 @@ export class WPTRunner extends EventEmitter { * Called after a test has succeeded or failed. */ handleIndividualTestCompletion (message, fileName) { - const { fail, allowUnexpectedFailures, flaky } = this.#status[fileName] ?? {} + const { fail, allowUnexpectedFailures, flaky } = this.#status[fileName] ?? this.#status if (message.type === 'result') { this.#stats.completed += 1 @@ -182,12 +188,13 @@ export class WPTRunner extends EventEmitter { ) this.emit('completion') - const { completed, failed, success, expectedFailures } = this.#stats + const { completed, failed, success, expectedFailures, skipped } = this.#stats console.log( `[${this.#folderName}]: ` + `Completed: ${completed}, failed: ${failed}, success: ${success}, ` + `expected failures: ${expectedFailures}, ` + - `unexpected failures: ${failed - expectedFailures}` + `unexpected failures: ${failed - expectedFailures}, ` + + `skipped: ${skipped}` ) } diff --git a/test/wpt/runner/runner/util.mjs b/test/wpt/runner/runner/util.mjs index 6c5e5d1cc7e..c45fdc1df44 100644 --- a/test/wpt/runner/runner/util.mjs +++ b/test/wpt/runner/runner/util.mjs @@ -39,6 +39,7 @@ export function parseMeta (fileContents) { } switch (groups.type) { + case 'variant': case 'title': case 'timeout': { meta[groups.type] = groups.match diff --git a/test/wpt/runner/runner/worker.mjs b/test/wpt/runner/runner/worker.mjs index f5ff9a1cc95..bf50dca4396 100644 --- a/test/wpt/runner/runner/worker.mjs +++ b/test/wpt/runner/runner/worker.mjs @@ -12,6 +12,8 @@ import { Headers, FileReader } from '../../../../index.js' +import { WebSocket } from '../../../../lib/websocket/websocket.js' +import { CloseEvent } from '../../../../lib/websocket/events.js' const { initScripts, meta, test, url, path } = workerData @@ -53,6 +55,14 @@ Object.defineProperties(globalThis, { FileReader: { ...globalPropertyDescriptors, value: FileReader + }, + WebSocket: { + ...globalPropertyDescriptors, + value: WebSocket + }, + CloseEvent: { + ...globalPropertyDescriptors, + value: CloseEvent } }) diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs index 86edcf65150..b8dd9fc3143 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -43,6 +43,7 @@ const server = createServer(async (req, res) => { case '/interfaces/html.idl': case '/interfaces/fetch.idl': case '/interfaces/FileAPI.idl': + case '/interfaces/websockets.idl': case '/interfaces/referrer-policy.idl': case '/xhr/resources/utf16-bom.json': case '/fetch/data-urls/resources/base64.json': @@ -338,3 +339,5 @@ process.on('message', (message) => { server.close((err) => err ? send(err) : send({ message: 'shutdown' })) } }) + +export { server } diff --git a/test/wpt/server/websocket.mjs b/test/wpt/server/websocket.mjs new file mode 100644 index 00000000000..4d27eea3257 --- /dev/null +++ b/test/wpt/server/websocket.mjs @@ -0,0 +1,52 @@ +import { WebSocketServer } from 'ws' +import { server } from './server.mjs' + +// When sending a buffer to ws' send method, it auto +// sets the type to binary. This breaks some tests. +const textData = [ + '¥¥¥¥¥¥', + 'Message to send', + '𐐇', + '\ufffd', + '', + 'null', + 'c'.repeat(65000) +] + +// The file router server handles sending the url, closing, +// and sending messages back to the main process for us. +// The types for WebSocketServer don't include a `request` +// event, so I'm unsure if we can stop relying on server. + +const wss = new WebSocketServer({ + server, + handleProtocols: (protocols) => [...protocols].join(', ') +}) + +wss.on('connection', (ws) => { + ws.on('message', (data) => { + const str = data.toString('utf-8') + + if (str === 'Goodbye') { + // Close-server-initiated-close.any.js sends a "Goodbye" message + // when it wants the server to close the connection. + ws.close(1000) + return + } + + const binary = !textData.includes(str) + ws.send(data, { binary }) + }) + + // Some tests, such as `Create-blocked-port.any.js` do NOT + // close the connection automatically. + const timeout = setTimeout(() => { + if (ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) { + ws.close() + } + }, 2500) + + ws.on('close', () => { + clearTimeout(timeout) + }) +}) diff --git a/test/wpt/start-websockets.mjs b/test/wpt/start-websockets.mjs new file mode 100644 index 00000000000..a7078c6d8d3 --- /dev/null +++ b/test/wpt/start-websockets.mjs @@ -0,0 +1,26 @@ +import { WPTRunner } from './runner/runner/runner.mjs' +import { join } from 'path' +import { fileURLToPath } from 'url' +import { fork } from 'child_process' +import { on } from 'events' + +const serverPath = fileURLToPath(join(import.meta.url, '../server/websocket.mjs')) + +const child = fork(serverPath, [], { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'] +}) + +for await (const [message] of on(child, 'message')) { + if (message.server) { + const runner = new WPTRunner('websockets', message.server) + runner.run() + + runner.once('completion', () => { + if (child.connected) { + child.send('shutdown') + } + }) + } else if (message.message === 'shutdown') { + process.exit() + } +} diff --git a/test/wpt/status/fetch.status.json b/test/wpt/status/fetch.status.json index b9d689f4778..fdd45f568ec 100644 --- a/test/wpt/status/fetch.status.json +++ b/test/wpt/status/fetch.status.json @@ -207,4 +207,4 @@ "fetch() with value %1F" ] } -} \ No newline at end of file +} diff --git a/test/wpt/status/websockets.status.json b/test/wpt/status/websockets.status.json new file mode 100644 index 00000000000..4f0efbdded9 --- /dev/null +++ b/test/wpt/status/websockets.status.json @@ -0,0 +1,37 @@ +{ + "allowUnexpectedFailures": true, + "Create-on-worker-shutdown.any.js": { + "skip": true, + "//": "Node.js workers are different from web workers & don't work with blob: urls" + }, + "Close-delayed.any.js": { + "skip": true + }, + "bufferedAmount-unchanged-by-sync-xhr.any.js": { + "skip": true, + "//": "Node.js doesn't have XMLHttpRequest nor does this test make sense regardless" + }, + "referrer.any.js": { + "skip": true + }, + "Send-binary-blob.any.js": { + "flaky": [ + "Send binary data on a WebSocket - Blob - Connection should be closed" + ] + }, + "Send-65K-data.any.js": { + "flaky": [ + "Send 65K data on a WebSocket - Connection should be closed" + ] + }, + "Send-binary-65K-arraybuffer.any.js": { + "flaky": [ + "Send 65K binary data on a WebSocket - ArrayBuffer - Connection should be closed" + ] + }, + "Send-0byte-data.any.js": { + "flaky": [ + "Send 0 byte data on a WebSocket - Connection should be closed" + ] + } +} diff --git a/test/wpt/tests/interfaces/websockets.idl b/test/wpt/tests/interfaces/websockets.idl new file mode 100644 index 00000000000..6ff16790b03 --- /dev/null +++ b/test/wpt/tests/interfaces/websockets.idl @@ -0,0 +1,48 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: WebSockets Standard (https://websockets.spec.whatwg.org/) + +enum BinaryType { "blob", "arraybuffer" }; + +[Exposed=(Window,Worker)] +interface WebSocket : EventTarget { + constructor(USVString url, optional (DOMString or sequence) protocols = []); + readonly attribute USVString url; + + // ready state + const unsigned short CONNECTING = 0; + const unsigned short OPEN = 1; + const unsigned short CLOSING = 2; + const unsigned short CLOSED = 3; + readonly attribute unsigned short readyState; + readonly attribute unsigned long long bufferedAmount; + + // networking + attribute EventHandler onopen; + attribute EventHandler onerror; + attribute EventHandler onclose; + readonly attribute DOMString extensions; + readonly attribute DOMString protocol; + undefined close(optional [Clamp] unsigned short code, optional USVString reason); + + // messaging + attribute EventHandler onmessage; + attribute BinaryType binaryType; + undefined send((BufferSource or Blob or USVString) data); +}; + +[Exposed=(Window,Worker)] +interface CloseEvent : Event { + constructor(DOMString type, optional CloseEventInit eventInitDict = {}); + + readonly attribute boolean wasClean; + readonly attribute unsigned short code; + readonly attribute USVString reason; +}; + +dictionary CloseEventInit : EventInit { + boolean wasClean = false; + unsigned short code = 0; + USVString reason = ""; +}; diff --git a/test/wpt/tests/websockets/Close-1000-reason.any.js b/test/wpt/tests/websockets/Close-1000-reason.any.js new file mode 100644 index 00000000000..6fc3c1fade8 --- /dev/null +++ b/test/wpt/tests/websockets/Close-1000-reason.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - close(1000, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.close(1000, "Clean Close"); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be opened"); + assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)"); + assert_equals(evt.wasClean, true, "wasClean should be TRUE"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Close-1000-verify-code.any.js b/test/wpt/tests/websockets/Close-1000-verify-code.any.js new file mode 100644 index 00000000000..de501306602 --- /dev/null +++ b/test/wpt/tests/websockets/Close-1000-verify-code.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - close(1000, reason) - event.code == 1000 and event.reason = 'Clean Close'"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.close(1000, "Clean Close"); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(evt.code, 1000, "CloseEvent.code should be 1000"); + assert_equals(evt.reason, "Clean Close", "CloseEvent.reason should be the same as the reason sent in close"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Close-1000.any.js b/test/wpt/tests/websockets/Close-1000.any.js new file mode 100644 index 00000000000..f3100c6caaa --- /dev/null +++ b/test/wpt/tests/websockets/Close-1000.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - close(1000) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.close(1000); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be opened"); + assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)"); + assert_equals(evt.wasClean, true, "wasClean should be TRUE"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Close-1005-verify-code.any.js b/test/wpt/tests/websockets/Close-1005-verify-code.any.js new file mode 100644 index 00000000000..afa7d7b0d98 --- /dev/null +++ b/test/wpt/tests/websockets/Close-1005-verify-code.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - close() - return close code is 1005 - Connection should be closed"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.close(); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(evt.code, 1005, "CloseEvent.code should be 1005"); + assert_equals(evt.reason, "", "CloseEvent.reason should be empty"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Close-1005.any.js b/test/wpt/tests/websockets/Close-1005.any.js new file mode 100644 index 00000000000..514d03ac632 --- /dev/null +++ b/test/wpt/tests/websockets/Close-1005.any.js @@ -0,0 +1,18 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - close(1005) - see '7.1.5. The WebSocket Connection Close Code' in http://www.ietf.org/rfc/rfc6455.txt"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + assert_throws_dom("INVALID_ACCESS_ERR", function() { + wsocket.close(1005, "1005 - reserved code") + }); + test.done(); +}), true); + +wsocket.addEventListener('close', test.unreached_func('close event should not fire'), true); diff --git a/test/wpt/tests/websockets/Close-2999-reason.any.js b/test/wpt/tests/websockets/Close-2999-reason.any.js new file mode 100644 index 00000000000..95e481e53cf --- /dev/null +++ b/test/wpt/tests/websockets/Close-2999-reason.any.js @@ -0,0 +1,17 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - close(2999, reason) - INVALID_ACCESS_ERR is thrown"); + +var wsocket = CreateWebSocket(false, false); + +wsocket.addEventListener('open', test.step_func(function(evt) { + assert_throws_dom("INVALID_ACCESS_ERR", function() { + wsocket.close(2999, "Close not in range 3000-4999") + }); + test.done(); +}), true); + +wsocket.addEventListener('close', test.unreached_func('close event should not fire'), true); diff --git a/test/wpt/tests/websockets/Close-3000-reason.any.js b/test/wpt/tests/websockets/Close-3000-reason.any.js new file mode 100644 index 00000000000..2db122934c7 --- /dev/null +++ b/test/wpt/tests/websockets/Close-3000-reason.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - close(3000, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.close(3000, "Clean Close with code - 3000"); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)"); + assert_equals(evt.wasClean, true, "wasClean should be TRUE"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Close-3000-verify-code.any.js b/test/wpt/tests/websockets/Close-3000-verify-code.any.js new file mode 100644 index 00000000000..bfa441f1ee4 --- /dev/null +++ b/test/wpt/tests/websockets/Close-3000-verify-code.any.js @@ -0,0 +1,20 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - close(3000, reason) - verify return code is 3000 - Connection should be closed"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.close(3000, "Clean Close"); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(evt.code, 3000, "CloseEvent.code should be 3000"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Close-4999-reason.any.js b/test/wpt/tests/websockets/Close-4999-reason.any.js new file mode 100644 index 00000000000..3516dc2f462 --- /dev/null +++ b/test/wpt/tests/websockets/Close-4999-reason.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - close(4999, reason) - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.close(3000, "Clean Close with code - 4999"); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)"); + assert_equals(evt.wasClean, true, "wasClean should be TRUE"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Close-Reason-124Bytes.any.js b/test/wpt/tests/websockets/Close-Reason-124Bytes.any.js new file mode 100644 index 00000000000..aa7fc8ffe83 --- /dev/null +++ b/test/wpt/tests/websockets/Close-Reason-124Bytes.any.js @@ -0,0 +1,20 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Create WebSocket - Close the Connection - close(code, 'reason more than 123 bytes') - SYNTAX_ERR is thrown"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + var reason = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123"; + assert_equals(reason.length, 124); + assert_throws_dom("SYNTAX_ERR", function() { + wsocket.close(1000, reason) + }); + test.done(); +}), true); + +wsocket.addEventListener('close', test.unreached_func('close event should not fire'), true); diff --git a/test/wpt/tests/websockets/Close-delayed.any.js b/test/wpt/tests/websockets/Close-delayed.any.js new file mode 100644 index 00000000000..212925bb931 --- /dev/null +++ b/test/wpt/tests/websockets/Close-delayed.any.js @@ -0,0 +1,27 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - close should not emit until handshake completes - Connection should be closed"); + +var wsocket = new WebSocket(`${SCHEME_DOMAIN_PORT}/delayed-passive-close`); +var startTime; +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + startTime = performance.now(); + wsocket.close(); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + const elapsed = performance.now() - startTime; + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)"); + assert_equals(evt.wasClean, true, "wasClean should be TRUE"); + const jitterAllowance = 100; + assert_greater_than_equal(elapsed, 1000 - jitterAllowance, + 'one second should have elapsed') + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Close-onlyReason.any.js b/test/wpt/tests/websockets/Close-onlyReason.any.js new file mode 100644 index 00000000000..7c5d10d2a83 --- /dev/null +++ b/test/wpt/tests/websockets/Close-onlyReason.any.js @@ -0,0 +1,17 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - close(only reason) - INVALID_ACCESS_ERR is thrown"); + +var wsocket = CreateWebSocket(false, false); + +wsocket.addEventListener('open', test.step_func(function(evt) { + assert_throws_dom("INVALID_ACCESS_ERR", function() { + wsocket.close("Close with only reason") + }); + test.done(); +}), true); + +wsocket.addEventListener('close', test.unreached_func('close event should not fire'), true); diff --git a/test/wpt/tests/websockets/Close-readyState-Closed.any.js b/test/wpt/tests/websockets/Close-readyState-Closed.any.js new file mode 100644 index 00000000000..bfd61c48c14 --- /dev/null +++ b/test/wpt/tests/websockets/Close-readyState-Closed.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.close(); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)"); + assert_equals(evt.wasClean, true, "wasClean should be TRUE"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Close-readyState-Closing.any.js b/test/wpt/tests/websockets/Close-readyState-Closing.any.js new file mode 100644 index 00000000000..554744d6297 --- /dev/null +++ b/test/wpt/tests/websockets/Close-readyState-Closing.any.js @@ -0,0 +1,20 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - readyState should be in CLOSING state just before onclose is called"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + isOpenCalled = true; + wsocket.close(); + assert_equals(wsocket.readyState, 2, "readyState should be 2(CLOSING)"); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, 'open must be called'); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Close-reason-unpaired-surrogates.any.js b/test/wpt/tests/websockets/Close-reason-unpaired-surrogates.any.js new file mode 100644 index 00000000000..647a1216b99 --- /dev/null +++ b/test/wpt/tests/websockets/Close-reason-unpaired-surrogates.any.js @@ -0,0 +1,22 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Close the Connection - close(reason with unpaired surrogates) - connection should get closed"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var replacementChar = "\uFFFD"; +var reason = "\uD807"; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.close(1000, reason); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be opened"); + assert_equals(evt.reason, replacementChar, "reason replaced with replacement character"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Close-server-initiated-close.any.js b/test/wpt/tests/websockets/Close-server-initiated-close.any.js new file mode 100644 index 00000000000..c86793b23a1 --- /dev/null +++ b/test/wpt/tests/websockets/Close-server-initiated-close.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Create WebSocket - Server initiated Close - Client sends back a CLOSE - readyState should be in CLOSED state and wasClean is TRUE - Connection should be closed"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.send("Goodbye"); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(wsocket.readyState, 3, "readyState should be 3(CLOSED)"); + assert_equals(evt.wasClean, true, "wasClean should be TRUE"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Close-undefined.any.js b/test/wpt/tests/websockets/Close-undefined.any.js new file mode 100644 index 00000000000..a8106c6f155 --- /dev/null +++ b/test/wpt/tests/websockets/Close-undefined.any.js @@ -0,0 +1,19 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test(); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + isOpenCalled = true; + wsocket.close(undefined); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, 'open event must fire'); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Create-asciiSep-protocol-string.any.js b/test/wpt/tests/websockets/Create-asciiSep-protocol-string.any.js new file mode 100644 index 00000000000..1221c561145 --- /dev/null +++ b/test/wpt/tests/websockets/Create-asciiSep-protocol-string.any.js @@ -0,0 +1,12 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +test(function() { + var asciiWithSep = "/echo"; + var wsocket; + assert_throws_dom("SYNTAX_ERR", function() { + wsocket = CreateWebSocketWithAsciiSep(asciiWithSep) + }); +}, "Create WebSocket - Pass a valid URL and a protocol string with an ascii separator character - SYNTAX_ERR is thrown") diff --git a/test/wpt/tests/websockets/Create-blocked-port.any.js b/test/wpt/tests/websockets/Create-blocked-port.any.js new file mode 100644 index 00000000000..c670009b25d --- /dev/null +++ b/test/wpt/tests/websockets/Create-blocked-port.any.js @@ -0,0 +1,97 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +async_test(t => { + const ws = CreateWebSocketWithBlockedPort(__PORT) + ws.onerror = t.unreached_func() + ws.onopen = t.step_func_done() +}, 'Basic check'); +// list of bad ports according to +// https://fetch.spec.whatwg.org/#port-blocking +[ + 1, // tcpmux + 7, // echo + 9, // discard + 11, // systat + 13, // daytime + 15, // netstat + 17, // qotd + 19, // chargen + 20, // ftp-data + 21, // ftp + 22, // ssh + 23, // telnet + 25, // smtp + 37, // time + 42, // name + 43, // nicname + 53, // domain + 69, // tftp + 77, // priv-rjs + 79, // finger + 87, // ttylink + 95, // supdup + 101, // hostriame + 102, // iso-tsap + 103, // gppitnp + 104, // acr-nema + 109, // pop2 + 110, // pop3 + 111, // sunrpc + 113, // auth + 115, // sftp + 117, // uucp-path + 119, // nntp + 123, // ntp + 135, // loc-srv / epmap + 137, // netbios-ns + 139, // netbios-ssn + 143, // imap2 + 179, // bgp + 389, // ldap + 427, // afp (alternate) + 465, // smtp (alternate) + 512, // print / exec + 513, // login + 514, // shell + 515, // printer + 526, // tempo + 530, // courier + 531, // chat + 532, // netnews + 540, // uucp + 548, // afp + 554, // rtsp + 556, // remotefs + 563, // nntp+ssl + 587, // smtp (outgoing) + 601, // syslog-conn + 636, // ldap+ssl + 989, // ftps-data + 990, // ftps + 993, // ldap+ssl + 995, // pop3+ssl + 1719, // h323gatestat + 1720, // h323hostcall + 1723, // pptp + 2049, // nfs + 3659, // apple-sasl + 4045, // lockd + 6000, // x11 + 6566, // sane-port + 6665, // irc (alternate) + 6666, // irc (alternate) + 6667, // irc (default) + 6668, // irc (alternate) + 6669, // irc (alternate) + 6697, // irc+tls + 10080, // amanda +].forEach(blockedPort => { + async_test(t => { + const ws = CreateWebSocketWithBlockedPort(blockedPort) + ws.onerror = t.step_func_done() + ws.onopen = t.unreached_func() + }, "WebSocket blocked port test " + blockedPort) +}) diff --git a/test/wpt/tests/websockets/Create-extensions-empty.any.js b/test/wpt/tests/websockets/Create-extensions-empty.any.js new file mode 100644 index 00000000000..1fba4bd2cc5 --- /dev/null +++ b/test/wpt/tests/websockets/Create-extensions-empty.any.js @@ -0,0 +1,20 @@ +// META: timeout=long +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Create WebSocket - wsocket.extensions should be set to '' after connection is established - Connection should be closed"); + +var wsocket = new WebSocket(SCHEME_DOMAIN_PORT + "/handshake_no_extensions"); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func_done(function(evt) { + wsocket.close(); + isOpenCalled = true; + assert_equals(wsocket.extensions, "", "extensions should be empty"); +}), true); + +wsocket.addEventListener('close', test.step_func_done(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be closed"); +}), true); diff --git a/test/wpt/tests/websockets/Create-invalid-urls.any.js b/test/wpt/tests/websockets/Create-invalid-urls.any.js new file mode 100644 index 00000000000..89783a9ea74 --- /dev/null +++ b/test/wpt/tests/websockets/Create-invalid-urls.any.js @@ -0,0 +1,34 @@ +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var wsocket; +test(function() { + assert_throws_dom("SYNTAX_ERR", function() { + wsocket = new WebSocket("/echo") + }); +}, "Url is /echo - should throw SYNTAX_ERR"); + +test(function() { + assert_throws_dom("SYNTAX_ERR", function() { + wsocket = new WebSocket("mailto:microsoft@microsoft.com") + }); +}, "Url is a mail address - should throw SYNTAX_ERR"); + +test(function() { + assert_throws_dom("SYNTAX_ERR", function() { + wsocket = new WebSocket("about:blank") + }); +}, "Url is about:blank - should throw SYNTAX_ERR"); + +test(function() { + assert_throws_dom("SYNTAX_ERR", function() { + wsocket = new WebSocket("?test") + }); +}, "Url is ?test - should throw SYNTAX_ERR"); + +test(function() { + assert_throws_dom("SYNTAX_ERR", function() { + wsocket = new WebSocket("#test") + }); +}, "Url is #test - should throw SYNTAX_ERR"); diff --git a/test/wpt/tests/websockets/Create-non-absolute-url.any.js b/test/wpt/tests/websockets/Create-non-absolute-url.any.js new file mode 100644 index 00000000000..8d533fd2e04 --- /dev/null +++ b/test/wpt/tests/websockets/Create-non-absolute-url.any.js @@ -0,0 +1,11 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +test(function() { + var wsocket; + assert_throws_dom("SYNTAX_ERR", function() { + wsocket = CreateWebSocketNonAbsolute() + }); +}, "Create WebSocket - Pass a non absolute URL - SYNTAX_ERR is thrown") diff --git a/test/wpt/tests/websockets/Create-nonAscii-protocol-string.any.js b/test/wpt/tests/websockets/Create-nonAscii-protocol-string.any.js new file mode 100644 index 00000000000..1b56cc914b7 --- /dev/null +++ b/test/wpt/tests/websockets/Create-nonAscii-protocol-string.any.js @@ -0,0 +1,12 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +test(function() { + var nonAsciiProtocol = "\u0080echo"; + var wsocket; + assert_throws_dom("SYNTAX_ERR", function() { + wsocket = CreateWebSocketNonAsciiProtocol(nonAsciiProtocol) + }); +}, "Create WebSocket - Pass a valid URL and a protocol string with non-ascii values - SYNTAX_ERR is thrown") diff --git a/test/wpt/tests/websockets/Create-on-worker-shutdown.any.js b/test/wpt/tests/websockets/Create-on-worker-shutdown.any.js new file mode 100644 index 00000000000..218bf7cf196 --- /dev/null +++ b/test/wpt/tests/websockets/Create-on-worker-shutdown.any.js @@ -0,0 +1,26 @@ +async_test(t => { + function workerCode() { + close(); + var ws = new WebSocket(self.location.origin.replace('http', 'ws')); + var data = { + originalState: ws.readyState, + afterCloseState: null + }; + + ws.close(); + + data.afterCloseState = ws.readyState; + postMessage(data); + } + + var workerBlob = new Blob([workerCode.toString() + ";workerCode();"], { + type: "application/javascript" + }); + + var w = new Worker(URL.createObjectURL(workerBlob)); + w.onmessage = t.step_func(function(e) { + assert_equals(e.data.originalState, WebSocket.CONNECTING, "WebSocket created on worker shutdown is in connecting state."); + assert_equals(e.data.afterCloseState, WebSocket.CLOSING, "Closed WebSocket created on worker shutdown is in closing state."); + t.done(); + }); +}, 'WebSocket created after a worker self.close()'); diff --git a/test/wpt/tests/websockets/Create-protocol-with-space.any.js b/test/wpt/tests/websockets/Create-protocol-with-space.any.js new file mode 100644 index 00000000000..f49d1fec0c3 --- /dev/null +++ b/test/wpt/tests/websockets/Create-protocol-with-space.any.js @@ -0,0 +1,11 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +test(function() { + var wsocket; + assert_throws_dom("SYNTAX_ERR", function() { + wsocket = CreateWebSocketWithSpaceInProtocol("ec ho") + }); +}, "Create WebSocket - Pass a valid URL and a protocol string with a space in it - SYNTAX_ERR is thrown") diff --git a/test/wpt/tests/websockets/Create-protocols-repeated-case-insensitive.any.js b/test/wpt/tests/websockets/Create-protocols-repeated-case-insensitive.any.js new file mode 100644 index 00000000000..41f78396fc3 --- /dev/null +++ b/test/wpt/tests/websockets/Create-protocols-repeated-case-insensitive.any.js @@ -0,0 +1,11 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +test(function() { + var wsocket; + assert_throws_dom("SYNTAX_ERR", function() { + wsocket = CreateWebSocketWithRepeatedProtocolsCaseInsensitive() + }); +}, "Create WebSocket - Pass a valid URL and an array of protocol strings with repeated values but different case - SYNTAX_ERR is thrown") diff --git a/test/wpt/tests/websockets/Create-protocols-repeated.any.js b/test/wpt/tests/websockets/Create-protocols-repeated.any.js new file mode 100644 index 00000000000..fc7d1b6ad2f --- /dev/null +++ b/test/wpt/tests/websockets/Create-protocols-repeated.any.js @@ -0,0 +1,11 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +test(function() { + var wsocket; + assert_throws_dom("SYNTAX_ERR", function() { + wsocket = CreateWebSocketWithRepeatedProtocols() + }); +}, "Create WebSocket - Pass a valid URL and an array of protocol strings with repeated values - SYNTAX_ERR is thrown") diff --git a/test/wpt/tests/websockets/Create-url-with-space.any.js b/test/wpt/tests/websockets/Create-url-with-space.any.js new file mode 100644 index 00000000000..d1e1ea1cba9 --- /dev/null +++ b/test/wpt/tests/websockets/Create-url-with-space.any.js @@ -0,0 +1,12 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +test(function() { + var wsocket; + var spaceUrl = "web platform.test"; + assert_throws_dom("SYNTAX_ERR", function() { + wsocket = CreateWebSocketWithSpaceInUrl(spaceUrl) + }); +}, "Create WebSocket - Pass a URL with a space - SYNTAX_ERR should be thrown") diff --git a/test/wpt/tests/websockets/Create-valid-url-array-protocols.any.js b/test/wpt/tests/websockets/Create-valid-url-array-protocols.any.js new file mode 100644 index 00000000000..00ab1ca9873 --- /dev/null +++ b/test/wpt/tests/websockets/Create-valid-url-array-protocols.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Create WebSocket - Pass a valid URL and array of protocol strings - Connection should be closed"); + +var wsocket = CreateWebSocket(false, true); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)"); + wsocket.close(); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Create-valid-url-binaryType-blob.any.js b/test/wpt/tests/websockets/Create-valid-url-binaryType-blob.any.js new file mode 100644 index 00000000000..59eec8e29d3 --- /dev/null +++ b/test/wpt/tests/websockets/Create-valid-url-binaryType-blob.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Create WebSocket - wsocket.binaryType should be set to 'blob' after connection is established - Connection should be closed"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + assert_equals(wsocket.binaryType, "blob", "binaryType should be set to Blob"); + wsocket.close(); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Create-valid-url-protocol-empty.any.js b/test/wpt/tests/websockets/Create-valid-url-protocol-empty.any.js new file mode 100644 index 00000000000..9e1de6dab46 --- /dev/null +++ b/test/wpt/tests/websockets/Create-valid-url-protocol-empty.any.js @@ -0,0 +1,10 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +test(function() { + var wsocket = CreateWebSocket(true, false); + assert_equals(wsocket.protocol, "", "protocol should be empty"); + wsocket.close(); +}, "Create WebSocket - wsocket.protocol should be empty before connection is established") diff --git a/test/wpt/tests/websockets/Create-valid-url-protocol-setCorrectly.any.js b/test/wpt/tests/websockets/Create-valid-url-protocol-setCorrectly.any.js new file mode 100644 index 00000000000..bb1f32fbce1 --- /dev/null +++ b/test/wpt/tests/websockets/Create-valid-url-protocol-setCorrectly.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Create WebSocket - Pass a valid URL and protocol string - Connection should be closed"); + +var wsocket = CreateWebSocket(true, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + assert_equals(wsocket.protocol, "echo", "protocol should be set to echo"); + wsocket.close(); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Create-valid-url-protocol-string.any.js b/test/wpt/tests/websockets/Create-valid-url-protocol-string.any.js new file mode 100644 index 00000000000..4f730db94f7 --- /dev/null +++ b/test/wpt/tests/websockets/Create-valid-url-protocol-string.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - Pass a valid URL and protocol string - Connection should be closed"); + +var wsocket = CreateWebSocket(true, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)"); + wsocket.close(); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Create-valid-url-protocol.any.js b/test/wpt/tests/websockets/Create-valid-url-protocol.any.js new file mode 100644 index 00000000000..599a9eb8f1b --- /dev/null +++ b/test/wpt/tests/websockets/Create-valid-url-protocol.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Create WebSocket - Pass a valid URL and a protocol string - Connection should be closed"); + +var wsocket = CreateWebSocket(true, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)"); + wsocket.close(); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Create-valid-url.any.js b/test/wpt/tests/websockets/Create-valid-url.any.js new file mode 100644 index 00000000000..edb27f61d3c --- /dev/null +++ b/test/wpt/tests/websockets/Create-valid-url.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Create WebSocket - Pass a valid URL - Connection should be closed"); + +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + assert_equals(wsocket.readyState, 1, "readyState should be 1(OPEN)"); + wsocket.close(); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Create-wrong-scheme.any.js b/test/wpt/tests/websockets/Create-wrong-scheme.any.js new file mode 100644 index 00000000000..00cfffece60 --- /dev/null +++ b/test/wpt/tests/websockets/Create-wrong-scheme.any.js @@ -0,0 +1,11 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +test(function() { + var wsocket; + assert_throws_dom("SYNTAX_ERR", function() { + wsocket = CreateWebSocketNonWsScheme() + }); +}, "Create WebSocket - Pass a URL with a non ws/wss scheme - SYNTAX_ERR is thrown") diff --git a/test/wpt/tests/websockets/Send-0byte-data.any.js b/test/wpt/tests/websockets/Send-0byte-data.any.js new file mode 100644 index 00000000000..b984b641084 --- /dev/null +++ b/test/wpt/tests/websockets/Send-0byte-data.any.js @@ -0,0 +1,30 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Send 0 byte data on a WebSocket - Connection should be closed"); + +var data = ""; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.send(data); + assert_equals(data.length, wsocket.bufferedAmount); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + assert_equals(evt.data, data); + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received") + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-65K-data.any.js b/test/wpt/tests/websockets/Send-65K-data.any.js new file mode 100644 index 00000000000..5c3437999b9 --- /dev/null +++ b/test/wpt/tests/websockets/Send-65K-data.any.js @@ -0,0 +1,33 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send 65K data on a WebSocket - Connection should be closed"); + +var data = ""; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + for (var i = 0; i < 65000; i++) { + data = data + "c"; + } + wsocket.send(data); + assert_equals(data.length, wsocket.bufferedAmount); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + assert_equals(evt.data, data); + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received") + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-before-open.any.js b/test/wpt/tests/websockets/Send-before-open.any.js new file mode 100644 index 00000000000..5982535583f --- /dev/null +++ b/test/wpt/tests/websockets/Send-before-open.any.js @@ -0,0 +1,11 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +test(function() { + var wsocket = CreateWebSocket(false, false); + assert_throws_dom("INVALID_STATE_ERR", function() { + wsocket.send("Message to send") + }); +}, "Send data on a WebSocket before connection is opened - INVALID_STATE_ERR is returned") diff --git a/test/wpt/tests/websockets/Send-binary-65K-arraybuffer.any.js b/test/wpt/tests/websockets/Send-binary-65K-arraybuffer.any.js new file mode 100644 index 00000000000..1e02ac2d37f --- /dev/null +++ b/test/wpt/tests/websockets/Send-binary-65K-arraybuffer.any.js @@ -0,0 +1,33 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send 65K binary data on a WebSocket - ArrayBuffer - Connection should be closed"); + +var data = ""; +var datasize = 65000; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.binaryType = "arraybuffer"; + data = new ArrayBuffer(datasize); + wsocket.send(data); + assert_equals(datasize, wsocket.bufferedAmount); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + assert_equals(evt.data.byteLength, datasize); + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received") + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-binary-arraybuffer.any.js b/test/wpt/tests/websockets/Send-binary-arraybuffer.any.js new file mode 100644 index 00000000000..5c985edd616 --- /dev/null +++ b/test/wpt/tests/websockets/Send-binary-arraybuffer.any.js @@ -0,0 +1,33 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send binary data on a WebSocket - ArrayBuffer - Connection should be closed"); + +var data = ""; +var datasize = 15; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.binaryType = "arraybuffer"; + data = new ArrayBuffer(datasize); + wsocket.send(data); + assert_equals(datasize, wsocket.bufferedAmount); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + assert_equals(evt.data.byteLength, datasize); + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received") + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-float32.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-float32.any.js new file mode 100644 index 00000000000..9a8e3426f49 --- /dev/null +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-float32.any.js @@ -0,0 +1,40 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Float32Array - Connection should be closed"); + +var data = ""; +var datasize = 8; +var view; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.binaryType = "arraybuffer"; + data = new ArrayBuffer(datasize); + view = new Float32Array(data); + for (var i = 0; i < 2; i++) { + view[i] = i; + } + wsocket.send(view); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + var resultView = new Float32Array(evt.data); + for (var i = 0; i < resultView.length; i++) { + assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same"); + } + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received") + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-float64.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-float64.any.js new file mode 100644 index 00000000000..d71d2d8c58f --- /dev/null +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-float64.any.js @@ -0,0 +1,40 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Float64Array - Connection should be closed"); + +var data = ""; +var datasize = 8; +var view; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.binaryType = "arraybuffer"; + data = new ArrayBuffer(datasize); + view = new Float64Array(data); + for (var i = 0; i < 1; i++) { + view[i] = i; + } + wsocket.send(view); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + var resultView = new Float64Array(evt.data); + for (var i = 0; i < resultView.length; i++) { + assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same"); + } + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received") + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-int16-offset.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-int16-offset.any.js new file mode 100644 index 00000000000..bb77d300ad1 --- /dev/null +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-int16-offset.any.js @@ -0,0 +1,40 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Int16Array with offset - Connection should be closed"); + +var data = ""; +var datasize = 8; +var view; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.binaryType = "arraybuffer"; + data = new ArrayBuffer(datasize); + view = new Int16Array(data, 2); + for (var i = 0; i < 4; i++) { + view[i] = i; + } + wsocket.send(view); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + var resultView = new Int16Array(evt.data); + for (var i = 0; i < resultView.length; i++) { + assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same"); + } + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received") + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-int32.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-int32.any.js new file mode 100644 index 00000000000..f4312e410ab --- /dev/null +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-int32.any.js @@ -0,0 +1,40 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Int32Array - Connection should be closed"); + +var data = ""; +var datasize = 8; +var view; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.binaryType = "arraybuffer"; + data = new ArrayBuffer(datasize); + view = new Int32Array(data); + for (var i = 0; i < 2; i++) { + view[i] = i; + } + wsocket.send(view); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + var resultView = new Int32Array(evt.data); + for (var i = 0; i < resultView.length; i++) { + assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same"); + } + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received") + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-int8.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-int8.any.js new file mode 100644 index 00000000000..f2374fb4139 --- /dev/null +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-int8.any.js @@ -0,0 +1,40 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Int8Array - Connection should be closed"); + +var data = ""; +var datasize = 8; +var int8View; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.binaryType = "arraybuffer"; + data = new ArrayBuffer(datasize); + int8View = new Int8Array(data); + for (var i = 0; i < 8; i++) { + int8View[i] = i; + } + wsocket.send(int8View); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + var resultView = new Int8Array(evt.data); + for (var i = 0; i < resultView.length; i++) { + assert_equals(resultView[i], int8View[i], "ArrayBufferView returned is the same"); + } + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received") + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint16-offset-length.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint16-offset-length.any.js new file mode 100644 index 00000000000..f917a3af007 --- /dev/null +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint16-offset-length.any.js @@ -0,0 +1,40 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Uint16Array with offset and length - Connection should be closed"); + +var data = ""; +var datasize = 8; +var view; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.binaryType = "arraybuffer"; + data = new ArrayBuffer(datasize); + view = new Uint16Array(data, 2, 2); + for (var i = 0; i < 4; i++) { + view[i] = i; + } + wsocket.send(view); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + var resultView = new Uint16Array(evt.data); + for (var i = 0; i < resultView.length; i++) { + assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same"); + } + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received") + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint32-offset.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint32-offset.any.js new file mode 100644 index 00000000000..33758dc6544 --- /dev/null +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint32-offset.any.js @@ -0,0 +1,40 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Uint32Array with offset - Connection should be closed"); + +var data = ""; +var datasize = 8; +var view; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.binaryType = "arraybuffer"; + data = new ArrayBuffer(datasize); + view = new Uint32Array(data, 0); + for (var i = 0; i < 2; i++) { + view[i] = i; + } + wsocket.send(view); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + var resultView = new Uint32Array(evt.data); + for (var i = 0; i < resultView.length; i++) { + assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same"); + } + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received") + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset-length.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset-length.any.js new file mode 100644 index 00000000000..1d256dbdca1 --- /dev/null +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset-length.any.js @@ -0,0 +1,40 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset and length - Connection should be closed"); + +var data = ""; +var datasize = 8; +var view; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.binaryType = "arraybuffer"; + data = new ArrayBuffer(datasize); + view = new Uint8Array(data, 2, 4); + for (var i = 0; i < 8; i++) { + view[i] = i; + } + wsocket.send(view); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + var resultView = new Uint8Array(evt.data); + for (var i = 0; i < resultView.length; i++) { + assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same"); + } + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received") + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset.any.js b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset.any.js new file mode 100644 index 00000000000..43e9fe68499 --- /dev/null +++ b/test/wpt/tests/websockets/Send-binary-arraybufferview-uint8-offset.any.js @@ -0,0 +1,40 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send binary data on a WebSocket - ArrayBufferView - Uint8Array with offset - Connection should be closed"); + +var data = ""; +var datasize = 8; +var view; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.binaryType = "arraybuffer"; + data = new ArrayBuffer(datasize); + view = new Uint8Array(data, 2); + for (var i = 0; i < 8; i++) { + view[i] = i; + } + wsocket.send(view); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + var resultView = new Uint8Array(evt.data); + for (var i = 0; i < resultView.length; i++) { + assert_equals(resultView[i], view[i], "ArrayBufferView returned is the same"); + } + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received") + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-binary-blob.any.js b/test/wpt/tests/websockets/Send-binary-blob.any.js new file mode 100644 index 00000000000..56c89a1b53c --- /dev/null +++ b/test/wpt/tests/websockets/Send-binary-blob.any.js @@ -0,0 +1,36 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send binary data on a WebSocket - Blob - Connection should be closed"); + +var data = ""; +var datasize = 65000; +var isOpenCalled = false; +var isMessageCalled = false; + +var wsocket = CreateWebSocket(false, false); + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.binaryType = "blob"; + for (var i = 0; i < datasize; i++) + data += String.fromCharCode(0); + data = new Blob([data]); + isOpenCalled = true; + wsocket.send(data); +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + assert_true(evt.data instanceof Blob); + assert_equals(evt.data.size, datasize); + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received"); + assert_true(evt.wasClean, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-data.any.js b/test/wpt/tests/websockets/Send-data.any.js new file mode 100644 index 00000000000..203ab54dffc --- /dev/null +++ b/test/wpt/tests/websockets/Send-data.any.js @@ -0,0 +1,30 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Send data on a WebSocket - Connection should be closed"); + +var data = "Message to send"; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.send(data); + assert_equals(data.length, wsocket.bufferedAmount); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + assert_equals(evt.data, data); + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isMessageCalled, "message should be received"); + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-null.any.js b/test/wpt/tests/websockets/Send-null.any.js new file mode 100644 index 00000000000..a12eaf9c591 --- /dev/null +++ b/test/wpt/tests/websockets/Send-null.any.js @@ -0,0 +1,32 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send null data on a WebSocket - Connection should be closed"); + +var data = null; +var nullReturned = false; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.send(data); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + if ("null" == evt.data || "" == evt.data) + nullReturned = true; + assert_true(nullReturned); + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received"); + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-paired-surrogates.any.js b/test/wpt/tests/websockets/Send-paired-surrogates.any.js new file mode 100644 index 00000000000..e2dc004bfcf --- /dev/null +++ b/test/wpt/tests/websockets/Send-paired-surrogates.any.js @@ -0,0 +1,30 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send paired surrogates data on a WebSocket - Connection should be closed"); + +var data = "\uD801\uDC07"; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.send(data); + assert_equals(data.length * 2, wsocket.bufferedAmount); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + assert_equals(evt.data, data); + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received"); + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-unicode-data.any.js b/test/wpt/tests/websockets/Send-unicode-data.any.js new file mode 100644 index 00000000000..f22094a243c --- /dev/null +++ b/test/wpt/tests/websockets/Send-unicode-data.any.js @@ -0,0 +1,30 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wpt_flags=h2 +// META: variant=?wss + +var test = async_test("Send unicode data on a WebSocket - Connection should be closed"); + +var data = "¥¥¥¥¥¥"; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.send(data); + assert_equals(data.length * 2, wsocket.bufferedAmount); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + assert_equals(evt.data, data); + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received"); + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/Send-unpaired-surrogates.any.js b/test/wpt/tests/websockets/Send-unpaired-surrogates.any.js new file mode 100644 index 00000000000..1cb5d0ac9cd --- /dev/null +++ b/test/wpt/tests/websockets/Send-unpaired-surrogates.any.js @@ -0,0 +1,30 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Send unpaired surrogates on a WebSocket - Connection should be closed"); + +var data = "\uD807"; +var replacementChar = "\uFFFD"; +var wsocket = CreateWebSocket(false, false); +var isOpenCalled = false; +var isMessageCalled = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + wsocket.send(data); + isOpenCalled = true; +}), true); + +wsocket.addEventListener('message', test.step_func(function(evt) { + isMessageCalled = true; + assert_equals(evt.data, replacementChar); + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(isOpenCalled, "WebSocket connection should be open"); + assert_true(isMessageCalled, "message should be received"); + assert_equals(evt.wasClean, true, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/basic-auth.any.js b/test/wpt/tests/websockets/basic-auth.any.js new file mode 100644 index 00000000000..9fbdc5d5ced --- /dev/null +++ b/test/wpt/tests/websockets/basic-auth.any.js @@ -0,0 +1,17 @@ +// META: global=window,worker +// META: script=constants.sub.js +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +async_test(t => { + const url = __SCHEME + '://' + 'foo:bar@' + __SERVER__NAME + ':' + __PORT + '/basic_auth'; + const ws = new WebSocket(url); + ws.onopen = () => { + ws.onclose = ws.onerror = null; + ws.close(); + t.done(); + }; + ws.onerror = ws.onclose = t.unreached_func('open should succeed'); +}, 'HTTP basic authentication should work with WebSockets'); + +done(); diff --git a/test/wpt/tests/websockets/binaryType-wrong-value.any.js b/test/wpt/tests/websockets/binaryType-wrong-value.any.js new file mode 100644 index 00000000000..007510d030b --- /dev/null +++ b/test/wpt/tests/websockets/binaryType-wrong-value.any.js @@ -0,0 +1,23 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +var test = async_test("Create WebSocket - set binaryType to something other than blob or arraybuffer - SYNTAX_ERR is returned - Connection should be closed"); + +let wsocket = CreateWebSocket(false, false); +let opened = false; + +wsocket.addEventListener('open', test.step_func(function(evt) { + opened = true; + assert_equals(wsocket.binaryType, "blob"); + wsocket.binaryType = "notBlobOrArrayBuffer"; + assert_equals(wsocket.binaryType, "blob"); + wsocket.close(); +}), true); + +wsocket.addEventListener('close', test.step_func(function(evt) { + assert_true(opened, "connection should be opened"); + assert_true(evt.wasClean, "wasClean should be true"); + test.done(); +}), true); diff --git a/test/wpt/tests/websockets/bufferedAmount-unchanged-by-sync-xhr.any.js b/test/wpt/tests/websockets/bufferedAmount-unchanged-by-sync-xhr.any.js new file mode 100644 index 00000000000..b247ee56f62 --- /dev/null +++ b/test/wpt/tests/websockets/bufferedAmount-unchanged-by-sync-xhr.any.js @@ -0,0 +1,25 @@ +// META: script=constants.sub.js +// META: global=window,dedicatedworker,sharedworker +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +async_test(t => { + const ws = CreateWebSocket(false, false); + ws.onopen = t.step_func(() => { + ws.onclose = ws.onerror = null; + assert_equals(ws.bufferedAmount, 0); + ws.send('hello'); + assert_equals(ws.bufferedAmount, 5); + // Stop execution for 1s with a sync XHR. + const xhr = new XMLHttpRequest(); + xhr.open('GET', '/common/blank.html?pipe=trickle(d1)', false); + xhr.send(); + assert_equals(ws.bufferedAmount, 5); + ws.close(); + t.done(); + }); + ws.onerror = ws.onclose = t.unreached_func('open should succeed'); +}, 'bufferedAmount should not be updated during a sync XHR'); + +done(); diff --git a/test/wpt/tests/websockets/close-invalid.any.js b/test/wpt/tests/websockets/close-invalid.any.js new file mode 100644 index 00000000000..3223063765a --- /dev/null +++ b/test/wpt/tests/websockets/close-invalid.any.js @@ -0,0 +1,21 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +[ + [0, "0"], + [500, "500"], + [NaN, "NaN"], + ["string", "String"], + [null, "null"], + [0x10000 + 1000, "2**16+1000"], +].forEach(function(t) { + test(function() { + var ws = CreateWebSocket(false, false); + assert_throws_dom("InvalidAccessError", function() { + ws.close(t[0]); + }); + ws.onerror = this.unreached_func(); + }, t[1] + " on a websocket"); +}); diff --git a/test/wpt/tests/websockets/constants.sub.js b/test/wpt/tests/websockets/constants.sub.js new file mode 100644 index 00000000000..65ea4f66f29 --- /dev/null +++ b/test/wpt/tests/websockets/constants.sub.js @@ -0,0 +1,100 @@ +const __SERVER__NAME = "{{host}}"; +const __PATH = "echo"; + +var __SCHEME; +var __PORT; +if (url_has_variant('wss')) { + __SCHEME = 'wss'; + __PORT = "{{ports[wss][0]}}"; +} else if (url_has_flag('h2')) { + __SCHEME = 'wss'; + __PORT = "{{ports[h2][0]}}"; +} else { + __SCHEME = 'ws'; + __PORT = "{{ports[ws][0]}}"; +} + +const SCHEME_DOMAIN_PORT = __SCHEME + '://' + __SERVER__NAME + ':' + __PORT; + +function url_has_variant(variant) { + const params = new URLSearchParams(location.search); + return params.get(variant) === ""; +} + +function url_has_flag(flag) { + const params = new URLSearchParams(location.search); + return params.getAll("wpt_flags").indexOf(flag) !== -1; +} + +function IsWebSocket() { + if (!self.WebSocket) { + assert_true(false, "Browser does not support WebSocket"); + } +} + +function CreateWebSocketNonAbsolute() { + IsWebSocket(); + const url = __SERVER__NAME; + return new WebSocket(url); +} + +function CreateWebSocketNonWsScheme() { + IsWebSocket(); + const url = "http://" + __SERVER__NAME + ":" + __PORT + "/" + __PATH; + return new WebSocket(url); +} + +function CreateWebSocketNonAsciiProtocol(nonAsciiProtocol) { + IsWebSocket(); + const url = SCHEME_DOMAIN_PORT + "/" + __PATH; + return new WebSocket(url, nonAsciiProtocol); +} + +function CreateWebSocketWithAsciiSep(asciiWithSep) { + IsWebSocket(); + const url = SCHEME_DOMAIN_PORT + "/" + __PATH; + return new WebSocket(url, asciiWithSep); +} + +function CreateWebSocketWithBlockedPort(blockedPort) { + IsWebSocket(); + const url = __SCHEME + "://" + __SERVER__NAME + ":" + blockedPort + "/" + __PATH; + return new WebSocket(url); +} + +function CreateWebSocketWithSpaceInUrl(urlWithSpace) { + IsWebSocket(); + const url = __SCHEME + "://" + urlWithSpace + ":" + __PORT + "/" + __PATH; + return new WebSocket(url); +} + +function CreateWebSocketWithSpaceInProtocol(protocolWithSpace) { + IsWebSocket(); + const url = SCHEME_DOMAIN_PORT + "/" + __PATH; + return new WebSocket(url, protocolWithSpace); +} + +function CreateWebSocketWithRepeatedProtocols() { + IsWebSocket(); + const url = SCHEME_DOMAIN_PORT + "/" + __PATH; + return new WebSocket(url, ["echo", "echo"]); +} + +function CreateWebSocketWithRepeatedProtocolsCaseInsensitive() { + IsWebSocket(); + const url = SCHEME_DOMAIN_PORT + "/" + __PATH; + wsocket = new WebSocket(url, ["echo", "eCho"]); +} + +function CreateWebSocket(isProtocol, isProtocols) { + IsWebSocket(); + const url = SCHEME_DOMAIN_PORT + "/" + __PATH; + + if (isProtocol) { + return new WebSocket(url, "echo"); + } + if (isProtocols) { + return new WebSocket(url, ["echo", "chat"]); + } + return new WebSocket(url); +} diff --git a/test/wpt/tests/websockets/constructor.any.js b/test/wpt/tests/websockets/constructor.any.js new file mode 100644 index 00000000000..c92dda4567c --- /dev/null +++ b/test/wpt/tests/websockets/constructor.any.js @@ -0,0 +1,10 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +test(function() { + var ws = new WebSocket(SCHEME_DOMAIN_PORT + "/" + __PATH, + "echo", "Stray argument") + assert_true(ws instanceof WebSocket, "Expected a WebSocket instance.") +}, "Calling the WebSocket constructor with too many arguments should not throw.") diff --git a/test/wpt/tests/websockets/eventhandlers.any.js b/test/wpt/tests/websockets/eventhandlers.any.js new file mode 100644 index 00000000000..7bccd47139b --- /dev/null +++ b/test/wpt/tests/websockets/eventhandlers.any.js @@ -0,0 +1,15 @@ +// META: script=constants.sub.js +// META: variant= +// META: variant=?wss +// META: variant=?wpt_flags=h2 + +function testEventHandler(name) { + test(function() { + var ws = CreateWebSocket(true, false); + assert_equals(ws["on" + name], null); + ws["on" + name] = function() {}; + ws["on" + name] = 2; + assert_equals(ws["on" + name], null); + }, "Event handler for " + name + " should have [TreatNonCallableAsNull]") +} +["open", "error", "close", "message"].forEach(testEventHandler); diff --git a/test/wpt/tests/websockets/idlharness.any.js b/test/wpt/tests/websockets/idlharness.any.js new file mode 100644 index 00000000000..653cc36d21d --- /dev/null +++ b/test/wpt/tests/websockets/idlharness.any.js @@ -0,0 +1,17 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +// https://websockets.spec.whatwg.org/ + +"use strict"; + +idl_test( + ['websockets'], + ['html', 'dom'], + idl_array => { + idl_array.add_objects({ + WebSocket: ['new WebSocket("ws://invalid")'], + CloseEvent: ['new CloseEvent("close")'], + }); + } +); diff --git a/test/wpt/tests/websockets/referrer.any.js b/test/wpt/tests/websockets/referrer.any.js new file mode 100644 index 00000000000..0972a1dc4df --- /dev/null +++ b/test/wpt/tests/websockets/referrer.any.js @@ -0,0 +1,13 @@ +// META: script=constants.sub.js + +async_test(t => { + const ws = new WebSocket(SCHEME_DOMAIN_PORT + "/referrer"); + ws.onmessage = t.step_func_done(e => { + assert_equals(e.data, "MISSING AS PER FETCH"); + ws.close(); + }); + + // Avoid timeouts in case of failure + ws.onclose = t.unreached_func("close"); + ws.onerror = t.unreached_func("error"); +}, "Ensure no Referer header is included"); diff --git a/types/patch.d.ts b/types/patch.d.ts index f3500b589a4..3871acfebc6 100644 --- a/types/patch.d.ts +++ b/types/patch.d.ts @@ -49,3 +49,23 @@ export interface EventInit { cancelable?: boolean composed?: boolean } + +export interface EventListenerOptions { + capture?: boolean +} + +export interface AddEventListenerOptions extends EventListenerOptions { + once?: boolean + passive?: boolean + signal?: AbortSignal +} + +export type EventListenerOrEventListenerObject = EventListener | EventListenerObject + +export interface EventListenerObject { + handleEvent (object: Event): void +} + +export interface EventListener { + (evt: Event): void +} diff --git a/types/webidl.d.ts b/types/webidl.d.ts index 1b49fd6d769..ae33d2f87c1 100644 --- a/types/webidl.d.ts +++ b/types/webidl.d.ts @@ -102,10 +102,15 @@ interface WebidlConverters { */ ['unsigned long long'] (V: unknown): number + /** + * @see https://webidl.spec.whatwg.org/#es-unsigned-long + */ + ['unsigned long'] (V: unknown): number + /** * @see https://webidl.spec.whatwg.org/#es-unsigned-short */ - ['unsigned short'] (V: unknown): number + ['unsigned short'] (V: unknown, opts?: ConvertToIntOpts): number /** * @see https://webidl.spec.whatwg.org/#idl-ArrayBuffer diff --git a/types/websocket.d.ts b/types/websocket.d.ts new file mode 100644 index 00000000000..25c46a14c37 --- /dev/null +++ b/types/websocket.d.ts @@ -0,0 +1,121 @@ +/// + +import { + EventTarget, + Event, + EventInit, + EventListenerOptions, + AddEventListenerOptions, + EventListenerOrEventListenerObject +} from './patch' + +export type BinaryType = 'blob' | 'arraybuffer' + +interface WebSocketEventMap { + close: CloseEvent + error: Event + message: MessageEvent + open: Event +} + +interface WebSocket extends EventTarget { + binaryType: BinaryType + + readonly bufferedAmount: number + readonly extensions: string + + onclose: ((this: WebSocket, ev: WebSocketEventMap['close']) => any) | null + onerror: ((this: WebSocket, ev: WebSocketEventMap['error']) => any) | null + onmessage: ((this: WebSocket, ev: WebSocketEventMap['message']) => any) | null + onopen: ((this: WebSocket, ev: WebSocketEventMap['open']) => any) | null + + readonly protocol: string + readonly readyState: number + readonly url: string + + close(code?: number, reason?: string): void + send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void + + readonly CLOSED: number + readonly CLOSING: number + readonly CONNECTING: number + readonly OPEN: number + + addEventListener( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + options?: boolean | AddEventListenerOptions + ): void + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions + ): void + removeEventListener( + type: K, + listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, + options?: boolean | EventListenerOptions + ): void + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions + ): void +} + +export declare const WebSocket: { + prototype: WebSocket + new (url: string | URL, protocols?: string | string[]): WebSocket + readonly CLOSED: number + readonly CLOSING: number + readonly CONNECTING: number + readonly OPEN: number +} + +interface CloseEventInit extends EventInit { + code?: number + reason?: string + wasClean?: boolean +} + +interface CloseEvent extends Event { + readonly code: number + readonly reason: string + readonly wasClean: boolean +} + +export declare const CloseEvent: { + prototype: CloseEvent + new (type: string, eventInitDict?: CloseEventInit): CloseEvent +} + +interface MessageEventInit extends EventInit { + data?: T + lastEventId?: string + origin?: string + ports?: (typeof MessagePort)[] + source?: typeof MessagePort | null +} + +interface MessageEvent extends Event { + readonly data: T + readonly lastEventId: string + readonly origin: string + readonly ports: ReadonlyArray + readonly source: typeof MessagePort | null + initMessageEvent( + type: string, + bubbles?: boolean, + cancelable?: boolean, + data?: any, + origin?: string, + lastEventId?: string, + source?: typeof MessagePort | null, + ports?: (typeof MessagePort)[] + ): void; +} + +export declare const MessageEvent: { + prototype: MessageEvent + new(type: string, eventInitDict?: MessageEventInit): MessageEvent +}