import { core, primordials } from "ext:core/mod.js";
const { internalRidSymbol } = core;
const {
ArrayIsArray,
ArrayPrototypePush,
Boolean,
FunctionPrototypeBind,
ObjectAssign,
ObjectHasOwn,
ObjectKeys,
Proxy,
ReflectApply,
ReflectGetPrototypeOf,
ReflectHas,
SafeArrayIterator,
StringPrototypeIncludes,
StringPrototypeToLowerCase,
StringPrototypeTrim,
Symbol,
} = primordials;
import { op_http2_constants } from "ext:core/ops";
import assert from "node:assert";
import Stream, { Readable } from "node:stream";
const {
HTTP2_HEADER_AUTHORITY,
HTTP2_HEADER_CONNECTION,
HTTP2_HEADER_METHOD,
HTTP2_HEADER_PATH,
HTTP2_HEADER_SCHEME,
HTTP2_HEADER_STATUS,
HTTP_STATUS_CONTINUE,
HTTP_STATUS_EARLY_HINTS,
HTTP_STATUS_EXPECTATION_FAILED,
HTTP_STATUS_METHOD_NOT_ALLOWED,
HTTP_STATUS_OK,
} = op_http2_constants();
import { codes, hideStackFrames } from "ext:deno_node/internal/errors.ts";
const {
ERR_HTTP2_HEADERS_SENT,
ERR_HTTP2_INFO_STATUS_NOT_ALLOWED,
ERR_HTTP2_INVALID_HEADER_VALUE,
ERR_HTTP2_INVALID_STREAM,
ERR_HTTP2_NO_SOCKET_MANIPULATION,
ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED,
ERR_HTTP2_STATUS_INVALID,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_HTTP_TOKEN,
ERR_STREAM_WRITE_AFTER_END,
} = codes;
import {
validateFunction,
validateLinkHeaderValue,
validateObject,
validateString,
} from "ext:deno_node/internal/validators.mjs";
import {
assertValidPseudoHeader,
getAuthority,
kProxySocket,
kRequest,
kSocket,
} from "ext:deno_node/internal/http2/util.ts";
import { _checkIsHttpToken as checkIsHttpToken } from "node:_http_common";
const kBeginSend = Symbol("begin-send");
const kState = Symbol("state");
const kStream = Symbol("stream");
const kResponse = Symbol("response");
const kHeaders = Symbol("headers");
const kRawHeaders = Symbol("rawHeaders");
const kTrailers = Symbol("trailers");
const kRawTrailers = Symbol("rawTrailers");
const kSetHeader = Symbol("setHeader");
const kAppendHeader = Symbol("appendHeader");
const kAborted = Symbol("aborted");
let statusMessageWarned = false;
let statusConnectionHeaderWarned = false;
const assertValidHeader = hideStackFrames((name, value) => {
if (
name === "" ||
typeof name !== "string" ||
StringPrototypeIncludes(name, " ")
) {
throw new ERR_INVALID_HTTP_TOKEN.HideStackFramesError("Header name", name);
}
if (isPseudoHeader(name)) {
throw new ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED.HideStackFramesError();
}
if (value === undefined || value === null) {
throw new ERR_HTTP2_INVALID_HEADER_VALUE.HideStackFramesError(value, name);
}
if (!isConnectionHeaderAllowed(name, value)) {
connectionHeaderMessageWarn();
}
});
function isPseudoHeader(name) {
switch (name) {
case HTTP2_HEADER_STATUS: case HTTP2_HEADER_METHOD: case HTTP2_HEADER_PATH: case HTTP2_HEADER_AUTHORITY: case HTTP2_HEADER_SCHEME: return true;
default:
return false;
}
}
function statusMessageWarn() {
if (statusMessageWarned === false) {
process.emitWarning(
"Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)",
"UnsupportedWarning",
);
statusMessageWarned = true;
}
}
function isConnectionHeaderAllowed(name, value) {
return name !== HTTP2_HEADER_CONNECTION ||
value === "trailers";
}
function connectionHeaderMessageWarn() {
if (statusConnectionHeaderWarned === false) {
process.emitWarning(
"The provided connection header is not valid, " +
"the value will be dropped from the header and " +
"will never be in use.",
"UnsupportedWarning",
);
statusConnectionHeaderWarned = true;
}
}
function onStreamData(chunk) {
const request = this[kRequest];
if (request !== undefined && !request.push(chunk)) {
this.pause();
}
}
function onStreamTrailers(trailers, flags, rawTrailers) {
const request = this[kRequest];
if (request !== undefined) {
ObjectAssign(request[kTrailers], trailers);
for (const trailer of new SafeArrayIterator(rawTrailers)) {
ArrayPrototypePush(request[kRawTrailers], trailer);
}
}
}
function onStreamEnd() {
const request = this[kRequest];
if (request !== undefined) {
this[kRequest].push(null);
}
}
function onStreamError(error) {
}
function onRequestPause() {
this[kStream].pause();
}
function onRequestResume() {
this[kStream].resume();
}
function onStreamDrain() {
const response = this[kResponse];
if (response !== undefined) {
response.emit("drain");
}
}
function onStreamAbortedRequest() {
const request = this[kRequest];
if (request !== undefined && request[kState].closed === false) {
request[kAborted] = true;
request.emit("aborted");
}
}
function onStreamAbortedResponse() {
}
function resumeStream(stream) {
stream.resume();
}
const proxySocketHandler = {
has(stream, prop) {
const ref = stream.session !== undefined ? stream.session[kSocket] : stream;
return ReflectHas(stream, prop) || ReflectHas(ref, prop);
},
get(stream, prop) {
switch (prop) {
case "on":
case "once":
case "end":
case "emit":
case "destroy":
return FunctionPrototypeBind(stream[prop], stream);
case "writable":
case "destroyed":
return stream[prop];
case "readable": {
if (stream.destroyed) {
return false;
}
const request = stream[kRequest];
return request ? request.readable : stream.readable;
}
case "setTimeout": {
const session = stream.session;
if (session !== undefined) {
return FunctionPrototypeBind(session.setTimeout, session);
}
return FunctionPrototypeBind(stream.setTimeout, stream);
}
case "write":
case "read":
case "pause":
case "resume":
throw new ERR_HTTP2_NO_SOCKET_MANIPULATION();
default: {
const ref = stream.session !== undefined
? stream.session[kSocket]
: stream;
const value = ref[prop];
return typeof value === "function"
? FunctionPrototypeBind(value, ref)
: value;
}
}
},
getPrototypeOf(stream) {
if (stream.session !== undefined) {
return ReflectGetPrototypeOf(stream.session[kSocket]);
}
return ReflectGetPrototypeOf(stream);
},
set(stream, prop, value) {
switch (prop) {
case "writable":
case "readable":
case "destroyed":
case "on":
case "once":
case "end":
case "emit":
case "destroy":
stream[prop] = value;
return true;
case "setTimeout": {
const session = stream.session;
if (session !== undefined) {
session.setTimeout = value;
} else {
stream.setTimeout = value;
}
return true;
}
case "write":
case "read":
case "pause":
case "resume":
throw new ERR_HTTP2_NO_SOCKET_MANIPULATION();
default: {
const ref = stream.session !== undefined
? stream.session[kSocket]
: stream;
ref[prop] = value;
return true;
}
}
},
};
function onStreamCloseRequest() {
const req = this[kRequest];
if (req === undefined) {
return;
}
const state = req[kState];
state.closed = true;
req.push(null);
if (!state.didRead && !req._readableState.resumeScheduled) {
req.resume();
}
this[kProxySocket] = null;
this[kRequest] = undefined;
req.emit("close");
}
function onStreamTimeout(kind) {
return function onStreamTimeout() {
const obj = this[kind];
obj.emit("timeout");
};
}
class Http2ServerRequest extends Readable {
constructor(stream, headers, options, rawHeaders) {
super({ autoDestroy: false, ...options });
this[kState] = {
closed: false,
didRead: false,
};
this[kHeaders] = headers;
this[kRawHeaders] = rawHeaders;
this[kTrailers] = {};
this[kRawTrailers] = [];
this[kStream] = stream;
this[kAborted] = false;
stream[kProxySocket] = null;
stream[kRequest] = this;
stream.on("trailers", onStreamTrailers);
stream.on("end", onStreamEnd);
stream.on("error", onStreamError);
stream.on("aborted", onStreamAbortedRequest);
stream.on("close", onStreamCloseRequest);
stream.on("timeout", onStreamTimeout(kRequest));
this.on("pause", onRequestPause);
this.on("resume", onRequestResume);
}
get aborted() {
return this[kAborted];
}
get complete() {
return this[kAborted] ||
this.readableEnded ||
this[kState].closed ||
this[kStream].destroyed;
}
get stream() {
return this[kStream];
}
get headers() {
return this[kHeaders];
}
get rawHeaders() {
return this[kRawHeaders];
}
get trailers() {
return this[kTrailers];
}
get rawTrailers() {
return this[kRawTrailers];
}
get httpVersionMajor() {
return 2;
}
get httpVersionMinor() {
return 0;
}
get httpVersion() {
return "2.0";
}
get socket() {
const stream = this[kStream];
const proxySocket = stream[kProxySocket];
if (proxySocket === null) {
return stream[kProxySocket] = new Proxy(stream, proxySocketHandler);
}
return proxySocket;
}
get connection() {
return this.socket;
}
_read(nread) {
const state = this[kState];
assert(!state.closed);
if (!state.didRead) {
state.didRead = true;
this[kStream].on("data", onStreamData);
} else {
process.nextTick(resumeStream, this[kStream]);
}
}
get method() {
return this[kHeaders][HTTP2_HEADER_METHOD];
}
set method(method) {
validateString(method, "method");
if (StringPrototypeTrim(method) === "") {
throw new ERR_INVALID_ARG_VALUE("method", method);
}
this[kHeaders][HTTP2_HEADER_METHOD] = method;
}
get authority() {
return getAuthority(this[kHeaders]);
}
get scheme() {
return this[kHeaders][HTTP2_HEADER_SCHEME];
}
get url() {
return this[kHeaders][HTTP2_HEADER_PATH];
}
set url(url) {
this[kHeaders][HTTP2_HEADER_PATH] = url;
}
setTimeout(msecs, callback) {
if (!this[kState].closed) {
this[kStream].setTimeout(msecs, callback);
}
return this;
}
}
function onStreamTrailersReady() {
this.sendTrailers(this[kResponse][kTrailers]);
}
function onStreamCloseResponse() {
const res = this[kResponse];
if (res === undefined) {
return;
}
const state = res[kState];
if (this.headRequest !== state.headRequest) {
return;
}
state.closed = true;
this[kProxySocket] = null;
this.removeListener("wantTrailers", onStreamTrailersReady);
this[kResponse] = undefined;
res.emit("finish");
res.emit("close");
}
class Http2ServerResponse extends Stream {
constructor(stream, options) {
super(options);
this[kState] = {
closed: false,
ending: false,
destroyed: false,
headRequest: false,
sendDate: true,
statusCode: HTTP_STATUS_OK,
};
this[kHeaders] = { __proto__: null };
this[kTrailers] = { __proto__: null };
this[kStream] = stream;
stream[kProxySocket] = null;
stream[kResponse] = this;
this.writable = true;
this.req = stream[kRequest];
stream.on("drain", onStreamDrain);
stream.on("aborted", onStreamAbortedResponse);
stream.on("close", onStreamCloseResponse);
stream.on("wantTrailers", onStreamTrailersReady);
stream.on("timeout", onStreamTimeout(kResponse));
}
get _header() {
return this.headersSent;
}
get writableEnded() {
const state = this[kState];
return state.ending;
}
get finished() {
const state = this[kState];
return state.ending;
}
get socket() {
if (this[kState].closed) {
return undefined;
}
const stream = this[kStream];
const proxySocket = stream[kProxySocket];
if (proxySocket === null) {
return stream[kProxySocket] = new Proxy(stream, proxySocketHandler);
}
return proxySocket;
}
get connection() {
return this.socket;
}
get stream() {
return this[kStream];
}
get headersSent() {
return this[kStream].headersSent;
}
get sendDate() {
return this[kState].sendDate;
}
set sendDate(bool) {
this[kState].sendDate = Boolean(bool);
}
get statusCode() {
return this[kState].statusCode;
}
get writableCorked() {
return this[kStream].writableCorked;
}
get writableHighWaterMark() {
return this[kStream].writableHighWaterMark;
}
get writableFinished() {
return this[kStream].writableFinished;
}
get writableLength() {
return this[kStream].writableLength;
}
set statusCode(code) {
code |= 0;
if (code >= 100 && code < 200) {
throw new ERR_HTTP2_INFO_STATUS_NOT_ALLOWED();
}
if (code < 100 || code > 599) {
throw new ERR_HTTP2_STATUS_INVALID(code);
}
this[kState].statusCode = code;
}
setTrailer(name, value) {
validateString(name, "name");
name = StringPrototypeToLowerCase(StringPrototypeTrim(name));
assertValidHeader(name, value);
this[kTrailers][name] = value;
}
addTrailers(headers) {
const keys = ObjectKeys(headers);
let key = "";
for (let i = 0; i < keys.length; i++) {
key = keys[i];
this.setTrailer(key, headers[key]);
}
}
getHeader(name) {
validateString(name, "name");
name = StringPrototypeToLowerCase(StringPrototypeTrim(name));
return this[kHeaders][name];
}
getHeaderNames() {
return ObjectKeys(this[kHeaders]);
}
getHeaders() {
const headers = { __proto__: null };
return ObjectAssign(headers, this[kHeaders]);
}
hasHeader(name) {
validateString(name, "name");
name = StringPrototypeToLowerCase(StringPrototypeTrim(name));
return ObjectHasOwn(this[kHeaders], name);
}
removeHeader(name) {
validateString(name, "name");
if (this[kStream].headersSent) {
throw new ERR_HTTP2_HEADERS_SENT();
}
name = StringPrototypeToLowerCase(StringPrototypeTrim(name));
if (name === "date") {
this[kState].sendDate = false;
return;
}
delete this[kHeaders][name];
}
setHeader(name, value) {
validateString(name, "name");
if (this[kStream].headersSent) {
throw new ERR_HTTP2_HEADERS_SENT();
}
this[kSetHeader](name, value);
}
[kSetHeader](name, value) {
name = StringPrototypeToLowerCase(StringPrototypeTrim(name));
assertValidHeader(name, value);
if (!isConnectionHeaderAllowed(name, value)) {
return;
}
if (name[0] === ":") {
assertValidPseudoHeader(name);
} else if (!checkIsHttpToken(name)) {
this.destroy(new ERR_INVALID_HTTP_TOKEN("Header name", name));
}
this[kHeaders][name] = value;
}
appendHeader(name, value) {
validateString(name, "name");
if (this[kStream].headersSent) {
throw new ERR_HTTP2_HEADERS_SENT();
}
this[kAppendHeader](name, value);
}
[kAppendHeader](name, value) {
name = StringPrototypeToLowerCase(StringPrototypeTrim(name));
assertValidHeader(name, value);
if (!isConnectionHeaderAllowed(name, value)) {
return;
}
if (name[0] === ":") {
assertValidPseudoHeader(name);
} else if (!checkIsHttpToken(name)) {
this.destroy(new ERR_INVALID_HTTP_TOKEN("Header name", name));
}
const headers = this[kHeaders];
if (headers === null || !headers[name]) {
return this.setHeader(name, value);
}
if (!ArrayIsArray(headers[name])) {
headers[name] = [headers[name]];
}
const existingValues = headers[name];
if (ArrayIsArray(value)) {
for (let i = 0, length = value.length; i < length; i++) {
ArrayPrototypePush(existingValues, value[i]);
}
} else {
ArrayPrototypePush(existingValues, value);
}
}
get statusMessage() {
statusMessageWarn();
return "";
}
set statusMessage(msg) {
statusMessageWarn();
}
flushHeaders() {
const state = this[kState];
if (!state.closed && !this[kStream].headersSent) {
this.writeHead(state.statusCode);
}
}
writeHead(statusCode, statusMessage, headers) {
const state = this[kState];
if (state.closed || this.stream.destroyed || this.stream.closed) {
return this;
}
if (this[kStream].headersSent) {
throw new ERR_HTTP2_HEADERS_SENT();
}
if (typeof statusMessage === "string") {
statusMessageWarn();
}
if (headers === undefined && typeof statusMessage === "object") {
headers = statusMessage;
}
let i;
if (ArrayIsArray(headers)) {
if (this[kHeaders]) {
if (headers.length && ArrayIsArray(headers[0])) {
for (let n = 0; n < headers.length; n += 1) {
const key = headers[n + 0][0];
this.removeHeader(key);
}
} else {
for (let n = 0; n < headers.length; n += 2) {
const key = headers[n + 0];
this.removeHeader(key);
}
}
}
if (headers.length && ArrayIsArray(headers[0])) {
for (i = 0; i < headers.length; i++) {
const header = headers[i];
this[kAppendHeader](header[0], header[1]);
}
} else {
if (headers.length % 2 !== 0) {
throw new ERR_INVALID_ARG_VALUE("headers", headers);
}
for (i = 0; i < headers.length; i += 2) {
this[kAppendHeader](headers[i], headers[i + 1]);
}
}
} else if (typeof headers === "object") {
const keys = ObjectKeys(headers);
let key = "";
for (i = 0; i < keys.length; i++) {
key = keys[i];
this[kSetHeader](key, headers[key]);
}
}
state.statusCode = statusCode;
this[kBeginSend]();
return this;
}
cork() {
this[kStream].cork();
}
uncork() {
this[kStream].uncork();
}
write(chunk, encoding, cb) {
const state = this[kState];
if (typeof encoding === "function") {
cb = encoding;
encoding = "utf8";
}
let err;
if (state.ending) {
err = new ERR_STREAM_WRITE_AFTER_END();
} else if (state.closed) {
err = new ERR_HTTP2_INVALID_STREAM();
} else if (state.destroyed) {
return false;
}
if (err) {
if (typeof cb === "function") {
process.nextTick(cb, err);
}
this.destroy(err);
return false;
}
const stream = this[kStream];
if (!stream.headersSent) {
this.writeHead(state.statusCode);
}
return stream.write(chunk, encoding, cb);
}
end(chunk, encoding, cb) {
const stream = this[kStream];
const state = this[kState];
if (typeof chunk === "function") {
cb = chunk;
chunk = null;
} else if (typeof encoding === "function") {
cb = encoding;
encoding = "utf8";
}
if (
(state.closed || state.ending) &&
state.headRequest === stream.headRequest
) {
if (typeof cb === "function") {
process.nextTick(cb);
}
return this;
}
if (chunk !== null && chunk !== undefined) {
this.write(chunk, encoding);
}
state.headRequest = stream.headRequest;
state.ending = true;
if (typeof cb === "function") {
if (stream.writableEnded) {
this.once("finish", cb);
} else {
stream.once("finish", cb);
}
}
if (!stream.headersSent) {
this.writeHead(this[kState].statusCode);
}
if (this[kState].closed || stream.destroyed) {
ReflectApply(onStreamCloseResponse, stream, []);
} else {
stream.end();
}
return this;
}
destroy(err) {
if (this[kState].destroyed) {
return;
}
this[kState].destroyed = true;
this[kStream].destroy(err);
}
setTimeout(msecs, callback) {
if (this[kState].closed) {
return;
}
this[kStream].setTimeout(msecs, callback);
}
createPushResponse(headers, callback) {
validateFunction(callback, "callback");
if (this[kState].closed) {
process.nextTick(callback, new ERR_HTTP2_INVALID_STREAM());
return;
}
this[kStream].pushStream(headers, {}, (err, stream, headers, options) => {
if (err) {
callback(err);
return;
}
callback(null, new Http2ServerResponse(stream));
});
}
[kBeginSend]() {
const state = this[kState];
const headers = this[kHeaders];
headers[HTTP2_HEADER_STATUS] = state.statusCode;
const options = {
endStream: state.ending,
waitForTrailers: true,
sendDate: state.sendDate,
};
this[kStream].respond(headers, options);
}
writeContinue() {
const stream = this[kStream];
if (stream.headersSent || this[kState].closed) {
return false;
}
stream.additionalHeaders({
[HTTP2_HEADER_STATUS]: HTTP_STATUS_CONTINUE,
});
return true;
}
writeEarlyHints(hints) {
validateObject(hints, "hints");
const headers = { __proto__: null };
const linkHeaderValue = validateLinkHeaderValue(hints.link);
for (const key of new SafeArrayIterator(ObjectKeys(hints))) {
if (key !== "link") {
headers[key] = hints[key];
}
}
if (linkHeaderValue.length === 0) {
return false;
}
const stream = this[kStream];
if (stream.headersSent || this[kState].closed) {
return false;
}
stream.additionalHeaders({
...headers,
[HTTP2_HEADER_STATUS]: HTTP_STATUS_EARLY_HINTS,
"Link": linkHeaderValue,
});
return true;
}
}
function onServerStream(
ServerRequest,
ServerResponse,
stream,
headers,
flags,
rawHeaders,
) {
const server = this;
const request = new ServerRequest(stream, headers, undefined, rawHeaders);
const response = new ServerResponse(stream);
const method = headers[HTTP2_HEADER_METHOD];
if (method === "CONNECT") {
if (!server.emit("connect", request, response)) {
response.statusCode = HTTP_STATUS_METHOD_NOT_ALLOWED;
response.end();
}
return;
}
if (headers.expect !== undefined) {
if (headers.expect === "100-continue") {
if (server.listenerCount("checkContinue")) {
server.emit("checkContinue", request, response);
} else {
response.writeContinue();
server.emit("request", request, response);
}
} else if (server.listenerCount("checkExpectation")) {
server.emit("checkExpectation", request, response);
} else {
response.statusCode = HTTP_STATUS_EXPECTATION_FAILED;
response.end();
}
return;
}
server.emit("request", request, response);
}
export { Http2ServerRequest, Http2ServerResponse, onServerStream };
export default {
onServerStream,
Http2ServerRequest,
Http2ServerResponse,
};