// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright Joyent and Node contributors. All rights reserved. MIT license. // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials import { core, primordials } from "ext:core/mod.js"; const { internalRidSymbol } = core; import { op_http2_client_get_response, op_http2_client_get_response_body_chunk, op_http2_client_get_response_trailers, op_http2_client_request, op_http2_client_reset_stream, op_http2_client_send_data, op_http2_client_send_trailers, op_http2_connect, op_http2_poll_client_connection, op_http_set_response_trailers, } from "ext:core/ops"; import { notImplemented, warnNotImplemented } from "ext:deno_node/_utils.ts"; import { toInnerRequest } from "ext:deno_fetch/23_request.js"; import { Readable } from "node:stream"; import { EventEmitter } from "node:events"; import { Buffer } from "node:buffer"; import { emitWarning } from "node:process"; import Stream from "node:stream"; import { connect as netConnect, Server, Socket, TCP } from "node:net"; import { connect as tlsConnect } from "node:tls"; import { TypedArray } from "ext:deno_node/internal/util/types.ts"; import { kHandle, kMaybeDestroy, kUpdateTimer, setStreamTimeout, } from "ext:deno_node/internal/stream_base_commons.ts"; import { FileHandle } from "node:fs/promises"; import { kStreamBaseField } from "ext:deno_node/internal_binding/stream_wrap.ts"; import { serveHttpOnConnection } from "ext:deno_http/00_serve.ts"; import { nextTick } from "ext:deno_node/_next_tick.ts"; import { TextEncoder } from "ext:deno_web/08_text_encoding.js"; import { Duplex } from "node:stream"; import { AbortError, ERR_HTTP2_CONNECT_AUTHORITY, ERR_HTTP2_CONNECT_PATH, ERR_HTTP2_CONNECT_SCHEME, ERR_HTTP2_GOAWAY_SESSION, ERR_HTTP2_HEADERS_SENT, ERR_HTTP2_INFO_STATUS_NOT_ALLOWED, ERR_HTTP2_INVALID_PSEUDOHEADER, ERR_HTTP2_INVALID_SESSION, ERR_HTTP2_INVALID_STREAM, ERR_HTTP2_NO_SOCKET_MANIPULATION, ERR_HTTP2_SESSION_ERROR, ERR_HTTP2_SOCKET_UNBOUND, ERR_HTTP2_STATUS_INVALID, ERR_HTTP2_STREAM_CANCEL, ERR_HTTP2_STREAM_ERROR, ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS, ERR_HTTP2_TRAILERS_ALREADY_SENT, ERR_HTTP2_TRAILERS_NOT_READY, ERR_HTTP2_UNSUPPORTED_PROTOCOL, ERR_INVALID_ARG_VALUE, ERR_INVALID_HTTP_TOKEN, ERR_SOCKET_CLOSED, ERR_STREAM_WRITE_AFTER_END, } from "ext:deno_node/internal/errors.ts"; import { _checkIsHttpToken } from "node:_http_common"; const { StringPrototypeTrim, FunctionPrototypeBind, ObjectKeys, ReflectGetPrototypeOf, ObjectAssign, StringPrototypeToLowerCase, ReflectApply, ArrayIsArray, ObjectPrototypeHasOwnProperty, } = primordials; const kSession = Symbol("session"); const kOptions = Symbol("options"); const kAlpnProtocol = Symbol("alpnProtocol"); const kAuthority = Symbol("authority"); const kEncrypted = Symbol("encrypted"); const kID = Symbol("id"); const kInit = Symbol("init"); const kInfoHeaders = Symbol("sent-info-headers"); const kOrigin = Symbol("origin"); const kPendingRequestCalls = Symbol("kPendingRequestCalls"); const kProtocol = Symbol("protocol"); const kSentHeaders = Symbol("sent-headers"); const kSentTrailers = Symbol("sent-trailers"); const kState = Symbol("state"); const kType = Symbol("type"); const kTimeout = Symbol("timeout"); const kSocket = Symbol("socket"); const kProxySocket = Symbol("proxySocket"); const kDenoResponse = Symbol("kDenoResponse"); const kDenoRid = Symbol("kDenoRid"); const kDenoClientRid = Symbol("kDenoClientRid"); const kDenoConnRid = Symbol("kDenoConnRid"); const kPollConnPromise = Symbol("kPollConnPromise"); const STREAM_FLAGS_PENDING = 0x0; const STREAM_FLAGS_READY = 0x1; const STREAM_FLAGS_CLOSED = 0x2; const STREAM_FLAGS_HEADERS_SENT = 0x4; const STREAM_FLAGS_HEAD_REQUEST = 0x8; const STREAM_FLAGS_ABORTED = 0x10; const STREAM_FLAGS_HAS_TRAILERS = 0x20; // Maximum number of allowed additional settings const MAX_ADDITIONAL_SETTINGS = 10; const SESSION_FLAGS_PENDING = 0x0; const SESSION_FLAGS_READY = 0x1; const SESSION_FLAGS_CLOSED = 0x2; const SESSION_FLAGS_DESTROYED = 0x4; const ENCODER = new TextEncoder(); type Http2Headers = Record; const debugHttp2Enabled = false; function debugHttp2(...args) { if (debugHttp2Enabled) { // deno-lint-ignore no-console console.log(...args); } } const sessionProxySocketHandler = { get(session, prop) { switch (prop) { case "setTimeout": case "ref": case "unref": return FunctionPrototypeBind(session[prop], session); case "destroy": case "emit": case "end": case "pause": case "read": case "resume": case "write": case "setEncoding": case "setKeepAlive": case "setNoDelay": throw new ERR_HTTP2_NO_SOCKET_MANIPULATION(); default: { const socket = session[kSocket]; if (socket === undefined) { throw new ERR_HTTP2_SOCKET_UNBOUND(); } const value = socket[prop]; return typeof value === "function" ? FunctionPrototypeBind(value, socket) : value; } } }, getPrototypeOf(session) { const socket = session[kSocket]; if (socket === undefined) { throw new ERR_HTTP2_SOCKET_UNBOUND(); } return ReflectGetPrototypeOf(socket); }, set(session, prop, value) { switch (prop) { case "setTimeout": case "ref": case "unref": session[prop] = value; return true; case "destroy": case "emit": case "end": case "pause": case "read": case "resume": case "write": case "setEncoding": case "setKeepAlive": case "setNoDelay": throw new ERR_HTTP2_NO_SOCKET_MANIPULATION(); default: { const socket = session[kSocket]; if (socket === undefined) { throw new ERR_HTTP2_SOCKET_UNBOUND(); } socket[prop] = value; return true; } } }, }; export class Http2Session extends EventEmitter { constructor(type, _options, socket) { super(); this[kState] = { destroyCode: constants.NGHTTP2_NO_ERROR, flags: SESSION_FLAGS_PENDING, goawayCode: null, goawayLastStreamID: null, streams: new Map(), pendingStreams: new Set(), pendingAck: 0, writeQueueSize: 0, originSet: undefined, }; this[kEncrypted] = undefined; this[kAlpnProtocol] = undefined; this[kType] = type; this[kProxySocket] = null; this[kSocket] = socket; this[kTimeout] = null; debugHttp2(type, "created"); } get encrypted(): boolean { return this[kEncrypted]; } get alpnProtocol(): string | undefined { return this[kAlpnProtocol]; } get originSet(): string[] | undefined { if (!this.encrypted || this.destroyed) { return undefined; } // TODO(bartlomieju): return []; } get connecting(): boolean { return (this[kState].flags & SESSION_FLAGS_READY) === 0; } get closed(): boolean { return !!(this[kState].flags & SESSION_FLAGS_CLOSED); } get destroyed(): boolean { return !!(this[kState].flags & SESSION_FLAGS_DESTROYED); } [kUpdateTimer]() { if (this.destroyed) { return; } if (this[kTimeout]) { this[kTimeout].refresh(); } } setLocalWindowSize(_windowSize: number) { notImplemented("Http2Session.setLocalWindowSize"); } ping( _payload: Buffer | TypedArray | DataView, _callback: () => void, ): boolean { notImplemented("Http2Session.ping"); return false; } get socket(): Socket { const proxySocket = this[kProxySocket]; if (proxySocket === null) { return this[kProxySocket] = new Proxy(this, sessionProxySocketHandler); } return proxySocket; } get type(): number { return this[kType]; } get pendingSettingsAck() { return this[kState].pendingAck > 0; } get state(): Record { return {}; } get localSettings(): Record { notImplemented("Http2Session.localSettings"); return {}; } get remoteSettings(): Record { notImplemented("Http2Session.remoteSettings"); return {}; } settings(_settings: Record, _callback: () => void) { notImplemented("Http2Session.settings"); } goaway( code?: number, lastStreamID?: number, opaqueData?: Buffer | TypedArray | DataView, ) { // TODO(satyarohith): create goaway op and pass the args debugHttp2(">>> goaway - ignored args", code, lastStreamID, opaqueData); if (this[kDenoConnRid]) { core.tryClose(this[kDenoConnRid]); } if (this[kDenoClientRid]) { core.tryClose(this[kDenoClientRid]); } } destroy(error = constants.NGHTTP2_NO_ERROR, code?: number) { if (this.destroyed) { return; } if (typeof error === "number") { code = error; error = code !== constants.NGHTTP2_NO_ERROR ? new ERR_HTTP2_SESSION_ERROR(code) : undefined; } if (code === undefined && error != null) { code = constants.NGHTTP2_INTERNAL_ERROR; } closeSession(this, code, error); } close(callback?: () => void) { if (this.closed || this.destroyed) { return; } debugHttp2(this, "marking session closed"); this[kState].flags |= SESSION_FLAGS_CLOSED; if (typeof callback === "function") { this.once("close", callback); } this.goaway(); this[kMaybeDestroy](); } [kMaybeDestroy](error?: number) { if (!error) { const state = this[kState]; // Don't destroy if the session is not closed or there are pending or open // streams. if ( !this.closed || state.streams.size > 0 || state.pendingStreams.size > 0 ) { return; } } this.destroy(error); } ref() { warnNotImplemented("Http2Session.ref"); } unref() { warnNotImplemented("Http2Session.unref"); } _onTimeout() { callTimeout(this, this); } setTimeout(msecs: number, callback?: () => void) { setStreamTimeout.call(this, msecs, callback); } } function emitClose(session: Http2Session, error?: Error) { if (error) { session.emit("error", error); } session.emit("close"); } function finishSessionClose(session: Http2Session, error?: Error) { // TODO(bartlomieju): handle sockets nextTick(emitClose, session, error); } function closeSession(session: Http2Session, code?: number, error?: Error) { const state = session[kState]; state.flags |= SESSION_FLAGS_DESTROYED; state.destroyCode = code; session.setTimeout(0); session.removeAllListeners("timeout"); // Destroy open and pending streams if (state.pendingStreams.size > 0 || state.streams.size > 0) { const cancel = new ERR_HTTP2_STREAM_CANCEL(error); state.pendingStreams.forEach((stream) => stream.destroy(cancel)); state.streams.forEach((stream) => stream.destroy(cancel)); } // TODO(bartlomieju): handle sockets debugHttp2( ">>> closeSession", session[kDenoConnRid], session[kDenoClientRid], ); if (session[kDenoConnRid]) { core.tryClose(session[kDenoConnRid]); } if (session[kDenoClientRid]) { core.tryClose(session[kDenoClientRid]); } finishSessionClose(session, error); } export class ServerHttp2Session extends Http2Session { constructor() { // TODO(satyarohith): pass socket instead of undefined super(constants.NGHTTP2_SESSION_SERVER, {}, undefined); } altsvc( _alt: string, _originOrStream: number | string | URL | { origin: string }, ) { notImplemented("ServerHttp2Session.altsvc"); } origin(..._origins: (string | URL | { origin: string })[]) { notImplemented("ServerHttp2Session.origins"); } } function assertValidPseudoHeader(header: string) { switch (header) { case ":authority": case ":path": case ":method": case ":scheme": case ":status": return; default: throw new ERR_HTTP2_INVALID_PSEUDOHEADER(header); } } export class ClientHttp2Session extends Http2Session { #connectPromise: Promise; #refed = true; constructor( // deno-lint-ignore no-explicit-any socket: any, url: string, options: Record, ) { super(constants.NGHTTP2_SESSION_CLIENT, options, socket); this[kPendingRequestCalls] = null; this[kDenoClientRid] = undefined; this[kDenoConnRid] = undefined; this[kPollConnPromise] = undefined; socket.on("error", socketOnError); socket.on("close", socketOnClose); const connPromise = new Promise((resolve) => { const eventName = url.startsWith("https") ? "secureConnect" : "connect"; socket.once(eventName, () => { const rid = socket[kHandle][kStreamBaseField][internalRidSymbol]; nextTick(() => { resolve(rid); }); }); }); socket[kSession] = this; // TODO(bartlomieju): cleanup this.#connectPromise = (async () => { debugHttp2(">>> before connect"); const connRid_ = await connPromise; // console.log(">>>> awaited connRid", connRid_, url); const [clientRid, connRid] = await op_http2_connect(connRid_, url); debugHttp2(">>> after connect", clientRid, connRid); this[kDenoClientRid] = clientRid; this[kDenoConnRid] = connRid; (async () => { try { const promise = op_http2_poll_client_connection( this[kDenoConnRid], ); this[kPollConnPromise] = promise; if (!this.#refed) { this.unref(); } await promise; } catch (e) { this.emit("error", e); } })(); this[kState].flags |= SESSION_FLAGS_READY; this.emit("connect", this, {}); })(); } ref() { this.#refed = true; if (this[kPollConnPromise]) { core.refOpPromise(this[kPollConnPromise]); } } unref() { this.#refed = false; if (this[kPollConnPromise]) { core.unrefOpPromise(this[kPollConnPromise]); } } request( headers: Http2Headers, options?: Record, ): ClientHttp2Stream { if (this.destroyed) { throw new ERR_HTTP2_INVALID_SESSION(); } if (this.closed) { throw new ERR_HTTP2_GOAWAY_SESSION(); } this[kUpdateTimer](); if (headers !== null && headers !== undefined) { const keys = Object.keys(headers); for (let i = 0; i < keys.length; i++) { const header = keys[i]; if (header[0] === ":") { assertValidPseudoHeader(header); } else if (header && !_checkIsHttpToken(header)) { this.destroy(new ERR_INVALID_HTTP_TOKEN("Header name", header)); } } } headers = Object.assign({ __proto__: null }, headers); options = { ...options }; if (headers[constants.HTTP2_HEADER_METHOD] === undefined) { headers[constants.HTTP2_HEADER_METHOD] = constants.HTTP2_METHOD_GET; } const connect = headers[constants.HTTP2_HEADER_METHOD] === constants.HTTP2_METHOD_CONNECT; if (!connect || headers[constants.HTTP2_HEADER_PROTOCOL] !== undefined) { if (getAuthority(headers) === undefined) { headers[constants.HTTP2_HEADER_AUTHORITY] = this[kAuthority]; } if (headers[constants.HTTP2_HEADER_SCHEME] === undefined) { headers[constants.HTTP2_HEADER_SCHEME] = this[kProtocol].slice(0, -1); } if (headers[constants.HTTP2_HEADER_PATH] === undefined) { headers[constants.HTTP2_HEADER_PATH] = "/"; } } else { if (headers[constants.HTTP2_HEADER_AUTHORITY] === undefined) { throw new ERR_HTTP2_CONNECT_AUTHORITY(); } if (headers[constants.HTTP2_HEADER_SCHEME] === undefined) { throw new ERR_HTTP2_CONNECT_SCHEME(); } if (headers[constants.HTTP2_HEADER_PATH] === undefined) { throw new ERR_HTTP2_CONNECT_PATH(); } } if (options.endStream === undefined) { const method = headers[constants.HTTP2_HEADER_METHOD]; options.endStream = method === constants.HTTP2_METHOD_DELETE || method === constants.HTTP2_METHOD_GET || method === constants.HTTP2_METHOD_HEAD; } else { options.endStream = !!options.endStream; } const stream = new ClientHttp2Stream( options, this, this.#connectPromise, headers, ); stream[kSentHeaders] = headers; stream[kOrigin] = `${headers[constants.HTTP2_HEADER_SCHEME]}://${ getAuthority(headers) }`; if (options.endStream) { stream.end(); } if (options.waitForTrailers) { stream[kState].flags |= STREAM_FLAGS_HAS_TRAILERS; } const { signal } = options; if (signal) { const aborter = () => { stream.destroy(new AbortError(undefined, { cause: signal.reason })); }; if (signal.aborted) { aborter(); } else { // TODO(bartlomieju): handle this // const disposable = EventEmitter.addAbortListener(signal, aborter); // stream.once("close", disposable[Symbol.dispose]); } } // TODO(bartlomieju): handle this const onConnect = () => {}; if (this.connecting) { if (this[kPendingRequestCalls] !== null) { this[kPendingRequestCalls].push(onConnect); } else { this[kPendingRequestCalls] = [onConnect]; this.once("connect", () => { this[kPendingRequestCalls].forEach((f) => f()); this[kPendingRequestCalls] = null; }); } } else { onConnect(); } return stream; } } function getAuthority(headers) { if (headers[constants.HTTP2_HEADER_AUTHORITY] !== undefined) { return headers[constants.HTTP2_HEADER_AUTHORITY]; } if (headers[constants.HTTP2_HEADER_HOST] !== undefined) { return headers[constants.HTTP2_HEADER_HOST]; } return undefined; } export class Http2Stream extends EventEmitter { #session: Http2Session; #headers: Promise; #controllerPromise: Promise>; #readerPromise: Promise>; #closed: boolean; _response: Response; // This is required to set the trailers on the response. _request: Request; constructor( session: Http2Session, headers: Promise, controllerPromise: Promise>, readerPromise: Promise>, ) { super(); this.#session = session; this.#headers = headers; this.#controllerPromise = controllerPromise; this.#readerPromise = readerPromise; this.#closed = false; nextTick(() => { (async () => { const headers = await this.#headers; this.emit("headers", headers); })(); (async () => { const reader = await this.#readerPromise; if (reader) { for await (const data of reader) { this.emit("data", new Buffer(data)); } } this.emit("end"); })(); }); } // TODO(mmastrac): Implement duplex end() { (async () => { const controller = await this.#controllerPromise; controller.close(); })(); } write(buffer, callback?: () => void) { (async () => { const controller = await this.#controllerPromise; if (typeof buffer === "string") { controller.enqueue(ENCODER.encode(buffer)); } else { controller.enqueue(buffer); } callback?.(); })(); } setEncoding(_encoding) {} resume() { } pause() { } get aborted(): boolean { notImplemented("Http2Stream.aborted"); return false; } get bufferSize(): number { notImplemented("Http2Stream.bufferSize"); return 0; } close(_code: number, _callback: () => void) { this.#closed = true; this.emit("close"); } get closed(): boolean { return this.#closed; } get destroyed(): boolean { return false; } get endAfterHeaders(): boolean { notImplemented("Http2Stream.endAfterHeaders"); return false; } get id(): number | undefined { notImplemented("Http2Stream.id"); return undefined; } get pending(): boolean { notImplemented("Http2Stream.pending"); return false; } priority(_options: Record) { notImplemented("Http2Stream.priority"); } get rstCode(): number { // notImplemented("Http2Stream.rstCode"); return 0; } get sentHeaders(): boolean { notImplemented("Http2Stream.sentHeaders"); return false; } get sentInfoHeaders(): Record { notImplemented("Http2Stream.sentInfoHeaders"); return {}; } get sentTrailers(): Record { notImplemented("Http2Stream.sentTrailers"); return {}; } get session(): Http2Session { return this.#session; } setTimeout(msecs: number, callback?: () => void) { setStreamTimeout(this, msecs, callback); } get state(): Record { notImplemented("Http2Stream.state"); return {}; } sendTrailers(headers: Record) { const request = toInnerRequest(this._request); op_http_set_response_trailers(request.external, Object.entries(headers)); } } async function clientHttp2Request( session, sessionConnectPromise, headers, options, ) { debugHttp2( ">>> waiting for connect promise", sessionConnectPromise, headers, options, ); await sessionConnectPromise; const reqHeaders: string[][] = []; const pseudoHeaders = {}; for (const [key, value] of Object.entries(headers)) { if (key[0] === ":") { pseudoHeaders[key] = value; } else { reqHeaders.push([key, Array.isArray(value) ? value[0] : value]); } } debugHttp2( "waited for connect promise", !!options.waitForTrailers, pseudoHeaders, reqHeaders, ); if (session.closed || session.destroyed) { debugHttp2(">>> session closed during request promise"); throw new ERR_HTTP2_STREAM_CANCEL(); } return await op_http2_client_request( session[kDenoClientRid], pseudoHeaders, reqHeaders, ); } export class ClientHttp2Stream extends Duplex { #requestPromise: Promise<[number, number]>; #responsePromise: Promise; #rid: number | undefined = undefined; #encoding = "utf8"; constructor( options: Record, session: Http2Session, sessionConnectPromise: Promise, headers: Record, ) { options.allowHalfOpen = true; options.decodeString = false; options.autoDestroy = false; super(options); this.cork(); this[kSession] = session; session[kState].pendingStreams.add(this); this._readableState.readingMore = true; this[kState] = { didRead: false, flags: STREAM_FLAGS_PENDING | STREAM_FLAGS_HEADERS_SENT, rstCode: constants.NGHTTP2_NO_ERROR, writeQueueSize: 0, trailersReady: false, endAfterHeaders: false, shutdownWritableCalled: false, serverEndedCall: false, }; this[kDenoResponse] = undefined; this[kDenoRid] = undefined; this.#requestPromise = clientHttp2Request( session, sessionConnectPromise, headers, options, ); debugHttp2(">>> created clienthttp2stream"); // TODO(bartlomieju): save it so we can unref this.#responsePromise = (async () => { debugHttp2(">>> before request promise", session[kDenoClientRid]); const [streamRid, streamId] = await this.#requestPromise; this.#rid = streamRid; this[kDenoRid] = streamRid; this[kInit](streamId); debugHttp2( ">>> after request promise", session[kDenoClientRid], this.#rid, ); if (session.closed || session.destroyed) { debugHttp2(">>> session closed during response promise"); throw new ERR_HTTP2_STREAM_CANCEL(); } const [response, endStream] = await op_http2_client_get_response( this.#rid, ); debugHttp2(">>> after get response", response); const headers = { ":status": response.statusCode, ...Object.fromEntries(response.headers), }; debugHttp2(">>> emitting response", headers); this.emit( "response", headers, endStream ? constants.NGHTTP2_FLAG_END_STREAM : constants.NGHTTP2_FLAG_NONE, ); this[kDenoResponse] = response; this.emit("ready"); })().catch((e) => { if (!(e instanceof ERR_HTTP2_STREAM_CANCEL)) { debugHttp2(">>> request/response promise error", e); } this.destroy(e); }); } [kUpdateTimer]() { if (this.destroyed) { return; } if (this[kTimeout]) { this[kTimeout].refresh(); } if (this[kSession]) { this[kSession][kUpdateTimer](); } } [kInit](id) { const state = this[kState]; state.flags |= STREAM_FLAGS_READY; const session = this[kSession]; session[kState].pendingStreams.delete(this); session[kState].streams.set(id, this); // TODO(bartlomieju): handle socket handle this[kID] = id; this.uncork(); this.emit("ready"); } get bufferSize() { return this[kState].writeQueueSize + this.writableLength; } get endAfterHeaders() { return this[kState].endAfterHeaders; } get sentHeaders() { return this[kSentHeaders]; } get sentTrailers() { return this[kSentTrailers]; } get sendInfoHeaders() { return this[kInfoHeaders]; } get pending(): boolean { return this[kID] === undefined; } get id(): number | undefined { return this[kID]; } get session(): Http2Session { return this[kSession]; } _onTimeout() { callTimeout(this, kSession); } get headersSent() { return !!(this[kState].flags & STREAM_FLAGS_HEADERS_SENT); } get aborted() { return !!(this[kState].flags & STREAM_FLAGS_ABORTED); } get headRequest() { return !!(this[kState].flags & STREAM_FLAGS_HEAD_REQUEST); } get rstCode() { return this[kState].rstCode; } get state(): Record { notImplemented("Http2Stream.state"); return {}; } // [kAfterAsyncWrite]() {} // [kWriteGeneric]() {} // TODO(bartlomieju): clean up _write(chunk, encoding, callback?: () => void) { debugHttp2(">>> _write", encoding, callback); if (typeof encoding === "function") { callback = encoding; encoding = this.#encoding; } let data; if (encoding === "utf8") { data = ENCODER.encode(chunk); } else if (encoding === "buffer") { this.#encoding = encoding; data = chunk.buffer; } this.#requestPromise .then(() => { debugHttp2(">>> _write", this.#rid, data, encoding, callback); return op_http2_client_send_data(this.#rid, new Uint8Array(data)); }) .then(() => { callback?.(); debugHttp2( "this.writableFinished", this.pending, this.destroyed, this.writableFinished, ); }) .catch((e) => { callback?.(e); }); } // TODO(bartlomieju): finish this method _writev(_chunks, _callback?) { notImplemented("ClientHttp2Stream._writev"); } _final(cb) { debugHttp2("_final", new Error()); if (this.pending) { this.once("ready", () => this._final(cb)); return; } shutdownWritable(this, cb, this.#rid); } // TODO(bartlomieju): needs a proper cleanup _read() { if (this.destroyed) { this.push(null); return; } if (!this[kState].didRead) { this._readableState.readingMore = false; this[kState].didRead = true; } // if (!this.pending) { // streamOnResume(this); // } else { // this.once("ready", () => streamOnResume(this)); // } if (!this[kDenoResponse]) { this.once("ready", this._read); return; } debugHttp2(">>> read"); (async () => { const [chunk, finished, cancelled] = await op_http2_client_get_response_body_chunk( this[kDenoResponse].bodyRid, ); if (cancelled) { return; } debugHttp2(">>> chunk", chunk, finished, this[kDenoResponse].bodyRid); if (finished || chunk === null) { this[kState].serverEndedCall = true; const trailerList = await op_http2_client_get_response_trailers( this[kDenoResponse].bodyRid, ); if (trailerList) { const trailers = Object.fromEntries(trailerList); this.emit("trailers", trailers); } debugHttp2(">>> tryClose", this[kDenoResponse]?.bodyRid); core.tryClose(this[kDenoResponse].bodyRid); this.push(null); debugHttp2(">>> read null chunk"); this.read(0); this[kMaybeDestroy](); return; } let result; if (this.#encoding === "utf8") { result = this.push(new TextDecoder().decode(new Uint8Array(chunk))); } else { result = this.push(new Uint8Array(chunk)); } debugHttp2(">>> read result", result); })(); } // TODO(bartlomieju): priority(_options: Record) { notImplemented("Http2Stream.priority"); } sendTrailers(trailers: Record) { debugHttp2("sendTrailers", trailers); if (this.destroyed || this.closed) { throw new ERR_HTTP2_INVALID_STREAM(); } if (this[kSentTrailers]) { throw new ERR_HTTP2_TRAILERS_ALREADY_SENT(); } if (!this[kState].trailersReady) { throw new ERR_HTTP2_TRAILERS_NOT_READY(); } trailers = Object.assign({ __proto__: null }, trailers); const trailerList = []; for (const [key, value] of Object.entries(trailers)) { trailerList.push([key, value]); } this[kSentTrailers] = trailers; // deno-lint-ignore no-this-alias const stream = this; stream[kState].flags &= ~STREAM_FLAGS_HAS_TRAILERS; debugHttp2("sending trailers", this.#rid, trailers); op_http2_client_send_trailers( this.#rid, trailerList, ).then(() => { stream[kMaybeDestroy](); core.tryClose(this.#rid); }).catch((e) => { debugHttp2(">>> send trailers error", e); core.tryClose(this.#rid); stream._destroy(e); }); } get closed(): boolean { return !!(this[kState].flags & STREAM_FLAGS_CLOSED); } close(code: number = constants.NGHTTP2_NO_ERROR, callback?: () => void) { debugHttp2(">>> close", code, this.closed, callback); if (this.closed) { return; } if (typeof callback !== "undefined") { this.once("close", callback); } closeStream(this, code); } _destroy(err, callback) { debugHttp2(">>> ClientHttp2Stream._destroy", err, callback); const session = this[kSession]; const id = this[kID]; const state = this[kState]; const sessionState = session[kState]; const sessionCode = sessionState.goawayCode || sessionState.destroyCode; let code = this.closed ? this.rstCode : sessionCode; if (err != null) { if (sessionCode) { code = sessionCode; } else if (err instanceof AbortError) { code = constants.NGHTTP2_CANCEL; } else { code = constants.NGHTTP2_INTERNAL_ERROR; } } if (!this.closed) { // TODO(bartlomieju): this not handle `socket handle` closeStream(this, code, kNoRstStream); } sessionState.streams.delete(id); sessionState.pendingStreams.delete(this); sessionState.writeQueueSize -= state.writeQueueSize; state.writeQueueSize = 0; const nameForErrorCode = {}; if ( err == null && code !== constants.NGHTTP2_NO_ERROR && code !== constants.NGHTTP2_CANCEL ) { err = new ERR_HTTP2_STREAM_ERROR(nameForErrorCode[code] || code); } this[kSession] = undefined; session[kMaybeDestroy](); if (callback) { callback(err); } } [kMaybeDestroy](code = constants.NGHTTP2_NO_ERROR) { debugHttp2( ">>> ClientHttp2Stream[kMaybeDestroy]", code, this.writableFinished, this.readable, this.closed, ); if (code !== constants.NGHTTP2_NO_ERROR) { this._destroy(); return; } if (this.writableFinished) { if (!this.readable && this.closed) { debugHttp2("going into _destroy"); this._destroy(); return; } } } setTimeout(msecs: number, callback?: () => void) { // TODO(bartlomieju): fix this call, it's crashing on `this` being undefined; // some strange transpilation quirk going on here. setStreamTimeout.call(this, msecs, callback); } } function shutdownWritable(stream, callback, streamRid) { debugHttp2(">>> shutdownWritable", callback); const state = stream[kState]; if (state.shutdownWritableCalled) { debugHttp2(">>> shutdownWritable() already called"); return callback(); } state.shutdownWritableCalled = true; if (state.flags & STREAM_FLAGS_HAS_TRAILERS) { onStreamTrailers(stream); callback(); } else if (state.serverEndedCall) { debugHttp2(">>> stream finished"); callback(); } else { op_http2_client_send_data(streamRid, new Uint8Array(), true) .then(() => { callback(); stream[kMaybeDestroy](); core.tryClose(streamRid); }) .catch((e) => { callback(e); core.tryClose(streamRid); stream._destroy(e); }); } // TODO(bartlomieju): might have to add "finish" event listener here, // check it. } function onStreamTrailers(stream) { stream[kState].trailersReady = true; debugHttp2(">>> onStreamTrailers", stream.destroyed, stream.closed); if (stream.destroyed || stream.closed) { return; } if (!stream.emit("wantTrailers")) { debugHttp2(">>> onStreamTrailers no wantTrailers"); stream.sendTrailers({}); } debugHttp2(">>> onStreamTrailers wantTrailers"); } const kNoRstStream = 0; const kSubmitRstStream = 1; const kForceRstStream = 2; function closeStream(stream, code, rstStreamStatus = kSubmitRstStream) { const state = stream[kState]; state.flags |= STREAM_FLAGS_CLOSED; state.rstCode = code; stream.setTimeout(0); stream.removeAllListeners("timeout"); const { ending } = stream._writableState; if (!ending) { if (!stream.aborted) { state.flags |= STREAM_FLAGS_ABORTED; stream.emit("aborted"); } stream.end(); } if (rstStreamStatus != kNoRstStream) { debugHttp2( ">>> closeStream", !ending, stream.writableFinished, code !== constants.NGHTTP2_NO_ERROR, rstStreamStatus === kForceRstStream, ); if ( !ending || stream.writableFinished || code !== constants.NGHTTP2_NO_ERROR || rstStreamStatus === kForceRstStream ) { finishCloseStream(stream, code); } else { stream.once("finish", () => finishCloseStream(stream, code)); } } } function finishCloseStream(stream, code) { debugHttp2(">>> finishCloseStream", stream.readableEnded, code); if (stream.pending) { stream.push(null); stream.once("ready", () => { op_http2_client_reset_stream( stream[kDenoRid], code, ).then(() => { debugHttp2( ">>> finishCloseStream close", stream[kDenoRid], stream[kDenoResponse]?.bodyRid, ); core.tryClose(stream[kDenoRid]); if (stream[kDenoResponse]) { core.tryClose(stream[kDenoResponse].bodyRid); } stream.emit("close"); }); }); } else { stream.resume(); op_http2_client_reset_stream( stream[kDenoRid], code, ).then(() => { debugHttp2( ">>> finishCloseStream close2", stream[kDenoRid], stream[kDenoResponse].bodyRid, ); core.tryClose(stream[kDenoRid]); if (stream[kDenoResponse]) { core.tryClose(stream[kDenoResponse].bodyRid); } nextTick(() => { stream.emit("close"); }); }).catch(() => { debugHttp2( ">>> finishCloseStream close2 catch", stream[kDenoRid], stream[kDenoResponse]?.bodyRid, ); core.tryClose(stream[kDenoRid]); if (stream[kDenoResponse]) { core.tryClose(stream[kDenoResponse].bodyRid); } nextTick(() => { stream.emit("close"); }); }); } } function callTimeout() { notImplemented("callTimeout"); } export class ServerHttp2Stream extends Http2Stream { _deferred: ReturnType>; #body: ReadableStream; #waitForTrailers: boolean; #headersSent: boolean; constructor( session: Http2Session, headers: Promise, controllerPromise: Promise>, reader: ReadableStream, body: ReadableStream, // This is required to set the trailers on the response. req: Request, ) { super(session, headers, controllerPromise, Promise.resolve(reader)); this._deferred = Promise.withResolvers(); this.#body = body; this._request = req; } additionalHeaders(_headers: Record) { notImplemented("ServerHttp2Stream.additionalHeaders"); } end(): void { super.end(); if (this.#waitForTrailers) { this.emit("wantTrailers"); } } get headersSent(): boolean { return this.#headersSent; } get pushAllowed(): boolean { notImplemented("ServerHttp2Stream.pushAllowed"); return false; } pushStream( _headers: Record, _options: Record, _callback: () => unknown, ) { notImplemented("ServerHttp2Stream.pushStream"); } respond( headers: Http2Headers, options: Record, ) { this.#headersSent = true; const response: ResponseInit = {}; if (headers) { for (const [name, value] of Object.entries(headers)) { if (name == constants.HTTP2_HEADER_STATUS) { response.status = Number(value); } } } if (options?.endStream) { this._deferred.resolve(this._response = new Response("", response)); } else { this.#waitForTrailers = options?.waitForTrailers; this._deferred.resolve( this._response = new Response(this.#body, response), ); } } respondWithFD( _fd: number | FileHandle, _headers: Record, _options: Record, ) { notImplemented("ServerHttp2Stream.respondWithFD"); } respondWithFile( _path: string | Buffer | URL, _headers: Record, _options: Record, ) { notImplemented("ServerHttp2Stream.respondWithFile"); } } function setupCompat(ev) { if (ev === "request") { this.removeListener("newListener", setupCompat); this.on( "stream", FunctionPrototypeBind( onServerStream, this, this[kOptions].Http2ServerRequest, this[kOptions].Http2ServerResponse, ), ); } } function onServerStream( ServerRequest, ServerResponse, stream, headers, _flags, rawHeaders, ) { const request = new ServerRequest(stream, headers, undefined, rawHeaders); const response = new ServerResponse(stream); // Check for the CONNECT method const method = headers[constants.HTTP2_HEADER_METHOD]; if (method === "CONNECT") { if (!this.emit("connect", request, response)) { response.statusCode = constants.HTTP_STATUS_METHOD_NOT_ALLOWED; response.end(); } return; } // Check for Expectations if (headers.expect !== undefined) { if (headers.expect === "100-continue") { if (this.listenerCount("checkContinue")) { this.emit("checkContinue", request, response); } else { response.writeContinue(); this.emit("request", request, response); } } else if (this.listenerCount("checkExpectation")) { this.emit("checkExpectation", request, response); } else { response.statusCode = constants.HTTP_STATUS_EXPECTATION_FAILED; response.end(); } return; } this.emit("request", request, response); } function initializeOptions(options) { // assertIsObject(options, 'options'); options = { ...options }; // assertIsObject(options.settings, 'options.settings'); options.settings = { ...options.settings }; // assertIsArray(options.remoteCustomSettings, 'options.remoteCustomSettings'); if (options.remoteCustomSettings) { options.remoteCustomSettings = [...options.remoteCustomSettings]; if (options.remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS) { throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(); } } // if (options.maxSessionInvalidFrames !== undefined) // validateUint32(options.maxSessionInvalidFrames, 'maxSessionInvalidFrames'); // if (options.maxSessionRejectedStreams !== undefined) { // validateUint32( // options.maxSessionRejectedStreams, // 'maxSessionRejectedStreams', // ); // } if (options.unknownProtocolTimeout !== undefined) { // validateUint32(options.unknownProtocolTimeout, 'unknownProtocolTimeout'); } else { // TODO(danbev): is this a good default value? options.unknownProtocolTimeout = 10000; } // Used only with allowHTTP1 // options.Http1IncomingMessage = options.Http1IncomingMessage || // http.IncomingMessage; // options.Http1ServerResponse = options.Http1ServerResponse || // http.ServerResponse; options.Http2ServerRequest = options.Http2ServerRequest || Http2ServerRequest; options.Http2ServerResponse = options.Http2ServerResponse || Http2ServerResponse; return options; } export class Http2Server extends Server { #options: Record = {}; #abortController; #server; timeout = 0; constructor( options: Record, requestListener: () => unknown, ) { options = initializeOptions(options); super(options); this[kOptions] = options; this.#abortController = new AbortController(); this.on("newListener", setupCompat); this.on( "connection", (conn: Deno.Conn) => { try { const session = new ServerHttp2Session(); this.emit("session", session); this.#server = serveHttpOnConnection( conn, this.#abortController.signal, async (req: Request) => { try { const controllerDeferred = Promise.withResolvers< ReadableStreamDefaultController >(); const body = new ReadableStream({ start(controller) { controllerDeferred.resolve(controller); }, }); const headers: Http2Headers = {}; for (const [name, value] of req.headers) { headers[name] = value; } headers[constants.HTTP2_HEADER_PATH] = new URL(req.url).pathname; const stream = new ServerHttp2Stream( session, Promise.resolve(headers), controllerDeferred.promise, req.body, body, req, ); this.emit("stream", stream, headers); return await stream._deferred.promise; } catch (e) { // deno-lint-ignore no-console console.log(">>> Error in serveHttpOnConnection", e); } return new Response(""); }, () => { // deno-lint-ignore no-console console.log(">>> error"); }, () => {}, ); } catch (e) { // deno-lint-ignore no-console console.log(">>> Error in Http2Server", e); } }, ); this.#options = options; if (typeof requestListener === "function") { this.on("request", requestListener); } } // Prevent the TCP server from wrapping this in a socket, since we need it to serve HTTP _createSocket(clientHandle: TCP) { return clientHandle[kStreamBaseField]; } setTimeout(msecs: number, callback?: () => unknown) { this.timeout = msecs; if (callback !== undefined) { this.on("timeout", callback); } } updateSettings(settings: Record) { this.#options.settings = { ...this.#options.settings, ...settings }; } } export class Http2SecureServer extends Server { #options: Record = {}; timeout = 0; constructor( options: Record, requestListener: () => unknown, ) { super(options, function () { notImplemented("connectionListener"); }); this.#options = options; if (typeof requestListener === "function") { this.on("request", requestListener); } } close(_callback?: () => unknown) { notImplemented("Http2SecureServer.close"); } setTimeout(msecs: number, callback?: () => unknown) { this.timeout = msecs; if (callback !== undefined) { this.on("timeout", callback); } } updateSettings(settings: Record) { this.#options.settings = { ...this.#options.settings, ...settings }; } } export function createServer( options: Record, onRequestHandler: () => unknown, ): Http2Server { if (typeof options === "function") { onRequestHandler = options; options = {}; } return new Http2Server(options, onRequestHandler); } export function createSecureServer( _options: Record, _onRequestHandler: () => unknown, ): Http2SecureServer { notImplemented("http2.createSecureServer"); return new Http2SecureServer(); } export function connect( authority: string | URL, options: Record, callback: (session: ClientHttp2Session) => void, ): ClientHttp2Session { debugHttp2(">>> http2.connect", options); if (typeof options === "function") { callback = options; options = undefined; } options = { ...options }; if (typeof authority === "string") { authority = new URL(authority); } const protocol = authority.protocol || options.protocol || "https:"; let port = 0; if (authority.port !== "") { port = Number(authority.port); } else if (protocol === "http:") { port = 80; } else { port = 443; } if (port == 0) { throw new Error("Invalid port"); } let host = "localhost"; if (authority.hostname) { host = authority.hostname; if (host[0] === "[") { host = host.slice(1, -1); } } else if (authority.host) { host = authority.host; } let url, socket; if (typeof options.createConnection === "function") { url = `http://${host}${port == 80 ? "" : (":" + port)}`; socket = options.createConnection(host, options); } else { switch (protocol) { case "http:": url = `http://${host}${port == 80 ? "" : (":" + port)}`; socket = netConnect({ port, host, ...options, pauseOnCreate: true }); break; case "https:": // TODO(bartlomieju): handle `initializeTLSOptions` here url = `https://${host}${port == 443 ? "" : (":" + port)}`; socket = tlsConnect(port, host, { manualStart: true, ALPNProtocols: ["h2", "http/1.1"], }); break; default: throw new ERR_HTTP2_UNSUPPORTED_PROTOCOL(protocol); } } // Pause so no "socket.read()" starts in the background that would // prevent us from taking ownership of the socket in `ClientHttp2Session` socket.pause(); const session = new ClientHttp2Session(socket, url, options); session[kAuthority] = `${options.servername || host}:${port}`; session[kProtocol] = protocol; if (typeof callback === "function") { session.once("connect", callback); } return session; } function socketOnError(error) { const session = this[kSession]; if (session !== undefined) { if (error.code === "ECONNRESET" && session[kState].goawayCode !== null) { return session.destroy(); } debugHttp2(">>>> socket error", error); session.destroy(error); } } function socketOnClose() { const session = this[kSession]; if (session !== undefined) { debugHttp2(">>>> socket closed"); const err = session.connecting ? new ERR_SOCKET_CLOSED() : null; const state = session[kState]; state.streams.forEach((stream) => stream.close(constants.NGHTTP2_CANCEL)); state.pendingStreams.forEach((stream) => stream.close(constants.NGHTTP2_CANCEL) ); session.close(); session[kMaybeDestroy](err); } } export const constants = { NGHTTP2_ERR_FRAME_SIZE_ERROR: -522, NGHTTP2_NV_FLAG_NONE: 0, NGHTTP2_NV_FLAG_NO_INDEX: 1, NGHTTP2_SESSION_SERVER: 0, NGHTTP2_SESSION_CLIENT: 1, NGHTTP2_STREAM_STATE_IDLE: 1, NGHTTP2_STREAM_STATE_OPEN: 2, NGHTTP2_STREAM_STATE_RESERVED_LOCAL: 3, NGHTTP2_STREAM_STATE_RESERVED_REMOTE: 4, NGHTTP2_STREAM_STATE_HALF_CLOSED_LOCAL: 5, NGHTTP2_STREAM_STATE_HALF_CLOSED_REMOTE: 6, NGHTTP2_STREAM_STATE_CLOSED: 7, NGHTTP2_FLAG_NONE: 0, NGHTTP2_FLAG_END_STREAM: 1, NGHTTP2_FLAG_END_HEADERS: 4, NGHTTP2_FLAG_ACK: 1, NGHTTP2_FLAG_PADDED: 8, NGHTTP2_FLAG_PRIORITY: 32, DEFAULT_SETTINGS_HEADER_TABLE_SIZE: 4096, DEFAULT_SETTINGS_ENABLE_PUSH: 1, DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS: 4294967295, DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE: 65535, DEFAULT_SETTINGS_MAX_FRAME_SIZE: 16384, DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE: 65535, DEFAULT_SETTINGS_ENABLE_CONNECT_PROTOCOL: 0, MAX_MAX_FRAME_SIZE: 16777215, MIN_MAX_FRAME_SIZE: 16384, MAX_INITIAL_WINDOW_SIZE: 2147483647, NGHTTP2_SETTINGS_HEADER_TABLE_SIZE: 1, NGHTTP2_SETTINGS_ENABLE_PUSH: 2, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: 3, NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: 4, NGHTTP2_SETTINGS_MAX_FRAME_SIZE: 5, NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE: 6, NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: 8, PADDING_STRATEGY_NONE: 0, PADDING_STRATEGY_ALIGNED: 1, PADDING_STRATEGY_MAX: 2, PADDING_STRATEGY_CALLBACK: 1, NGHTTP2_NO_ERROR: 0, NGHTTP2_PROTOCOL_ERROR: 1, NGHTTP2_INTERNAL_ERROR: 2, NGHTTP2_FLOW_CONTROL_ERROR: 3, NGHTTP2_SETTINGS_TIMEOUT: 4, NGHTTP2_STREAM_CLOSED: 5, NGHTTP2_FRAME_SIZE_ERROR: 6, NGHTTP2_REFUSED_STREAM: 7, NGHTTP2_CANCEL: 8, NGHTTP2_COMPRESSION_ERROR: 9, NGHTTP2_CONNECT_ERROR: 10, NGHTTP2_ENHANCE_YOUR_CALM: 11, NGHTTP2_INADEQUATE_SECURITY: 12, NGHTTP2_HTTP_1_1_REQUIRED: 13, NGHTTP2_DEFAULT_WEIGHT: 16, HTTP2_HEADER_STATUS: ":status", HTTP2_HEADER_METHOD: ":method", HTTP2_HEADER_AUTHORITY: ":authority", HTTP2_HEADER_SCHEME: ":scheme", HTTP2_HEADER_PATH: ":path", HTTP2_HEADER_PROTOCOL: ":protocol", HTTP2_HEADER_ACCEPT_ENCODING: "accept-encoding", HTTP2_HEADER_ACCEPT_LANGUAGE: "accept-language", HTTP2_HEADER_ACCEPT_RANGES: "accept-ranges", HTTP2_HEADER_ACCEPT: "accept", HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS: "access-control-allow-credentials", HTTP2_HEADER_ACCESS_CONTROL_ALLOW_HEADERS: "access-control-allow-headers", HTTP2_HEADER_ACCESS_CONTROL_ALLOW_METHODS: "access-control-allow-methods", HTTP2_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN: "access-control-allow-origin", HTTP2_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS: "access-control-expose-headers", HTTP2_HEADER_ACCESS_CONTROL_REQUEST_HEADERS: "access-control-request-headers", HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD: "access-control-request-method", HTTP2_HEADER_AGE: "age", HTTP2_HEADER_AUTHORIZATION: "authorization", HTTP2_HEADER_CACHE_CONTROL: "cache-control", HTTP2_HEADER_CONNECTION: "connection", HTTP2_HEADER_CONTENT_DISPOSITION: "content-disposition", HTTP2_HEADER_CONTENT_ENCODING: "content-encoding", HTTP2_HEADER_CONTENT_LENGTH: "content-length", HTTP2_HEADER_CONTENT_TYPE: "content-type", HTTP2_HEADER_COOKIE: "cookie", HTTP2_HEADER_DATE: "date", HTTP2_HEADER_ETAG: "etag", HTTP2_HEADER_FORWARDED: "forwarded", HTTP2_HEADER_HOST: "host", HTTP2_HEADER_IF_MODIFIED_SINCE: "if-modified-since", HTTP2_HEADER_IF_NONE_MATCH: "if-none-match", HTTP2_HEADER_IF_RANGE: "if-range", HTTP2_HEADER_LAST_MODIFIED: "last-modified", HTTP2_HEADER_LINK: "link", HTTP2_HEADER_LOCATION: "location", HTTP2_HEADER_RANGE: "range", HTTP2_HEADER_REFERER: "referer", HTTP2_HEADER_SERVER: "server", HTTP2_HEADER_SET_COOKIE: "set-cookie", HTTP2_HEADER_STRICT_TRANSPORT_SECURITY: "strict-transport-security", HTTP2_HEADER_TRANSFER_ENCODING: "transfer-encoding", HTTP2_HEADER_TE: "te", HTTP2_HEADER_UPGRADE_INSECURE_REQUESTS: "upgrade-insecure-requests", HTTP2_HEADER_UPGRADE: "upgrade", HTTP2_HEADER_USER_AGENT: "user-agent", HTTP2_HEADER_VARY: "vary", HTTP2_HEADER_X_CONTENT_TYPE_OPTIONS: "x-content-type-options", HTTP2_HEADER_X_FRAME_OPTIONS: "x-frame-options", HTTP2_HEADER_KEEP_ALIVE: "keep-alive", HTTP2_HEADER_PROXY_CONNECTION: "proxy-connection", HTTP2_HEADER_X_XSS_PROTECTION: "x-xss-protection", HTTP2_HEADER_ALT_SVC: "alt-svc", HTTP2_HEADER_CONTENT_SECURITY_POLICY: "content-security-policy", HTTP2_HEADER_EARLY_DATA: "early-data", HTTP2_HEADER_EXPECT_CT: "expect-ct", HTTP2_HEADER_ORIGIN: "origin", HTTP2_HEADER_PURPOSE: "purpose", HTTP2_HEADER_TIMING_ALLOW_ORIGIN: "timing-allow-origin", HTTP2_HEADER_X_FORWARDED_FOR: "x-forwarded-for", HTTP2_HEADER_PRIORITY: "priority", HTTP2_HEADER_ACCEPT_CHARSET: "accept-charset", HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE: "access-control-max-age", HTTP2_HEADER_ALLOW: "allow", HTTP2_HEADER_CONTENT_LANGUAGE: "content-language", HTTP2_HEADER_CONTENT_LOCATION: "content-location", HTTP2_HEADER_CONTENT_MD5: "content-md5", HTTP2_HEADER_CONTENT_RANGE: "content-range", HTTP2_HEADER_DNT: "dnt", HTTP2_HEADER_EXPECT: "expect", HTTP2_HEADER_EXPIRES: "expires", HTTP2_HEADER_FROM: "from", HTTP2_HEADER_IF_MATCH: "if-match", HTTP2_HEADER_IF_UNMODIFIED_SINCE: "if-unmodified-since", HTTP2_HEADER_MAX_FORWARDS: "max-forwards", HTTP2_HEADER_PREFER: "prefer", HTTP2_HEADER_PROXY_AUTHENTICATE: "proxy-authenticate", HTTP2_HEADER_PROXY_AUTHORIZATION: "proxy-authorization", HTTP2_HEADER_REFRESH: "refresh", HTTP2_HEADER_RETRY_AFTER: "retry-after", HTTP2_HEADER_TRAILER: "trailer", HTTP2_HEADER_TK: "tk", HTTP2_HEADER_VIA: "via", HTTP2_HEADER_WARNING: "warning", HTTP2_HEADER_WWW_AUTHENTICATE: "www-authenticate", HTTP2_HEADER_HTTP2_SETTINGS: "http2-settings", HTTP2_METHOD_ACL: "ACL", HTTP2_METHOD_BASELINE_CONTROL: "BASELINE-CONTROL", HTTP2_METHOD_BIND: "BIND", HTTP2_METHOD_CHECKIN: "CHECKIN", HTTP2_METHOD_CHECKOUT: "CHECKOUT", HTTP2_METHOD_CONNECT: "CONNECT", HTTP2_METHOD_COPY: "COPY", HTTP2_METHOD_DELETE: "DELETE", HTTP2_METHOD_GET: "GET", HTTP2_METHOD_HEAD: "HEAD", HTTP2_METHOD_LABEL: "LABEL", HTTP2_METHOD_LINK: "LINK", HTTP2_METHOD_LOCK: "LOCK", HTTP2_METHOD_MERGE: "MERGE", HTTP2_METHOD_MKACTIVITY: "MKACTIVITY", HTTP2_METHOD_MKCALENDAR: "MKCALENDAR", HTTP2_METHOD_MKCOL: "MKCOL", HTTP2_METHOD_MKREDIRECTREF: "MKREDIRECTREF", HTTP2_METHOD_MKWORKSPACE: "MKWORKSPACE", HTTP2_METHOD_MOVE: "MOVE", HTTP2_METHOD_OPTIONS: "OPTIONS", HTTP2_METHOD_ORDERPATCH: "ORDERPATCH", HTTP2_METHOD_PATCH: "PATCH", HTTP2_METHOD_POST: "POST", HTTP2_METHOD_PRI: "PRI", HTTP2_METHOD_PROPFIND: "PROPFIND", HTTP2_METHOD_PROPPATCH: "PROPPATCH", HTTP2_METHOD_PUT: "PUT", HTTP2_METHOD_REBIND: "REBIND", HTTP2_METHOD_REPORT: "REPORT", HTTP2_METHOD_SEARCH: "SEARCH", HTTP2_METHOD_TRACE: "TRACE", HTTP2_METHOD_UNBIND: "UNBIND", HTTP2_METHOD_UNCHECKOUT: "UNCHECKOUT", HTTP2_METHOD_UNLINK: "UNLINK", HTTP2_METHOD_UNLOCK: "UNLOCK", HTTP2_METHOD_UPDATE: "UPDATE", HTTP2_METHOD_UPDATEREDIRECTREF: "UPDATEREDIRECTREF", HTTP2_METHOD_VERSION_CONTROL: "VERSION-CONTROL", HTTP_STATUS_CONTINUE: 100, HTTP_STATUS_SWITCHING_PROTOCOLS: 101, HTTP_STATUS_PROCESSING: 102, HTTP_STATUS_EARLY_HINTS: 103, HTTP_STATUS_OK: 200, HTTP_STATUS_CREATED: 201, HTTP_STATUS_ACCEPTED: 202, HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION: 203, HTTP_STATUS_NO_CONTENT: 204, HTTP_STATUS_RESET_CONTENT: 205, HTTP_STATUS_PARTIAL_CONTENT: 206, HTTP_STATUS_MULTI_STATUS: 207, HTTP_STATUS_ALREADY_REPORTED: 208, HTTP_STATUS_IM_USED: 226, HTTP_STATUS_MULTIPLE_CHOICES: 300, HTTP_STATUS_MOVED_PERMANENTLY: 301, HTTP_STATUS_FOUND: 302, HTTP_STATUS_SEE_OTHER: 303, HTTP_STATUS_NOT_MODIFIED: 304, HTTP_STATUS_USE_PROXY: 305, HTTP_STATUS_TEMPORARY_REDIRECT: 307, HTTP_STATUS_PERMANENT_REDIRECT: 308, HTTP_STATUS_BAD_REQUEST: 400, HTTP_STATUS_UNAUTHORIZED: 401, HTTP_STATUS_PAYMENT_REQUIRED: 402, HTTP_STATUS_FORBIDDEN: 403, HTTP_STATUS_NOT_FOUND: 404, HTTP_STATUS_METHOD_NOT_ALLOWED: 405, HTTP_STATUS_NOT_ACCEPTABLE: 406, HTTP_STATUS_PROXY_AUTHENTICATION_REQUIRED: 407, HTTP_STATUS_REQUEST_TIMEOUT: 408, HTTP_STATUS_CONFLICT: 409, HTTP_STATUS_GONE: 410, HTTP_STATUS_LENGTH_REQUIRED: 411, HTTP_STATUS_PRECONDITION_FAILED: 412, HTTP_STATUS_PAYLOAD_TOO_LARGE: 413, HTTP_STATUS_URI_TOO_LONG: 414, HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE: 415, HTTP_STATUS_RANGE_NOT_SATISFIABLE: 416, HTTP_STATUS_EXPECTATION_FAILED: 417, HTTP_STATUS_TEAPOT: 418, HTTP_STATUS_MISDIRECTED_REQUEST: 421, HTTP_STATUS_UNPROCESSABLE_ENTITY: 422, HTTP_STATUS_LOCKED: 423, HTTP_STATUS_FAILED_DEPENDENCY: 424, HTTP_STATUS_TOO_EARLY: 425, HTTP_STATUS_UPGRADE_REQUIRED: 426, HTTP_STATUS_PRECONDITION_REQUIRED: 428, HTTP_STATUS_TOO_MANY_REQUESTS: 429, HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE: 431, HTTP_STATUS_UNAVAILABLE_FOR_LEGAL_REASONS: 451, HTTP_STATUS_INTERNAL_SERVER_ERROR: 500, HTTP_STATUS_NOT_IMPLEMENTED: 501, HTTP_STATUS_BAD_GATEWAY: 502, HTTP_STATUS_SERVICE_UNAVAILABLE: 503, HTTP_STATUS_GATEWAY_TIMEOUT: 504, HTTP_STATUS_HTTP_VERSION_NOT_SUPPORTED: 505, HTTP_STATUS_VARIANT_ALSO_NEGOTIATES: 506, HTTP_STATUS_INSUFFICIENT_STORAGE: 507, HTTP_STATUS_LOOP_DETECTED: 508, HTTP_STATUS_BANDWIDTH_LIMIT_EXCEEDED: 509, HTTP_STATUS_NOT_EXTENDED: 510, HTTP_STATUS_NETWORK_AUTHENTICATION_REQUIRED: 511, }; // const kSingleValueHeaders = new Set([ // constants.HTTP2_HEADER_STATUS, // constants.HTTP2_HEADER_METHOD, // constants.HTTP2_HEADER_AUTHORITY, // constants.HTTP2_HEADER_SCHEME, // constants.HTTP2_HEADER_PATH, // constants.HTTP2_HEADER_PROTOCOL, // constants.HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, // constants.HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE, // constants.HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD, // constants.HTTP2_HEADER_AGE, // constants.HTTP2_HEADER_AUTHORIZATION, // constants.HTTP2_HEADER_CONTENT_ENCODING, // constants.HTTP2_HEADER_CONTENT_LANGUAGE, // constants.HTTP2_HEADER_CONTENT_LENGTH, // constants.HTTP2_HEADER_CONTENT_LOCATION, // constants.HTTP2_HEADER_CONTENT_MD5, // constants.HTTP2_HEADER_CONTENT_RANGE, // constants.HTTP2_HEADER_CONTENT_TYPE, // constants.HTTP2_HEADER_DATE, // constants.HTTP2_HEADER_DNT, // constants.HTTP2_HEADER_ETAG, // constants.HTTP2_HEADER_EXPIRES, // constants.HTTP2_HEADER_FROM, // constants.HTTP2_HEADER_HOST, // constants.HTTP2_HEADER_IF_MATCH, // constants.HTTP2_HEADER_IF_MODIFIED_SINCE, // constants.HTTP2_HEADER_IF_NONE_MATCH, // constants.HTTP2_HEADER_IF_RANGE, // constants.HTTP2_HEADER_IF_UNMODIFIED_SINCE, // constants.HTTP2_HEADER_LAST_MODIFIED, // constants.HTTP2_HEADER_LOCATION, // constants.HTTP2_HEADER_MAX_FORWARDS, // constants.HTTP2_HEADER_PROXY_AUTHORIZATION, // constants.HTTP2_HEADER_RANGE, // constants.HTTP2_HEADER_REFERER, // constants.HTTP2_HEADER_RETRY_AFTER, // constants.HTTP2_HEADER_TK, // constants.HTTP2_HEADER_UPGRADE_INSECURE_REQUESTS, // constants.HTTP2_HEADER_USER_AGENT, // constants.HTTP2_HEADER_X_CONTENT_TYPE_OPTIONS, // ]); export function getDefaultSettings(): Record { notImplemented("http2.getDefaultSettings"); return {}; } export function getPackedSettings(_settings: Record): Buffer { notImplemented("http2.getPackedSettings"); return {}; } export function getUnpackedSettings( _buffer: Buffer | TypedArray, ): Record { notImplemented("http2.getUnpackedSettings"); return {}; } export const sensitiveHeaders = Symbol("nodejs.http2.sensitiveHeaders"); const kBeginSend = Symbol("begin-send"); 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"); const kRequest = Symbol("request"); const streamProxySocketHandler = { has(stream, prop) { const ref = stream.session !== undefined ? stream.session[kSocket] : stream; return (prop in stream) || (prop in ref); }, 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 the user didn't interact with incoming data and didn't pipe it, // dump it for compatibility with http1 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"); }; } export class Http2ServerRequest extends Readable { readableEnded = false; constructor(stream, headers, options, rawHeaders) { super({ autoDestroy: false, ...options }); this[kState] = { closed: false, didRead: false, }; // Headers in HTTP/1 are not initialized using Object.create(null) which, // although preferable, would simply break too much code. Ergo header // initialization using Object.create(null) in HTTP/2 is intentional. this[kHeaders] = headers; this[kRawHeaders] = rawHeaders; this[kTrailers] = {}; this[kRawTrailers] = []; this[kStream] = stream; this[kAborted] = false; stream[kProxySocket] = null; stream[kRequest] = this; // Pause the stream.. 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, streamProxySocketHandler); } 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 { // nextTick(resumeStream, this[kStream]); // } // } get method() { return this[kHeaders][constants.HTTP2_HEADER_METHOD]; } set method(method) { // validateString(method, "method"); if (StringPrototypeTrim(method) === "") { throw new ERR_INVALID_ARG_VALUE("method", method); } this[kHeaders][constants.HTTP2_HEADER_METHOD] = method; } get authority() { return getAuthority(this[kHeaders]); } get scheme() { return this[kHeaders][constants.HTTP2_HEADER_SCHEME]; } get url() { return this[kHeaders][constants.HTTP2_HEADER_PATH]; } set url(url) { this[kHeaders][constants.HTTP2_HEADER_PATH] = url; } setTimeout(msecs, callback) { if (!this[kState].closed) { this[kStream].setTimeout(msecs, callback); } return this; } } function onStreamEnd() { // Cause the request stream to end as well. const request = this[kRequest]; if (request !== undefined) { this[kRequest].push(null); } } function onStreamError(_error) { // This is purposefully left blank // // errors in compatibility mode are // not forwarded to the request // and response objects. } 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 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"); } function onStreamAbortedResponse() { // non-op for now } let statusMessageWarned = false; // Defines and implements an API compatibility layer on top of the core // HTTP/2 implementation, intended to provide an interface that is as // close as possible to the current require('http') API function statusMessageWarn() { if (statusMessageWarned === false) { emitWarning( "Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)", "UnsupportedWarning", ); statusMessageWarned = true; } } function isConnectionHeaderAllowed(name, value) { return name !== constants.HTTP2_HEADER_CONNECTION || value === "trailers"; } export class Http2ServerResponse extends Stream { writable = false; req = null; constructor(stream, options) { super(options); this[kState] = { closed: false, ending: false, destroyed: false, headRequest: false, sendDate: true, statusCode: constants.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)); } // User land modules such as finalhandler just check truthiness of this // but if someone is actually trying to use this for more than that // then we simply can't support such use cases 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() { // This is compatible with http1 which removes socket reference // only from ServerResponse but not IncomingMessage if (this[kState].closed) { return undefined; } const stream = this[kStream]; const proxySocket = stream[kProxySocket]; if (proxySocket === null) { return stream[kProxySocket] = new Proxy(stream, streamProxySocketHandler); } 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 writableCorked() { return this[kStream].writableCorked; } get writableHighWaterMark() { return this[kStream].writableHighWaterMark; } get writableFinished() { return this[kStream].writableFinished; } get writableLength() { return this[kStream].writableLength; } get statusCode() { return this[kState].statusCode; } 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 ObjectPrototypeHasOwnProperty(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)); } // Handle various possible cases the same as OutgoingMessage.appendHeader: 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++) { existingValues.push(value[i]); } } else { existingValues.push(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) { 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]) { // Headers in obj should override previous headers but still // allow explicit duplicates. To do so, we first remove any // existing conflicts, then use appendHeader. This is the // slow path, which only applies when you use setHeader and // then pass headers in writeHead too. // We need to handle both the tuple and flat array formats, just // like the logic further below. 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); } } } // Append all the headers provided in the array: 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") { 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") { 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) { 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, options)); }); } [kBeginSend]() { const state = this[kState]; const headers = this[kHeaders]; headers[constants.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({ [constants.HTTP2_HEADER_STATUS]: constants.HTTP_STATUS_CONTINUE, }); return true; } writeEarlyHints(hints) { // validateObject(hints, "hints"); const headers = { __proto__: null }; // const linkHeaderValue = validateLinkHeaderValue(hints.link); for (const key of 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, [constants.HTTP2_HEADER_STATUS]: constants.HTTP_STATUS_EARLY_HINTS, // "Link": linkHeaderValue, }); return true; } } export default { createServer, createSecureServer, connect, constants, getDefaultSettings, getPackedSettings, getUnpackedSettings, sensitiveHeaders, Http2ServerRequest, Http2ServerResponse, };