1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-28 10:09:20 -05:00
denoland-deno/ext/websocket/01_websocket.js
Ricardo Iván Vieitez Parra 98403691d1
fix: call setIsTrusted for generated events (MessageEvent) (#19919)
This addresses issue #19918.

## Issue description

Event messages have the wrong isTrusted value when they are not
triggered by user interaction, which differs from the browser. In
particular, all MessageEvents created by Deno have isTrusted set to
false, even though it should be true.

This is my first ever contribution to Deno, so I might be missing
something.
2023-07-31 23:22:07 +02:00

596 lines
15 KiB
JavaScript

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
// deno-lint-ignore-file camelcase
/// <reference path="../../core/internal.d.ts" />
const core = globalThis.Deno.core;
import { URL } from "ext:deno_url/00_url.js";
import * as webidl from "ext:deno_webidl/00_webidl.js";
import { HTTP_TOKEN_CODE_POINT_RE } from "ext:deno_web/00_infra.js";
import DOMException from "ext:deno_web/01_dom_exception.js";
import {
_skipInternalInit,
CloseEvent,
defineEventHandler,
dispatch,
ErrorEvent,
Event,
EventTarget,
MessageEvent,
setIsTrusted,
} from "ext:deno_web/02_event.js";
import { Blob, BlobPrototype } from "ext:deno_web/09_file.js";
import { getLocationHref } from "ext:deno_web/12_location.js";
const primordials = globalThis.__bootstrap.primordials;
const {
ArrayBufferPrototype,
ArrayBufferIsView,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypeSome,
DataView,
ErrorPrototypeToString,
ObjectDefineProperties,
ObjectPrototypeIsPrototypeOf,
PromisePrototypeThen,
RegExpPrototypeExec,
SafeSet,
SetPrototypeGetSize,
// TODO(lucacasonato): add SharedArrayBuffer to primordials
// SharedArrayBufferPrototype
String,
StringPrototypeEndsWith,
StringPrototypeToLowerCase,
Symbol,
SymbolIterator,
PromisePrototypeCatch,
SymbolFor,
TypedArrayPrototypeGetByteLength,
} = primordials;
const op_ws_check_permission_and_cancel_handle =
core.ops.op_ws_check_permission_and_cancel_handle;
const {
op_ws_create,
op_ws_close,
op_ws_send_binary,
op_ws_send_text,
op_ws_next_event,
op_ws_get_buffer,
op_ws_get_buffer_as_string,
op_ws_get_error,
op_ws_send_ping,
op_ws_get_buffered_amount,
} = core.ensureFastOps();
webidl.converters["sequence<DOMString> or DOMString"] = (
V,
prefix,
context,
opts,
) => {
// Union for (sequence<DOMString> or DOMString)
if (webidl.type(V) === "Object" && V !== null) {
if (V[SymbolIterator] !== undefined) {
return webidl.converters["sequence<DOMString>"](V, prefix, context, opts);
}
}
return webidl.converters.DOMString(V, prefix, context, opts);
};
webidl.converters["WebSocketSend"] = (V, prefix, context, opts) => {
// Union for (Blob or ArrayBufferView or ArrayBuffer or USVString)
if (ObjectPrototypeIsPrototypeOf(BlobPrototype, V)) {
return webidl.converters["Blob"](V, prefix, context, opts);
}
if (typeof V === "object") {
if (
ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, V) ||
// deno-lint-ignore prefer-primordials
ObjectPrototypeIsPrototypeOf(SharedArrayBuffer.prototype, V)
) {
return webidl.converters["ArrayBuffer"](V, prefix, context, opts);
}
if (ArrayBufferIsView(V)) {
return webidl.converters["ArrayBufferView"](V, prefix, context, opts);
}
}
return webidl.converters["USVString"](V, prefix, context, opts);
};
/** role */
const SERVER = 0;
const CLIENT = 1;
/** state */
const CONNECTING = 0;
const OPEN = 1;
const CLOSING = 2;
const CLOSED = 3;
const _readyState = Symbol("[[readyState]]");
const _url = Symbol("[[url]]");
const _rid = Symbol("[[rid]]");
const _role = Symbol("[[role]]");
const _extensions = Symbol("[[extensions]]");
const _protocol = Symbol("[[protocol]]");
const _binaryType = Symbol("[[binaryType]]");
const _eventLoop = Symbol("[[eventLoop]]");
const _server = Symbol("[[server]]");
const _idleTimeoutDuration = Symbol("[[idleTimeout]]");
const _idleTimeoutTimeout = Symbol("[[idleTimeoutTimeout]]");
const _serverHandleIdleTimeout = Symbol("[[serverHandleIdleTimeout]]");
class WebSocket extends EventTarget {
constructor(url, protocols = []) {
super();
this[webidl.brand] = webidl.brand;
this[_rid] = undefined;
this[_role] = undefined;
this[_readyState] = CONNECTING;
this[_extensions] = "";
this[_protocol] = "";
this[_url] = "";
this[_binaryType] = "blob";
this[_idleTimeoutDuration] = 0;
this[_idleTimeoutTimeout] = undefined;
const prefix = "Failed to construct 'WebSocket'";
webidl.requiredArguments(arguments.length, 1, prefix);
url = webidl.converters.USVString(url, prefix, "Argument 1");
protocols = webidl.converters["sequence<DOMString> or DOMString"](
protocols,
prefix,
"Argument 2",
);
let wsURL;
try {
wsURL = new URL(url, getLocationHref());
} catch (e) {
throw new DOMException(e.message, "SyntaxError");
}
if (wsURL.protocol === "http:") {
wsURL.protocol = "ws:";
} else if (wsURL.protocol === "https:") {
wsURL.protocol = "wss:";
}
if (wsURL.protocol !== "ws:" && wsURL.protocol !== "wss:") {
throw new DOMException(
"Only ws & wss schemes are allowed in a WebSocket URL.",
"SyntaxError",
);
}
if (wsURL.hash !== "" || StringPrototypeEndsWith(wsURL.href, "#")) {
throw new DOMException(
"Fragments are not allowed in a WebSocket URL.",
"SyntaxError",
);
}
this[_url] = wsURL.href;
this[_role] = CLIENT;
op_ws_check_permission_and_cancel_handle(
"WebSocket.abort()",
this[_url],
false,
);
if (typeof protocols === "string") {
protocols = [protocols];
}
if (
protocols.length !==
SetPrototypeGetSize(
new SafeSet(
ArrayPrototypeMap(protocols, (p) => StringPrototypeToLowerCase(p)),
),
)
) {
throw new DOMException(
"Can't supply multiple times the same protocol.",
"SyntaxError",
);
}
if (
ArrayPrototypeSome(
protocols,
(protocol) =>
RegExpPrototypeExec(HTTP_TOKEN_CODE_POINT_RE, protocol) === null,
)
) {
throw new DOMException(
"Invalid protocol value.",
"SyntaxError",
);
}
PromisePrototypeThen(
op_ws_create(
"new WebSocket()",
wsURL.href,
ArrayPrototypeJoin(protocols, ", "),
),
(create) => {
this[_rid] = create.rid;
this[_extensions] = create.extensions;
this[_protocol] = create.protocol;
if (this[_readyState] === CLOSING) {
PromisePrototypeThen(
op_ws_close(this[_rid]),
() => {
this[_readyState] = CLOSED;
const errEvent = new ErrorEvent("error");
this.dispatchEvent(errEvent);
const event = new CloseEvent("close");
this.dispatchEvent(event);
core.tryClose(this[_rid]);
},
);
} else {
this[_readyState] = OPEN;
const event = new Event("open");
this.dispatchEvent(event);
this[_eventLoop]();
}
},
(err) => {
this[_readyState] = CLOSED;
const errorEv = new ErrorEvent(
"error",
{ error: err, message: ErrorPrototypeToString(err) },
);
this.dispatchEvent(errorEv);
const closeEv = new CloseEvent("close");
this.dispatchEvent(closeEv);
},
);
}
get readyState() {
webidl.assertBranded(this, WebSocketPrototype);
return this[_readyState];
}
get CONNECTING() {
webidl.assertBranded(this, WebSocketPrototype);
return CONNECTING;
}
get OPEN() {
webidl.assertBranded(this, WebSocketPrototype);
return OPEN;
}
get CLOSING() {
webidl.assertBranded(this, WebSocketPrototype);
return CLOSING;
}
get CLOSED() {
webidl.assertBranded(this, WebSocketPrototype);
return CLOSED;
}
get extensions() {
webidl.assertBranded(this, WebSocketPrototype);
return this[_extensions];
}
get protocol() {
webidl.assertBranded(this, WebSocketPrototype);
return this[_protocol];
}
get url() {
webidl.assertBranded(this, WebSocketPrototype);
return this[_url];
}
get binaryType() {
webidl.assertBranded(this, WebSocketPrototype);
return this[_binaryType];
}
set binaryType(value) {
webidl.assertBranded(this, WebSocketPrototype);
value = webidl.converters.DOMString(
value,
"Failed to set 'binaryType' on 'WebSocket'",
);
if (value === "blob" || value === "arraybuffer") {
this[_binaryType] = value;
}
}
get bufferedAmount() {
webidl.assertBranded(this, WebSocketPrototype);
if (this[_readyState] === OPEN) {
return op_ws_get_buffered_amount(this[_rid]);
} else {
return 0;
}
}
send(data) {
webidl.assertBranded(this, WebSocketPrototype);
const prefix = "Failed to execute 'send' on 'WebSocket'";
webidl.requiredArguments(arguments.length, 1, prefix);
data = webidl.converters.WebSocketSend(data, prefix, "Argument 1");
if (this[_readyState] !== OPEN) {
throw new DOMException("readyState not OPEN", "InvalidStateError");
}
if (ArrayBufferIsView(data)) {
op_ws_send_binary(this[_rid], data);
} else if (ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, data)) {
// deno-lint-ignore prefer-primordials
op_ws_send_binary(this[_rid], new Uint8Array(data));
} else if (ObjectPrototypeIsPrototypeOf(BlobPrototype, data)) {
PromisePrototypeThen(
// deno-lint-ignore prefer-primordials
data.slice().arrayBuffer(),
(ab) =>
op_ws_send_binary(
this[_rid],
new DataView(ab),
),
);
} else {
const string = String(data);
op_ws_send_text(
this[_rid],
string,
);
}
}
close(code = undefined, reason = undefined) {
webidl.assertBranded(this, WebSocketPrototype);
const prefix = "Failed to execute 'close' on 'WebSocket'";
if (code !== undefined) {
code = webidl.converters["unsigned short"](code, prefix, "Argument 1", {
clamp: true,
});
}
if (reason !== undefined) {
reason = webidl.converters.USVString(reason, prefix, "Argument 2");
}
if (!this[_server]) {
if (
code !== undefined &&
!(code === 1000 || (3000 <= code && code < 5000))
) {
throw new DOMException(
"The close code must be either 1000 or in the range of 3000 to 4999.",
"InvalidAccessError",
);
}
}
if (
reason !== undefined &&
TypedArrayPrototypeGetByteLength(core.encode(reason)) > 123
) {
throw new DOMException(
"The close reason may not be longer than 123 bytes.",
"SyntaxError",
);
}
if (this[_readyState] === CONNECTING) {
this[_readyState] = CLOSING;
} else if (this[_readyState] === OPEN) {
this[_readyState] = CLOSING;
PromisePrototypeCatch(
op_ws_close(
this[_rid],
code,
reason,
),
(err) => {
this[_readyState] = CLOSED;
const errorEv = new ErrorEvent("error", {
error: err,
message: ErrorPrototypeToString(err),
});
this.dispatchEvent(errorEv);
const closeEv = new CloseEvent("close");
this.dispatchEvent(closeEv);
core.tryClose(this[_rid]);
},
);
}
}
async [_eventLoop]() {
const rid = this[_rid];
while (this[_readyState] !== CLOSED) {
const kind = await op_ws_next_event(rid);
switch (kind) {
case 0: {
/* string */
this[_serverHandleIdleTimeout]();
const event = new MessageEvent("message", {
data: op_ws_get_buffer_as_string(rid),
origin: this[_url],
});
setIsTrusted(event, true);
dispatch(this, event);
break;
}
case 1: {
/* binary */
this[_serverHandleIdleTimeout]();
// deno-lint-ignore prefer-primordials
const buffer = op_ws_get_buffer(rid).buffer;
let data;
if (this.binaryType === "blob") {
data = new Blob([buffer]);
} else {
data = buffer;
}
const event = new MessageEvent("message", {
data,
origin: this[_url],
[_skipInternalInit]: true,
});
setIsTrusted(event, true);
dispatch(this, event);
break;
}
case 2: {
/* pong */
this[_serverHandleIdleTimeout]();
break;
}
case 3: {
/* error */
this[_readyState] = CLOSED;
const errorEv = new ErrorEvent("error", {
message: op_ws_get_error(rid),
});
this.dispatchEvent(errorEv);
const closeEv = new CloseEvent("close");
this.dispatchEvent(closeEv);
core.tryClose(rid);
break;
}
default: {
/* close */
const code = kind;
const reason = code == 1005 ? "" : op_ws_get_error(rid);
const prevState = this[_readyState];
this[_readyState] = CLOSED;
clearTimeout(this[_idleTimeoutTimeout]);
if (prevState === OPEN) {
try {
await op_ws_close(
rid,
code,
reason,
);
} catch {
// ignore failures
}
}
const event = new CloseEvent("close", {
wasClean: true,
code: code,
reason,
});
this.dispatchEvent(event);
core.tryClose(rid);
break;
}
}
}
}
[_serverHandleIdleTimeout]() {
if (this[_idleTimeoutDuration]) {
clearTimeout(this[_idleTimeoutTimeout]);
this[_idleTimeoutTimeout] = setTimeout(async () => {
if (this[_readyState] === OPEN) {
await op_ws_send_ping(this[_rid]);
this[_idleTimeoutTimeout] = setTimeout(async () => {
if (this[_readyState] === OPEN) {
this[_readyState] = CLOSING;
const reason = "No response from ping frame.";
await op_ws_close(this[_rid], 1001, reason);
this[_readyState] = CLOSED;
const errEvent = new ErrorEvent("error", {
message: reason,
});
this.dispatchEvent(errEvent);
const event = new CloseEvent("close", {
wasClean: false,
code: 1001,
reason,
});
this.dispatchEvent(event);
core.tryClose(this[_rid]);
} else {
clearTimeout(this[_idleTimeoutTimeout]);
}
}, (this[_idleTimeoutDuration] / 2) * 1000);
} else {
clearTimeout(this[_idleTimeoutTimeout]);
}
}, (this[_idleTimeoutDuration] / 2) * 1000);
}
}
[SymbolFor("Deno.customInspect")](inspect) {
return `${this.constructor.name} ${
inspect({
url: this.url,
readyState: this.readyState,
extensions: this.extensions,
protocol: this.protocol,
binaryType: this.binaryType,
bufferedAmount: this.bufferedAmount,
})
}`;
}
}
ObjectDefineProperties(WebSocket, {
CONNECTING: {
value: 0,
},
OPEN: {
value: 1,
},
CLOSING: {
value: 2,
},
CLOSED: {
value: 3,
},
});
defineEventHandler(WebSocket.prototype, "message");
defineEventHandler(WebSocket.prototype, "error");
defineEventHandler(WebSocket.prototype, "close");
defineEventHandler(WebSocket.prototype, "open");
webidl.configurePrototype(WebSocket);
const WebSocketPrototype = WebSocket.prototype;
export {
_eventLoop,
_idleTimeoutDuration,
_idleTimeoutTimeout,
_protocol,
_readyState,
_rid,
_role,
_server,
_serverHandleIdleTimeout,
SERVER,
WebSocket,
};