Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Preserve custom Headers iterators and copy server.fetch header ownership. Thanks @robobun!
- Match Undici timeout metadata and WebSocket heartbeat APIs, HTTP listen errors and byte counters, and HTTPS constructor, half-open, and ALPN behavior.

- Preserve Node-alias stdin, eval arguments, version queries, and lexical entry paths; close transferred MessagePorts when worker startup fails.
- Honor dynamic plugin targets and import kinds, preserve literal POSIX paths and ESM fragments, and match Node loading for untyped dependencies and inline TypeScript type clauses.
- Report referenced Timeout and Immediate resources from `process.getActiveResourcesInfo()`, isolated per worker.
Expand Down
55 changes: 55 additions & 0 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ static int us_ssl_bio_type = 0;
* entry — see us_ssl_ctx_set_sni_policy. Absent on node:tls SecureContexts,
* whose policy is server-level. */
static int us_ctx_sni_policy_ex_idx = -1;
/* (SSL_CTX) owned wire-format ALPN protocol list used by the server selector. */
static int us_ctx_alpn_protocols_ex_idx = -1;
/* Defined in Rust (src/uws_sys/SocketKind.rs) so the ordinal tracks the enum. */
extern const unsigned char BUN_SOCKET_KIND_BUN_SOCKET_TLS;
extern const unsigned char BUN_SOCKET_KIND_UWS_HTTP_TLS;
Expand Down Expand Up @@ -366,13 +368,26 @@ static void ssl_flush_pending_events(struct us_socket_t *s) {
extern void bun_ssl_ctx_cache_on_free(void *parent, void *ptr, CRYPTO_EX_DATA *ad,
int index, long argl, void *argp);

struct us_ssl_alpn_protocols_t {
unsigned int length;
unsigned char data[];
};

static void us_ssl_alpn_protocols_free(void *parent, void *ptr, CRYPTO_EX_DATA *ad,
int index, long argl, void *argp) {
(void)parent; (void)ad; (void)index; (void)argl; (void)argp;
if (ptr) us_free(ptr);
}

static void us_ex_idx_init(void) {
us_ctx_ex_idx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, us_ctx_ex_free);
us_sni_ex_idx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL);
us_ctx_cache_ex_idx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, bun_ssl_ctx_cache_on_free);
us_ctx_user_ca_ex_idx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL);
us_ctx_use_system_ca_ex_idx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL);
us_ctx_sni_policy_ex_idx = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL);
us_ctx_alpn_protocols_ex_idx =
SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, us_ssl_alpn_protocols_free);
us_ssl_rare_ex_idx = SSL_get_ex_new_index(0, NULL, NULL, NULL, us_ssl_rare_free);
us_ssl_wrapper_ex_idx = SSL_get_ex_new_index(0, NULL, NULL, NULL, NULL);
us_ssl_bio_type = BIO_get_new_index() | BIO_TYPE_SOURCE_SINK;
Expand Down Expand Up @@ -1634,6 +1649,46 @@ static int us_alpn_select_h2(SSL *ssl, const unsigned char **out, unsigned char
return allow_http1 ? SSL_TLSEXT_ERR_NOACK : SSL_TLSEXT_ERR_ALERT_FATAL;
}

static int us_alpn_select_protocols(SSL *ssl, const unsigned char **out,
unsigned char *outlen, const unsigned char *in,
unsigned int inlen, void *arg) {
(void)ssl;
struct us_ssl_alpn_protocols_t *protocols = arg;
if (!protocols) return SSL_TLSEXT_ERR_NOACK;
return SSL_select_next_proto((unsigned char **)out, outlen, protocols->data,
protocols->length, in, inlen) == OPENSSL_NPN_NEGOTIATED
? SSL_TLSEXT_ERR_OK
: SSL_TLSEXT_ERR_ALERT_FATAL;
}

