Skip to content
Merged
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,137 changes: 1,069 additions & 1,068 deletions cmake/sources/ZigSources.txt

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/bun.js/event_loop.zig
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ pub fn pipeReadBuffer(this: *const EventLoop) []u8 {
}

pub const Queue = std.fifo.LinearFifo(Task, .Dynamic);
const log = bun.Output.scoped(.EventLoop, .visible);
const log = bun.Output.scoped(.EventLoop, .hidden);

pub fn tickWhilePaused(this: *EventLoop, done: *bool) void {
while (!done.*) {
Expand Down
90 changes: 77 additions & 13 deletions src/sql/mysql/MySQLConnection.zig
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,14 @@ fn drainInternal(this: *@This()) void {
defer event_loop.exit();

this.flushData();

if (!this.flags.has_backpressure) {
// no backpressure yet so pipeline more if possible and flush again
this.advance();
this.flushData();
if (this.tls_status == .message_sent) {
this.upgradeToTLS();
} else {
// no backpressure yet so pipeline more if possible and flush again
this.advance();
this.flushData();
}
}
}
pub fn finalize(this: *MySQLConnection) void {
Expand Down Expand Up @@ -820,6 +823,7 @@ pub fn call(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS
return globalObject.throwValue(err.toJS(globalObject));
}

debug("configured TLS context", .{});
uws.NewSocketHandler(true).configure(tls_ctx.?, true, *@This(), SocketHandler(true));
}

Expand Down Expand Up @@ -908,6 +912,7 @@ pub fn call(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS
};

if (path.len > 0) {
debug("connecting to mysql with path", .{});
ptr.socket = .{
.SocketTCP = uws.SocketTCP.connectUnixAnon(path, ctx, ptr, false) catch |err| {
tls_config.deinit();
Expand All @@ -919,6 +924,7 @@ pub fn call(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS
},
};
} else {
debug("connecting to mysql with hostname", .{});
ptr.socket = .{
.SocketTCP = uws.SocketTCP.connectAnon(hostname.slice(), port, ctx, ptr, false) catch |err| {
tls_config.deinit();
Expand Down Expand Up @@ -973,26 +979,49 @@ pub fn deinit(this: *MySQLConnection) void {
bun.default_allocator.destroy(this);
}

pub fn upgradeToTLS(this: *MySQLConnection) void {
if (this.socket == .SocketTCP) {
const new_socket = this.socket.SocketTCP.socket.connected.upgrade(this.tls_ctx.?, this.tls_config.server_name) orelse {
this.fail("Failed to upgrade to TLS", error.AuthenticationFailed);
return;
};
this.socket = .{
.SocketTLS = .{
.socket = .{
.connected = new_socket,
},
},
};
}
}

pub fn onOpen(this: *MySQLConnection, socket: Socket) void {
debug("onOpen", .{});
this.setupMaxLifetimeTimerIfNecessary();
this.resetConnectionTimeout();
this.socket = socket;
this.setStatus(.handshaking);
if (socket == .SocketTCP) {
// when upgrading to TLS the onOpen callback will be called again and at this moment we dont wanna to change the status to handshaking
this.setStatus(.handshaking);
}
this.poll_ref.ref(this.vm);
this.updateHasPendingActivity();
}

pub fn onHandshake(this: *MySQLConnection, success: i32, ssl_error: uws.us_bun_verify_error_t) void {
debug("onHandshake: {d} {d}", .{ success, ssl_error.error_no });
debug("onHandshake: {d} {d} {s}", .{ success, ssl_error.error_no, @tagName(this.ssl_mode) });
const handshake_success = if (success == 1) true else false;
this.sequence_id = this.sequence_id +% 1;
if (handshake_success) {
this.tls_status = .ssl_ok;
if (this.tls_config.reject_unauthorized != 0) {
// follow the same rules as postgres
// https://github.com/porsager/postgres/blob/6ec85a432b17661ccacbdf7f765c651e88969d36/src/connection.js#L272-L279
// only reject the connection if reject_unauthorized == true
switch (this.ssl_mode) {
// https://github.com/porsager/postgres/blob/6ec85a432b17661ccacbdf7f765c651e88969d36/src/connection.js#L272-L279

.verify_ca, .verify_full => {
if (ssl_error.error_no != 0) {
this.tls_status = .ssl_failed;
this.failWithJSValue(ssl_error.toJS(this.globalObject));
return;
}
Expand All @@ -1001,16 +1030,18 @@ pub fn onHandshake(this: *MySQLConnection, success: i32, ssl_error: uws.us_bun_v
if (BoringSSL.c.SSL_get_servername(ssl_ptr, 0)) |servername| {
const hostname = servername[0..bun.len(servername)];
if (!BoringSSL.checkServerIdentity(ssl_ptr, hostname)) {
this.failWithJSValue(ssl_error.toJS(this.globalObject));
this.tls_status = .ssl_failed;
return this.failWithJSValue(ssl_error.toJS(this.globalObject));
}
}
},
else => {
return;
},
// require is the same as prefer
.require, .prefer, .disable => {},
}
}
this.sendHandshakeResponse() catch |err| this.failFmt(err, "Failed to send handshake response", .{});
} else {
this.tls_status = .ssl_failed;
// if we are here is because server rejected us, and the error_no is the cause of this
// no matter if reject_unauthorized is false because we are disconnected by the server
this.failWithJSValue(ssl_error.toJS(this.globalObject));
Expand Down Expand Up @@ -1186,6 +1217,36 @@ pub fn handleHandshake(this: *MySQLConnection, comptime Context: type, reader: N
// Update status
this.setStatus(.authenticating);

// https://dev.mysql.com/doc/dev/mysql-server/8.4.6/page_protocol_connection_phase_packets_protocol_ssl_request.html
if (this.capabilities.CLIENT_SSL) {
var response = SSLRequest{
.capability_flags = this.capabilities,
.max_packet_size = 0, //16777216,
.character_set = CharacterSet.default,
// bun always send connection attributes
.has_connection_attributes = true,
};
defer response.deinit();
try response.write(this.writer());
this.capabilities = response.capability_flags;
this.tls_status = .message_sent;
this.flushData();
if (!this.flags.has_backpressure) {
this.upgradeToTLS();
}
return;
}
if (this.tls_status != .none) {
this.tls_status = .ssl_not_available;

switch (this.ssl_mode) {
.verify_ca, .verify_full => {
return this.failFmt(error.AuthenticationFailed, "SSL is not available", .{});
},
// require is the same as prefer
.require, .prefer, .disable => {},
}
}
// Send auth response
try this.sendHandshakeResponse();
}
Expand Down Expand Up @@ -1325,7 +1386,7 @@ pub fn handleAuth(this: *MySQLConnection, comptime Context: type, reader: NewRea
debug("sending password TLS enabled", .{});
// SSL mode is enabled, send password as is
var packet = try this.writer().start(this.sequence_id);
try this.writer().write(this.password);
try this.writer().writeZ(this.password);
try packet.end();
this.flushData();
}
Expand Down Expand Up @@ -1427,6 +1488,7 @@ pub fn handleCommand(this: *MySQLConnection, comptime Context: type, reader: New
}

pub fn sendHandshakeResponse(this: *MySQLConnection) AnyMySQLError.Error!void {
debug("sendHandshakeResponse", .{});
// Only require password for caching_sha2_password when connecting for the first time
if (this.auth_plugin) |plugin| {
const requires_password = switch (plugin) {
Expand Down Expand Up @@ -1458,6 +1520,7 @@ pub fn sendHandshakeResponse(this: *MySQLConnection) AnyMySQLError.Error!void {
"",
},
.auth_response = .{ .empty = {} },
.sequence_id = this.sequence_id,
};
defer response.deinit();

Expand Down Expand Up @@ -1938,6 +2001,7 @@ const PacketHeader = @import("./protocol/PacketHeader.zig");
const PreparedStatement = @import("./protocol/PreparedStatement.zig");
const ResultSet = @import("./protocol/ResultSet.zig");
const ResultSetHeader = @import("./protocol/ResultSetHeader.zig");
const SSLRequest = @import("./protocol/SSLRequest.zig");
const SocketMonitor = @import("../postgres/SocketMonitor.zig");
const StackReader = @import("./protocol/StackReader.zig");
const StmtPrepareOKPacket = @import("./protocol/StmtPrepareOKPacket.zig");
Expand Down
3 changes: 2 additions & 1 deletion src/sql/mysql/TLSStatus.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ pub const TLSStatus = union(enum) {

/// Number of bytes sent of the 8-byte SSL request message.
/// Since we may send a partial message, we need to know how many bytes were sent.
message_sent: u8,
message_sent,

ssl_not_available,
ssl_failed,
ssl_ok,
};
5 changes: 3 additions & 2 deletions src/sql/mysql/protocol/HandshakeResponse41.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ auth_response: Data,
database: Data,
auth_plugin_name: Data,
connect_attrs: bun.StringHashMapUnmanaged([]const u8) = .{},
sequence_id: u8,

pub fn deinit(this: *HandshakeResponse41) void {
this.username.deinit();
Expand All @@ -24,14 +25,14 @@ pub fn deinit(this: *HandshakeResponse41) void {
}

pub fn writeInternal(this: *HandshakeResponse41, comptime Context: type, writer: NewWriter(Context)) !void {
var packet = try writer.start(1);
var packet = try writer.start(this.sequence_id);

this.capability_flags.CLIENT_CONNECT_ATTRS = this.connect_attrs.count() > 0;

// Write client capabilities flags (4 bytes)
const caps = this.capability_flags.toInt();
try writer.int4(caps);
debug("Client capabilities: [{}] 0x{x:0>8}", .{ this.capability_flags, caps });
debug("Client capabilities: [{}] 0x{x:0>8} sequence_id: {d}", .{ this.capability_flags, caps, this.sequence_id });

// Write max packet size (4 bytes)
try writer.int4(this.max_packet_size);
Expand Down
42 changes: 42 additions & 0 deletions src/sql/mysql/protocol/SSLRequest.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// https://dev.mysql.com/doc/dev/mysql-server/8.4.6/page_protocol_connection_phase_packets_protocol_ssl_request.html
// SSLRequest
const SSLRequest = @This();
capability_flags: Capabilities,
max_packet_size: u32 = 0xFFFFFF, // 16MB default
character_set: CharacterSet = CharacterSet.default,
has_connection_attributes: bool = false,

pub fn deinit(_: *SSLRequest) void {}

pub fn writeInternal(this: *SSLRequest, comptime Context: type, writer: NewWriter(Context)) !void {
var packet = try writer.start(1);

this.capability_flags.CLIENT_CONNECT_ATTRS = this.has_connection_attributes;

// Write client capabilities flags (4 bytes)
const caps = this.capability_flags.toInt();
try writer.int4(caps);
debug("Client capabilities: [{}] 0x{x:0>8}", .{ this.capability_flags, caps });

// Write max packet size (4 bytes)
try writer.int4(this.max_packet_size);

// Write character set (1 byte)
try writer.int1(@intFromEnum(this.character_set));

// Write 23 bytes of padding
try writer.write(&[_]u8{0} ** 23);

try packet.end();
}

pub const write = writeWrap(SSLRequest, writeInternal).write;

const debug = bun.Output.scoped(.MySQLConnection, .hidden);

const Capabilities = @import("../Capabilities.zig");
const bun = @import("bun");
const CharacterSet = @import("./CharacterSet.zig").CharacterSet;

const NewWriter = @import("./NewWriter.zig").NewWriter;
const writeWrap = @import("./NewWriter.zig").writeWrap;
5 changes: 2 additions & 3 deletions src/sql/postgres/PostgresSQLConnection.zig
Original file line number Diff line number Diff line change
Expand Up @@ -443,9 +443,8 @@ pub fn onHandshake(this: *PostgresSQLConnection, success: i32, ssl_error: uws.us
}
}
},
else => {
return;
},
// require is the same as prefer
.require, .prefer, .disable => {},
}
}
} else {
Expand Down
19 changes: 19 additions & 0 deletions test/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { readdir, readFile, readlink, rm, writeFile } from "fs/promises";
import fs, { closeSync, openSync, rmSync } from "node:fs";
import os from "node:os";
import { dirname, isAbsolute, join } from "path";
import { execSync } from "child_process";

type Awaitable<T> = T | Promise<T>;

Expand Down Expand Up @@ -856,6 +857,24 @@ export function dockerExe(): string | null {
return which("docker") || which("podman") || null;
}

export function isDockerEnabled(): boolean {
const dockerCLI = dockerExe();
if (!dockerCLI) {
return false;
}

// TODO: investigate why its not starting on Linux arm64
if ((isLinux && process.arch === "arm64") || isMacOS) {
return false;
}

try {
const info = execSync(`${dockerCLI} info`, { stdio: ["ignore", "pipe", "inherit"] });
return info.toString().indexOf("Server Version:") !== -1;
} catch {
return false;
}
}
Comment thread
cirospaciari marked this conversation as resolved.
export async function waitForPort(port: number, timeout: number = 60_000): Promise<void> {
let deadline = Date.now() + Math.max(1, timeout);
let error: unknown;
Expand Down
22 changes: 22 additions & 0 deletions test/js/sql/mysql-tls/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Dockerfile
ARG MYSQL_VERSION=8.4
FROM mysql:${MYSQL_VERSION}

# Copy TLS materials + config
# Expect these in the build context:
# ssl/ca.pem
# ssl/server-cert.pem
# ssl/server-key.pem
# conf.d/ssl.cnf
COPY ssl /etc/mysql/ssl
COPY conf.d /etc/mysql/conf.d

# Lock down permissions so mysqld accepts the key
# The official image runs mysqld as user "mysql"
RUN chown -R mysql:mysql /etc/mysql/ssl /etc/mysql/conf.d \
&& chmod 600 /etc/mysql/ssl/server-key.pem \
&& find /etc/mysql/ssl -type f -name "*.pem" -exec chmod 640 {} \; \
&& echo "require_secure_transport=ON" >> /etc/mysql/conf.d/force_tls.cnf

# Expose MySQL
EXPOSE 3306
7 changes: 7 additions & 0 deletions test/js/sql/mysql-tls/conf.d/ssl.cnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[mysqld]
require_secure_transport=ON
ssl_ca=/etc/mysql/ssl/ca.pem
ssl_cert=/etc/mysql/ssl/server-cert.pem
ssl_key=/etc/mysql/ssl/server-key.pem
tls_version=TLSv1.2,TLSv1.3
skip_name_resolve=ON
Loading
Loading