Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/js/internal/net/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export default {
// 'secureConnect' (node parity), so internal deferrals park on this instead.
kSecureConnectDone: Symbol("kSecureConnectDone"),
kVerifyError: Symbol("kVerifyError"),
// net.Socket.prototype method behind the client-side `new tls.TLSSocket(socket)`.
kUpgradeClientTLS: Symbol("kUpgradeClientTLS"),
};
80 changes: 38 additions & 42 deletions src/js/node/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import type { TLSSocket } from "node:tls";
const { kTimeout, getTimerDuration } = require("internal/timers");
const { validateFunction, validateNumber, validateAbortSignal, validatePort, validateBoolean, validateInt32, validateString } = require("internal/validators"); // prettier-ignore
const { isIPv4, isIPv6, isIP } = require("internal/net/isIP");
const { kArmHandshakeTimeout, kSecureConnectDone, kVerifyError } = require("internal/net/symbols");
const { kArmHandshakeTimeout, kSecureConnectDone, kVerifyError, kUpgradeClientTLS } = require("internal/net/symbols");

const ArrayPrototypeIncludes = Array.prototype.includes;
const ArrayPrototypeJoin = Array.prototype.join;
Expand Down Expand Up @@ -144,6 +144,7 @@ const kConnectOptions = Symbol("connect-options");
const kAttach = Symbol("kAttach");
const kCloseRawConnection = Symbol("kCloseRawConnection");
const kupgraded = Symbol("kupgraded");
const kStandaloneWrap = Symbol("kStandaloneWrap");
const kAdoptedTLSRaw = Symbol("kAdoptedTLSRaw");
const ksocket = Symbol("ksocket");
const khandlers = Symbol("khandlers");
Expand Down Expand Up @@ -299,14 +300,17 @@ function onClientHandshakeComplete(self, socket, verifyError) {
self._secureEstablished = true;
self[kVerifyError] = verifyError ?? null;
self.alpnProtocol = socket.alpnProtocol;
// Only tls.connect() installs the onConnectSecure half; a constructor wrap gets _finishInit alone.
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L1810
Comment thread
robobun marked this conversation as resolved.
const connectSecure = !self[kStandaloneWrap];
// Node has no try/catch around these emits; a listener throw reaches
// InternalCallbackScope as uncaughtException. reportError mirrors that
// without changing Bun.connect's handshake-throw-to-error-handler contract.
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L1107
try {
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L1662-L1673
const { checkServerIdentity } = self[bunTLSConnectOptions];
if (!verifyError && !self.isSessionReused() && typeof checkServerIdentity === "function") {
if (connectSecure && !verifyError && !self.isSessionReused() && typeof checkServerIdentity === "function") {
const hostname = self.servername || self._host || "localhost";
const cert = self.getPeerCertificate(true);
if (cert) {
Expand All @@ -321,7 +325,8 @@ function onClientHandshakeComplete(self, socket, verifyError) {
if (rejectUnauthorized ?? self._rejectUnauthorized) {
self.destroy(verifyError);
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L1686-L1688
self.emit("secure", self);
// A wrap's 'secure' listeners read ssl.verifyError(), which is null once destroyed.
if (connectSecure) self.emit("secure", self);
return;
}
} else {
Expand All @@ -333,7 +338,7 @@ function onClientHandshakeComplete(self, socket, verifyError) {
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L1697-L1698
self.secureConnecting = false;
self.emit(kSecureConnectDone);
self.emit("secureConnect", verifyError);
if (connectSecure) self.emit("secureConnect", verifyError);
const pendingSession = self[kpendingSession];
if (pendingSession) {
self[kpendingSession] = null;
Expand Down Expand Up @@ -1940,10 +1945,6 @@ Socket.prototype.connect = function connect(...args) {
}
tls.checkServerIdentity = checkServerIdentity || tls.checkServerIdentity;
this[bunTLSConnectOptions] = tls;
let tlsSocket;
if (!connection && (tlsSocket = tls.socket)) {
connection = tlsSocket;
}
}
if (connection) {
if (
Expand Down Expand Up @@ -1994,10 +1995,7 @@ Socket.prototype.connect = function connect(...args) {
tls,
socket: this[khandlers],
});
connection.on("data", events[0]);
connection.on("end", events[1]);
connection.on("drain", events[2]);
connection.on("close", events[3]);
listenToUpgradedDuplex(this, connection, events);
this._handle = result;
} else {
// upgradeTLS requires an established socket; a socket that is still
Expand All @@ -2024,8 +2022,13 @@ Socket.prototype.connect = function connect(...args) {
throw new Error("Invalid socket");
}
} else {
// Destroying a not-yet-upgraded wrap destroys the stream under it, like Node's JSStreamSocket.doClose.
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/js_stream_socket.js#L242-L253
Comment thread
robobun marked this conversation as resolved.
const destroyConnection = () => connection.destroy();
this.once("close", destroyConnection);
// wait to be connected
connection.once("connect", () => {
this.removeListener("close", destroyConnection);
// The TLS socket may have been destroyed before the underlying
// socket connected (e.g. tls.connect({ socket }).destroy()); don't
// start a handshake on a dead socket.
Expand All @@ -2045,10 +2048,7 @@ Socket.prototype.connect = function connect(...args) {
tls,
socket: this[khandlers],
});
connection.on("data", events[0]);
connection.on("end", events[1]);
connection.on("drain", events[2]);
connection.on("close", events[3]);
listenToUpgradedDuplex(this, connection, events);
this._handle = result;
} else {
this[kupgraded] = connection;
Expand Down Expand Up @@ -2278,6 +2278,20 @@ function hasUnflushedWrites(connection) {
return connection.writableLength > 0 || connection[kwriteCallback] != null;
}

// The stream's 'error' becomes the TLS socket's (as in Node); unhandled, it would also swallow the 'close' below.
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L977
Comment thread
robobun marked this conversation as resolved.
function listenToUpgradedDuplex(self, connection, events) {
connection.on("data", events[0]);
connection.on("end", events[1]);
connection.on("drain", events[2]);
connection.on("close", events[3]);
connection.on("error", onUpgradedDuplexError.bind(self));
}

function onUpgradedDuplexError(err) {
if (!this.destroyed) this.destroy(err);
}

function drainOnreadTail(self, fromRead?) {
if (self[kOnreadTail] === undefined) return false;
if (fromRead) self[kOnreadReadRequested] = true;
Expand Down Expand Up @@ -2360,10 +2374,7 @@ Socket.prototype[Symbol.for("::bunUpgradeServerTLS::")] = function (connection,
socket: serverHandlersFor(this),
isServer: true,
});
connection.on("data", events[0]);
connection.on("end", events[1]);
connection.on("drain", events[2]);
connection.on("close", events[3]);
listenToUpgradedDuplex(this, connection, events);
this[kupgraded] = connection;
this._handle = result;
return;
Expand All @@ -2389,10 +2400,7 @@ Socket.prototype[Symbol.for("::bunUpgradeServerTLS::")] = function (connection,
socket: serverHandlersFor(this),
isServer: true,
});
connection.on("data", events[0]);
connection.on("end", events[1]);
connection.on("drain", events[2]);
connection.on("close", events[3]);
listenToUpgradedDuplex(this, connection, events);
this._handle = result;
this.emit(kUpgradeAttached);
return;
Expand Down Expand Up @@ -2423,6 +2431,12 @@ Socket.prototype[Symbol.for("::bunUpgradeServerTLS::")] = function (connection,
});
};

// Client-side counterpart for `new tls.TLSSocket(socket)`: the tls.connect({ socket }) upgrade minus onConnectSecure.
Socket.prototype[kUpgradeClientTLS] = function (connection) {
this[kStandaloneWrap] = true;
Socket.prototype.connect.$call(this, { socket: connection });
};

Socket.prototype.read = function read(size) {
if (!this.connecting && !drainOnreadTail(this, true)) {
this._handle?.resume?.();
Expand Down Expand Up @@ -3073,11 +3087,6 @@ function internalConnect(self, options, address, port, addressType, localAddress
}

//TLS
let connection = self[ksocket];
const optionsSocket = options.socket;
if (optionsSocket) {
connection = optionsSocket;
}
let tls = undefined;
const bunTLS = self[bunTlsSymbol];
if (typeof bunTLS === "function") {
Expand All @@ -3091,10 +3100,6 @@ function internalConnect(self, options, address, port, addressType, localAddress
self.servername = tls.servername;
tls.checkServerIdentity = checkServerIdentity || tls.checkServerIdentity;
self[bunTLSConnectOptions] = tls;
let tlsSocket;
if (!connection && (tlsSocket = tls.socket)) {
connection = tlsSocket;
}
}
self.authorized = false;
self.secureConnecting = true;
Expand Down Expand Up @@ -3221,11 +3226,6 @@ function internalConnectMultiple(context, canceled?) {
}

//TLS
let connection = self[ksocket];
const contextOptionsSocket = context.options.socket;
if (contextOptionsSocket) {
connection = contextOptionsSocket;
}
let tls = undefined;
const bunTLS = self[bunTlsSymbol];
if (typeof bunTLS === "function") {
Expand All @@ -3239,10 +3239,6 @@ function internalConnectMultiple(context, canceled?) {
self.servername = tls.servername;
tls.checkServerIdentity = checkServerIdentity || tls.checkServerIdentity;
self[bunTLSConnectOptions] = tls;
let tlsSocket;
if (!connection && (tlsSocket = tls.socket)) {
connection = tlsSocket;
}
}
self.authorized = false;
self.secureConnecting = true;
Expand Down
32 changes: 13 additions & 19 deletions src/js/node/tls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const {
} = require("internal/validators");

const { Server: NetServer, Socket: NetSocket } = net;
const { kArmHandshakeTimeout, kSecureConnectDone, kVerifyError } = require("internal/net/symbols");
const { kArmHandshakeTimeout, kSecureConnectDone, kVerifyError, kUpgradeClientTLS } = require("internal/net/symbols");

const getBundledRootCertificates = $newCppFunction("NodeTLS.cpp", "getBundledRootCertificates", 1);
const getExtraCACertificates = $newCppFunction("NodeTLS.cpp", "getExtraCACertificates", 1);
Expand Down Expand Up @@ -783,15 +783,6 @@ function TLSSocket(socket?, options?) {
if (ALPNProtocols) {
convertALPNProtocols(ALPNProtocols, this);
}

if (isNetSocketOrDuplex && !this.isServer) {
this._handle = socket;
// keep compatibility with http2-wrapper or other places that try to grab JSStreamSocket in node.js, with here is just the TLSSocket
this._handle._parentWrap = this;
}
// For the server wrap, _handle is assigned the upgraded TLS handle by the
// server-upgrade method below; leaving it unset until then means a synchronous
// teardown during upgradeTLS won't call close() on the bare net.Socket.
}
// Internal path: keep the per-digest cache (the user-facing constructors,
// createSecureContext() and new tls.SecureContext(), own theirs exclusively).
Expand All @@ -807,12 +798,16 @@ function TLSSocket(socket?, options?) {
this[kcheckServerIdentity] = checkServerIdentityOption || checkServerIdentity;
this[ksession] = options.session || null;

// `new tls.TLSSocket(socket, { isServer: true })`: drive the server-side TLS
// handshake over the provided socket via net.ts's native upgrade path (reaches
// the module-private kupgraded + the shared ServerHandlers). Client-side wraps
// go through the connect path elsewhere.
if (isNetSocketOrDuplex && this.isServer) {
this[Symbol.for("::bunUpgradeServerTLS::")](socket, this[buntls](null, null));
// Both upgrades live in net.ts (module-private state); _handle stays unset until one hands back the TLS handle.
if (isNetSocketOrDuplex) {
if (isServer) {
this[Symbol.for("::bunUpgradeServerTLS::")](socket, this[buntls](null, null));
} else {
this[kUpgradeClientTLS](socket);
// http2-wrapper reads `new TLSSocket(new PassThrough())._handle._parentWrap.constructor` as its JSStreamSocket.
const handle = this._handle;
if (handle) handle._parentWrap = this;
}
}
}
$toClass(TLSSocket, "TLSSocket", NetSocket);
Expand Down Expand Up @@ -871,8 +866,8 @@ TLSSocket.prototype._destroySSL = function _destroySSL() {
};

TLSSocket.prototype._start = function _start() {
// some frameworks uses this _start internal implementation is suposed to start TLS handshake/connect
this.connect();
// Node sends a constructor wrap's ClientHello here (the mysql driver calls it); ours went out in the constructor.
// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/tls/wrap.js#L1110-L1129
Comment thread
robobun marked this conversation as resolved.
};

TLSSocket.prototype._final = function _final(callback) {
Expand Down Expand Up @@ -1117,7 +1112,6 @@ TLSSocket.prototype[buntls] = function (port, host) {
servername = host && !net.isIP(host) ? host : "";
}
return {
socket: this._handle,
ALPNProtocols: this.ALPNProtocols,
checkServerIdentity: this[kcheckServerIdentity],
session: this[ksession],
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/socket/UpgradedDuplex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,18 +245,18 @@ impl UpgradedDuplex {
let buffer = match bun_jsc::array_buffer::BinaryType::Buffer.to_js(data, &global) {
Ok(b) => b,
Err(err) => {
(self.handlers.on_error)(self.handlers.ctx, global.take_exception(err));
(self.handlers.on_error)(self.handlers.ctx, global.take_error(err));
return;
}
};
buffer.ensure_still_alive();

if let Err(err) = write_or_end.call(&global, duplex, &[buffer]) {
(self.handlers.on_error)(self.handlers.ctx, global.take_exception(err));
(self.handlers.on_error)(self.handlers.ctx, global.take_error(err));
}
} else {
if let Err(err) = write_or_end.call(&global, duplex, &[JSValue::NULL]) {
(self.handlers.on_error)(self.handlers.ctx, global.take_exception(err));
(self.handlers.on_error)(self.handlers.ctx, global.take_error(err));
}
}
}
Expand Down
Loading
Loading