summaryrefslogtreecommitdiff
path: root/ext/http/02_websocket.ts
diff options
context:
space:
mode:
authorMatt Mastracci <matthew@mastracci.com>2024-04-19 20:02:39 -0600
committerGitHub <noreply@github.com>2024-04-19 20:02:39 -0600
commit9425dce6dbc3a10bbde963d7a6192884c47185a5 (patch)
tree0dd4aeaf3dc3e5403095a5e37750a09adb3599a8 /ext/http/02_websocket.ts
parent79e6751cf753612f99438ee2f158f54a1bf44815 (diff)
refactor(ext/http): extract 02_websocket.ts from 01_http.js (#23460)
Landing part of https://github.com/denoland/deno/pull/21903 This will allow us to more easily refactor `serveHttp` to live on top of `serve` by splitting the websocket code out. There's probably a lot more we could do here but this helps.
Diffstat (limited to 'ext/http/02_websocket.ts')
-rw-r--r--ext/http/02_websocket.ts185
1 files changed, 185 insertions, 0 deletions
diff --git a/ext/http/02_websocket.ts b/ext/http/02_websocket.ts
new file mode 100644
index 000000000..073929961
--- /dev/null
+++ b/ext/http/02_websocket.ts
@@ -0,0 +1,185 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+import { internals, primordials } from "ext:core/mod.js";
+import { op_http_websocket_accept_header } from "ext:core/ops";
+const {
+ ArrayPrototypeIncludes,
+ ArrayPrototypeMap,
+ ArrayPrototypePush,
+ StringPrototypeCharCodeAt,
+ StringPrototypeSplit,
+ StringPrototypeToLowerCase,
+ StringPrototypeToUpperCase,
+ TypeError,
+ Symbol,
+} = primordials;
+import { toInnerRequest } from "ext:deno_fetch/23_request.js";
+import {
+ fromInnerResponse,
+ newInnerResponse,
+} from "ext:deno_fetch/23_response.js";
+import { setEventTargetData } from "ext:deno_web/02_event.js";
+import {
+ _eventLoop,
+ _idleTimeoutDuration,
+ _idleTimeoutTimeout,
+ _protocol,
+ _readyState,
+ _rid,
+ _role,
+ _server,
+ _serverHandleIdleTimeout,
+ createWebSocketBranded,
+ WebSocket,
+} from "ext:deno_websocket/01_websocket.js";
+
+const _ws = Symbol("[[associated_ws]]");
+
+const websocketCvf = buildCaseInsensitiveCommaValueFinder("websocket");
+const upgradeCvf = buildCaseInsensitiveCommaValueFinder("upgrade");
+
+function upgradeWebSocket(request, options = {}) {
+ const inner = toInnerRequest(request);
+ const upgrade = request.headers.get("upgrade");
+ const upgradeHasWebSocketOption = upgrade !== null &&
+ websocketCvf(upgrade);
+ if (!upgradeHasWebSocketOption) {
+ throw new TypeError(
+ "Invalid Header: 'upgrade' header must contain 'websocket'",
+ );
+ }
+
+ const connection = request.headers.get("connection");
+ const connectionHasUpgradeOption = connection !== null &&
+ upgradeCvf(connection);
+ if (!connectionHasUpgradeOption) {
+ throw new TypeError(
+ "Invalid Header: 'connection' header must contain 'Upgrade'",
+ );
+ }
+
+ const websocketKey = request.headers.get("sec-websocket-key");
+ if (websocketKey === null) {
+ throw new TypeError(
+ "Invalid Header: 'sec-websocket-key' header must be set",
+ );
+ }
+
+ const accept = op_http_websocket_accept_header(websocketKey);
+
+ const r = newInnerResponse(101);
+ r.headerList = [
+ ["upgrade", "websocket"],
+ ["connection", "Upgrade"],
+ ["sec-websocket-accept", accept],
+ ];
+
+ const protocolsStr = request.headers.get("sec-websocket-protocol") || "";
+ const protocols = StringPrototypeSplit(protocolsStr, ", ");
+ if (protocols && options.protocol) {
+ if (ArrayPrototypeIncludes(protocols, options.protocol)) {
+ ArrayPrototypePush(r.headerList, [
+ "sec-websocket-protocol",
+ options.protocol,
+ ]);
+ } else {
+ throw new TypeError(
+ `Protocol '${options.protocol}' not in the request's protocol list (non negotiable)`,
+ );
+ }
+ }
+
+ const socket = createWebSocketBranded(WebSocket);
+ setEventTargetData(socket);
+ socket[_server] = true;
+ socket[_idleTimeoutDuration] = options.idleTimeout ?? 120;
+ socket[_idleTimeoutTimeout] = null;
+
+ if (inner._wantsUpgrade) {
+ return inner._wantsUpgrade("upgradeWebSocket", r, socket);
+ }
+
+ const response = fromInnerResponse(r, "immutable");
+
+ response[_ws] = socket;
+
+ return { response, socket };
+}
+
+const spaceCharCode = StringPrototypeCharCodeAt(" ", 0);
+const tabCharCode = StringPrototypeCharCodeAt("\t", 0);
+const commaCharCode = StringPrototypeCharCodeAt(",", 0);
+
+/** Builds a case function that can be used to find a case insensitive
+ * value in some text that's separated by commas.
+ *
+ * This is done because it doesn't require any allocations.
+ * @param checkText {string} - The text to find. (ex. "websocket")
+ */
+function buildCaseInsensitiveCommaValueFinder(checkText) {
+ const charCodes = ArrayPrototypeMap(
+ StringPrototypeSplit(
+ StringPrototypeToLowerCase(checkText),
+ "",
+ ),
+ (c) => [
+ StringPrototypeCharCodeAt(c, 0),
+ StringPrototypeCharCodeAt(StringPrototypeToUpperCase(c), 0),
+ ],
+ );
+ /** @type {number} */
+ let i;
+ /** @type {number} */
+ let char;
+
+ /** @param {string} value */
+ return function (value) {
+ for (i = 0; i < value.length; i++) {
+ char = StringPrototypeCharCodeAt(value, i);
+ skipWhitespace(value);
+
+ if (hasWord(value)) {
+ skipWhitespace(value);
+ if (i === value.length || char === commaCharCode) {
+ return true;
+ }
+ } else {
+ skipUntilComma(value);
+ }
+ }
+
+ return false;
+ };
+
+ /** @param value {string} */
+ function hasWord(value) {
+ for (let j = 0; j < charCodes.length; ++j) {
+ const { 0: cLower, 1: cUpper } = charCodes[j];
+ if (cLower === char || cUpper === char) {
+ char = StringPrototypeCharCodeAt(value, ++i);
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** @param value {string} */
+ function skipWhitespace(value) {
+ while (char === spaceCharCode || char === tabCharCode) {
+ char = StringPrototypeCharCodeAt(value, ++i);
+ }
+ }
+
+ /** @param value {string} */
+ function skipUntilComma(value) {
+ while (char !== commaCharCode && i < value.length) {
+ char = StringPrototypeCharCodeAt(value, ++i);
+ }
+ }
+}
+
+// Expose this function for unit tests
+internals.buildCaseInsensitiveCommaValueFinder =
+ buildCaseInsensitiveCommaValueFinder;
+
+export { _ws, upgradeWebSocket };