Skip to content
Closed
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 Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,9 @@ boringssl-debug: boringssl-build-debug boringssl-copy
compile-ffi-test:
clang $(OPTIMIZATION_LEVEL) -shared -undefined dynamic_lookup -o /tmp/bun-ffi-test.dylib -fPIC ./test/js/bun/ffi/ffi-test.c

.PHONY: compile-direct-fd-test
compile-direct-fd-test:
zig build-lib ./test/js/bun/net/direct-fd-test.zig -dynamic -OReleaseFast -femit-bin=/tmp/libdirect-fd-test
sqlite:


Expand Down
18 changes: 16 additions & 2 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1512,6 +1512,18 @@ declare module "bun" {
*/
hostname?: string;

/**
* Instead of binding and listening to a hostname and port, the server
* can operate off a socket which has already been bound and listened
* by a separate process. This enables "socket activated" deployments.
*
* @example
* ```js
* process.env.LISTEN_FDS // Use fd passed by systemd socket activation
* ```
*/
fd?: string | number;

/**
* What URI should be used to make {@link Request.url} absolute?
*
Expand Down Expand Up @@ -3030,8 +3042,9 @@ declare module "bun" {

interface TCPSocketListenOptions<Data = undefined>
extends SocketOptions<Data> {
hostname: string;
port: number;
fd?: number;
hostname?: string;
port?: number;
tls?: TLSOptions;
}

Expand Down Expand Up @@ -3075,6 +3088,7 @@ declare module "bun" {
* @param options.data The per-instance data context
* @param options.hostname The hostname to connect to
* @param options.port The port to connect to
* @param options.fd The bound socket to attach to
* @param options.tls The TLS configuration object
* @param options.unix The unix socket to connect to
*
Expand Down
138 changes: 84 additions & 54 deletions src/bun.js/api/bun/socket.zig
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ const Handlers = struct {
pub const SocketConfig = struct {
hostname_or_unix: JSC.ZigString.Slice,
port: ?u16 = null,
fd: ?uws.socket_t = null,
ssl: ?JSC.API.ServerConfig.SSLConfig = null,
handlers: Handlers,
default_data: JSC.JSValue = .zero,
Expand All @@ -229,6 +230,7 @@ pub const SocketConfig = struct {
globalObject: *JSC.JSGlobalObject,
exception: JSC.C.ExceptionRef,
) ?SocketConfig {
var fd: ?uws.socket_t = null;
var hostname_or_unix: JSC.ZigString.Slice = JSC.ZigString.Slice.empty;
var port: ?u16 = null;
var exclusive = false;
Expand All @@ -252,71 +254,76 @@ pub const SocketConfig = struct {
}
}

hostname_or_unix: {
if (opts.getTruthy(globalObject, "unix")) |unix_socket| {
if (!unix_socket.isString()) {
exception.* = JSC.toInvalidArguments("Expected \"unix\" to be a string", .{}, globalObject).asObjectRef();
return null;
}
// Currently excludes 0 (stdin)
if (opts.getTruthy(globalObject, "fd")) |fd_value| {
if (!fd_value.isNumber() or fd_value.toInt64() < 0) {
exception.* = JSC.toInvalidArguments("Need \"fd\" to be a nonnegative integer", .{}, globalObject).asObjectRef();
return null;
}

hostname_or_unix = unix_socket.getZigString(globalObject).toSlice(bun.default_allocator);
fd = @as(uws.socket_t, fd_value.toInt32());
} else {
hostname_or_unix: {
if (opts.getTruthy(globalObject, "unix")) |unix_socket| {
if (!unix_socket.isString()) {
exception.* = JSC.toInvalidArguments("Expected \"unix\" to be a string", .{}, globalObject).asObjectRef();
return null;
}

if (strings.hasPrefixComptime(hostname_or_unix.slice(), "file://") or strings.hasPrefixComptime(hostname_or_unix.slice(), "unix://") or strings.hasPrefixComptime(hostname_or_unix.slice(), "sock://")) {
hostname_or_unix.ptr += 7;
hostname_or_unix.len -|= 7;
}
hostname_or_unix = unix_socket.getZigString(globalObject).toSlice(bun.default_allocator);

if (hostname_or_unix.len > 0) {
break :hostname_or_unix;
}
}
if (strings.hasPrefixComptime(hostname_or_unix.slice(), "file://") or strings.hasPrefixComptime(hostname_or_unix.slice(), "unix://") or strings.hasPrefixComptime(hostname_or_unix.slice(), "sock://")) {
hostname_or_unix.ptr += 7;
hostname_or_unix.len -|= 7;
}

if (opts.getTruthy(globalObject, "exclusive")) |_| {
exclusive = true;
}
if (hostname_or_unix.len > 0) {
break :hostname_or_unix;
}
}

if (opts.getTruthy(globalObject, "hostname") orelse opts.getTruthy(globalObject, "host")) |hostname| {
if (!hostname.isString()) {
exception.* = JSC.toInvalidArguments("Expected \"hostname\" to be a string", .{}, globalObject).asObjectRef();
return null;
if (opts.getTruthy(globalObject, "exclusive")) |_| {
exclusive = true;
}

var port_value = opts.get(globalObject, "port") orelse JSValue.zero;
hostname_or_unix = hostname.getZigString(globalObject).toSlice(bun.default_allocator);
if (opts.getTruthy(globalObject, "hostname") orelse opts.getTruthy(globalObject, "host")) |hostname| {
if (!hostname.isString()) {
exception.* = JSC.toInvalidArguments("Expected \"hostname\" to be a string", .{}, globalObject).asObjectRef();
return null;
}

var port_value = opts.get(globalObject, "port") orelse JSValue.zero;
hostname_or_unix = hostname.getZigString(globalObject).toSlice(bun.default_allocator);

if (port_value.isEmptyOrUndefinedOrNull() and hostname_or_unix.len > 0) {
const parsed_url = bun.URL.parse(hostname_or_unix.slice());
if (parsed_url.getPort()) |port_num| {
port_value = JSValue.jsNumber(port_num);
hostname_or_unix.ptr = parsed_url.hostname.ptr;
hostname_or_unix.len = @truncate(u32, parsed_url.hostname.len);
if (port_value.isEmptyOrUndefinedOrNull() and hostname_or_unix.len > 0) {
const parsed_url = bun.URL.parse(hostname_or_unix.slice());
if (parsed_url.getPort()) |port_num| {
port_value = JSValue.jsNumber(port_num);
hostname_or_unix.ptr = parsed_url.hostname.ptr;
hostname_or_unix.len = @truncate(u32, parsed_url.hostname.len);
}
}
}

if (port_value.isEmptyOrUndefinedOrNull() or !port_value.isNumber() or port_value.toInt64() > std.math.maxInt(u16) or port_value.toInt64() < 0) {
exception.* = JSC.toInvalidArguments("Expected \"port\" to be a number between 0 and 65535", .{}, globalObject).asObjectRef();
return null;
}
if (port_value.isEmptyOrUndefinedOrNull() or !port_value.isNumber() or port_value.toInt64() > std.math.maxInt(u16) or port_value.toInt64() < 0) {
exception.* = JSC.toInvalidArguments("Expected \"port\" to be a number between 0 and 65535", .{}, globalObject).asObjectRef();
return null;
}

port = port_value.toU16();
port = port_value.toU16();

if (hostname_or_unix.len == 0) {
exception.* = JSC.toInvalidArguments("Expected \"hostname\" to be a non-empty string", .{}, globalObject).asObjectRef();
return null;
}
if (hostname_or_unix.len == 0) {
exception.* = JSC.toInvalidArguments("Expected \"hostname\" to be a non-empty string", .{}, globalObject).asObjectRef();
return null;
}

if (hostname_or_unix.len > 0) {
break :hostname_or_unix;
if (hostname_or_unix.len > 0) {
break :hostname_or_unix;
}
}
}

if (hostname_or_unix.len == 0) {
exception.* = JSC.toInvalidArguments("Expected \"unix\" or \"hostname\" to be a non-empty string", .{}, globalObject).asObjectRef();
exception.* = JSC.toInvalidArguments("Expected \"fd\", \"hostname\" or \"unix\"", .{}, globalObject).asObjectRef();
return null;
}

exception.* = JSC.toInvalidArguments("Expected either \"hostname\" or \"unix\"", .{}, globalObject).asObjectRef();
return null;
}

const handlers = Handlers.fromJS(globalObject, opts.get(globalObject, "socket") orelse JSValue.zero, exception) orelse {
Expand All @@ -329,6 +336,7 @@ pub const SocketConfig = struct {
}

return SocketConfig{
.fd = fd,
.hostname_or_unix = hostname_or_unix,
.port = port,
.ssl = ssl,
Expand All @@ -345,7 +353,7 @@ pub const Listener = struct {
handlers: Handlers,
listener: ?*uws.ListenSocket = null,
poll_ref: JSC.PollRef = JSC.PollRef.init(),
connection: UnixOrHost,
connection: FdOrUnixOrHost,
socket_context: ?*uws.SocketContext = null,
ssl: bool = false,

Expand All @@ -372,15 +380,19 @@ pub const Listener = struct {
return true;
}

const UnixOrHost = union(enum) {
const FdOrUnixOrHost = union(enum) {
fd: uws.socket_t,
unix: []const u8,
host: struct {
host: []const u8,
port: u16,
},

pub fn deinit(this: UnixOrHost) void {
pub fn deinit(this: FdOrUnixOrHost) void {
switch (this) {
.fd => {
// nothing
},
.unix => |u| {
bun.default_allocator.destroy(@intToPtr([*]u8, @ptrToInt(u.ptr)));
},
Expand Down Expand Up @@ -439,6 +451,7 @@ pub const Listener = struct {
var socket_config = SocketConfig.fromJS(opts, globalObject, exception) orelse {
return .zero;
};
var fd = socket_config.fd;
var hostname_or_unix = socket_config.hostname_or_unix;
var port = socket_config.port;
var ssl = socket_config.ssl;
Expand Down Expand Up @@ -515,14 +528,19 @@ pub const Listener = struct {
);
}

var connection: Listener.UnixOrHost = if (port) |port_| .{
var connection: Listener.FdOrUnixOrHost = if (fd) |fd_| .{
.fd = fd_
} else if (port) |port_| .{
.host = .{ .host = (hostname_or_unix.cloneIfNeeded(bun.default_allocator) catch unreachable).slice(), .port = port_ },
} else .{
.unix = (hostname_or_unix.cloneIfNeeded(bun.default_allocator) catch unreachable).slice(),
};

var listen_socket: *uws.ListenSocket = brk: {
switch (connection) {
.fd => |f| {
break :brk uws.us_socket_context_listen_direct(@boolToInt(ssl_enabled), socket_context, f, socket_flags, 8);
},
.host => |c| {
var host = bun.default_allocator.dupeZ(u8, c.host) catch unreachable;
defer bun.default_allocator.free(host);
Expand Down Expand Up @@ -729,6 +747,14 @@ pub const Listener = struct {

return JSValue.jsNumber(this.connection.host.port);
}

pub fn getFD(this: *Listener, _: *JSC.JSGlobalObject) callconv(.C) JSValue {
if (this.connection != .fd) {
return JSValue.jsUndefined();
}

return JSValue.jsNumber(this.connection.fd);
}

pub fn ref(this: *Listener, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSValue {
var this_value = callframe.this();
Expand Down Expand Up @@ -774,7 +800,7 @@ pub const Listener = struct {
globalObject.bunVM().eventLoop().ensureWaker();

var socket_context = uws.us_create_bun_socket_context(@boolToInt(ssl_enabled), uws.Loop.get().?, @sizeOf(usize), ctx_opts).?;
var connection: Listener.UnixOrHost = if (port) |port_| .{
var connection: Listener.FdOrUnixOrHost = if (port) |port_| .{
.host = .{ .host = (hostname_or_unix.cloneIfNeeded(bun.default_allocator) catch unreachable).slice(), .port = port_ },
} else .{
.unix = (hostname_or_unix.cloneIfNeeded(bun.default_allocator) catch unreachable).slice(),
Expand Down Expand Up @@ -913,8 +939,12 @@ fn NewSocket(comptime ssl: bool) type {
return this.has_pending_activity.load(.Acquire);
}

pub fn doConnect(this: *This, connection: Listener.UnixOrHost, socket_ctx: *uws.SocketContext) !void {
pub fn doConnect(this: *This, connection: Listener.FdOrUnixOrHost, socket_ctx: *uws.SocketContext) !void {
switch (connection) {
.fd => {
// fd used only for listen
return error.ConnectionFailed;
},
.host => |c| {
_ = @This().Socket.connectPtr(
normalizeHost(c.host),
Expand Down
8 changes: 8 additions & 0 deletions src/bun.js/api/server.zig
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const DateTime = bun.DateTime;
const linux = std.os.linux;

pub const ServerConfig = struct {
fd: ?uws.socket_t = null,
port: u16 = 0,
hostname: [*:0]const u8 = "localhost",

Expand Down Expand Up @@ -556,6 +557,12 @@ pub const ServerConfig = struct {
}
}

if (arg.getTruthy(global, "fd")) |fd_| {
args.fd = @intCast(
i32,
fd_.coerce(i32, global));
}

if (arg.getTruthy(global, "port")) |port_| {
args.port = @intCast(
u16,
Expand Down Expand Up @@ -5206,6 +5213,7 @@ pub fn NewServer(comptime ssl_enabled_: bool, comptime debug_mode_: bool) type {
}

this.app.listenWithConfig(*ThisServer, this, onListen, .{
.fd = this.config.fd orelse 0,
.port = this.config.port,
.host = host,
.options = 0,
Expand Down
20 changes: 16 additions & 4 deletions src/deps/_libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ typedef struct StringPointer {
} StringPointer;
#endif

/* Define what a socket descriptor is based on platform */
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <winsock2.h>
#define LIBUS_SOCKET_DESCRIPTOR SOCKET
#else
#define LIBUS_SOCKET_DESCRIPTOR int
#endif

#ifdef __cplusplus
extern "C" {
#endif
Expand Down Expand Up @@ -63,7 +74,7 @@ enum uws_opcode_t : int32_t {
enum uws_sendstatus_t : uint32_t { BACKPRESSURE, SUCCESS, DROPPED };

typedef struct {

LIBUS_SOCKET_DESCRIPTOR fd;
int port;
const char *host;
int options;
Expand Down Expand Up @@ -167,8 +178,9 @@ void uws_app_run(int ssl, uws_app_t *);

void uws_app_listen(int ssl, uws_app_t *app, int port,
uws_listen_handler handler, void *user_data);
void uws_app_listen_with_config(int ssl, uws_app_t *app, const char *host,
uint16_t port, int32_t options,
void uws_app_listen_with_config(int ssl, uws_app_t *app,
LIBUS_SOCKET_DESCRIPTOR fd, uint16_t port,
const char *host, int32_t options,
uws_listen_handler handler, void *user_data);
void uws_app_listen_domain(int ssl, uws_app_t *app, const char *domain,
uws_listen_domain_handler handler, void *user_data);
Expand Down Expand Up @@ -336,4 +348,4 @@ void uws_app_close(int ssl, uws_app_t *app);
}
#endif

#endif
#endif
Loading