summaryrefslogtreecommitdiff
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
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.
-rw-r--r--ext/http/01_http.js167
-rw-r--r--ext/http/02_websocket.ts185
-rw-r--r--ext/http/lib.rs2
-rw-r--r--runtime/js/90_deno_ns.js3
4 files changed, 191 insertions, 166 deletions
diff --git a/ext/http/01_http.js b/ext/http/01_http.js
index b41c36446..580ba1166 100644
--- a/ext/http/01_http.js
+++ b/ext/http/01_http.js
@@ -12,45 +12,34 @@ import {
op_http_shutdown,
op_http_start,
op_http_upgrade_websocket,
- op_http_websocket_accept_header,
op_http_write,
op_http_write_headers,
op_http_write_resource,
} from "ext:core/ops";
const {
- ArrayPrototypeIncludes,
- ArrayPrototypeMap,
- ArrayPrototypePush,
ObjectPrototypeIsPrototypeOf,
SafeSet,
SafeSetIterator,
SetPrototypeAdd,
SetPrototypeDelete,
- StringPrototypeCharCodeAt,
StringPrototypeIncludes,
- StringPrototypeSplit,
- StringPrototypeToLowerCase,
- StringPrototypeToUpperCase,
Symbol,
SymbolAsyncIterator,
TypeError,
TypedArrayPrototypeGetSymbolToStringTag,
Uint8Array,
} = primordials;
-
+import { _ws } from "ext:deno_http/02_websocket.ts";
import { InnerBody } from "ext:deno_fetch/22_body.js";
-import { Event, setEventTargetData } from "ext:deno_web/02_event.js";
+import { Event } from "ext:deno_web/02_event.js";
import { BlobPrototype } from "ext:deno_web/09_file.js";
import {
- fromInnerResponse,
- newInnerResponse,
ResponsePrototype,
toInnerResponse,
} from "ext:deno_fetch/23_response.js";
import {
fromInnerRequest,
newInnerRequest,
- toInnerRequest,
} from "ext:deno_fetch/23_request.js";
import { AbortController } from "ext:deno_web/03_abort_signal.js";
import {
@@ -63,7 +52,6 @@ import {
_role,
_server,
_serverHandleIdleTimeout,
- createWebSocketBranded,
SERVER,
WebSocket,
} from "ext:deno_websocket/01_websocket.js";
@@ -409,155 +397,6 @@ function createRespondWith(
};
}
-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;
-
function serveHttp(conn) {
internals.warnOnDeprecatedApi(
"Deno.serveHttp()",
@@ -568,4 +407,4 @@ function serveHttp(conn) {
return new HttpConn(rid, conn.remoteAddr, conn.localAddr);
}
-export { _ws, HttpConn, serveHttp, upgradeWebSocket };
+export { HttpConn, serveHttp };
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 };
diff --git a/ext/http/lib.rs b/ext/http/lib.rs
index df31b9c44..6fc7207be 100644
--- a/ext/http/lib.rs
+++ b/ext/http/lib.rs
@@ -131,7 +131,7 @@ deno_core::extension!(
http_next::op_http_close,
http_next::op_http_cancel,
],
- esm = ["00_serve.js", "01_http.js"],
+ esm = ["00_serve.js", "01_http.js", "02_websocket.ts"],
);
pub enum HttpSocketAddr {
diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js
index aa52b0c33..96799cb09 100644
--- a/runtime/js/90_deno_ns.js
+++ b/runtime/js/90_deno_ns.js
@@ -15,6 +15,7 @@ import * as net from "ext:deno_net/01_net.js";
import * as tls from "ext:deno_net/02_tls.js";
import * as serve from "ext:deno_http/00_serve.js";
import * as http from "ext:deno_http/01_http.js";
+import * as websocket from "ext:deno_http/02_websocket.ts";
import * as errors from "ext:runtime/01_errors.js";
import * as version from "ext:runtime/01_version.ts";
import * as permissions from "ext:runtime/10_permissions.js";
@@ -227,7 +228,7 @@ const denoNs = {
serveHttp: http.serveHttp,
serve: serve.serve,
resolveDns: net.resolveDns,
- upgradeWebSocket: http.upgradeWebSocket,
+ upgradeWebSocket: websocket.upgradeWebSocket,
utime: fs.utime,
utimeSync: fs.utimeSync,
kill: process.kill,