diff --git a/cli/tests/unit/http_test.ts b/cli/tests/unit/http_test.ts index 73ff35e09a4e72..57039984111370 100644 --- a/cli/tests/unit/http_test.ts +++ b/cli/tests/unit/http_test.ts @@ -14,6 +14,11 @@ import { } from "./test_util.ts"; import { join } from "../../../test_util/std/path/mod.ts"; +const { + buildCaseInsensitiveCommaValueFinder, + // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol +} = Deno[Deno.internal]; + async function writeRequestAndReadResponse(conn: Deno.Conn): Promise { const encoder = new TextEncoder(); const decoder = new TextDecoder(); @@ -2612,6 +2617,30 @@ Deno.test({ }, }); +Deno.test("case insensitive comma value finder", async (t) => { + const cases = /** @type {[string, boolean][]} */ ([ + ["websocket", true], + ["wEbSOcKET", true], + [",wEbSOcKET", true], + [",wEbSOcKET,", true], + [", wEbSOcKET ,", true], + ["test, wEbSOcKET ,", true], + ["test ,\twEbSOcKET\t\t ,", true], + ["test , wEbSOcKET", true], + ["test, asdf,web,wEbSOcKET", true], + ["test, asdf,web,wEbSOcKETs", false], + ["test, asdf,awebsocket,wEbSOcKETs", false], + ]); + + const findValue = buildCaseInsensitiveCommaValueFinder("websocket"); + for (const [input, expected] of cases) { + await t.step(input.toString(), () => { + const actual = findValue(input); + assertEquals(actual, expected); + }); + } +}); + async function httpServerWithErrorBody( listener: Deno.Listener, compression: boolean, diff --git a/ext/http/01_http.js b/ext/http/01_http.js index 1da371e8dd9f53..f9e15e7d59e6c1 100644 --- a/ext/http/01_http.js +++ b/ext/http/01_http.js @@ -1,5 +1,6 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. const core = globalThis.Deno.core; +const internals = globalThis.__bootstrap.internals; const primordials = globalThis.__bootstrap.primordials; const { BadResourcePrototype, InterruptedPrototype, ops } = core; import * as webidl from "internal:deno_webidl/00_webidl.js"; @@ -40,18 +41,18 @@ import { } from "internal:deno_web/06_streams.js"; const { ArrayPrototypeIncludes, + ArrayPrototypeMap, ArrayPrototypePush, - ArrayPrototypeSome, Error, ObjectPrototypeIsPrototypeOf, SafeSetIterator, Set, SetPrototypeAdd, SetPrototypeDelete, + StringPrototypeCharCodeAt, StringPrototypeIncludes, StringPrototypeToLowerCase, StringPrototypeSplit, - StringPrototypeTrim, Symbol, SymbolAsyncIterator, TypeError, @@ -389,15 +390,13 @@ function createRespondWith( } const _ws = Symbol("[[associated_ws]]"); +const websocketCvf = buildCaseInsensitiveCommaValueFinder("websocket"); +const upgradeCvf = buildCaseInsensitiveCommaValueFinder("upgrade"); function upgradeWebSocket(request, options = {}) { const upgrade = request.headers.get("upgrade"); const upgradeHasWebSocketOption = upgrade !== null && - ArrayPrototypeSome( - StringPrototypeSplit(upgrade, ","), - (option) => - StringPrototypeToLowerCase(StringPrototypeTrim(option)) === "websocket", - ); + websocketCvf(upgrade); if (!upgradeHasWebSocketOption) { throw new TypeError( "Invalid Header: 'upgrade' header must contain 'websocket'", @@ -406,11 +405,7 @@ function upgradeWebSocket(request, options = {}) { const connection = request.headers.get("connection"); const connectionHasUpgradeOption = connection !== null && - ArrayPrototypeSome( - StringPrototypeSplit(connection, ","), - (option) => - StringPrototypeToLowerCase(StringPrototypeTrim(option)) === "upgrade", - ); + upgradeCvf(connection); if (!connectionHasUpgradeOption) { throw new TypeError( "Invalid Header: 'connection' header must contain 'Upgrade'", @@ -471,4 +466,77 @@ function upgradeHttp(req) { return req[_deferred].promise; } +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) => [c.charCodeAt(0), c.toUpperCase().charCodeAt(0)], + ); + /** @type {number} */ + let i; + /** @type {number} */ + let char; + + /** @param value {string} */ + return function (value) { + for (i = 0; i < value.length; i++) { + char = value.charCodeAt(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 (const [cLower, cUpper] of charCodes) { + 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, HttpConn, upgradeHttp, upgradeWebSocket };