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
51 changes: 50 additions & 1 deletion src/js/node/_http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ function normalizeServerTls(tls) {
// works for foreign Duplex sockets. The native listener handles its own sockets end to end;
// this picks up the rest. https://github.com/nodejs/node/blob/main/lib/_http_server.js
function connectionListener(this: Server, socket) {
if (socket instanceof NodeHTTPServerSocket) return;
if (isNodeHTTPServerSocket(socket)) return;
connectionListenerHTTP1(this, socket, {
http1Options: {
IncomingMessage: this[kIncomingMessage],
Expand Down Expand Up @@ -1579,6 +1579,12 @@ const NodeHTTPServerSocket = class Socket extends NetSocket {
handle.duplex = this;

this.encrypted = encrypted;
if (encrypted) {
// Overwrites the own `isServer = false` net.Socket's constructor set, so
// the inherited TLSSocket server-side guards (setServername) apply.
Comment thread
robobun marked this conversation as resolved.
this.isServer = true;
Object.setPrototypeOf(this, getNodeHTTPTLSServerSocketPrototype());
}
this.on("timeout", onNodeHTTPServerSocketTimeout);
// Like Node.js's socketOnError: connection errors are routed to the
// server's 'clientError' event instead of crashing as unhandled 'error'
Expand Down Expand Up @@ -2158,6 +2164,49 @@ function _writeHead(statusCode, reason, obj, response) {

Object.defineProperty(NodeHTTPServerSocket, "name", { value: "Socket" });

// A native server socket is either a plain NodeHTTPServerSocket or, when
// encrypted, one whose prototype the constructor swapped to the TLS variant.
Comment thread
robobun marked this conversation as resolved.
function isNodeHTTPServerSocket(socket) {
return (
socket instanceof NodeHTTPServerSocket ||
(NodeHTTPTLSServerSocketPrototype !== undefined &&
Object.getPrototypeOf(socket) === NodeHTTPTLSServerSocketPrototype)
);
}

// node:https request handlers must see a tls.TLSSocket: this prototype keeps
// every NodeHTTPServerSocket member as own properties but chains through
// TLSSocket.prototype instead of going straight to net.Socket.prototype.
Comment thread
robobun marked this conversation as resolved.
let NodeHTTPTLSServerSocketPrototype;
function getNodeHTTPTLSServerSocketPrototype() {
if (NodeHTTPTLSServerSocketPrototype === undefined) {
const { TLSSocket } = require("node:tls");
NodeHTTPTLSServerSocketPrototype = Object.create(TLSSocket.prototype, {
...Object.getOwnPropertyDescriptors(NodeHTTPServerSocket.prototype),
...Object.getOwnPropertyDescriptors({
constructor: TLSSocket,
// Unlike the inherited TLSSocket methods (which read this._handle,
// always null here), these read the native NodeHTTP handle.
Comment thread
robobun marked this conversation as resolved.
getPeerCertificate(detailed) {
const handle = this[kHandle];
if (!handle) return null;
// The native parameter means "abbreviated": the inverse of Node's
// `detailed`, matching TLSSocket.prototype.getPeerCertificate.
Comment thread
robobun marked this conversation as resolved.
const cert = arguments.length < 1 ? handle.getPeerCertificate() : handle.getPeerCertificate(!detailed);
return cert || {};
},
getCipher() {
return this[kHandle]?.getCipher();
},
getProtocol() {
return this[kHandle]?.getTLSVersion() ?? null;
},
}),
});
}
return NodeHTTPTLSServerSocketPrototype;
}

function ServerResponse(req, options): void {
if (!(this instanceof ServerResponse)) return new ServerResponse(req, options);
OutgoingMessage.$call(this, options);
Expand Down
53 changes: 53 additions & 0 deletions src/jsc/bindings/node/JSNodeHTTPServerSocketPrototype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ extern "C" uint64_t uws_res_get_remote_address_info(void* res, const char** dest
extern "C" uint64_t uws_res_get_local_address_info(void* res, const char** dest, int* port, bool* is_ipv6);
extern "C" void us_socket_resume(us_socket_t*);
extern "C" void us_socket_pause(us_socket_t*);
extern "C" void* us_socket_get_native_handle(us_socket_t*);
extern "C" EncodedJSValue Bun__NodeHTTPServerSocket__getPeerCertificate(void* ssl, JSC::JSGlobalObject* globalObject, bool abbreviated);
extern "C" EncodedJSValue Bun__NodeHTTPServerSocket__getCipher(void* ssl, JSC::JSGlobalObject* globalObject);
extern "C" EncodedJSValue Bun__NodeHTTPServerSocket__getTLSVersion(void* ssl, JSC::JSGlobalObject* globalObject);

namespace Bun {

Expand Down Expand Up @@ -45,6 +49,9 @@ JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterIsSecureEstablished);
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterServername);
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterAuthorizationError);
JSC_DECLARE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterPeerCertVerified);
JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketGetPeerCertificate);
JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketGetCipher);
JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketGetTLSVersion);

JSC_DEFINE_CUSTOM_SETTER(noOpSetter, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName propertyName))
{
Expand Down Expand Up @@ -76,6 +83,9 @@ static const JSC::HashTableValue JSNodeHTTPServerSocketPrototypeTableValues[] =
{ "servername"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterServername, noOpSetter } },
{ "authorizationError"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterAuthorizationError, noOpSetter } },
{ "peerCertVerified"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly), JSC::NoIntrinsic, { JSC::HashTableValue::GetterSetterType, jsNodeHttpServerSocketGetterPeerCertVerified, noOpSetter } },
{ "getPeerCertificate"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketGetPeerCertificate, 1 } },
{ "getCipher"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketGetCipher, 0 } },
{ "getTLSVersion"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontEnum), JSC::NoIntrinsic, { JSC::HashTableValue::NativeFunctionType, jsFunctionNodeHTTPServerSocketGetTLSVersion, 0 } },
};

