Skip to content

Commit

Permalink
fix(Bun.Socket) onDrain will only be called after handshake + docs up…
Browse files Browse the repository at this point in the history
…date (#11499)

Co-authored-by: Jarred Sumner <[email protected]>
  • Loading branch information
cirospaciari and Jarred-Sumner authored Jun 1, 2024
1 parent dc051ae commit e3b7635
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 12 deletions.
177 changes: 176 additions & 1 deletion packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
declare module "bun" {
import type { Encoding as CryptoEncoding } from "crypto";

import type { CipherNameAndProtocol, EphemeralKeyInfo, PeerCertificate } from "tls";
interface Env {
NODE_ENV?: string;
/**
Expand Down Expand Up @@ -3919,6 +3919,181 @@ declare module "bun" {
* local port connected to the socket
*/
readonly localPort: number;

/**
* This property is `true` if the peer certificate was signed by one of the CAs
* specified when creating the `Socket` instance, otherwise `false`.
*/
readonly authorized: boolean;

/**
* String containing the selected ALPN protocol.
* Before a handshake has completed, this value is always null.
* When a handshake is completed but not ALPN protocol was selected, socket.alpnProtocol equals false.
*/
readonly alpnProtocol: string | false | null;

/**
* Disables TLS renegotiation for this `Socket` instance. Once called, attempts
* to renegotiate will trigger an `error` handler on the `Socket`.
*
* There is no support for renegotiation as a server. (Attempts by clients will result in a fatal alert so that ClientHello messages cannot be used to flood a server and escape higher-level limits.)
*/
disableRenegotiation(): void;

/**
* Keying material is used for validations to prevent different kind of attacks in
* network protocols, for example in the specifications of IEEE 802.1X.
*
* Example
*
* ```js
* const keyingMaterial = socket.exportKeyingMaterial(
* 128,
* 'client finished');
*
* /*
* Example return value of keyingMaterial:
* <Buffer 76 26 af 99 c5 56 8e 42 09 91 ef 9f 93 cb ad 6c 7b 65 f8 53 f1 d8 d9
* 12 5a 33 b8 b5 25 df 7b 37 9f e0 e2 4f b8 67 83 a3 2f cd 5d 41 42 4c 91
* 74 ef 2c ... 78 more bytes>
*
* ```
*
* @param length number of bytes to retrieve from keying material
* @param label an application specific label, typically this will be a value from the [IANA Exporter Label
* Registry](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#exporter-labels).
* @param context Optionally provide a context.
* @return requested bytes of the keying material
*/
exportKeyingMaterial(length: number, label: string, context: Buffer): Buffer;

/**
* Returns the reason why the peer's certificate was not been verified. This
* property is set only when `socket.authorized === false`.
*/
getAuthorizationError(): Error | null;

/**
* Returns an object representing the local certificate. The returned object has
* some properties corresponding to the fields of the certificate.
*
* If there is no local certificate, an empty object will be returned. If the
* socket has been destroyed, `null` will be returned.
*/
getCertificate(): PeerCertificate | object | null;

/**
* Returns an object containing information on the negotiated cipher suite.
*
* For example, a TLSv1.2 protocol with AES256-SHA cipher:
*
* ```json
* {
* "name": "AES256-SHA",
* "standardName": "TLS_RSA_WITH_AES_256_CBC_SHA",
* "version": "SSLv3"
* }
* ```
*
*/
getCipher(): CipherNameAndProtocol;

/**
* Returns an object representing the type, name, and size of parameter of
* an ephemeral key exchange in `perfect forward secrecy` on a client
* connection. It returns an empty object when the key exchange is not
* ephemeral. As this is only supported on a client socket; `null` is returned
* if called on a server socket. The supported types are `'DH'` and `'ECDH'`. The`name` property is available only when type is `'ECDH'`.
*
* For example: `{ type: 'ECDH', name: 'prime256v1', size: 256 }`.
*/
getEphemeralKeyInfo(): EphemeralKeyInfo | object | null;

/**
* Returns an object representing the peer's certificate. If the peer does not
* provide a certificate, an empty object will be returned. If the socket has been
* destroyed, `null` will be returned.
*
* If the full certificate chain was requested, each certificate will include an`issuerCertificate` property containing an object representing its issuer's
* certificate.
* @return A certificate object.
*/
getPeerCertificate(): PeerCertificate;

/**
* See [SSL\_get\_shared\_sigalgs](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_shared_sigalgs.html) for more information.
* @since v12.11.0
* @return List of signature algorithms shared between the server and the client in the order of decreasing preference.
*/
getSharedSigalgs(): string[];

/**
* As the `Finished` messages are message digests of the complete handshake
* (with a total of 192 bits for TLS 1.0 and more for SSL 3.0), they can
* be used for external authentication procedures when the authentication
* provided by SSL/TLS is not desired or is not enough.
*
* @return The latest `Finished` message that has been sent to the socket as part of a SSL/TLS handshake, or `undefined` if no `Finished` message has been sent yet.
*/
getTLSFinishedMessage(): Buffer | undefined;

/**
* As the `Finished` messages are message digests of the complete handshake
* (with a total of 192 bits for TLS 1.0 and more for SSL 3.0), they can
* be used for external authentication procedures when the authentication
* provided by SSL/TLS is not desired or is not enough.
*
* @return The latest `Finished` message that is expected or has actually been received from the socket as part of a SSL/TLS handshake, or `undefined` if there is no `Finished` message so
* far.
*/
getTLSPeerFinishedMessage(): Buffer | undefined;

/**
* For a client, returns the TLS session ticket if one is available, or`undefined`. For a server, always returns `undefined`.
*
* It may be useful for debugging.
*
* See `Session Resumption` for more information.
*/
getTLSTicket(): Buffer | undefined;

/**
* Returns a string containing the negotiated SSL/TLS protocol version of the
* current connection. The value `'unknown'` will be returned for connected
* sockets that have not completed the handshaking process. The value `null` will
* be returned for server sockets or disconnected client sockets.
*
* Protocol versions are:
*
* * `'SSLv3'`
* * `'TLSv1'`
* * `'TLSv1.1'`
* * `'TLSv1.2'`
* * `'TLSv1.3'`
*
*/
getTLSVersion(): string;

/**
* See `Session Resumption` for more information.
* @return `true` if the session was reused, `false` otherwise.
*/
isSessionReused(): boolean;

/**
* The `socket.setMaxSendFragment()` method sets the maximum TLS fragment size.
* Returns `true` if setting the limit succeeded; `false` otherwise.
*
* Smaller fragment sizes decrease the buffering latency on the client: larger
* fragments are buffered by the TLS layer until the entire fragment is received
* and its integrity is verified; large fragments can span multiple roundtrips
* and their processing can be delayed due to packet loss or reordering. However,
* smaller fragments add extra TLS framing bytes and CPU overhead, which may
* decrease overall server throughput.
* @param [size=16384] The maximum TLS fragment size. The maximum value is `16384`.
*/
setMaxSendFragment(size: number): boolean;
}

interface SocketListener<Data = undefined> {
Expand Down
4 changes: 3 additions & 1 deletion packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,9 @@ ssl_on_writable(struct us_internal_ssl_socket_t *s) {
return 0;
}

s = context->on_writable(s);
if (s->handshake_state == HANDSHAKE_COMPLETED) {
s = context->on_writable(s);
}

return s;
}
Expand Down
29 changes: 19 additions & 10 deletions src/bun.js/api/bun/socket.zig
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,7 @@ pub const Listener = struct {
}
}

var this: *Listener = handlers.vm.allocator.create(Listener) catch @panic("OOM");
var this: *Listener = handlers.vm.allocator.create(Listener) catch bun.outOfMemory();
this.* = socket;
this.socket_context.?.ext(ssl_enabled, *Listener).?.* = this;

Expand Down Expand Up @@ -798,7 +798,7 @@ pub const Listener = struct {
const Socket = NewSocket(ssl);
bun.assert(ssl == listener.ssl);

var this_socket = listener.handlers.vm.allocator.create(Socket) catch @panic("Out of memory");
var this_socket = listener.handlers.vm.allocator.create(Socket) catch bun.outOfMemory();
this_socket.* = Socket{
.handlers = &listener.handlers,
.this_value = .zero,
Expand Down Expand Up @@ -1048,7 +1048,7 @@ pub const Listener = struct {

default_data.ensureStillAlive();

var handlers_ptr = handlers.vm.allocator.create(Handlers) catch @panic("OOM");
var handlers_ptr = handlers.vm.allocator.create(Handlers) catch bun.outOfMemory();
handlers_ptr.* = handlers;
handlers_ptr.is_server = false;

Expand All @@ -1057,7 +1057,7 @@ pub const Listener = struct {
handlers_ptr.promise.set(globalObject, promise_value);

if (ssl_enabled) {
var tls = handlers.vm.allocator.create(TLSSocket) catch @panic("OOM");
var tls = handlers.vm.allocator.create(TLSSocket) catch bun.outOfMemory();

tls.* = .{
.handlers = handlers_ptr,
Expand All @@ -1078,7 +1078,7 @@ pub const Listener = struct {

return promise_value;
} else {
var tcp = handlers.vm.allocator.create(TCPSocket) catch @panic("OOM");
var tcp = handlers.vm.allocator.create(TCPSocket) catch bun.outOfMemory();

tcp.* = .{
.handlers = handlers_ptr,
Expand Down Expand Up @@ -1225,6 +1225,7 @@ fn NewSocket(comptime ssl: bool) type {
const handlers = this.handlers;
const callback = handlers.onWritable;
if (callback == .zero) return;

var vm = handlers.vm;
vm.eventLoop().enter();
defer vm.eventLoop().exit();
Expand Down Expand Up @@ -1488,7 +1489,6 @@ fn NewSocket(comptime ssl: bool) type {
log("onHandshake({d})", .{success});
JSC.markBinding(@src());
if (this.detached) return;

const authorized = if (success == 1) true else false;

this.authorized = authorized;
Expand Down Expand Up @@ -1519,6 +1519,15 @@ fn NewSocket(comptime ssl: bool) type {
// you should use getAuthorizationError and authorized getter to get those values in this case
if (is_open) {
result = callback.callWithThis(globalObject, this_value, &[_]JSValue{this_value});

// only call onOpen once for clients
if (!handlers.is_server) {
// clean onOpen callback so only called in the first handshake and not in every renegotiation
// on servers this would require a different approach but it's not needed because our servers will not call handshake multiple times
// servers don't support renegotiation
this.handlers.onOpen.unprotect();
this.handlers.onOpen = .zero;
}
} else {
// call handhsake callback with authorized and authorization error if has one
var authorization_error: JSValue = undefined;
Expand Down Expand Up @@ -2939,8 +2948,8 @@ fn NewSocket(comptime ssl: bool) type {
const ext_size = @sizeOf(WrappedSocket);

const is_server = this.handlers.is_server;
var tls = handlers.vm.allocator.create(TLSSocket) catch @panic("OOM");
var handlers_ptr = handlers.vm.allocator.create(Handlers) catch @panic("OOM");
var tls = handlers.vm.allocator.create(TLSSocket) catch bun.outOfMemory();
var handlers_ptr = handlers.vm.allocator.create(Handlers) catch bun.outOfMemory();
handlers_ptr.* = handlers;
handlers_ptr.is_server = is_server;
handlers_ptr.protect();
Expand Down Expand Up @@ -2979,8 +2988,8 @@ fn NewSocket(comptime ssl: bool) type {

tls.socket = new_socket;

var raw = handlers.vm.allocator.create(TLSSocket) catch @panic("OOM");
var raw_handlers_ptr = handlers.vm.allocator.create(Handlers) catch @panic("OOM");
var raw = handlers.vm.allocator.create(TLSSocket) catch bun.outOfMemory();
var raw_handlers_ptr = handlers.vm.allocator.create(Handlers) catch bun.outOfMemory();
raw_handlers_ptr.* = .{
.vm = globalObject.bunVM(),
.globalObject = globalObject,
Expand Down
21 changes: 21 additions & 0 deletions test/js/bun/net/socket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,24 @@ it("should connect directly when using an ip address", async () => {
await promise;
expect(opened).toBe(true);
});

it("should not call drain before handshake", async () => {
const { promise, resolve, reject } = Promise.withResolvers();
using socket = await Bun.connect({
hostname: "www.example.com",
tls: true,
port: 443,
socket: {
drain() {
if (!socket.authorized) {
reject(new Error("Socket not authorized"));
}
},
handshake() {
resolve();
},
},
});
await promise;
expect(socket.authorized).toBe(true);
});

0 comments on commit e3b7635

Please sign in to comment.