diff --git a/ext/http/01_http.js b/ext/http/01_http.js index b41c36446a..580ba11666 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 0000000000..073929961a --- /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 df31b9c445..6fc7207bea 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 aa52b0c337..96799cb090 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,