diff options
author | 2022-10-08 01:05:19 -0700 | |
---|---|---|
committer | 2022-10-08 01:06:35 -0700 | |
commit | c2c9173eff9e929004d73e087b62ffdc25f0f4ee (patch) | |
tree | 06357b3ab4761503d35c0d5aeef294706b4036e0 | |
parent | 99e7856269ab084c0b5c69d8dac3bcd89ec08e8d (diff) | |
download | bun-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.zig | 2 | ||||
-rw-r--r-- | src/deps/boringssl.translated.zig | 20 | ||||
-rw-r--r-- | src/deps/uws.zig | 12 | ||||
-rw-r--r-- | src/http/async_bio.zig | 437 | ||||
-rw-r--r-- | src/http/async_socket.zig | 897 | ||||
-rw-r--r-- | src/http/websocket_http_client.zig | 16 | ||||
-rw-r--r-- | src/http_client_async.zig | 25 | ||||
-rw-r--r-- | src/string_immutable.zig | 11 | ||||
-rw-r--r-- | test/bun.js/fetch.test.js | 35 |
9 files changed, 110 insertions, 1345 deletions
@@ -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 [ |