void JSNodeHTTPServerSocketPrototype::finishCreation(JSC::VM& vm)
Expand Down Expand Up @@ -289,6 +299,49 @@ JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterPeerCertVerified, (JSC::JSG
return JSValue::encode(JSC::jsBoolean(thisObject->isPeerCertificateVerified()));
}

// The SSL* of the uWS socket; null for plain HTTP or a closed socket (the
// Rust side then answers like a detached TLS handle).
Comment thread
robobun marked this conversation as resolved.
static void* sslHandleFor(JSNodeHTTPServerSocket* socket)
{
if (!socket->is_ssl || !socket->socket) {
return nullptr;
}
return us_socket_get_native_handle(socket->socket);
}

JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketGetPeerCertificate, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto* thisObject = dynamicDowncast<JSNodeHTTPServerSocket>(callFrame->thisValue());
if (!thisObject) [[unlikely]] {
return JSValue::encode(JSC::jsUndefined());
}
// Same contract as the tls.TLSSocket handle: no argument means the
// abbreviated (leaf-only) form; the argument is `!detailed`.
Comment thread
robobun marked this conversation as resolved.
bool abbreviated = true;
if (callFrame->argumentCount() > 0) {
abbreviated = callFrame->uncheckedArgument(0).toBoolean(globalObject);
}
return Bun__NodeHTTPServerSocket__getPeerCertificate(sslHandleFor(thisObject), globalObject, abbreviated);
}

JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketGetCipher, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto* thisObject = dynamicDowncast<JSNodeHTTPServerSocket>(callFrame->thisValue());
if (!thisObject) [[unlikely]] {
return JSValue::encode(JSC::jsUndefined());
}
return Bun__NodeHTTPServerSocket__getCipher(sslHandleFor(thisObject), globalObject);
}

JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeHTTPServerSocketGetTLSVersion, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto* thisObject = dynamicDowncast<JSNodeHTTPServerSocket>(callFrame->thisValue());
if (!thisObject) [[unlikely]] {
return JSValue::encode(JSC::jsNull());
}
return Bun__NodeHTTPServerSocket__getTLSVersion(sslHandleFor(thisObject), globalObject);
}

