2023-04-22 13:48:21 -04:00
|
|
|
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
2023-04-30 04:50:24 -04:00
|
|
|
// deno-lint-ignore-file camelcase
|
2023-04-22 13:48:21 -04:00
|
|
|
const core = globalThis.Deno.core;
|
|
|
|
const primordials = globalThis.__bootstrap.primordials;
|
2023-04-26 18:58:18 -04:00
|
|
|
const internals = globalThis.__bootstrap.internals;
|
2023-04-22 13:48:21 -04:00
|
|
|
|
|
|
|
const { BadResourcePrototype } = core;
|
|
|
|
import { InnerBody } from "ext:deno_fetch/22_body.js";
|
|
|
|
import { Event } from "ext:deno_web/02_event.js";
|
|
|
|
import {
|
|
|
|
fromInnerResponse,
|
|
|
|
newInnerResponse,
|
|
|
|
toInnerResponse,
|
|
|
|
} from "ext:deno_fetch/23_response.js";
|
2023-04-26 18:58:18 -04:00
|
|
|
import { fromInnerRequest, toInnerRequest } from "ext:deno_fetch/23_request.js";
|
2023-04-22 13:48:21 -04:00
|
|
|
import { AbortController } from "ext:deno_web/03_abort_signal.js";
|
|
|
|
import {
|
|
|
|
_eventLoop,
|
|
|
|
_idleTimeoutDuration,
|
|
|
|
_idleTimeoutTimeout,
|
|
|
|
_protocol,
|
|
|
|
_readyState,
|
|
|
|
_rid,
|
|
|
|
_role,
|
|
|
|
_server,
|
|
|
|
_serverHandleIdleTimeout,
|
|
|
|
SERVER,
|
|
|
|
WebSocket,
|
|
|
|
} from "ext:deno_websocket/01_websocket.js";
|
|
|
|
import {
|
|
|
|
Deferred,
|
|
|
|
getReadableStreamResourceBacking,
|
2023-04-26 09:33:30 -04:00
|
|
|
readableStreamClose,
|
2023-04-22 13:48:21 -04:00
|
|
|
readableStreamForRid,
|
|
|
|
ReadableStreamPrototype,
|
|
|
|
} from "ext:deno_web/06_streams.js";
|
2023-05-31 19:20:39 -04:00
|
|
|
import { listen, TcpConn } from "ext:deno_net/01_net.js";
|
|
|
|
import { listenTls } from "ext:deno_net/02_tls.js";
|
2023-04-22 13:48:21 -04:00
|
|
|
const {
|
2023-06-05 15:57:01 -04:00
|
|
|
ArrayPrototypePush,
|
2023-04-22 13:48:21 -04:00
|
|
|
ObjectPrototypeIsPrototypeOf,
|
2023-05-01 09:30:02 -04:00
|
|
|
PromisePrototypeCatch,
|
2023-04-22 13:48:21 -04:00
|
|
|
SafeSet,
|
|
|
|
SafeSetIterator,
|
|
|
|
SetPrototypeAdd,
|
|
|
|
SetPrototypeDelete,
|
|
|
|
Symbol,
|
2023-05-19 17:14:40 -04:00
|
|
|
SymbolFor,
|
2023-04-22 13:48:21 -04:00
|
|
|
TypeError,
|
|
|
|
Uint8Array,
|
2023-05-01 09:30:02 -04:00
|
|
|
Uint8ArrayPrototype,
|
2023-04-22 13:48:21 -04:00
|
|
|
} = primordials;
|
|
|
|
|
2023-04-30 04:50:24 -04:00
|
|
|
const {
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_get_request_headers,
|
|
|
|
op_http_get_request_method_and_url,
|
|
|
|
op_http_read_request_body,
|
|
|
|
op_http_serve,
|
2023-05-31 19:20:39 -04:00
|
|
|
op_http_serve_on,
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_set_promise_complete,
|
|
|
|
op_http_set_response_body_bytes,
|
|
|
|
op_http_set_response_body_resource,
|
|
|
|
op_http_set_response_body_stream,
|
|
|
|
op_http_set_response_body_text,
|
|
|
|
op_http_set_response_header,
|
|
|
|
op_http_set_response_headers,
|
2023-05-18 22:10:25 -04:00
|
|
|
op_http_set_response_trailers,
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_upgrade_raw,
|
2023-05-15 19:24:41 -04:00
|
|
|
op_http_upgrade_websocket_next,
|
2023-05-30 20:02:52 -04:00
|
|
|
op_http_try_wait,
|
2023-05-15 19:24:41 -04:00
|
|
|
op_http_wait,
|
2023-06-06 05:01:28 -04:00
|
|
|
} = core.ensureFastOps();
|
2023-04-22 13:48:21 -04:00
|
|
|
const _upgraded = Symbol("_upgraded");
|
|
|
|
|
|
|
|
function internalServerError() {
|
|
|
|
// "Internal Server Error"
|
|
|
|
return new Response(
|
|
|
|
new Uint8Array([
|
|
|
|
73,
|
|
|
|
110,
|
|
|
|
116,
|
|
|
|
101,
|
|
|
|
114,
|
|
|
|
110,
|
|
|
|
97,
|
|
|
|
108,
|
|
|
|
32,
|
|
|
|
83,
|
|
|
|
101,
|
|
|
|
114,
|
|
|
|
118,
|
|
|
|
101,
|
|
|
|
114,
|
|
|
|
32,
|
|
|
|
69,
|
|
|
|
114,
|
|
|
|
114,
|
|
|
|
111,
|
|
|
|
114,
|
|
|
|
]),
|
|
|
|
{ status: 500 },
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Used to ensure that user returns a valid response (but not a different response) from handlers that are upgraded.
|
|
|
|
const UPGRADE_RESPONSE_SENTINEL = fromInnerResponse(
|
|
|
|
newInnerResponse(101),
|
|
|
|
"immutable",
|
|
|
|
);
|
|
|
|
|
2023-04-26 18:58:18 -04:00
|
|
|
function upgradeHttpRaw(req, conn) {
|
|
|
|
const inner = toInnerRequest(req);
|
|
|
|
if (inner._wantsUpgrade) {
|
|
|
|
return inner._wantsUpgrade("upgradeHttpRaw", conn);
|
|
|
|
}
|
|
|
|
throw new TypeError("upgradeHttpRaw may only be used with Deno.serve");
|
|
|
|
}
|
|
|
|
|
2023-05-18 22:10:25 -04:00
|
|
|
function addTrailers(resp, headerList) {
|
|
|
|
const inner = toInnerResponse(resp);
|
|
|
|
op_http_set_response_trailers(inner.slabId, headerList);
|
|
|
|
}
|
|
|
|
|
2023-04-22 13:48:21 -04:00
|
|
|
class InnerRequest {
|
|
|
|
#slabId;
|
|
|
|
#context;
|
|
|
|
#methodAndUri;
|
|
|
|
#streamRid;
|
|
|
|
#body;
|
|
|
|
#upgraded;
|
|
|
|
|
|
|
|
constructor(slabId, context) {
|
|
|
|
this.#slabId = slabId;
|
|
|
|
this.#context = context;
|
|
|
|
this.#upgraded = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
|
|
if (this.#streamRid !== undefined) {
|
|
|
|
core.close(this.#streamRid);
|
|
|
|
this.#streamRid = undefined;
|
|
|
|
}
|
|
|
|
this.#slabId = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
get [_upgraded]() {
|
|
|
|
return this.#upgraded;
|
|
|
|
}
|
|
|
|
|
|
|
|
_wantsUpgrade(upgradeType, ...originalArgs) {
|
2023-04-23 11:59:46 -04:00
|
|
|
if (this.#upgraded) {
|
|
|
|
throw new Deno.errors.Http("already upgraded");
|
|
|
|
}
|
|
|
|
if (this.#slabId === undefined) {
|
|
|
|
throw new Deno.errors.Http("already closed");
|
|
|
|
}
|
|
|
|
|
2023-04-22 13:48:21 -04:00
|
|
|
// upgradeHttp is async
|
|
|
|
// TODO(mmastrac)
|
|
|
|
if (upgradeType == "upgradeHttp") {
|
|
|
|
throw "upgradeHttp is unavailable in Deno.serve at this time";
|
|
|
|
}
|
|
|
|
|
2023-04-26 18:58:18 -04:00
|
|
|
// upgradeHttpRaw is sync
|
2023-04-22 13:48:21 -04:00
|
|
|
if (upgradeType == "upgradeHttpRaw") {
|
2023-04-26 18:58:18 -04:00
|
|
|
const slabId = this.#slabId;
|
|
|
|
const underlyingConn = originalArgs[0];
|
|
|
|
|
|
|
|
this.url();
|
|
|
|
this.headerList;
|
|
|
|
this.close();
|
|
|
|
|
|
|
|
this.#upgraded = () => {};
|
|
|
|
|
2023-05-08 17:07:45 -04:00
|
|
|
const upgradeRid = op_http_upgrade_raw(slabId);
|
2023-04-26 18:58:18 -04:00
|
|
|
|
|
|
|
const conn = new TcpConn(
|
|
|
|
upgradeRid,
|
|
|
|
underlyingConn?.remoteAddr,
|
|
|
|
underlyingConn?.localAddr,
|
|
|
|
);
|
|
|
|
|
|
|
|
return { response: UPGRADE_RESPONSE_SENTINEL, conn };
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// upgradeWebSocket is sync
|
|
|
|
if (upgradeType == "upgradeWebSocket") {
|
|
|
|
const response = originalArgs[0];
|
|
|
|
const ws = originalArgs[1];
|
|
|
|
|
2023-04-23 11:59:46 -04:00
|
|
|
const slabId = this.#slabId;
|
|
|
|
|
2023-04-22 13:48:21 -04:00
|
|
|
this.url();
|
|
|
|
this.headerList;
|
|
|
|
this.close();
|
|
|
|
|
|
|
|
const goAhead = new Deferred();
|
|
|
|
this.#upgraded = () => {
|
|
|
|
goAhead.resolve();
|
|
|
|
};
|
|
|
|
|
|
|
|
// Start the upgrade in the background.
|
|
|
|
(async () => {
|
|
|
|
try {
|
2023-05-15 19:24:41 -04:00
|
|
|
// Returns the upgraded websocket connection
|
|
|
|
const wsRid = await op_http_upgrade_websocket_next(
|
2023-04-23 11:59:46 -04:00
|
|
|
slabId,
|
2023-04-22 13:48:21 -04:00
|
|
|
response.headerList,
|
|
|
|
);
|
|
|
|
|
|
|
|
// We have to wait for the go-ahead signal
|
|
|
|
await goAhead;
|
|
|
|
|
|
|
|
ws[_rid] = wsRid;
|
|
|
|
ws[_readyState] = WebSocket.OPEN;
|
|
|
|
ws[_role] = SERVER;
|
|
|
|
const event = new Event("open");
|
|
|
|
ws.dispatchEvent(event);
|
|
|
|
|
|
|
|
ws[_eventLoop]();
|
|
|
|
if (ws[_idleTimeoutDuration]) {
|
|
|
|
ws.addEventListener(
|
|
|
|
"close",
|
|
|
|
() => clearTimeout(ws[_idleTimeoutTimeout]),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
ws[_serverHandleIdleTimeout]();
|
|
|
|
} catch (error) {
|
|
|
|
const event = new ErrorEvent("error", { error });
|
|
|
|
ws.dispatchEvent(event);
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
return { response: UPGRADE_RESPONSE_SENTINEL, socket: ws };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
url() {
|
|
|
|
if (this.#methodAndUri === undefined) {
|
|
|
|
if (this.#slabId === undefined) {
|
|
|
|
throw new TypeError("request closed");
|
|
|
|
}
|
|
|
|
// TODO(mmastrac): This is quite slow as we're serializing a large number of values. We may want to consider
|
|
|
|
// splitting this up into multiple ops.
|
2023-05-08 17:07:45 -04:00
|
|
|
this.#methodAndUri = op_http_get_request_method_and_url(this.#slabId);
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const path = this.#methodAndUri[2];
|
|
|
|
|
|
|
|
// * is valid for OPTIONS
|
|
|
|
if (path === "*") {
|
|
|
|
return "*";
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the path is empty, return the authority (valid for CONNECT)
|
|
|
|
if (path == "") {
|
|
|
|
return this.#methodAndUri[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
// CONNECT requires an authority
|
|
|
|
if (this.#methodAndUri[0] == "CONNECT") {
|
|
|
|
return this.#methodAndUri[1];
|
|
|
|
}
|
|
|
|
|
|
|
|
const hostname = this.#methodAndUri[1];
|
|
|
|
if (hostname) {
|
|
|
|
// Construct a URL from the scheme, the hostname, and the path
|
|
|
|
return this.#context.scheme + hostname + path;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Construct a URL from the scheme, the fallback hostname, and the path
|
|
|
|
return this.#context.scheme + this.#context.fallbackHost + path;
|
|
|
|
}
|
|
|
|
|
|
|
|
get remoteAddr() {
|
|
|
|
if (this.#methodAndUri === undefined) {
|
|
|
|
if (this.#slabId === undefined) {
|
|
|
|
throw new TypeError("request closed");
|
|
|
|
}
|
2023-05-08 17:07:45 -04:00
|
|
|
this.#methodAndUri = op_http_get_request_method_and_url(this.#slabId);
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
return {
|
|
|
|
transport: "tcp",
|
|
|
|
hostname: this.#methodAndUri[3],
|
|
|
|
port: this.#methodAndUri[4],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
get method() {
|
|
|
|
if (this.#methodAndUri === undefined) {
|
|
|
|
if (this.#slabId === undefined) {
|
|
|
|
throw new TypeError("request closed");
|
|
|
|
}
|
2023-05-08 17:07:45 -04:00
|
|
|
this.#methodAndUri = op_http_get_request_method_and_url(this.#slabId);
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
return this.#methodAndUri[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
get body() {
|
|
|
|
if (this.#slabId === undefined) {
|
|
|
|
throw new TypeError("request closed");
|
|
|
|
}
|
|
|
|
if (this.#body !== undefined) {
|
|
|
|
return this.#body;
|
|
|
|
}
|
|
|
|
// If the method is GET or HEAD, we do not want to include a body here, even if the Rust
|
|
|
|
// side of the code is willing to provide it to us.
|
|
|
|
if (this.method == "GET" || this.method == "HEAD") {
|
|
|
|
this.#body = null;
|
|
|
|
return null;
|
|
|
|
}
|
2023-05-08 17:07:45 -04:00
|
|
|
this.#streamRid = op_http_read_request_body(this.#slabId);
|
2023-04-22 13:48:21 -04:00
|
|
|
this.#body = new InnerBody(readableStreamForRid(this.#streamRid, false));
|
|
|
|
return this.#body;
|
|
|
|
}
|
|
|
|
|
|
|
|
get headerList() {
|
|
|
|
if (this.#slabId === undefined) {
|
|
|
|
throw new TypeError("request closed");
|
|
|
|
}
|
2023-06-02 11:59:16 -04:00
|
|
|
const headers = [];
|
|
|
|
const reqHeaders = op_http_get_request_headers(this.#slabId);
|
|
|
|
for (let i = 0; i < reqHeaders.length; i += 2) {
|
2023-06-05 15:57:01 -04:00
|
|
|
ArrayPrototypePush(headers, [reqHeaders[i], reqHeaders[i + 1]]);
|
2023-06-02 11:59:16 -04:00
|
|
|
}
|
|
|
|
return headers;
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
get slabId() {
|
|
|
|
return this.#slabId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class CallbackContext {
|
2023-05-31 19:20:39 -04:00
|
|
|
abortController;
|
|
|
|
responseBodies;
|
2023-04-22 13:48:21 -04:00
|
|
|
scheme;
|
|
|
|
fallbackHost;
|
|
|
|
serverRid;
|
|
|
|
closed;
|
|
|
|
|
2023-05-31 19:20:39 -04:00
|
|
|
constructor(signal, args) {
|
|
|
|
signal?.addEventListener(
|
|
|
|
"abort",
|
|
|
|
() => this.close(),
|
|
|
|
{ once: true },
|
|
|
|
);
|
|
|
|
this.abortController = new AbortController();
|
|
|
|
this.responseBodies = new SafeSet();
|
2023-04-22 13:48:21 -04:00
|
|
|
this.serverRid = args[0];
|
|
|
|
this.scheme = args[1];
|
|
|
|
this.fallbackHost = args[2];
|
|
|
|
this.closed = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
|
|
try {
|
|
|
|
this.closed = true;
|
|
|
|
core.tryClose(this.serverRid);
|
|
|
|
} catch {
|
|
|
|
// Pass
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function fastSyncResponseOrStream(req, respBody) {
|
|
|
|
if (respBody === null || respBody === undefined) {
|
|
|
|
// Don't set the body
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const stream = respBody.streamOrStatic;
|
|
|
|
const body = stream.body;
|
|
|
|
|
|
|
|
if (ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, body)) {
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_set_response_body_bytes(req, body);
|
2023-04-22 13:48:21 -04:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof body === "string") {
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_set_response_body_text(req, body);
|
2023-04-22 13:48:21 -04:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// At this point in the response it needs to be a stream
|
|
|
|
if (!ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, stream)) {
|
|
|
|
throw TypeError("invalid response");
|
|
|
|
}
|
|
|
|
const resourceBacking = getReadableStreamResourceBacking(stream);
|
|
|
|
if (resourceBacking) {
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_set_response_body_resource(
|
2023-04-22 13:48:21 -04:00
|
|
|
req,
|
|
|
|
resourceBacking.rid,
|
|
|
|
resourceBacking.autoClose,
|
|
|
|
);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return stream;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function asyncResponse(responseBodies, req, status, stream) {
|
|
|
|
const reader = stream.getReader();
|
2023-04-26 09:33:30 -04:00
|
|
|
let responseRid;
|
|
|
|
let closed = false;
|
|
|
|
let timeout;
|
|
|
|
|
2023-04-22 13:48:21 -04:00
|
|
|
try {
|
2023-04-26 09:33:30 -04:00
|
|
|
// IMPORTANT: We get a performance boost from this optimization, but V8 is very
|
|
|
|
// sensitive to the order and structure. Benchmark any changes to this code.
|
|
|
|
|
|
|
|
// Optimize for streams that are done in zero or one packets. We will not
|
|
|
|
// have to allocate a resource in this case.
|
|
|
|
const { value: value1, done: done1 } = await reader.read();
|
|
|
|
if (done1) {
|
|
|
|
closed = true;
|
|
|
|
// Exit 1: no response body at all, extreme fast path
|
|
|
|
// Reader will be closed by finally block
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The second value cannot block indefinitely, as someone may be waiting on a response
|
|
|
|
// of the first packet that may influence this packet. We set this timeout arbitrarily to 250ms
|
|
|
|
// and we race it.
|
|
|
|
let timeoutPromise;
|
|
|
|
timeout = setTimeout(() => {
|
2023-05-08 17:07:45 -04:00
|
|
|
responseRid = op_http_set_response_body_stream(req);
|
2023-04-26 09:33:30 -04:00
|
|
|
SetPrototypeAdd(responseBodies, responseRid);
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_set_promise_complete(req, status);
|
2023-04-26 09:33:30 -04:00
|
|
|
timeoutPromise = core.writeAll(responseRid, value1);
|
|
|
|
}, 250);
|
|
|
|
const { value: value2, done: done2 } = await reader.read();
|
|
|
|
|
|
|
|
if (timeoutPromise) {
|
|
|
|
await timeoutPromise;
|
|
|
|
if (done2) {
|
|
|
|
closed = true;
|
|
|
|
// Exit 2(a): read 2 is EOS, and timeout resolved.
|
|
|
|
// Reader will be closed by finally block
|
|
|
|
// Response stream will be closed by finally block.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Timeout resolved, value1 written but read2 is not EOS. Carry value2 forward.
|
|
|
|
} else {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
timeout = undefined;
|
|
|
|
|
|
|
|
if (done2) {
|
|
|
|
// Exit 2(b): read 2 is EOS, and timeout did not resolve as we read fast enough.
|
|
|
|
// Reader will be closed by finally block
|
|
|
|
// No response stream
|
|
|
|
closed = true;
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_set_response_body_bytes(req, value1);
|
2023-04-26 09:33:30 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-05-08 17:07:45 -04:00
|
|
|
responseRid = op_http_set_response_body_stream(req);
|
2023-04-26 09:33:30 -04:00
|
|
|
SetPrototypeAdd(responseBodies, responseRid);
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_set_promise_complete(req, status);
|
2023-04-26 09:33:30 -04:00
|
|
|
// Write our first packet
|
|
|
|
await core.writeAll(responseRid, value1);
|
|
|
|
}
|
|
|
|
|
|
|
|
await core.writeAll(responseRid, value2);
|
2023-04-22 13:48:21 -04:00
|
|
|
while (true) {
|
|
|
|
const { value, done } = await reader.read();
|
|
|
|
if (done) {
|
2023-04-26 09:33:30 -04:00
|
|
|
closed = true;
|
2023-04-22 13:48:21 -04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
await core.writeAll(responseRid, value);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
2023-04-26 09:33:30 -04:00
|
|
|
closed = true;
|
|
|
|
try {
|
|
|
|
await reader.cancel(error);
|
|
|
|
} catch {
|
|
|
|
// Pass
|
|
|
|
}
|
2023-04-22 13:48:21 -04:00
|
|
|
} finally {
|
2023-04-26 09:33:30 -04:00
|
|
|
if (!closed) {
|
|
|
|
readableStreamClose(reader);
|
|
|
|
}
|
|
|
|
if (timeout !== undefined) {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
}
|
|
|
|
if (responseRid) {
|
|
|
|
core.tryClose(responseRid);
|
|
|
|
SetPrototypeDelete(responseBodies, responseRid);
|
|
|
|
} else {
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_set_promise_complete(req, status);
|
2023-04-26 09:33:30 -04:00
|
|
|
}
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Maps the incoming request slab ID to a fully-fledged Request object, passes it to the user-provided
|
|
|
|
* callback, then extracts the response that was returned from that callback. The response is then pulled
|
|
|
|
* apart and handled on the Rust side.
|
|
|
|
*
|
|
|
|
* This function returns a promise that will only reject in the case of abnormal exit.
|
|
|
|
*/
|
2023-05-31 19:20:39 -04:00
|
|
|
function mapToCallback(context, callback, onError) {
|
|
|
|
const responseBodies = context.responseBodies;
|
|
|
|
const signal = context.abortController.signal;
|
2023-06-09 17:21:26 -04:00
|
|
|
const hasCallback = callback.length > 0;
|
|
|
|
const hasOneCallback = callback.length === 1;
|
|
|
|
|
2023-04-22 13:48:21 -04:00
|
|
|
return async function (req) {
|
|
|
|
// Get the response from the user-provided callback. If that fails, use onError. If that fails, return a fallback
|
|
|
|
// 500 error.
|
2023-04-26 12:41:54 -04:00
|
|
|
let innerRequest;
|
2023-04-22 13:48:21 -04:00
|
|
|
let response;
|
|
|
|
try {
|
2023-06-09 17:21:26 -04:00
|
|
|
if (hasCallback) {
|
2023-04-26 12:41:54 -04:00
|
|
|
innerRequest = new InnerRequest(req, context);
|
|
|
|
const request = fromInnerRequest(innerRequest, signal, "immutable");
|
2023-06-09 17:21:26 -04:00
|
|
|
if (hasOneCallback) {
|
2023-04-26 12:41:54 -04:00
|
|
|
response = await callback(request);
|
|
|
|
} else {
|
|
|
|
response = await callback(request, {
|
|
|
|
get remoteAddr() {
|
|
|
|
return innerRequest.remoteAddr;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
response = await callback();
|
|
|
|
}
|
2023-04-22 13:48:21 -04:00
|
|
|
} catch (error) {
|
|
|
|
try {
|
|
|
|
response = await onError(error);
|
|
|
|
} catch (error) {
|
|
|
|
console.error("Exception in onError while handling exception", error);
|
|
|
|
response = internalServerError();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const inner = toInnerResponse(response);
|
2023-04-26 12:41:54 -04:00
|
|
|
if (innerRequest?.[_upgraded]) {
|
2023-04-22 13:48:21 -04:00
|
|
|
// We're done here as the connection has been upgraded during the callback and no longer requires servicing.
|
|
|
|
if (response !== UPGRADE_RESPONSE_SENTINEL) {
|
|
|
|
console.error("Upgrade response was not returned from callback");
|
|
|
|
context.close();
|
|
|
|
}
|
2023-04-26 12:41:54 -04:00
|
|
|
innerRequest?.[_upgraded]();
|
2023-04-22 13:48:21 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Did everything shut down while we were waiting?
|
|
|
|
if (context.closed) {
|
2023-05-16 19:00:59 -04:00
|
|
|
// We're shutting down, so this status shouldn't make it back to the client but "Service Unavailable" seems appropriate
|
|
|
|
op_http_set_promise_complete(req, 503);
|
2023-04-26 12:41:54 -04:00
|
|
|
innerRequest?.close();
|
2023-04-22 13:48:21 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const status = inner.status;
|
|
|
|
const headers = inner.headerList;
|
|
|
|
if (headers && headers.length > 0) {
|
|
|
|
if (headers.length == 1) {
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_set_response_header(req, headers[0][0], headers[0][1]);
|
2023-04-22 13:48:21 -04:00
|
|
|
} else {
|
2023-06-06 10:55:37 -04:00
|
|
|
op_http_set_response_headers(req, headers);
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-30 20:02:52 -04:00
|
|
|
// Attempt to respond quickly to this request, otherwise extract the stream
|
2023-04-22 13:48:21 -04:00
|
|
|
const stream = fastSyncResponseOrStream(req, inner.body);
|
|
|
|
if (stream !== null) {
|
|
|
|
// Handle the stream asynchronously
|
|
|
|
await asyncResponse(responseBodies, req, status, stream);
|
|
|
|
} else {
|
2023-05-08 17:07:45 -04:00
|
|
|
op_http_set_promise_complete(req, status);
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
2023-04-26 12:41:54 -04:00
|
|
|
innerRequest?.close();
|
2023-04-22 13:48:21 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-05-18 20:59:23 -04:00
|
|
|
function serve(arg1, arg2) {
|
2023-04-22 13:48:21 -04:00
|
|
|
let options = undefined;
|
|
|
|
let handler = undefined;
|
|
|
|
if (typeof arg1 === "function") {
|
|
|
|
handler = arg1;
|
|
|
|
} else if (typeof arg2 === "function") {
|
|
|
|
handler = arg2;
|
|
|
|
options = arg1;
|
|
|
|
} else {
|
|
|
|
options = arg1;
|
|
|
|
}
|
|
|
|
if (handler === undefined) {
|
|
|
|
if (options === undefined) {
|
|
|
|
throw new TypeError(
|
|
|
|
"No handler was provided, so an options bag is mandatory.",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
handler = options.handler;
|
|
|
|
}
|
|
|
|
if (typeof handler !== "function") {
|
|
|
|
throw new TypeError("A handler function must be provided.");
|
|
|
|
}
|
|
|
|
if (options === undefined) {
|
|
|
|
options = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
const wantsHttps = options.cert || options.key;
|
|
|
|
const signal = options.signal;
|
|
|
|
const onError = options.onError ?? function (error) {
|
|
|
|
console.error(error);
|
|
|
|
return internalServerError();
|
|
|
|
};
|
|
|
|
const listenOpts = {
|
|
|
|
hostname: options.hostname ?? "0.0.0.0",
|
|
|
|
port: options.port ?? (wantsHttps ? 9000 : 8000),
|
|
|
|
reusePort: options.reusePort ?? false,
|
|
|
|
};
|
|
|
|
|
2023-05-31 19:20:39 -04:00
|
|
|
let listener;
|
2023-04-22 13:48:21 -04:00
|
|
|
if (wantsHttps) {
|
|
|
|
if (!options.cert || !options.key) {
|
|
|
|
throw new TypeError(
|
|
|
|
"Both cert and key must be provided to enable HTTPS.",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
listenOpts.cert = options.cert;
|
|
|
|
listenOpts.key = options.key;
|
|
|
|
listenOpts.alpnProtocols = ["h2", "http/1.1"];
|
2023-05-31 19:20:39 -04:00
|
|
|
listener = listenTls(listenOpts);
|
2023-04-22 13:48:21 -04:00
|
|
|
listenOpts.port = listener.addr.port;
|
|
|
|
} else {
|
2023-05-31 19:20:39 -04:00
|
|
|
listener = listen(listenOpts);
|
2023-04-22 13:48:21 -04:00
|
|
|
listenOpts.port = listener.addr.port;
|
|
|
|
}
|
|
|
|
|
2023-05-31 19:20:39 -04:00
|
|
|
const onListen = (scheme) => {
|
|
|
|
const port = listenOpts.port;
|
|
|
|
if (options.onListen) {
|
|
|
|
options.onListen({ port });
|
|
|
|
} else {
|
|
|
|
// If the hostname is "0.0.0.0", we display "localhost" in console
|
|
|
|
// because browsers in Windows don't resolve "0.0.0.0".
|
|
|
|
// See the discussion in https://github.com/denoland/deno_std/issues/1165
|
|
|
|
const hostname = listenOpts.hostname == "0.0.0.0"
|
|
|
|
? "localhost"
|
|
|
|
: listenOpts.hostname;
|
|
|
|
console.log(`Listening on ${scheme}${hostname}:${port}/`);
|
|
|
|
}
|
2023-04-22 13:48:21 -04:00
|
|
|
};
|
|
|
|
|
2023-05-31 19:20:39 -04:00
|
|
|
return serveHttpOnListener(listener, signal, handler, onError, onListen);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Serve HTTP/1.1 and/or HTTP/2 on an arbitrary listener.
|
|
|
|
*/
|
|
|
|
function serveHttpOnListener(listener, signal, handler, onError, onListen) {
|
|
|
|
const context = new CallbackContext(signal, op_http_serve(listener.rid));
|
|
|
|
const callback = mapToCallback(context, handler, onError);
|
|
|
|
|
|
|
|
onListen(context.scheme);
|
|
|
|
|
|
|
|
return serveHttpOn(context, callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Serve HTTP/1.1 and/or HTTP/2 on an arbitrary connection.
|
|
|
|
*/
|
|
|
|
function serveHttpOnConnection(connection, signal, handler, onError, onListen) {
|
|
|
|
const context = new CallbackContext(signal, op_http_serve_on(connection.rid));
|
|
|
|
const callback = mapToCallback(context, handler, onError);
|
|
|
|
|
|
|
|
onListen(context.scheme);
|
|
|
|
|
|
|
|
return serveHttpOn(context, callback);
|
|
|
|
}
|
2023-04-22 13:48:21 -04:00
|
|
|
|
2023-05-31 19:20:39 -04:00
|
|
|
function serveHttpOn(context, callback) {
|
2023-05-19 17:14:40 -04:00
|
|
|
let ref = true;
|
|
|
|
let currentPromise = null;
|
|
|
|
const promiseIdSymbol = SymbolFor("Deno.core.internalPromiseId");
|
|
|
|
|
2023-05-18 20:59:23 -04:00
|
|
|
// Run the server
|
|
|
|
const finished = (async () => {
|
2023-06-09 18:45:56 -04:00
|
|
|
const rid = context.serverRid;
|
2023-05-18 20:59:23 -04:00
|
|
|
while (true) {
|
|
|
|
let req;
|
|
|
|
try {
|
2023-05-30 20:02:52 -04:00
|
|
|
// Attempt to pull as many requests out of the queue as possible before awaiting. This API is
|
|
|
|
// a synchronous, non-blocking API that returns u32::MAX if anything goes wrong.
|
|
|
|
while ((req = op_http_try_wait(rid)) !== 0xffffffff) {
|
|
|
|
PromisePrototypeCatch(callback(req), (error) => {
|
|
|
|
// Abnormal exit
|
|
|
|
console.error(
|
|
|
|
"Terminating Deno.serve loop due to unexpected error",
|
|
|
|
error,
|
|
|
|
);
|
|
|
|
context.close();
|
|
|
|
});
|
|
|
|
}
|
2023-05-19 17:14:40 -04:00
|
|
|
currentPromise = op_http_wait(rid);
|
|
|
|
if (!ref) {
|
|
|
|
core.unrefOp(currentPromise[promiseIdSymbol]);
|
|
|
|
}
|
|
|
|
req = await currentPromise;
|
|
|
|
currentPromise = null;
|
2023-05-18 20:59:23 -04:00
|
|
|
} catch (error) {
|
|
|
|
if (ObjectPrototypeIsPrototypeOf(BadResourcePrototype, error)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
throw new Deno.errors.Http(error);
|
|
|
|
}
|
|
|
|
if (req === 0xffffffff) {
|
2023-04-22 13:48:21 -04:00
|
|
|
break;
|
|
|
|
}
|
2023-05-18 20:59:23 -04:00
|
|
|
PromisePrototypeCatch(callback(req), (error) => {
|
|
|
|
// Abnormal exit
|
|
|
|
console.error(
|
|
|
|
"Terminating Deno.serve loop due to unexpected error",
|
|
|
|
error,
|
|
|
|
);
|
|
|
|
context.close();
|
|
|
|
});
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
2023-05-18 20:59:23 -04:00
|
|
|
|
2023-05-31 19:20:39 -04:00
|
|
|
for (const streamRid of new SafeSetIterator(context.responseBodies)) {
|
2023-05-18 20:59:23 -04:00
|
|
|
core.tryClose(streamRid);
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
2023-05-18 20:59:23 -04:00
|
|
|
})();
|
2023-04-22 13:48:21 -04:00
|
|
|
|
2023-05-19 17:14:40 -04:00
|
|
|
return {
|
|
|
|
finished,
|
|
|
|
ref() {
|
|
|
|
ref = true;
|
|
|
|
if (currentPromise) {
|
|
|
|
core.refOp(currentPromise[promiseIdSymbol]);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
unref() {
|
|
|
|
ref = false;
|
|
|
|
if (currentPromise) {
|
|
|
|
core.unrefOp(currentPromise[promiseIdSymbol]);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
2023-04-22 13:48:21 -04:00
|
|
|
}
|
|
|
|
|
2023-05-18 22:10:25 -04:00
|
|
|
internals.addTrailers = addTrailers;
|
2023-04-26 18:58:18 -04:00
|
|
|
internals.upgradeHttpRaw = upgradeHttpRaw;
|
2023-05-31 19:20:39 -04:00
|
|
|
internals.serveHttpOnListener = serveHttpOnListener;
|
|
|
|
internals.serveHttpOnConnection = serveHttpOnConnection;
|
2023-04-26 18:58:18 -04:00
|
|
|
|
2023-06-06 06:29:55 -04:00
|
|
|
export {
|
|
|
|
addTrailers,
|
|
|
|
serve,
|
|
|
|
serveHttpOnConnection,
|
|
|
|
serveHttpOnListener,
|
|
|
|
upgradeHttpRaw,
|
|
|
|
};
|