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
24 changes: 20 additions & 4 deletions ext/node/polyfills/_tls_wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,11 @@ TLSSocket.prototype._wrapHandle = function (wrap, handle) {

// Set ownerSymbol on the parent handle so that connect callbacks
// (which receive the TCP handle, not the TLSWrap) can find the socket.
handle[ownerSymbol] = this;
// If we are wrapping a net.Socket that still needs to emit its connect
// event, keep the raw socket as owner until that event has fired.
if (!(wrap instanceof net.Socket && !wrap.remoteAddress)) {
handle[ownerSymbol] = this;
}

// Proxy methods from the parent TCP handle that callers expect on _handle.
// In Node, TLSWrap is a StreamBase that delegates these to the underlying
Expand Down Expand Up @@ -700,15 +704,27 @@ TLSSocket.prototype._init = function (socket, wrap) {

this.connecting = socket.connecting || !socket._handle;
socket.once("connect", () => {
if (this.destroyed) {
return;
}

this.connecting = false;
// If the original socket created its own TCP handle during
// connect() (because it had no handle when we wrapped it),
// re-attach the TLS wrap to the socket's actual TCP handle.
if (ssl && socket._handle) {
if (ssl && socket._handle && ssl._nativeTcpHandle !== socket._handle) {
const nativeHandle = socket._handle;
ssl.attach(nativeHandle);
ssl._attachNativeHandle(nativeHandle);
nativeHandle[ownerSymbol] = this;
} else if (ssl && socket._handle) {
socket._handle[ownerSymbol] = this;
ssl._installNativeOnread?.(socket._handle);
}
this.emit("connect");
nextTick(() => {
if (!this.destroyed) {
this.emit("connect");
}
});
});
}

Expand Down
72 changes: 39 additions & 33 deletions ext/node/polyfills/internal_binding/tls_wrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,40 @@ const { kReadBytesOrError, streamBaseState } = core.loadExtScript(
const kJSStreamHandle = Symbol.for("kJSStreamHandle");
const kOwner = Symbol.for("kJSStreamOwner");

function installNativeOnread(res: TLSWrap, nativeHandle: any) {
nativeHandle.onread = function (
buf: ArrayBuffer | Uint8Array | undefined,
) {
const nread = streamBaseState[kReadBytesOrError];
if (nread > 0 && buf) {
// LibUvStreamWrap passes an ArrayBuffer; convert to Uint8Array for receive()
const data = buf instanceof ArrayBuffer
? new Uint8Array(buf, 0, nread)
: buf.subarray(0, nread);
res.receive(data);
} else if (nread < 0) {
// EOF or error - stop native TCP reads and unref the handle.
// Without this, the libuv handle keeps a ref on the event loop
// and prevents process exit after the TLS connection ends.
nativeHandle.readStop();
nativeHandle.unref();
res.emitEof();
}
};
}

function attachNativeHandle(res: TLSWrap, nativeHandle: any) {
const attachResult = nativeHandle instanceof PipeWrap
? res.attachPipe(nativeHandle)
: res.attach(nativeHandle);
if (attachResult !== 0) {
throw new Error(`TLS wrap attach failed: ${attachResult}`);
}

installNativeOnread(res, nativeHandle);
res._nativeTcpHandle = nativeHandle;
}

/**
* Create a TLSWrap that intercepts an underlying stream handle.
* Mirrors Node's `internalBinding('tls_wrap').wrap(handle, context, isServer)`.
Expand Down Expand Up @@ -53,6 +87,10 @@ function wrap(
}

const nativeHandle = handle;
res._attachNativeHandle = (nativeHandle: any) =>
attachNativeHandle(res, nativeHandle);
res._installNativeOnread = (nativeHandle: any) =>
installNativeOnread(res, nativeHandle);

if (nativeHandle[kJSStreamHandle]) {
// JS-backed stream (e.g. JSStreamSocket wrapping a Duplex).
Expand Down Expand Up @@ -125,39 +163,7 @@ function wrap(
res._flushEncOut = flushEncOut;
} else {
// Native stream (TCP or Pipe handle).
// attach/attachPipe stores the stream pointer for encrypted writes.
const attachResult = nativeHandle instanceof PipeWrap
? res.attachPipe(nativeHandle)
: res.attach(nativeHandle);
if (attachResult !== 0) {
throw new Error(`TLS wrap attach failed: ${attachResult}`);
}

// Read interception at the JS layer: intercept the TCPWrap's onread
// callback to forward encrypted data from the TCP stream to the TLSWrap
// via receive().
// Note: LibUvStreamWrap's read callback uses (buf) signature with nread
// in streamBaseState, matching onStreamRead in stream_base_commons.ts.
nativeHandle.onread = function (buf: ArrayBuffer | Uint8Array | undefined) {
const nread = streamBaseState[kReadBytesOrError];
if (nread > 0 && buf) {
// LibUvStreamWrap passes an ArrayBuffer; convert to Uint8Array for receive()
const data = buf instanceof ArrayBuffer
? new Uint8Array(buf, 0, nread)
: buf.subarray(0, nread);
res.receive(data);
} else if (nread < 0) {
// EOF or error - stop native TCP reads and unref the handle.
// Without this, the libuv handle keeps a ref on the event loop
// and prevents process exit after the TLS connection ends.
nativeHandle.readStop();
nativeHandle.unref();
res.emitEof();
}
};

// Store the native handle so readStart/readStop can delegate to it.
res._nativeTcpHandle = nativeHandle;
attachNativeHandle(res, nativeHandle);
}

// Store the JS handle reference so Rust can call JS callbacks (onhandshakedone, etc.)
Expand Down
1 change: 0 additions & 1 deletion ext/node/polyfills/net.ts
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,6 @@ function _lookupAndConnect(self: Socket, options: TcpSocketConnectOptions) {
family: options.family,
hints: options.hints || 0,
all: false,
port,
};

if (
Expand Down
4 changes: 4 additions & 0 deletions tests/node_compat/config.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -3503,6 +3503,10 @@
"parallel/test-tls-close-notify.js": {},
"parallel/test-tls-connect-address-family.js": {},
"parallel/test-tls-connect-hwm-option.js": {},
"parallel/test-tls-connect-keepalive-nodelay.js": {
"ignore": true,
"reason": "Requires Node's internal/net module, which Deno does not implement."
},
"parallel/test-tls-connect-no-host.js": {},
"parallel/test-tls-connect-pipe.js": {},
"parallel/test-tls-connect-secure-context.js": {},
Expand Down
Loading