mirror of
https://github.com/denoland/deno.git
synced 2024-12-22 07:14:47 -05:00
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.
This commit is contained in:
parent
79e6751cf7
commit
9425dce6db
4 changed files with 191 additions and 166 deletions
|
@ -12,45 +12,34 @@ import {
|
||||||
op_http_shutdown,
|
op_http_shutdown,
|
||||||
op_http_start,
|
op_http_start,
|
||||||
op_http_upgrade_websocket,
|
op_http_upgrade_websocket,
|
||||||
op_http_websocket_accept_header,
|
|
||||||
op_http_write,
|
op_http_write,
|
||||||
op_http_write_headers,
|
op_http_write_headers,
|
||||||
op_http_write_resource,
|
op_http_write_resource,
|
||||||
} from "ext:core/ops";
|
} from "ext:core/ops";
|
||||||
const {
|
const {
|
||||||
ArrayPrototypeIncludes,
|
|
||||||
ArrayPrototypeMap,
|
|
||||||
ArrayPrototypePush,
|
|
||||||
ObjectPrototypeIsPrototypeOf,
|
ObjectPrototypeIsPrototypeOf,
|
||||||
SafeSet,
|
SafeSet,
|
||||||
SafeSetIterator,
|
SafeSetIterator,
|
||||||
SetPrototypeAdd,
|
SetPrototypeAdd,
|
||||||
SetPrototypeDelete,
|
SetPrototypeDelete,
|
||||||
StringPrototypeCharCodeAt,
|
|
||||||
StringPrototypeIncludes,
|
StringPrototypeIncludes,
|
||||||
StringPrototypeSplit,
|
|
||||||
StringPrototypeToLowerCase,
|
|
||||||
StringPrototypeToUpperCase,
|
|
||||||
Symbol,
|
Symbol,
|
||||||
SymbolAsyncIterator,
|
SymbolAsyncIterator,
|
||||||
TypeError,
|
TypeError,
|
||||||
TypedArrayPrototypeGetSymbolToStringTag,
|
TypedArrayPrototypeGetSymbolToStringTag,
|
||||||
Uint8Array,
|
Uint8Array,
|
||||||
} = primordials;
|
} = primordials;
|
||||||
|
import { _ws } from "ext:deno_http/02_websocket.ts";
|
||||||
import { InnerBody } from "ext:deno_fetch/22_body.js";
|
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 { BlobPrototype } from "ext:deno_web/09_file.js";
|
||||||
import {
|
import {
|
||||||
fromInnerResponse,
|
|
||||||
newInnerResponse,
|
|
||||||
ResponsePrototype,
|
ResponsePrototype,
|
||||||
toInnerResponse,
|
toInnerResponse,
|
||||||
} from "ext:deno_fetch/23_response.js";
|
} from "ext:deno_fetch/23_response.js";
|
||||||
import {
|
import {
|
||||||
fromInnerRequest,
|
fromInnerRequest,
|
||||||
newInnerRequest,
|
newInnerRequest,
|
||||||
toInnerRequest,
|
|
||||||
} from "ext:deno_fetch/23_request.js";
|
} from "ext:deno_fetch/23_request.js";
|
||||||
import { AbortController } from "ext:deno_web/03_abort_signal.js";
|
import { AbortController } from "ext:deno_web/03_abort_signal.js";
|
||||||
import {
|
import {
|
||||||
|
@ -63,7 +52,6 @@ import {
|
||||||
_role,
|
_role,
|
||||||
_server,
|
_server,
|
||||||
_serverHandleIdleTimeout,
|
_serverHandleIdleTimeout,
|
||||||
createWebSocketBranded,
|
|
||||||
SERVER,
|
SERVER,
|
||||||
WebSocket,
|
WebSocket,
|
||||||
} from "ext:deno_websocket/01_websocket.js";
|
} 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) {
|
function serveHttp(conn) {
|
||||||
internals.warnOnDeprecatedApi(
|
internals.warnOnDeprecatedApi(
|
||||||
"Deno.serveHttp()",
|
"Deno.serveHttp()",
|
||||||
|
@ -568,4 +407,4 @@ function serveHttp(conn) {
|
||||||
return new HttpConn(rid, conn.remoteAddr, conn.localAddr);
|
return new HttpConn(rid, conn.remoteAddr, conn.localAddr);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { _ws, HttpConn, serveHttp, upgradeWebSocket };
|
export { HttpConn, serveHttp };
|
||||||
|
|
185
ext/http/02_websocket.ts
Normal file
185
ext/http/02_websocket.ts
Normal file
|
@ -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 };
|
|
@ -131,7 +131,7 @@ deno_core::extension!(
|
||||||
http_next::op_http_close,
|
http_next::op_http_close,
|
||||||
http_next::op_http_cancel,
|
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 {
|
pub enum HttpSocketAddr {
|
||||||
|
|
|
@ -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 tls from "ext:deno_net/02_tls.js";
|
||||||
import * as serve from "ext:deno_http/00_serve.js";
|
import * as serve from "ext:deno_http/00_serve.js";
|
||||||
import * as http from "ext:deno_http/01_http.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 errors from "ext:runtime/01_errors.js";
|
||||||
import * as version from "ext:runtime/01_version.ts";
|
import * as version from "ext:runtime/01_version.ts";
|
||||||
import * as permissions from "ext:runtime/10_permissions.js";
|
import * as permissions from "ext:runtime/10_permissions.js";
|
||||||
|
@ -227,7 +228,7 @@ const denoNs = {
|
||||||
serveHttp: http.serveHttp,
|
serveHttp: http.serveHttp,
|
||||||
serve: serve.serve,
|
serve: serve.serve,
|
||||||
resolveDns: net.resolveDns,
|
resolveDns: net.resolveDns,
|
||||||
upgradeWebSocket: http.upgradeWebSocket,
|
upgradeWebSocket: websocket.upgradeWebSocket,
|
||||||
utime: fs.utime,
|
utime: fs.utime,
|
||||||
utimeSync: fs.utimeSync,
|
utimeSync: fs.utimeSync,
|
||||||
kill: process.kill,
|
kill: process.kill,
|
||||||
|
|
Loading…
Reference in a new issue