aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-10-08 01:05:19 -0700
committerGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-10-08 01:06:35 -0700
commitc2c9173eff9e929004d73e087b62ffdc25f0f4ee (patch)
tree06357b3ab4761503d35c0d5aeef294706b4036e0
parent99e7856269ab084c0b5c69d8dac3bcd89ec08e8d (diff)
downloadbun-c2c9173eff9e929004d73e087b62ffdc25f0f4ee.tar.gz
bun-c2c9173eff9e929004d73e087b62ffdc25f0f4ee.tar.zst
bun-c2c9173eff9e929004d73e087b62ffdc25f0f4ee.zip
Fix https://github.com/oven-sh/bun/issues/1263
What happened: when moving to uSockets for the http client, I forgot to call `SSL_set_tlsext_host_name` and uSockets apparently doesn't do that
-rw-r--r--build.zig2
-rw-r--r--src/deps/boringssl.translated.zig20
-rw-r--r--src/deps/uws.zig12
-rw-r--r--src/http/async_bio.zig437
-rw-r--r--src/http/async_socket.zig897
-rw-r--r--src/http/websocket_http_client.zig16
-rw-r--r--src/http_client_async.zig25
-rw-r--r--src/string_immutable.zig11
-rw-r--r--test/bun.js/fetch.test.js35
9 files changed, 110 insertions, 1345 deletions
diff --git a/build.zig b/build.zig
index f476ada85..b7c889b96 100644
--- a/build.zig
+++ b/build.zig
@@ -132,7 +132,7 @@ fn addInternalPackages(step: *std.build.LibExeObjStep, _: std.mem.Allocator, tar
};
io.dependencies = &.{analytics};
-
+ uws.dependencies = &.{boringssl};
javascript_core.dependencies = &.{ http, strings, picohttp, io, uws };
http.dependencies = &.{
strings,
diff --git a/src/deps/boringssl.translated.zig b/src/deps/boringssl.translated.zig
index 892c7a495..267fb639c 100644
--- a/src/deps/boringssl.translated.zig
+++ b/src/deps/boringssl.translated.zig
@@ -18755,6 +18755,26 @@ pub const SSL = opaque {
_ = SSL_set_tlsext_host_name(ssl, hostname);
}
+ pub fn configureHTTPClient(ssl: *SSL, hostname: [:0]const u8) void {
+ if (hostname.len > 0) ssl.setHostname(hostname);
+ _ = SSL_clear_options(ssl, SSL_OP_LEGACY_SERVER_CONNECT);
+ _ = SSL_set_options(ssl, SSL_OP_LEGACY_SERVER_CONNECT);
+ const mode = SSL_MODE_CBC_RECORD_SPLITTING | SSL_MODE_ENABLE_FALSE_START | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER;
+
+ _ = SSL_set_mode(ssl, mode);
+ _ = SSL_clear_mode(ssl, mode);
+
+ var alpns = &[_]u8{ 8, 'h', 't', 't', 'p', '/', '1', '.', '1' };
+ std.debug.assert(SSL_set_alpn_protos(ssl, alpns, alpns.len) == 0);
+
+ SSL_enable_signed_cert_timestamps(ssl);
+ SSL_enable_ocsp_stapling(ssl);
+
+ // std.debug.assert(SSL_set_strict_cipher_list(ssl, SSL_DEFAULT_CIPHER_LIST) == 0);
+
+ SSL_set_enable_ech_grease(ssl, 1);
+ }
+
pub fn handshake(this: *SSL) Error!void {
const rc = SSL_connect(this);
return switch (SSL_get_error(this, rc)) {
diff --git a/src/deps/uws.zig b/src/deps/uws.zig
index 5a634c119..923bb768d 100644
--- a/src/deps/uws.zig
+++ b/src/deps/uws.zig
@@ -11,6 +11,14 @@ pub const LIBUS_LISTEN_EXCLUSIVE_PORT: i32 = 1;
pub const Socket = opaque {};
const bun = @import("../global.zig");
+const BoringSSL = @import("boringssl");
+fn NativeSocketHandleType(comptime ssl: bool) type {
+ if (ssl) {
+ return BoringSSL.SSL;
+ } else {
+ return anyopaque;
+ }
+}
pub fn NewSocketHandler(comptime ssl: bool) type {
return struct {
const ssl_int: i32 = @boolToInt(ssl);
@@ -24,6 +32,10 @@ pub fn NewSocketHandler(comptime ssl: bool) type {
pub fn timeout(this: ThisSocket, seconds: c_uint) void {
return us_socket_timeout(comptime ssl_int, this.socket, seconds);
}
+
+ pub fn getNativeHandle(this: ThisSocket) *NativeSocketHandleType(ssl) {
+ return @ptrCast(*NativeSocketHandleType(ssl), us_socket_get_native_handle(comptime ssl_int, this.socket).?);
+ }
pub fn ext(this: ThisSocket, comptime ContextType: type) ?*ContextType {
const alignment = if (ContextType == *anyopaque)
@sizeOf(usize)
diff --git a/src/http/async_bio.zig b/src/http/async_bio.zig
deleted file mode 100644
index b9a926795..000000000
--- a/src/http/async_bio.zig
+++ /dev/null
@@ -1,437 +0,0 @@
-const boring = @import("boringssl");
-const std = @import("std");
-const AsyncIO = @import("io");
-const Completion = AsyncIO.Completion;
-const AsyncMessage = @import("./async_message.zig");
-const AsyncBIO = @This();
-const Output = @import("../global.zig").Output;
-const extremely_verbose = @import("../http_client_async.zig").extremely_verbose;
-const SOCKET_FLAGS = @import("../http_client_async.zig").SOCKET_FLAGS;
-const getAllocator = @import("../http_client_async.zig").getAllocator;
-const assert = std.debug.assert;
-const BufferPool = AsyncMessage.BufferPool;
-const buffer_pool_len = AsyncMessage.buffer_pool_len;
-const fail = -3;
-const connection_closed = -2;
-const pending = -1;
-const OK = 0;
-const ObjectPool = @import("../pool.zig").ObjectPool;
-const Environment = @import("../env.zig");
-
-const Packet = struct {
- completion: Completion,
- min: u32 = 0,
- owned_slice: []u8 = &[_]u8{},
-
- pub const Pool = ObjectPool(Packet, null, false, 32);
-};
-
-bio: ?*boring.BIO = null,
-socket_fd: std.os.socket_t = 0,
-
-allocator: std.mem.Allocator,
-
-pending_reads: u32 = 0,
-pending_sends: u32 = 0,
-recv_buffer: ?*BufferPool.Node = null,
-send_buffer: ?*BufferPool.Node = null,
-socket_recv_len: c_int = 0,
-socket_send_len: u32 = 0,
-bio_write_offset: u32 = 0,
-bio_read_offset: u32 = 0,
-socket_send_error: ?anyerror = null,
-socket_recv_error: ?anyerror = null,
-recv_eof: bool = false,
-
-onReady: ?Callback = null,
-
-pub const Callback = struct {
- ctx: *anyopaque,
- callback: fn (ctx: *anyopaque) void,
-
- pub inline fn run(this: Callback) void {
- this.callback(this.ctx);
- }
-
- pub fn Wrap(comptime Context: type, comptime func: anytype) type {
- return struct {
- pub fn wrap(context: *anyopaque) void {
- func(@ptrCast(*Context, @alignCast(@alignOf(*Context), context)));
- }
-
- pub fn get(ctx: *Context) Callback {
- return Callback{
- .ctx = ctx,
- .callback = wrap,
- };
- }
- };
- }
-};
-
-pub fn nextFrame(this: *AsyncBIO) void {
- if (this.onReady) |ready| {
- ready.run();
- }
-}
-
-var method: ?*boring.BIO_METHOD = null;
-pub fn initBoringSSL() void {
- method = boring.BIOMethod.init(async_bio_name, Bio.create, Bio.destroy, Bio.write, Bio.read, null, Bio.ctrl);
-}
-
-const async_bio_name: [:0]const u8 = "AsyncBIO";
-
-const Wait = enum {
- pending,
- suspended,
- completed,
-};
-
-pub fn init(this: *AsyncBIO) !void {
- if (this.bio == null) {
- this.bio = boring.BIO_new(
- method.?,
- );
- }
-
- _ = boring.BIO_set_data(this.bio.?, this);
-}
-
-const WaitResult = enum {
- none,
- read,
- send,
-};
-
-pub fn doSocketRead(this: *AsyncBIO, completion: *Completion, result_: AsyncIO.RecvError!usize) void {
- var ctx = @fieldParentPtr(Packet.Pool.Node, "data", @fieldParentPtr(Packet, "completion", completion));
- ctx.release();
-
- const socket_recv_len = @intCast(
- c_int,
- result_ catch |err| {
- this.socket_recv_error = err;
- this.socket_recv_len = fail;
- this.onSocketReadComplete();
- return;
- },
- );
- this.socket_recv_len += socket_recv_len;
- if (extremely_verbose) {
- Output.prettyErrorln("onRead: {d}", .{socket_recv_len});
- Output.flush();
- }
- this.recv_eof = this.recv_eof or socket_recv_len == 0;
-
- // if (socket_recv_len == 0) {
-
- this.onSocketReadComplete();
- // return;
- // }
-
- // this.read_wait = .pending;
- // this.scheduleSocketRead();
-}
-
-pub fn doSocketWrite(this: *AsyncBIO, completion: *Completion, result_: AsyncIO.SendError!usize) void {
- var ctx = @fieldParentPtr(Packet.Pool.Node, "data", @fieldParentPtr(Packet, "completion", completion));
- defer ctx.release();
-
- const socket_send_len = @truncate(
- u32,
- result_ catch |err| {
- this.socket_send_error = err;
- this.onSocketWriteComplete();
- return;
- },
- );
- this.socket_send_len += socket_send_len;
- this.recv_eof = this.recv_eof or socket_send_len == 0;
-
- const remain = ctx.data.min - @minimum(ctx.data.min, socket_send_len);
-
- if (socket_send_len == 0 or remain == 0) {
- this.onSocketWriteComplete();
- return;
- }
-
- if (this.socket_fd == 0) return;
- this.scheduleSocketWrite(completion.operation.slice()[remain..]);
-}
-
-fn onSocketReadComplete(this: *AsyncBIO) void {
- this.handleSocketReadComplete();
-
- this.nextFrame();
-}
-
-inline fn readBuf(this: *AsyncBIO) []u8 {
- return this.recv_buffer.?.data[@intCast(u32, this.socket_recv_len)..];
-}
-
-pub fn hasPendingReadData(this: *AsyncBIO) bool {
- return this.socket_recv_len - @intCast(c_int, this.bio_read_offset) > 0;
-}
-
-pub fn scheduleSocketRead(this: *AsyncBIO, min: u32) void {
- if (this.recv_buffer == null) {
- this.recv_buffer = BufferPool.get(getAllocator());
- }
-
- this.scheduleSocketReadBuf(min, this.readBuf());
-}
-
-pub fn scheduleSocketReadBuf(this: *AsyncBIO, min: u32, buf: []u8) void {
- var packet = Packet.Pool.get(getAllocator());
- packet.data.min = @truncate(u32, min);
-
- AsyncIO.global.recv(*AsyncBIO, this, doSocketRead, &packet.data.completion, this.socket_fd, buf);
-}
-
-pub fn scheduleSocketWrite(this: *AsyncBIO, buf: []const u8) void {
- var packet = Packet.Pool.get(getAllocator());
- packet.data.min = @truncate(u32, buf.len);
- AsyncIO.global.send(*AsyncBIO, this, doSocketWrite, &packet.data.completion, this.socket_fd, buf, SOCKET_FLAGS);
-}
-
-fn handleSocketReadComplete(
- this: *AsyncBIO,
-) void {
- this.pending_reads -|= 1;
-}
-
-pub fn onSocketWriteComplete(
- this: *AsyncBIO,
-) void {
- this.handleSocketWriteComplete();
- this.nextFrame();
-}
-
-pub fn handleSocketWriteComplete(
- this: *AsyncBIO,
-) void {
- this.pending_sends -|= 1;
-
- if (extremely_verbose) {
- Output.prettyErrorln("onWrite: {d}", .{this.socket_send_len});
- Output.flush();
- }
-
- if (this.pending_sends == 0) {
- if (this.send_buffer) |buf| {
- buf.release();
- this.send_buffer = null;
-
- // this might be incorrect!
- this.bio_write_offset = 0;
- this.socket_send_len = 0;
- }
- }
-}
-
-pub const Bio = struct {
- inline fn cast(bio: *boring.BIO) *AsyncBIO {
- return @ptrCast(*AsyncBIO, @alignCast(@alignOf(*AsyncBIO), boring.BIO_get_data(bio)));
- }
-
- pub fn create(this_bio: *boring.BIO) callconv(.C) c_int {
- boring.BIO_set_init(this_bio, 1);
- return 1;
- }
- pub fn destroy(this_bio: *boring.BIO) callconv(.C) c_int {
- boring.BIO_set_init(this_bio, 0);
-
- if (boring.BIO_get_data(this_bio) != null) {
- var this = cast(this_bio);
- this.bio = null;
- }
-
- return 0;
- }
- pub fn write(this_bio: *boring.BIO, ptr: [*c]const u8, len_: c_int) callconv(.C) c_int {
- if (len_ < 0) return len_;
- assert(@ptrToInt(ptr) > 0);
- {
- const retry_flags = boring.BIO_get_retry_flags(this_bio);
- boring.BIO_clear_retry_flags(this_bio);
- if ((retry_flags & boring.BIO_FLAGS_READ) != 0) {
- boring.BIO_set_retry_read(this_bio);
- }
- }
- var this = cast(this_bio);
- const len = @intCast(u32, len_);
-
- if (this.socket_fd == 0) {
- if (comptime Environment.allow_assert) std.debug.assert(this_bio.shutdown > 0); // socket_fd should never be 0
- return -1;
- }
-
- if (this.recv_eof) {
- this.recv_eof = false;
- return 0;
- }
-
- if (this.socket_send_error != null) {
- if (extremely_verbose) {
- Output.prettyErrorln("write: {s}", .{@errorName(this.socket_send_error.?)});
- Output.flush();
- }
- return -1;
- }
-
- if (this.send_buffer == null) {
- this.send_buffer = BufferPool.get(getAllocator());
- }
-
- var data = this.send_buffer.?.data[this.bio_write_offset..];
- const to_copy = @minimum(len, @intCast(u32, data.len));
-
- if (to_copy == 0) {
- boring.BIO_set_retry_write(this_bio);
- return -1;
- }
-
- @memcpy(data.ptr, ptr, to_copy);
- this.bio_write_offset += to_copy;
-
- this.scheduleSocketWrite(data[0..to_copy]);
- this.pending_sends += 1;
-
- return @intCast(c_int, to_copy);
- }
-
- pub fn read(this_bio: *boring.BIO, ptr: [*c]u8, len_: c_int) callconv(.C) c_int {
- if (len_ < 0) return len_;
- const len__: u32 = @intCast(u32, len_);
- assert(@ptrToInt(ptr) > 0);
- {
- const retry_flags = boring.BIO_get_retry_flags(this_bio);
- boring.BIO_clear_retry_flags(this_bio);
- if ((retry_flags & boring.BIO_FLAGS_WRITE) != 0) {
- boring.BIO_set_retry_write(this_bio);
- }
- }
-
- var this = cast(this_bio);
- assert(len_ < buffer_pool_len);
-
- var socket_recv_len = this.socket_recv_len;
- var bio_read_offset = this.bio_read_offset;
- defer {
- this.bio_read_offset = bio_read_offset;
- this.socket_recv_len = socket_recv_len;
- }
-
- if (this.socket_recv_error) |socket_err| {
- if (extremely_verbose) Output.prettyErrorln("SSL read error: {s}", .{@errorName(socket_err)});
- return -1;
- }
-
- // If there is no result available synchronously, report any Write() errors
- // that were observed. Otherwise the application may have encountered a socket
- // error while writing that would otherwise not be reported until the
- // application attempted to write again - which it may never do. See
- // https://crbug.com/249848.
- if ((this.socket_send_error != null) and (socket_recv_len == OK or socket_recv_len == pending)) {
- return -1;
- }
-
- if (this.recv_buffer == null and socket_recv_len > 0) {
- socket_recv_len = 0;
- bio_read_offset = 0;
- }
-
- if (socket_recv_len == 0) {
- // Instantiate the read buffer and read from the socket. Although only |len|
- // bytes were requested, intentionally read to the full buffer size. The SSL
- // layer reads the record header and body in separate reads to avoid
- // overreading, but issuing one is more efficient. SSL sockets are not
- // reused after shutdown for non-SSL traffic, so overreading is fine.
- assert(bio_read_offset == 0);
-
- if (this.socket_fd == 0) {
- if (comptime Environment.allow_assert) std.debug.assert(false); // socket_fd should never be 0
- return -1;
- }
-
- if (this.pending_reads == 0) {
- if (this.recv_eof) {
- this.recv_eof = false;
- return 0;
- }
-
- this.pending_reads += 1;
- this.scheduleSocketRead(len__);
- }
-
- boring.BIO_set_retry_read(this_bio);
- return pending;
- }
-
- // If the last Read() failed, report the error.
- if (socket_recv_len < 0) {
- if (extremely_verbose) Output.prettyErrorln("Unexpected ssl error: {d}", .{socket_recv_len});
- return -1;
- }
-
- const socket_recv_len_ = @intCast(u32, socket_recv_len);
-
- // Report the result of the last Read() if non-empty.
- const len = @minimum(len__, socket_recv_len_ - bio_read_offset);
- var bytes = this.recv_buffer.?.data[bio_read_offset..socket_recv_len_];
-
- if (len__ > @truncate(u32, bytes.len)) {
- if (this.socket_fd == 0) {
- if (comptime Environment.allow_assert) std.debug.assert(false); // socket_fd should never be 0
- return -1;
- }
-
- if (this.pending_reads == 0) {
- // if this is true, we will never have enough space
- if (socket_recv_len_ + len__ >= buffer_pool_len and len_ < buffer_pool_len) {
- const unread = socket_recv_len_ - bio_read_offset;
- // TODO: can we use virtual memory magic to skip the copy here?
- std.mem.copyBackwards(u8, this.recv_buffer.?.data[0..unread], bytes);
- bio_read_offset = 0;
- this.bio_read_offset = 0;
- this.socket_recv_len = @intCast(c_int, unread);
- socket_recv_len = this.socket_recv_len;
- bytes = this.recv_buffer.?.data[0..unread];
- }
-
- this.pending_reads += 1;
- this.scheduleSocketRead(@maximum(len__ - @truncate(u32, bytes.len), 1));
- }
-
- boring.BIO_set_retry_read(this_bio);
- return -1;
- }
- @memcpy(ptr, bytes.ptr, len);
- bio_read_offset += len;
-
- if (bio_read_offset == socket_recv_len_ and this.pending_reads == 0 and this.pending_sends == 0) {
- // The read buffer is empty.
- // we can reset the pointer back to the beginning of the buffer
- // if there is more data to read, we will ask for another
- bio_read_offset = 0;
- socket_recv_len = 0;
-
- if (this.recv_buffer) |buf| {
- buf.release();
- this.recv_buffer = null;
- }
- }
-
- return @intCast(c_int, len);
- }
-
- // https://chromium.googlesource.com/chromium/src/+/refs/heads/main/net/socket/socket_bio_adapter.cc#376
- pub fn ctrl(_: *boring.BIO, cmd: c_int, _: c_long, _: ?*anyopaque) callconv(.C) c_long {
- return switch (cmd) {
- // The SSL stack requires BIOs handle BIO_flush.
- boring.BIO_CTRL_FLUSH => 1,
- else => 0,
- };
- }
-};
diff --git a/src/http/async_socket.zig b/src/http/async_socket.zig
deleted file mode 100644
index 72980ec7c..000000000
--- a/src/http/async_socket.zig
+++ /dev/null
@@ -1,897 +0,0 @@
-const boring = @import("boringssl");
-const std = @import("std");
-const AsyncIO = @import("io");
-const AsyncMessage = @import("./async_message.zig");
-const AsyncBIO = @import("./async_bio.zig");
-const Completion = AsyncIO.Completion;
-const AsyncSocket = @This();
-const KeepAlive = @import("../http_client_async.zig").KeepAlive;
-const Output = @import("../global.zig").Output;
-const NetworkThread = @import("../network_thread.zig");
-const Environment = @import("../global.zig").Environment;
-const bun = @import("../global.zig");
-const extremely_verbose = @import("../http_client_async.zig").extremely_verbose;
-const SOCKET_FLAGS: u32 = @import("../http_client_async.zig").SOCKET_FLAGS;
-const getAllocator = @import("../http_client_async.zig").getAllocator;
-const OPEN_SOCKET_FLAGS: u32 = @import("../http_client_async.zig").OPEN_SOCKET_FLAGS;
-
-const log = Output.scoped(.AsyncSocket, true);
-
-const SSLFeatureFlags = struct {
- pub const early_data_enabled = true;
-};
-
-io: *AsyncIO = undefined,
-socket: std.os.socket_t = 0,
-head: *AsyncMessage = undefined,
-tail: *AsyncMessage = undefined,
-allocator: std.mem.Allocator,
-err: ?anyerror = null,
-queued: usize = 0,
-sent: usize = 0,
-send_frame: @Frame(AsyncSocket.send) = undefined,
-read_frame: @Frame(AsyncSocket.read) = undefined,
-connect_frame: Yield(AsyncSocket.connectToAddress) = Yield(AsyncSocket.connectToAddress){},
-close_frame: Yield(AsyncSocket.close) = Yield(AsyncSocket.close){},
-
-was_keepalive: bool = false,
-
-read_context: []u8 = undefined,
-read_offset: u64 = 0,
-read_completion: AsyncIO.Completion = undefined,
-connect_completion: AsyncIO.Completion = undefined,
-close_completion: AsyncIO.Completion = undefined,
-
-const ConnectError = AsyncIO.ConnectError || std.os.SocketError || std.os.SetSockOptError || error{ UnknownHostName, FailedToOpenSocket };
-
-pub fn init(io: *AsyncIO, socket: std.os.socket_t, allocator: std.mem.Allocator) !AsyncSocket {
- var head = AsyncMessage.get(allocator);
-
- return AsyncSocket{ .io = io, .socket = socket, .head = head, .tail = head, .allocator = allocator };
-}
-
-fn on_connect(this: *AsyncSocket, _: *Completion, err: ConnectError!void) void {
- err catch |resolved_err| {
- this.err = resolved_err;
- };
-
- this.connect_frame.maybeResume();
-}
-
-fn connectToAddress(this: *AsyncSocket, address: std.net.Address) ConnectError!void {
- const sockfd = if (this.socket > 0)
- this.socket
- else
- AsyncIO.openSocket(address.any.family, OPEN_SOCKET_FLAGS | std.os.SOCK.STREAM, std.os.IPPROTO.TCP) catch |err| {
- if (extremely_verbose) {
- Output.prettyErrorln("openSocket error: {s}", .{@errorName(err)});
- }
- this.socket = 0;
-
- return error.FailedToOpenSocket;
- };
- this.socket = sockfd;
-
- this.io.connect(*AsyncSocket, this, on_connect, &this.connect_completion, sockfd, address);
- suspend {
- this.connect_frame.set(@frame());
- }
-
- if (this.err) |e| {
- return @errSetCast(ConnectError, e);
- }
-}
-
-fn on_close(this: *AsyncSocket, _: *Completion, _: AsyncIO.CloseError!void) void {
- this.close_frame.maybeResume();
-}
-
-pub fn close(this: *AsyncSocket) void {
- if (this.socket == 0) return;
- const to_close = this.socket;
- this.socket = 0;
- this.io.close(*AsyncSocket, this, on_close, &this.close_completion, to_close);
- suspend {
- this.close_frame.set(@frame());
- }
-}
-pub fn connect(this: *AsyncSocket, name: []const u8, port: u16) ConnectError!void {
- if (!this.was_keepalive and !KeepAlive.disabled) {
- if (KeepAlive.instance.find(name, port)) |socket| {
- var err_code: i32 = undefined;
- var size: u32 = @sizeOf(u32);
- const rc = std.os.system.getsockopt(socket, std.os.SOL.SOCKET, std.os.SO.ERROR, @ptrCast([*]u8, &err_code), &size);
- switch (std.os.errno(rc)) {
- .SUCCESS => {
- this.socket = socket;
- this.was_keepalive = true;
- return;
- },
- .BADF, .FAULT, .INVAL => {},
- else => {
- std.os.closeSocket(socket);
- },
- }
- }
- }
-
- this.was_keepalive = false;
- return try this.doConnect(name, port);
-}
-const strings = @import("strings");
-fn doConnect(this: *AsyncSocket, name: []const u8, port: u16) ConnectError!void {
- this.was_keepalive = false;
-
- outer: while (true) {
- // getAddrList allocates, but it really shouldn't
- // it allocates about 1024 bytes, so we can just make it a stack allocation
- var stack_fallback_allocator = std.heap.stackFallback(4096, getAllocator());
- var allocator = stack_fallback_allocator.get();
-
- // on macOS, getaddrinfo() is very slow
- // If you send ~200 network requests, about 1.5s is spent on getaddrinfo()
- // So, we cache this.
- var list = NetworkThread.getAddressList(
- allocator,
- // There is a bug where getAddressList always fails on localhost
- // I don't understand why
- // but 127.0.0.1 works
- // so we can just use that
- // this is technically incorrect – one could remap localhost to something other than 127.0.0.1
- // but we will wait for someone to complain about it before addressing it
- if (!strings.eqlComptime(name, "localhost"))
- name
- else
- "127.0.0.1",
- port,
- ) catch |err| {
- return @errSetCast(ConnectError, err);
- };
-
- if (list.addrs.len == 0) return error.ConnectionRefused;
-
- for (list.addrs) |address| {
- this.connectToAddress(address) catch |err| {
- this.close();
-
- if (err == error.ConnectionRefused) continue;
- if (err == error.AddressNotAvailable or err == error.UnknownHostName) continue :outer;
- };
- return;
- }
-
- this.close();
-
- return error.ConnectionRefused;
- }
-}
-
-fn on_send(msg: *AsyncMessage, _: *Completion, result: SendError!usize) void {
- var this = @ptrCast(*AsyncSocket, @alignCast(@alignOf(*AsyncSocket), msg.context));
- const written = result catch |err| {
- this.err = err;
- resume this.send_frame;
- return;
- };
-
- if (written == 0) {
- resume this.send_frame;
- return;
- }
-
- msg.sent += @truncate(u16, written);
- const has_more = msg.used > msg.sent;
- this.sent += written;
-
- if (has_more) {
- this.io.send(
- *AsyncMessage,
- msg,
- on_send,
- &msg.completion,
- this.socket,
- msg.slice(),
- SOCKET_FLAGS,
- );
- } else {
- msg.release();
- }
-
- // complete
- if (this.queued <= this.sent) {
- resume this.send_frame;
- }
-}
-
-pub fn write(this: *AsyncSocket, buf: []const u8) usize {
- this.tail.context = this;
-
- const resp = this.tail.writeAll(buf);
- this.queued += resp.written;
-
- if (resp.overflow) {
- var next = AsyncMessage.get(getAllocator());
- this.tail.next = next;
- this.tail = next;
-
- return @as(usize, resp.written) + this.write(buf[resp.written..]);
- }
-
- return @as(usize, resp.written);
-}
-
-pub const SendError = AsyncIO.SendError;
-
-pub fn deinit(this: *AsyncSocket) void {
- this.head.release();
- this.err = null;
- this.queued = 0;
- this.sent = 0;
- this.read_context = &[_]u8{};
- this.read_offset = 0;
- this.socket = 0;
-}
-
-pub fn send(this: *AsyncSocket) SendError!usize {
- const original_sent = this.sent;
- this.head.context = this;
-
- this.io.send(
- *AsyncMessage,
- this.head,
- on_send,
- &this.head.completion,
- this.socket,
- this.head.slice(),
- SOCKET_FLAGS,
- );
-
- var node = this.head;
- while (node.next) |element| {
- this.io.send(
- *AsyncMessage,
- element,
- on_send,
- &element.completion,
- this.socket,
- element.slice(),
- SOCKET_FLAGS,
- );
- node = element.next orelse break;
- }
-
- suspend {
- this.send_frame = @frame().*;
- }
-
- if (this.err) |err| {
- this.err = null;
- return @errSetCast(AsyncSocket.SendError, err);
- }
-
- return this.sent - original_sent;
-}
-
-pub const RecvError = AsyncIO.RecvError;
-
-const Reader = struct {
- pub fn on_read(ctx: *AsyncSocket, _: *AsyncIO.Completion, result: RecvError!usize) void {
- const len = result catch |err| {
- ctx.err = err;
- resume ctx.read_frame;
- return;
- };
- ctx.read_offset += len;
- resume ctx.read_frame;
- }
-};
-
-pub inline fn bufferedReadAmount(_: *AsyncSocket) usize {
- return 0;
-}
-
-pub fn read(
- this: *AsyncSocket,
- bytes: []u8,
- /// offset is necessary here to be consistent with HTTPS
- /// HTTPs must have the same buffer pointer for each read
- offset: u64,
-) RecvError!u64 {
- this.read_context = bytes;
- this.read_offset = offset;
- const original_read_offset = this.read_offset;
-
- this.io.recv(
- *AsyncSocket,
- this,
- Reader.on_read,
- &this.read_completion,
- this.socket,
- bytes[original_read_offset..],
- );
-
- suspend {
- this.read_frame = @frame().*;
- }
-
- if (this.err) |err| {
- this.err = null;
- return @errSetCast(RecvError, err);
- }
-
- log(
- \\recv(offset: {d}, len: {d}, read_offset: {d})
- \\
- , .{
- offset,
- bytes[original_read_offset..].len,
- this.read_offset,
- });
-
- return this.read_offset - original_read_offset;
-}
-
-pub fn Yield(comptime Type: anytype) type {
- return struct {
- frame: @Frame(Type) = undefined,
- wait: bool = false,
-
- pub fn set(this: *@This(), frame: anytype) void {
- this.wait = true;
- this.frame = frame.*;
- }
-
- pub fn maybeResume(this: *@This()) void {
- if (!this.wait) return;
- this.wait = false;
- resume this.frame;
- }
- };
-}
-
-pub const SSL = struct {
- ssl: *boring.SSL = undefined,
- ssl_loaded: bool = false,
- socket: AsyncSocket,
- handshake_complete: bool = false,
- ssl_bio: AsyncBIO = undefined,
- ssl_bio_loaded: bool = false,
- unencrypted_bytes_to_send: ?*AsyncMessage = null,
- connect_frame: Yield(SSL.handshake) = Yield(SSL.handshake){},
- send_frame: Yield(SSL.send) = Yield(SSL.send){},
- read_frame: Yield(SSL.read) = Yield(SSL.read){},
-
- hostname: [bun.MAX_PATH_BYTES]u8 = undefined,
- is_ssl: bool = false,
-
- handshake_state: HandshakeState = HandshakeState.none,
- next_handshake_state: HandshakeState = HandshakeState.none,
- first_posthandshake_write: bool = true,
- in_confirm_handshake: bool = false,
- completed_connect: bool = false,
- disconnected: bool = false,
-
- pending_write_buffer: []const u8 = &[_]u8{},
- pending_read_buffer: []u8 = &[_]u8{},
- pending_read_result: anyerror!u32 = 0,
- pending_write_result: anyerror!u32 = 0,
-
- handshake_retry_count: u16 = 5,
-
- first_post_handshake_write: bool = true,
-
- handshake_result: ?anyerror = null,
-
- peek_complete: bool = false,
-
- pub const HandshakeState = enum {
- none,
- handshake,
- complete,
- };
-
- const SSLConnectError = ConnectError || HandshakeError;
- const HandshakeError = error{ ClientCertNeeded, OpenSSLError, WouldBlock };
-
- pub fn connect(this: *SSL, name: []const u8, port: u16) !void {
- this.is_ssl = true;
- try this.socket.connect(name, port);
-
- this.handshake_complete = false;
-
- var ssl = boring.initClient();
- this.ssl = ssl;
- this.ssl_loaded = true;
- errdefer {
- this.ssl_loaded = false;
- this.ssl.deinit();
- this.ssl = undefined;
- }
-
- // SNI should only contain valid DNS hostnames, not IP addresses (see RFC
- // 6066, Section 3).
- //
- // See https://crbug.com/496472 and https://crbug.com/496468 for discussion.
- {
- std.mem.copy(u8, &this.hostname, name);
- this.hostname[name.len] = 0;
- var name_ = this.hostname[0..name.len :0];
- ssl.setHostname(name_);
- }
-
- try this.ssl_bio.init();
- this.ssl_bio_loaded = true;
-
- this.ssl_bio.onReady = AsyncBIO.Callback.Wrap(SSL, SSL.retryAll).get(this);
- this.ssl_bio.socket_fd = this.socket.socket;
-
- boring.SSL_set_bio(ssl, this.ssl_bio.bio.?, this.ssl_bio.bio.?);
-
- // boring.SSL_set_early_data_enabled(ssl, 1);
- _ = boring.SSL_clear_options(ssl, boring.SSL_OP_LEGACY_SERVER_CONNECT);
- _ = boring.SSL_set_options(ssl, boring.SSL_OP_LEGACY_SERVER_CONNECT);
- const mode = boring.SSL_MODE_CBC_RECORD_SPLITTING | boring.SSL_MODE_ENABLE_FALSE_START | boring.SSL_MODE_RELEASE_BUFFERS;
-
- _ = boring.SSL_set_mode(ssl, mode);
- _ = boring.SSL_clear_mode(ssl, mode);
-
- var alpns = &[_]u8{ 8, 'h', 't', 't', 'p', '/', '1', '.', '1' };
- if (Environment.allow_assert) std.debug.assert(boring.SSL_set_alpn_protos(this.ssl, alpns, alpns.len) == 0);
-
- boring.SSL_enable_signed_cert_timestamps(ssl);
- boring.SSL_enable_ocsp_stapling(ssl);
-
- // std.debug.assert(boring.SSL_set_strict_cipher_list(ssl, boring.SSL_DEFAULT_CIPHER_LIST) == 0);
-
- boring.SSL_set_enable_ech_grease(ssl, 1);
-
- // Configure BoringSSL to allow renegotiations. Once the initial handshake
- // completes, if renegotiations are not allowed, the default reject value will
- // be restored. This is done in this order to permit a BoringSSL
- // optimization. See https://crbug.com/boringssl/123. Use
- // ssl_renegotiate_explicit rather than ssl_renegotiate_freely so DoPeek()
- // does not trigger renegotiations.
- boring.SSL_set_renegotiate_mode(ssl, boring.ssl_renegotiate_explicit);
-
- boring.SSL_set_shed_handshake_config(ssl, 1);
-
- this.unencrypted_bytes_to_send = this.socket.head;
-
- try this.handshake();
-
- this.completed_connect = true;
- }
-
- pub fn close(this: *SSL) void {
- if (this.ssl_loaded) {
- this.ssl.shutdown();
- this.ssl.deinit();
- this.ssl_loaded = false;
- }
-
- if (this.ssl_bio_loaded) {
- this.ssl_bio.socket_fd = 0;
- }
-
- this.socket.close();
- }
-
- pub fn handshake(this: *SSL) HandshakeError!void {
- this.next_handshake_state = .handshake;
- this.handshake_result = null;
- this.doHandshakeLoop() catch |err| {
- if (err == error.WouldBlock) {
- suspend {
- this.connect_frame.set(@frame());
- }
- } else {
- return err;
- }
- };
-
- if (this.handshake_result) |handshake_err| {
- const err2 = @errSetCast(HandshakeError, handshake_err);
- this.handshake_result = null;
- return err2;
- }
- }
-
- fn retryAll(this: *SSL) void {
- const had_handshaked = this.completed_connect;
- // SSL_do_handshake, SSL_read, and SSL_write may all be retried when blocked,
- // so retry all operations for simplicity. (Otherwise, SSL_get_error for each
- // operation may be remembered to retry only the blocked ones.)
- if (this.next_handshake_state == .handshake) {
- this.onHandshakeIOComplete() catch {};
- }
-
- this.doPeek();
- if (!had_handshaked or !this.peek_complete) return;
-
- if (this.pending_read_buffer.len > 0) {
- reader: {
- var count: u32 = this.pending_read_result catch unreachable;
- this.pending_read_result = this.doPayloadRead(this.pending_read_buffer, &count) catch |err| brk: {
- this.pending_read_result = count;
-
- if (err == error.WouldBlock) {
-
- // // partial reads are a success case
- // // allow the client to ask for more
- // if (count > 0) {
- // this.read_frame.maybeResume();
- // break :reader;
- // }
-
- break :reader;
- }
- break :brk err;
- };
-
- this.read_frame.maybeResume();
- }
- }
-
- if (this.pending_write_buffer.len > 0) {
- writer: {
- this.pending_write_result = this.doPayloadWrite() catch |err| brk: {
- if (err == error.WantWrite or err == error.WantRead) break :writer;
- break :brk err;
- };
-
- this.send_frame.maybeResume();
- }
- }
- }
-
- pub fn doPayloadWrite(this: *SSL) anyerror!u32 {
- const rv = try this.ssl.write(this.pending_write_buffer);
-
- if (rv >= 0) {
- this.pending_write_buffer = this.pending_write_buffer[rv..];
- }
-
- return rv;
- }
-
- pub fn doPayloadRead(this: *SSL, buffer: []u8, count: *u32) anyerror!u32 {
- if (this.ssl_bio.socket_recv_error != null) {
- const pending = this.ssl_bio.socket_recv_error.?;
- this.ssl_bio.socket_recv_error = null;
- return pending;
- }
-
- var total_bytes_read: u32 = count.*;
- var ssl_ret: c_int = 0;
- var ssl_err: c_int = 0;
- const buf_len = @truncate(u32, buffer.len);
- while (true) {
- boring.ERR_clear_error();
- ssl_ret = boring.SSL_read(this.ssl, buffer.ptr + total_bytes_read, @intCast(c_int, buf_len - total_bytes_read));
- ssl_err = boring.SSL_get_error(this.ssl, ssl_ret);
-
- if (ssl_ret > 0) {
- total_bytes_read += @intCast(u32, ssl_ret);
- } else if (ssl_err == boring.SSL_ERROR_WANT_RENEGOTIATE) {
- if (boring.SSL_renegotiate(this.ssl) == 0) {
- ssl_err = boring.SSL_ERROR_SSL;
- }
- }
-
- // Continue processing records as long as there is more data available
- // synchronously.
- if (!(ssl_err == boring.SSL_ERROR_WANT_RENEGOTIATE or (total_bytes_read < buf_len and ssl_ret > 0 and this.ssl_bio.hasPendingReadData()))) break;
- }
-
- // Although only the final SSL_read call may have failed, the failure needs to
- // processed immediately, while the information still available in OpenSSL's
- // error queue.
- var result: anyerror!u32 = total_bytes_read;
- count.* = total_bytes_read;
-
- if (ssl_ret <= 0) {
- switch (ssl_err) {
- boring.SSL_ERROR_ZERO_RETURN => {},
- boring.SSL_ERROR_WANT_X509_LOOKUP => {
- result = error.SSLErrorWantX509Lookup;
- },
- boring.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION => {
- result = error.SSLErrorWantPrivateKeyOperation;
- },
-
- // Do not treat insufficient data as an error to return in the next call to
- // DoPayloadRead() - instead, let the call fall through to check SSL_read()
- // again. The transport may have data available by then.
- boring.SSL_ERROR_WANT_READ, boring.SSL_ERROR_WANT_WRITE => {
- result = error.WouldBlock;
- },
- else => {
- if (extremely_verbose) {
- const err = boring.ERR_get_error();
-
- const version = std.mem.span(boring.SSL_get_version(this.ssl));
- var hostname = std.mem.span(std.mem.sliceTo(&this.hostname, 0));
- Output.prettyErrorln("[{s}] OpenSSLError reading (version: {s}, total read: {d}) - code: {d}", .{ hostname, version, total_bytes_read, err });
- }
- result = error.OpenSSLError;
- },
- }
- }
-
- // Many servers do not reliably send a close_notify alert when shutting down
- // a connection, and instead terminate the TCP connection. This is reported
- // as ERR_CONNECTION_CLOSED. Because of this, map the unclean shutdown to a
- // graceful EOF, instead of treating it as an error as it should be.
- if (this.ssl_bio.socket_recv_error) |err| {
- this.ssl_bio.socket_recv_error = null;
- return err;
- }
-
- return result;
- }
-
- fn doHandshakeLoop(
- this: *SSL,
- ) HandshakeError!void {
- while (true) {
- var state = this.next_handshake_state;
- this.next_handshake_state = HandshakeState.none;
- switch (state) {
- .handshake => {
- this.doHandshake() catch |err| {
- if (err != error.WouldBlock) {
- this.handshake_result = err;
- }
- return err;
- };
- },
- .complete => {
- this.doHandshakeComplete();
- },
- else => unreachable,
- }
- if (this.next_handshake_state == .none) return;
- }
- }
-
- fn onHandshakeIOComplete(this: *SSL) HandshakeError!void {
- this.doHandshakeLoop() catch |err| {
- if (err == error.WouldBlock) {
- return;
- }
- this.in_confirm_handshake = false;
- this.connect_frame.maybeResume();
- return;
- };
- this.connect_frame.maybeResume();
- }
-
- fn doHandshakeComplete(this: *SSL) void {
- if (this.in_confirm_handshake) {
- this.next_handshake_state = .none;
- return;
- }
-
- this.completed_connect = true;
- this.next_handshake_state = .none;
- this.doPeek();
- if (extremely_verbose) {
- const version = std.mem.span(boring.SSL_get_version(this.ssl));
- var hostname = std.mem.span(std.mem.sliceTo(&this.hostname, 0));
- Output.prettyErrorln("[{s}] Handshake complete.\n[{s}] TLS Version: {s}", .{
- hostname,
- hostname,
- version,
- });
- }
- }
-
- fn doPeek(this: *SSL) void {
- if (!this.completed_connect) {
- return;
- }
-
- if (this.peek_complete) {
- return;
- }
-
- var byte: u8 = 0;
- boring.ERR_clear_error();
- var rv = boring.SSL_peek(this.ssl, &byte, 1);
- var ssl_error = boring.SSL_get_error(this.ssl, rv);
- switch (ssl_error) {
- boring.SSL_ERROR_WANT_READ, boring.SSL_ERROR_WANT_WRITE => {},
- else => {
- this.peek_complete = true;
- },
- }
- }
-
- fn doHandshake(this: *SSL) HandshakeError!void {
- boring.ERR_clear_error();
-
- const rv = boring.SSL_do_handshake(this.ssl);
- if (rv <= 0) {
- const ssl_error = boring.SSL_get_error(this.ssl, rv);
-
- switch (ssl_error) {
- boring.SSL_ERROR_WANT_PRIVATE_KEY_OPERATION, boring.SSL_ERROR_WANT_X509_LOOKUP => {
- this.next_handshake_state = HandshakeState.handshake;
- return error.ClientCertNeeded;
- },
- boring.SSL_ERROR_WANT_CERTIFICATE_VERIFY => {
- this.next_handshake_state = HandshakeState.handshake;
- return error.ClientCertNeeded;
- },
- boring.SSL_ERROR_WANT_READ, boring.SSL_ERROR_WANT_WRITE => {
- this.next_handshake_state = HandshakeState.handshake;
- return error.WouldBlock;
- },
- boring.SSL_ERROR_SYSCALL => {
- this.handshake_retry_count -|= 1;
- if (this.handshake_retry_count > 0) {
- this.next_handshake_state = HandshakeState.handshake;
- return error.WouldBlock;
- }
-
- return error.OpenSSLError;
- },
- else => {
- if (extremely_verbose) {
- const err = boring.ERR_get_error();
- var error_buf: [1024]u8 = undefined;
- @memset(&error_buf, 0, 1024);
- var err_msg = std.mem.span(boring.ERR_error_string(err, &error_buf));
- Output.prettyErrorln("Handshaking error {s}", .{err_msg});
- }
- return error.OpenSSLError;
- },
- }
- }
-
- this.next_handshake_state = HandshakeState.complete;
- }
-
- pub fn write(this: *SSL, buffer_: []const u8) usize {
- return this.unencrypted_bytes_to_send.?.writeAll(buffer_).written;
- }
-
- pub fn bufferedReadAmount(this: *SSL) usize {
- const pend = boring.SSL_pending(this.ssl);
- return if (pend <= 0)
- 0
- else
- @intCast(usize, pend);
- }
-
- pub fn send(this: *SSL) anyerror!usize {
- this.unencrypted_bytes_to_send.?.sent = 0;
- this.pending_write_buffer = this.unencrypted_bytes_to_send.?.buf[this.unencrypted_bytes_to_send.?.sent..this.unencrypted_bytes_to_send.?.used];
- while (true) {
- const sent = this.doPayloadWrite() catch |err| {
- if (err == error.WantRead or err == error.WantWrite) {
- if (err == error.WantWrite) {
- if (this.first_post_handshake_write and boring.SSL_is_init_finished(this.ssl) != 0 and this.pending_write_buffer.len == 0) {
- this.first_post_handshake_write = false;
-
- if (boring.SSL_version(this.ssl) == boring.TLS1_3_VERSION) {
- if (Environment.allow_assert) std.debug.assert(boring.SSL_key_update(this.ssl, boring.SSL_KEY_UPDATE_REQUESTED) == 0);
- continue;
- }
- }
- }
-
- this.pending_write_result = 0;
- suspend {
- this.send_frame.set(@frame());
- }
- const result = this.pending_write_result;
- this.pending_write_result = 0;
- this.unencrypted_bytes_to_send.?.used = 0;
- if (result) |res| {
- return res;
- } else |er| {
- return er;
- }
- }
- if (extremely_verbose) {
- Output.prettyErrorln("SSL error: {s}", .{@errorName(err)});
- Output.flush();
- }
- return err;
- };
- this.unencrypted_bytes_to_send.?.sent += sent;
-
- if (this.unencrypted_bytes_to_send.?.sent == this.unencrypted_bytes_to_send.?.used) {
- this.unencrypted_bytes_to_send.?.used = 0;
- this.unencrypted_bytes_to_send.?.sent = 0;
- }
-
- return sent;
- }
- }
-
- pub fn read(this: *SSL, buf_: []u8, offset: u64) !u32 {
- var buf = buf_[offset..];
- var read_bytes: u32 = 0;
- this.pending_read_result = 0;
-
- return this.doPayloadRead(buf, &read_bytes) catch |err| {
- if (err == error.WouldBlock) {
- this.pending_read_result = (this.pending_read_result catch unreachable) + read_bytes;
- this.pending_read_buffer = buf;
-
- suspend {
- this.read_frame.set(@frame());
- }
- const result = this.pending_read_result;
- this.pending_read_result = 0;
-
- return result;
- }
- return err;
- };
- }
-
- pub inline fn init(allocator: std.mem.Allocator, io: *AsyncIO) !SSL {
- return SSL{
- .ssl_bio = AsyncBIO{
- .allocator = allocator,
- },
- .socket = try AsyncSocket.init(io, 0, allocator),
- };
- }
-
- pub fn deinit(this: *SSL) void {
- this.socket.deinit();
-
- if (this.ssl_loaded) {
- this.connect_frame.wait = false;
- this.read_frame.wait = false;
- this.send_frame.wait = false;
-
- this.ssl.shutdown();
- this.ssl.deinit();
- this.ssl_loaded = false;
- }
-
- if (this.ssl_bio_loaded) {
- this.ssl_bio_loaded = false;
- if (this.ssl_bio.recv_buffer) |recv| {
- recv.release();
- }
- this.ssl_bio.recv_buffer = null;
-
- if (this.ssl_bio.send_buffer) |recv| {
- recv.release();
- }
- this.ssl_bio.send_buffer = null;
-
- if (this.ssl_bio.bio) |bio| {
- bio.deinit();
- }
- this.ssl_bio.bio = null;
-
- this.ssl_bio.pending_reads = 0;
- this.ssl_bio.pending_sends = 0;
- this.ssl_bio.socket_recv_len = 0;
- this.ssl_bio.socket_send_len = 0;
- this.ssl_bio.bio_write_offset = 0;
- this.ssl_bio.recv_eof = false;
- this.ssl_bio.bio_read_offset = 0;
- this.ssl_bio.socket_send_error = null;
- this.ssl_bio.socket_recv_error = null;
-
- this.ssl_bio.socket_fd = 0;
- this.ssl_bio.onReady = null;
- } else {
- this.ssl_bio = AsyncBIO{ .allocator = getAllocator() };
- }
-
- this.handshake_complete = false;
-
- this.* = SSL{
- .socket = this.socket,
- };
- }
-};
diff --git a/src/http/websocket_http_client.zig b/src/http/websocket_http_client.zig
index b1b4eea4b..029eafef8 100644
--- a/src/http/websocket_http_client.zig
+++ b/src/http/websocket_http_client.zig
@@ -125,7 +125,7 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
body_written: usize = 0,
websocket_protocol: u64 = 0,
event_loop_ref: bool = false,
-
+ hostname: [:0]u8 = "",
pub const name = if (ssl) "WebSocketHTTPSClient" else "WebSocketHTTPClient";
pub const shim = JSC.Shimmer("Bun", name, @This());
@@ -190,6 +190,12 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
vm.eventLoop().start_server_on_next_tick = true;
if (Socket.connect(host_.slice(), port, @ptrCast(*uws.SocketContext, socket_ctx), HTTPClient, client, "tcp")) |out| {
+ if (comptime ssl) {
+ if (!strings.isIPAddress(host_.slice())) {
+ out.hostname = bun.default_allocator.dupeZ(u8, host_.slice()) catch "";
+ }
+ }
+
out.tcp.timeout(120);
return out;
}
@@ -250,6 +256,14 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
std.debug.assert(this.input_body_buf.len > 0);
std.debug.assert(this.to_send.len == 0);
+ if (comptime ssl) {
+ if (this.hostname.len > 0) {
+ socket.getNativeHandle().configureHTTPClient(this.hostname);
+ bun.default_allocator.free(this.hostname);
+ this.hostname = "";
+ }
+ }
+
const wrote = socket.write(this.input_body_buf, true);
if (wrote < 0) {
this.terminate(ErrorCode.failed_to_write);
diff --git a/src/http_client_async.zig b/src/http_client_async.zig
index 6b27c3c26..103db175c 100644
--- a/src/http_client_async.zig
+++ b/src/http_client_async.zig
@@ -25,8 +25,6 @@ const ObjectPool = @import("./pool.zig").ObjectPool;
const SOCK = os.SOCK;
const Arena = @import("./mimalloc_arena.zig").Arena;
const AsyncMessage = @import("./http/async_message.zig");
-const AsyncBIO = @import("./http/async_bio.zig");
-const AsyncSocket = @import("./http/async_socket.zig");
const ZlibPool = @import("./http/zlib.zig");
const URLBufferPool = ObjectPool([4096]u8, null, false, 10);
const uws = @import("uws");
@@ -472,6 +470,7 @@ pub const HTTPThread = struct {
const log = Output.scoped(.fetch, false);
+var temp_hostname: [8096]u8 = undefined;
pub fn onOpen(
client: *HTTPClient,
comptime is_ssl: bool,
@@ -482,6 +481,28 @@ pub fn onOpen(
}
log("Connected {s} \n", .{client.url.href});
+
+ if (comptime is_ssl) {
+ var ssl: *BoringSSL.SSL = @ptrCast(*BoringSSL.SSL, socket.getNativeHandle());
+ if (!ssl.isInitFinished()) {
+ var hostname: [:0]u8 = "";
+ var hostname_needs_free = false;
+ if (!strings.isIPAddress(client.url.hostname)) {
+ if (client.url.hostname.len < temp_hostname.len) {
+ @memcpy(&temp_hostname, client.url.hostname.ptr, client.url.hostname.len);
+ temp_hostname[client.url.hostname.len] = 0;
+ hostname = temp_hostname[0..client.url.hostname.len :0];
+ } else {
+ hostname = bun.default_allocator.dupeZ(u8, client.url.hostname) catch unreachable;
+ hostname_needs_free = true;
+ }
+ }
+
+ defer if (hostname_needs_free) bun.default_allocator.free(hostname);
+
+ ssl.configureHTTPClient(hostname);
+ }
+ }
if (client.state.request_stage == .pending) {
client.onWritable(true, comptime is_ssl, socket);
}
diff --git a/src/string_immutable.zig b/src/string_immutable.zig
index acf9d057c..f2ef8f252 100644
--- a/src/string_immutable.zig
+++ b/src/string_immutable.zig
@@ -3698,3 +3698,14 @@ test "eqlCaseInsensitiveASCII" {
try std.testing.expect(!eqlCaseInsensitiveASCII("aBcD", "NOOO", true));
try std.testing.expect(!eqlCaseInsensitiveASCII("aBcD", "LENGTH CHECK", true));
}
+
+pub fn isIPAddress(input: []const u8) bool {
+ if (containsChar(input, ':'))
+ return true;
+
+ if (std.x.os.IPv4.parse(input)) |_| {
+ return true;
+ } else |_| {
+ return false;
+ }
+}
diff --git a/test/bun.js/fetch.test.js b/test/bun.js/fetch.test.js
index 87d9e0576..1b9d0779e 100644
--- a/test/bun.js/fetch.test.js
+++ b/test/bun.js/fetch.test.js
@@ -2,6 +2,12 @@ import { it, describe, expect } from "bun:test";
import fs from "fs";
import { gc } from "./gc";
+const exampleFixture = fs.readFileSync(
+ import.meta.path.substring(0, import.meta.path.lastIndexOf("/")) +
+ "/fetch.js.txt",
+ "utf8"
+);
+
describe("fetch", () => {
const urls = ["https://example.com", "http://example.com"];
for (let url of urls) {
@@ -12,17 +18,32 @@ describe("fetch", () => {
gc();
const text = await response.text();
gc();
- expect(
- fs.readFileSync(
- import.meta.path.substring(0, import.meta.path.lastIndexOf("/")) +
- "/fetch.js.txt",
- "utf8"
- )
- ).toBe(text);
+ expect(exampleFixture).toBe(text);
});
}
});
+it("simultaneous HTTPS fetch", async () => {
+ const urls = ["https://example.com", "https://www.example.com"];
+ for (let batch = 0; batch < 4; batch++) {
+ const promises = new Array(20);
+ for (let i = 0; i < 20; i++) {
+ promises[i] = fetch(urls[i % 2]);
+ }
+ const result = await Promise.all(promises);
+ expect(result.length).toBe(20);
+ for (let i = 0; i < 20; i++) {
+ expect(result[i].status).toBe(200);
+ expect(await result[i].text()).toBe(exampleFixture);
+ }
+ }
+});
+
+it("website with tlsextname", async () => {
+ // irony
+ await fetch("https://bun.sh", { method: "HEAD" });
+});
+
function testBlobInterface(blobbyConstructor, hasBlobFn) {
for (let withGC of [false, true]) {
for (let jsonObject of [