int us_ssl_ctx_set_alpn_protocols(SSL_CTX *ctx, const unsigned char *protocols,
unsigned int protocols_len) {
if (!ctx || !protocols || !protocols_len) return 0;
for (unsigned int offset = 0; offset < protocols_len;) {
unsigned int length = protocols[offset];
if (!length || length > protocols_len - offset - 1) return 0;
offset += length + 1;
}

us_ex_idx_ensure();
if (us_ctx_alpn_protocols_ex_idx < 0) return 0;
struct us_ssl_alpn_protocols_t *owned =
us_malloc(sizeof(struct us_ssl_alpn_protocols_t) + protocols_len);
if (!owned) return 0;
owned->length = protocols_len;
memcpy(owned->data, protocols, protocols_len);

struct us_ssl_alpn_protocols_t *previous =
SSL_CTX_get_ex_data(ctx, us_ctx_alpn_protocols_ex_idx);
if (!SSL_CTX_set_ex_data(ctx, us_ctx_alpn_protocols_ex_idx, owned)) {
us_free(owned);
return 0;
}
SSL_CTX_set_alpn_select_cb(ctx, us_alpn_select_protocols, owned);
if (previous) us_free(previous);
return 1;
}

void us_ssl_ctx_enable_http2_alpn(SSL_CTX *ctx, int allow_http1) {
SSL_CTX_set_alpn_select_cb(ctx, us_alpn_select_h2, allow_http1 ? (void *) 1 : NULL);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/bun-usockets/src/libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,10 @@ void us_internal_ssl_ctx_unref(struct ssl_ctx_st *ssl_ctx);
/* Install an ALPN selector that prefers "h2", then "http/1.1" (when
* allow_http1). Used by uWS when an App has an HTTP/2 context attached. */
void us_ssl_ctx_enable_http2_alpn(struct ssl_ctx_st *ssl_ctx, int allow_http1);
/* Install a server ALPN selector from the TLS wire-format protocol list. The
* SSL_CTX owns its copy until its final reference is released. */
int us_ssl_ctx_set_alpn_protocols(struct ssl_ctx_st *ssl_ctx,
const unsigned char *protocols, unsigned int protocols_len);
/* 1 iff the completed handshake on `s` negotiated ALPN "h2". */
int us_socket_alpn_is_h2(us_socket_r s);
long us_ssl_ctx_live_count(void);
Expand Down
24 changes: 23 additions & 1 deletion packages/bun-uws/src/App.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ struct TemplatedApp {
HttpRouter<typename HttpContextData<SSL>::RouterData> *router;
};
std::vector<PendingServerName> pendingServerNames;
std::vector<unsigned char> alpnProtocols;
/* No raw us_listen_socket_t* cache here. src/runtime/server/mod.rs's non-abrupt stop calls
* us_listen_socket_close(ls) directly; the listener is queued for free in
* loop_post, so any vector we kept would dangle by the time the deferred
Expand Down Expand Up @@ -154,6 +155,11 @@ struct TemplatedApp {
if (applyClientCertPolicy) {
us_ssl_ctx_set_sni_policy(domainCtx, options.request_cert, options.reject_unauthorized);
}
if (!alpnProtocols.empty() && !us_ssl_ctx_set_alpn_protocols(domainCtx, alpnProtocols.data(), static_cast<unsigned int>(alpnProtocols.size()))) {
us_internal_ssl_ctx_unref(domainCtx);
if (success) *success = false;
return std::move(*this);
}
if (httpContext->getSocketContextData()->http2Context) {
us_ssl_ctx_enable_http2_alpn(domainCtx, httpContext->getSocketContextData()->allowHttp1);
}
Expand Down Expand Up @@ -218,7 +224,19 @@ struct TemplatedApp {
return sslCtx;
}

bool setSecureContext(SocketContextOptions options, const char *const *additionalCa, unsigned int additionalCaCount) {
bool setALPNProtocols(const unsigned char *protocols, unsigned int protocolsLength) {
if constexpr (!SSL) {
return false;
} else {
if (!us_ssl_ctx_set_alpn_protocols(sslCtx, protocols, protocolsLength)) {
return false;
}
alpnProtocols.assign(protocols, protocols + protocolsLength);
return true;
}
}

bool setSecureContext(SocketContextOptions options, const char *const *additionalCa, unsigned int additionalCaCount, const unsigned char *alpnProtocols, unsigned int alpnProtocolsLength) {
if constexpr (!SSL) {
return false;
} else {
Expand All @@ -232,6 +250,10 @@ struct TemplatedApp {
return false;
}
}
if (alpnProtocolsLength && !us_ssl_ctx_set_alpn_protocols(next, alpnProtocols, alpnProtocolsLength)) {
us_internal_ssl_ctx_unref(next);
return false;
}
if (httpContext->getSocketContextData()->http2Context) {
us_ssl_ctx_enable_http2_alpn(next, httpContext->getSocketContextData()->allowHttp1);
}
Expand Down
50 changes: 41 additions & 9 deletions src/js/node/_http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,31 @@ function emitListenErrorNextTick(self, err) {
self.emit("error", err);
}

function formatListenError(err, port, host, socketPath) {
const description =
err?.code === "EADDRINUSE"
? "address already in use"
: err?.code === "EACCES"
? "permission denied"
: err?.code === "EADDRNOTAVAIL"
? "address not available"
: err?.code === "EINVAL"
? "invalid argument"
: undefined;
if (!description) {
return err;
}
const address = socketPath ?? host ?? "::";
err.syscall = "listen";
err.address = address;
if (port) {
err.port = port;
}
const location = port ? `${address}:${port}` : address;
err.message = `listen ${err.code}: ${description}${location ? ` ${location}` : ""}`;
return err;
}

// Node.js only requests a client certificate when `requestCert: true`.
// The uSockets SSL context treats `ca` alone as "verify peer", so without
// these two flags an `https.Server({ ca })` would reject every client that
Expand Down Expand Up @@ -370,6 +395,7 @@ interface Server extends NodeHTTPServer {
httpValidation?: "strict" | "relaxed" | "insecure";
requireHostHeader: boolean;
httpAllowHalfOpen: boolean;
allowHalfOpen: boolean;
}
function Server(options, callback): void {
if (!(this instanceof Server)) return new Server(options, callback);
Expand Down Expand Up @@ -478,6 +504,7 @@ function Server(options, callback): void {
minVersion,
maxVersion,
ciphers: typeof options.ciphers === "string" && options.ciphers ? options.ciphers : undefined,
ALPNProtocols: options.ALPNProtocols,
requestCert: options.requestCert,
rejectUnauthorized: options.rejectUnauthorized,
_pfxExtraCACerts: pfxExtraCAs,
Expand All @@ -493,6 +520,9 @@ function Server(options, callback): void {

this[optionsSymbol] = options;
storeHTTPOptions.$call(this, options);
// Plain HTTP keeps its parser socket half-open, while Node's HTTPS server
// inherits tls.Server's default and only allows half-open sockets on request.
this.allowHalfOpen = this[isTlsSymbol] ? options.allowHalfOpen === true : true;

if (this[tlsSymbol]) {
this.on("secureConnection", secureConnectionListener);
Expand Down Expand Up @@ -710,6 +740,7 @@ Server.prototype[setSecureContextSymbol] = function (options) {
serverName: tlsOptions.servername,
requestCert: current.requestCert,
rejectUnauthorized: current.rejectUnauthorized,
ALPNProtocols: this.ALPNProtocols,
};
this[serverSymbol]?._setNodeHTTPSSecureContext(next, getAdditionalCAOptions(next));
this[tlsSymbol] = normalizeServerTls(next);
Expand Down Expand Up @@ -822,7 +853,7 @@ Server.prototype.listen = function () {
serverNameHost,
);
} catch (err) {
process.nextTick(emitListenErrorNextTick, server, err);
process.nextTick(emitListenErrorNextTick, server, formatListenError(err, port, address, socketPath));
}
});
return this;
Expand All @@ -831,7 +862,7 @@ Server.prototype.listen = function () {
try {
startServerListen(server, tls, port, host, socketPath, serverNameHost);
} catch (err) {
process.nextTick(emitListenErrorNextTick, server, err);
process.nextTick(emitListenErrorNextTick, server, formatListenError(err, port, host, socketPath));
}

return this;
Expand Down Expand Up @@ -1813,13 +1844,13 @@ function getNodeHTTPServerSocket() {
declare _writableState: { emitClose: boolean; decodeStrings: boolean };
declare _readableState: { emitClose: boolean };
constructor(server: Server, handle, encrypted, listenerGeneration) {
// allowHalfOpen: node's connectionListener sockets never auto-end the
// writable side on the peer's FIN (CONNECT/Upgrade tunnels stay writable);
// net.Socket would otherwise default it to false.
// Plain HTTP parser sockets stay half-open for CONNECT/Upgrade tunnels.
// HTTPS sockets inherit tls.Server's half-open policy instead.
const allowHalfOpen = encrypted ? server.allowHalfOpen : true;
super(
server[kHighWaterMark] !== undefined
? { highWaterMark: server[kHighWaterMark], allowHalfOpen: true }
: { allowHalfOpen: true },
? { highWaterMark: server[kHighWaterMark], allowHalfOpen }
: { allowHalfOpen },
);
// net.Socket's constructor wires net-handle machinery this class replaces:
// its 'end' listener installs writeAfterFIN (breaks half-open tunnels), and
Expand Down Expand Up @@ -1853,8 +1884,9 @@ function getNodeHTTPServerSocket() {

get bytesWritten() {
const handle = this[kHandle];
// HTTP response bodies and raw socket writes use separate counters.
return handle
? (handle.response?.getBytesWritten?.() ?? handle.bytesWritten ?? this[kBytesWritten] ?? 0)
? (handle.response?.getBytesWritten?.() ?? 0) + (handle.bytesWritten ?? 0)
: (this[kBytesWritten] ?? 0);
}
set bytesWritten(value) {
Expand Down Expand Up @@ -1904,7 +1936,7 @@ function getNodeHTTPServerSocket() {
}
#onDrain() {
const handle = this[kHandle];
this[kBytesWritten] = handle ? (handle.response?.getBytesWritten?.() ?? handle.bytesWritten ?? 0) : 0;
this[kBytesWritten] = handle ? (handle.response?.getBytesWritten?.() ?? 0) + (handle.bytesWritten ?? 0) : 0;
const callback = this.#pendingCallback;
if (callback) {
this.#pendingCallback = null;
Expand Down
23 changes: 16 additions & 7 deletions src/js/node/https.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,9 @@ const { shouldUseEnvProxy } = require("node:_http_agent");
// normalized protocol list / callback on the server instance the way
// tls.Server does (test-https-argument-of-creating.js).
// https://github.com/nodejs/node/blob/v26.3.0/lib/https.js#L82-L97
function createServer(options, requestListener) {
function Server(options, requestListener): void {
if (!(this instanceof Server)) return new Server(options, requestListener);

if (typeof options === "function") {
requestListener = options;
options = {};
Expand All @@ -536,14 +538,21 @@ function createServer(options, requestListener) {
// ALPN requests are always answered with http/1.1.
options.ALPNProtocols = ["http/1.1"];
}
const server = http.createServer(options, requestListener);
const optionsALPNProtocols = options.ALPNProtocols;
if (optionsALPNProtocols) {
require("node:tls").convertALPNProtocols(optionsALPNProtocols, server);
require("node:tls").convertALPNProtocols(optionsALPNProtocols, options);
}
server.ALPNCallback = options.ALPNCallback;
server.setSecureContext = server[setSecureContextSymbol];
return server;
http.Server.$call(this, options, requestListener);
this.ALPNProtocols = options.ALPNProtocols;
this.ALPNCallback = options.ALPNCallback;
}
$toClass(Server, "Server", http.Server);
Server.prototype.setSecureContext = function setSecureContext(options) {
return this[setSecureContextSymbol](options);
};

function createServer(options, requestListener) {
return new Server(options, requestListener);
}

var https = {
Expand All @@ -554,7 +563,7 @@ var https = {
timeout: 5000,
proxyEnv: shouldUseEnvProxy() ? process.env : undefined,
}),
Server: http.Server,
Server,
createServer,
get,
request,
Expand Down
Loading
Loading