JSC_DEFINE_CUSTOM_GETTER(jsNodeHttpServerSocketGetterDuplex, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName))
{
auto* thisObject = dynamicDowncast<JSNodeHTTPServerSocket>(JSC::JSValue::decode(thisValue));
Expand Down
76 changes: 70 additions & 6 deletions src/runtime/socket/tls_socket_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use bun_boringssl_sys as boringssl;
use bun_core::{String as BunString, ZigString, strings};
use bun_jsc::JsClass as _;
use bun_jsc::{
self as jsc, CallFrame, JSGlobalObject, JSValue, JsResult, StringJsc as _, ZigStringJsc as _,
self as jsc, CallFrame, HostReturn as _, JSGlobalObject, JSValue, JsResult, StringJsc as _,
ZigStringJsc as _,
};

use crate::api::bun_x509 as X509;
Expand Down Expand Up @@ -394,16 +395,65 @@ pub(super) fn get_tls_version(
let Some(ssl_ptr) = this.socket.get().ssl() else {
return Ok(JSValue::NULL);
};
Ok(tls_version_to_js(ssl_ptr, global))
}

fn tls_version_to_js(ssl_ptr: *mut boringssl::SSL, global: &JSGlobalObject) -> JSValue {
let version = ffi::SSL_get_version(boringssl::SSL::opaque_ref(ssl_ptr));
if version.is_null() {
return Ok(JSValue::NULL);
return JSValue::NULL;
}
// SAFETY: SSL_get_version returns a static NUL-terminated C string.
let slice = unsafe { bun_core::ffi::cstr(version) }.to_bytes();
if slice.is_empty() {
return Ok(JSValue::NULL);
return JSValue::NULL;
}
Ok(ZigString::from_utf8(slice).to_js(global))
ZigString::from_utf8(slice).to_js(global)
}

// ── C-exported TLS introspection for node:http server sockets
// (called from JSNodeHTTPServerSocketPrototype.cpp) ──
Comment thread
robobun marked this conversation as resolved.

/// # Safety
/// `ssl` must be null or the live `SSL*` of a TLS socket (the C++ caller
/// passes `us_socket_get_native_handle` of an SSL socket). On failure the
/// exception is pending on `global` and `JSValue::ZERO` is returned.
Comment thread
robobun marked this conversation as resolved.
#[unsafe(no_mangle)]
unsafe extern "C" fn Bun__NodeHTTPServerSocket__getPeerCertificate(
ssl: *mut boringssl::SSL,
global: &JSGlobalObject,
abbreviated: bool,
) -> JSValue {
if ssl.is_null() {
return JSValue::UNDEFINED;
}
peer_certificate_to_js(ssl, abbreviated, global).or_pending_exception()
}

/// # Safety
/// See [`Bun__NodeHTTPServerSocket__getPeerCertificate`].
Comment thread
robobun marked this conversation as resolved.
#[unsafe(no_mangle)]
unsafe extern "C" fn Bun__NodeHTTPServerSocket__getCipher(
ssl: *mut boringssl::SSL,
global: &JSGlobalObject,
) -> JSValue {
if ssl.is_null() {
return JSValue::UNDEFINED;
}
cipher_to_js(ssl, global)
}

/// # Safety
/// See [`Bun__NodeHTTPServerSocket__getPeerCertificate`].
Comment thread
robobun marked this conversation as resolved.
#[unsafe(no_mangle)]
unsafe extern "C" fn Bun__NodeHTTPServerSocket__getTLSVersion(
ssl: *mut boringssl::SSL,
global: &JSGlobalObject,
) -> JSValue {
if ssl.is_null() {
return JSValue::NULL;
}
tls_version_to_js(ssl, global)
}

pub(super) fn set_max_send_fragment(
Expand Down Expand Up @@ -457,6 +507,16 @@ pub(super) fn get_peer_certificate(
let Some(ssl_ptr) = this.socket.get().ssl() else {
return Ok(JSValue::UNDEFINED);
};
peer_certificate_to_js(ssl_ptr, abbreviated, global)
}

/// Node-shaped peer-certificate object for `ssl_ptr`; shared by the socket
/// binding above and the C-exported node:http server socket surface.
Comment thread
robobun marked this conversation as resolved.
fn peer_certificate_to_js(
ssl_ptr: *mut boringssl::SSL,
abbreviated: bool,
global: &JSGlobalObject,
) -> JsResult<JSValue> {
let is_server_ssl = ffi::SSL_is_server(boringssl::SSL::opaque_ref(ssl_ptr)) != 0;

if abbreviated {
Expand Down Expand Up @@ -783,14 +843,18 @@ pub(super) fn get_cipher(
let Some(ssl_ptr) = this.socket.get().ssl() else {
return Ok(JSValue::UNDEFINED);
};
Ok(cipher_to_js(ssl_ptr, global))
}

fn cipher_to_js(ssl_ptr: *mut boringssl::SSL, global: &JSGlobalObject) -> JSValue {
let cipher = ffi::SSL_get_current_cipher(boringssl::SSL::opaque_ref(ssl_ptr));
let result = JSValue::create_empty_object(global, 0);

if cipher.is_null() {
result.put(global, b"name", JSValue::NULL);
result.put(global, b"standardName", JSValue::NULL);
result.put(global, b"version", JSValue::NULL);
return Ok(result);
return result;
}
let cipher = ffi::SSL_CIPHER::opaque_ref(cipher);

Expand Down Expand Up @@ -825,7 +889,7 @@ pub(super) fn get_cipher(
result.put(global, b"version", ZigString::from_utf8(s).to_js(global));
}

Ok(result)
result
}

pub(super) fn get_tls_peer_finished_message(
Expand Down
Loading