gitoria
All repositories: gitoria
25.5 KB
// Shared TLS layer — OpenSSL via dlopen, compiled into each HTTP plugin at build time.// Not a standalone plugin .so, but a Zig module imported by hl:http1 and hl:http2// (the same shape as http_common.zig and ws_common.zig).//// Mission 121 EXTRACTED this file. Both http1 and http2 carried their own copy of// the same dlopen dance — http1's since the plugin was written, http2's since// mission 061 added ALPN on top of it. D8's rule applies here exactly as it does to// WS framing: the transport-agnostic half is shared, the per-protocol half stays in// the plugin. What is per-protocol about TLS is TWO things and nothing else:// • the ALPN list a server offers ("h2" for http2, "http/1.1" for http1)// • WHERE the handshake happens (http2 does it on the accept thread before the// socket goes back to non-blocking; http1 does it in the I/O worker)// Both are parameters here, so there is one implementation of the dlopen, the// SSL_CTX, the cert/key load, the ALPN callback and the read/write/shutdown paths.//// No compile-time dependency on OpenSSL: the library is resolved at runtime and a// server whose host has no libssl falls back to plaintext (the caller decides).//// NOT hl:http3. Its `Quic` struct dlopens the same library but through a DIFFERENT// OpenSSL surface — `OSSL_QUIC_server_method`, `SSL_new_listener`/`SSL_listen`,// `SSL_read_ex`/`SSL_write_ex`, event-driven, no `SSL_accept` and no socket fd —// so it is not a copy of this and folding it in would be a rewrite, not an// extraction. It stays where it is.const std = @import("std");const c_dlfcn = @cImport({@cInclude("dlfcn.h");});const linux = std.os.linux;// The plugins that use this module both run per-request allocations across threads,// so the thread-safe production allocator is the only correct choice here too.const allocator = std.heap.smp_allocator;// Direct syscall for stderr — std.debug.print pulls in std.Progress, whose global// state is ABI-incompatible when a .so is loaded into a differently-built binary.fn logMsg(msg: []const u8) void {_ = linux.write(2, msg.ptr, msg.len);}fn logFmt(comptime fmt: []const u8, args: anytype) void {var buf: [512]u8 = undefined;const s = std.fmt.bufPrint(&buf, fmt, args) catch return;logMsg(s);}pub const SSL_CTX = opaque {};pub const SSL = opaque {};pub const SSL_METHOD = opaque {};// OpenSSL function pointer typesconst SSL_library_init_fn = *const fn () callconv(.c) c_int;const SSL_load_error_strings_fn = *const fn () callconv(.c) void;const TLS_server_method_fn = *const fn () callconv(.c) ?*const SSL_METHOD;const SSL_CTX_new_fn = *const fn (?*const SSL_METHOD) callconv(.c) ?*SSL_CTX;const SSL_CTX_free_fn = *const fn (?*SSL_CTX) callconv(.c) void;const SSL_CTX_use_certificate_chain_file_fn = *const fn (?*SSL_CTX, [*:0]const u8) callconv(.c) c_int;const SSL_CTX_use_PrivateKey_file_fn = *const fn (?*SSL_CTX, [*:0]const u8, c_int) callconv(.c) c_int;const SSL_CTX_set_alpn_select_cb_fn = *const fn (?*SSL_CTX, ?AlpnSelectCallback, ?*anyopaque) callconv(.c) void;const SSL_new_fn = *const fn (?*SSL_CTX) callconv(.c) ?*SSL;const SSL_free_fn = *const fn (?*SSL) callconv(.c) void;const SSL_set_fd_fn = *const fn (?*SSL, c_int) callconv(.c) c_int;const SSL_accept_fn = *const fn (?*SSL) callconv(.c) c_int;const SSL_read_fn = *const fn (?*SSL, [*]u8, c_int) callconv(.c) c_int;const SSL_write_fn = *const fn (?*SSL, [*]const u8, c_int) callconv(.c) c_int;const SSL_shutdown_fn = *const fn (?*SSL) callconv(.c) c_int;const SSL_get_error_fn = *const fn (?*SSL, c_int) callconv(.c) c_int;const OPENSSL_init_ssl_fn = *const fn (u64, ?*anyopaque) callconv(.c) c_int;// ── The CLIENT half's symbols (hl:fetch, hl:smtp) ────────────────────────────// RESTORED 2026-08-29 (mission 257): `initClient`/`connect` and everything they// resolve were lost in the 2026-08-20 recovery replay, which nothing noticed// because neither plugin that calls them was in `native/build.zig`.const TLS_client_method_fn = *const fn () callconv(.c) ?*const SSL_METHOD;const SSL_CTX_set_default_verify_paths_fn = *const fn (?*SSL_CTX) callconv(.c) c_int;const SSL_CTX_load_verify_locations_fn = *const fn (?*SSL_CTX, ?[*:0]const u8, ?[*:0]const u8) callconv(.c) c_int;const SSL_CTX_set_verify_fn = *const fn (?*SSL_CTX, c_int, ?*anyopaque) callconv(.c) void;const SSL_connect_fn = *const fn (?*SSL) callconv(.c) c_int;const SSL_get_verify_result_fn = *const fn (?*SSL) callconv(.c) c_long;/// SNI goes through the generic control entry point: there is no exported/// `SSL_set_tlsext_host_name` — it is a macro over `SSL_ctrl` in the headers.const SSL_ctrl_fn = *const fn (?*SSL, c_int, c_long, ?*anyopaque) callconv(.c) c_long;const SSL_set1_host_fn = *const fn (?*SSL, [*:0]const u8) callconv(.c) c_int;pub const SSL_VERIFY_NONE: c_int = 0;pub const SSL_VERIFY_PEER: c_int = 1;pub const X509_V_OK: c_long = 0;const SSL_CTRL_SET_TLSEXT_HOSTNAME: c_int = 55;const TLSEXT_NAMETYPE_host_name: c_long = 0;pub const SSL_FILETYPE_PEM: c_int = 1;pub const SSL_ERROR_WANT_READ: c_int = 2;pub const SSL_ERROR_WANT_WRITE: c_int = 3;pub const SSL_ERROR_ZERO_RETURN: c_int = 6;pub const SSL_TLSEXT_ERR_OK: c_int = 0;pub const SSL_TLSEXT_ERR_NOACK: c_int = 2;const AlpnSelectCallback = *const fn (?*SSL, [*c][*c]const u8, [*c]u8, [*c]const u8, c_uint, ?*anyopaque) callconv(.c) c_int;/// The server's ALPN offer in wire format (each protocol length-prefixed), heap/// allocated because OpenSSL keeps the callback's user_data pointer for the life of/// the SSL_CTX and a TlsContext is returned BY VALUE (a pointer to it would dangle).const AlpnOffer = struct {wire: []u8,};/// RFC 7301 selection with SERVER preference: the first protocol this server offers/// that the client also listed wins. No overlap → NOACK, which leaves the connection/// without a negotiated protocol rather than failing the handshake (what http2 did/// before the extraction, and what http1 wants anyway — a browser that omits ALPN/// still speaks HTTP/1.1 over the socket).fn alpnSelect(ssl: ?*SSL, out: [*c][*c]const u8, outlen: [*c]u8, in: [*c]const u8, inlen: c_uint, user_data: ?*anyopaque) callconv(.c) c_int {_ = ssl;const offer: *const AlpnOffer = @ptrCast(@alignCast(user_data orelse return SSL_TLSEXT_ERR_NOACK));var si: usize = 0;while (si < offer.wire.len) {const slen = offer.wire[si];si += 1;if (si + slen > offer.wire.len) break;const ours = offer.wire[si .. si + slen];var ci: usize = 0;while (ci < inlen) {const clen = in[ci];ci += 1;if (ci + clen > inlen) break;if (clen == slen and std.mem.eql(u8, in[ci .. ci + clen], ours)) {out.* = in + ci;outlen.* = clen;return SSL_TLSEXT_ERR_OK;}ci += clen;}si += slen;}return SSL_TLSEXT_ERR_NOACK;}/// The per-protocol half, as data.pub const Options = struct {/// Protocols this server offers over ALPN, in preference order. Empty = no ALPN/// callback is registered at all (byte-identical to a server that never had one).alpn: []const []const u8 = &.{},/// Prefix for this plugin's log lines ("http1" / "http2" / "fetch" / "smtp").tag: []const u8 = "tls",// ── client-side only ──/// An EXTRA trust anchor on top of the system store, for a self-signed test/// fixture. null = the system store alone. Never a replacement: a caFile that/// fails to load fails the whole context rather than silently trusting less.ca_file: ?[]const u8 = null,/// Verify the peer's chain AND its hostname. hl:fetch never turns this off —/// it does not pass the field at all. hl:smtp exposes it, because a mail host/// with a self-signed certificate and no way to name its CA file is a real/// configuration, and a deliberate one.verify: bool = true,};pub const TlsContext = struct {ssl_ctx: ?*SSL_CTX = null,lib_ssl: ?*anyopaque = null,lib_crypto: ?*anyopaque = null,alpn: ?*AlpnOffer = null,fn_ssl_ctx_new: ?SSL_CTX_new_fn = null,fn_ssl_ctx_free: ?SSL_CTX_free_fn = null,fn_ssl_ctx_use_cert: ?SSL_CTX_use_certificate_chain_file_fn = null,fn_ssl_ctx_use_key: ?SSL_CTX_use_PrivateKey_file_fn = null,fn_ssl_ctx_set_alpn: ?SSL_CTX_set_alpn_select_cb_fn = null,fn_ssl_new: ?SSL_new_fn = null,fn_ssl_free: ?SSL_free_fn = null,fn_ssl_set_fd: ?SSL_set_fd_fn = null,fn_ssl_accept: ?SSL_accept_fn = null,fn_ssl_read: ?SSL_read_fn = null,fn_ssl_write: ?SSL_write_fn = null,fn_ssl_shutdown: ?SSL_shutdown_fn = null,fn_ssl_get_error: ?SSL_get_error_fn = null,// client halffn_ssl_connect: ?SSL_connect_fn = null,fn_ssl_ctrl: ?SSL_ctrl_fn = null,fn_ssl_set1_host: ?SSL_set1_host_fn = null,fn_ssl_get_verify_result: ?SSL_get_verify_result_fn = null,/// What this context was built for. `connect` refuses on a server context and/// `wrapConnection` on a client one, rather than calling a null function/// pointer somewhere deep inside OpenSSL.is_client: bool = false,/// Whether `connect` enforces the chain and the hostname (client only).verify: bool = true,fn loadSym(lib: ?*anyopaque, comptime T: type, name: [*:0]const u8) ?T {const sym = c_dlfcn.dlsym(lib, name) orelse return null;return @ptrCast(sym);}/// dlopen libssl+libcrypto, run OpenSSL's own initialiser, and resolve every/// symbol BOTH halves use. false = there is no usable OpenSSL on this host/// (already logged, and anything opened is already closed again).fn openOpenSsl(ctx: *TlsContext, tag: []const u8) bool {const ssl_paths = [_][*:0]const u8{ "libssl.so.3", "libssl.so.1.1", "libssl.so" };const crypto_paths = [_][*:0]const u8{ "libcrypto.so.3", "libcrypto.so.1.1", "libcrypto.so" };for (ssl_paths) |path| {ctx.lib_ssl = c_dlfcn.dlopen(path, c_dlfcn.RTLD_NOW | c_dlfcn.RTLD_LOCAL);if (ctx.lib_ssl != null) break;}if (ctx.lib_ssl == null) {logFmt("{s}: TLS: failed to load libssl.so\n", .{tag});return false;}for (crypto_paths) |path| {ctx.lib_crypto = c_dlfcn.dlopen(path, c_dlfcn.RTLD_NOW | c_dlfcn.RTLD_LOCAL);if (ctx.lib_crypto != null) break;}if (ctx.lib_crypto == null) {logFmt("{s}: TLS: failed to load libcrypto.so\n", .{tag});_ = c_dlfcn.dlclose(ctx.lib_ssl);ctx.lib_ssl = null;return false;}// OPENSSL_init_ssl first (OpenSSL 1.1+), SSL_library_init as the fallback.if (loadSym(ctx.lib_ssl, OPENSSL_init_ssl_fn, "OPENSSL_init_ssl")) |init_fn| {_ = init_fn(0, null);} else if (loadSym(ctx.lib_ssl, SSL_library_init_fn, "SSL_library_init")) |lib_init| {_ = lib_init();if (loadSym(ctx.lib_ssl, SSL_load_error_strings_fn, "SSL_load_error_strings")) |load_err| {load_err();}}ctx.fn_ssl_ctx_new = loadSym(ctx.lib_ssl, SSL_CTX_new_fn, "SSL_CTX_new");ctx.fn_ssl_ctx_free = loadSym(ctx.lib_ssl, SSL_CTX_free_fn, "SSL_CTX_free");ctx.fn_ssl_new = loadSym(ctx.lib_ssl, SSL_new_fn, "SSL_new");ctx.fn_ssl_free = loadSym(ctx.lib_ssl, SSL_free_fn, "SSL_free");ctx.fn_ssl_set_fd = loadSym(ctx.lib_ssl, SSL_set_fd_fn, "SSL_set_fd");ctx.fn_ssl_read = loadSym(ctx.lib_ssl, SSL_read_fn, "SSL_read");ctx.fn_ssl_write = loadSym(ctx.lib_ssl, SSL_write_fn, "SSL_write");ctx.fn_ssl_shutdown = loadSym(ctx.lib_ssl, SSL_shutdown_fn, "SSL_shutdown");ctx.fn_ssl_get_error = loadSym(ctx.lib_ssl, SSL_get_error_fn, "SSL_get_error");return true;}/// Resolve OpenSSL, build an SSL_CTX, load cert+key, register the ALPN offer./// null = no TLS on this host or a bad cert/key; the caller falls back to plaintext.pub fn init(cert_path: []const u8, key_path: []const u8, opts: Options) ?TlsContext {var ctx = TlsContext{};if (!openOpenSsl(&ctx, opts.tag)) return null;const method_fn = loadSym(ctx.lib_ssl, TLS_server_method_fn, "TLS_server_method") orelse {logFmt("{s}: TLS: TLS_server_method not found\n", .{opts.tag});ctx.deinit();return null;};ctx.fn_ssl_ctx_use_cert = loadSym(ctx.lib_ssl, SSL_CTX_use_certificate_chain_file_fn, "SSL_CTX_use_certificate_chain_file");ctx.fn_ssl_ctx_use_key = loadSym(ctx.lib_ssl, SSL_CTX_use_PrivateKey_file_fn, "SSL_CTX_use_PrivateKey_file");ctx.fn_ssl_ctx_set_alpn = loadSym(ctx.lib_ssl, SSL_CTX_set_alpn_select_cb_fn, "SSL_CTX_set_alpn_select_cb");ctx.fn_ssl_accept = loadSym(ctx.lib_ssl, SSL_accept_fn, "SSL_accept");if (ctx.fn_ssl_ctx_new == null or ctx.fn_ssl_new == null orctx.fn_ssl_set_fd == null or ctx.fn_ssl_accept == null orctx.fn_ssl_read == null or ctx.fn_ssl_write == null){logFmt("{s}: TLS: missing required SSL symbols\n", .{opts.tag});ctx.deinit();return null;}const method = method_fn();ctx.ssl_ctx = ctx.fn_ssl_ctx_new.?(method);if (ctx.ssl_ctx == null) {logFmt("{s}: TLS: SSL_CTX_new failed\n", .{opts.tag});ctx.deinit();return null;}if (opts.alpn.len > 0) {if (ctx.fn_ssl_ctx_set_alpn) |set_alpn| {var total: usize = 0;for (opts.alpn) |p| total += 1 + p.len;const wire = allocator.alloc(u8, total) catch {ctx.deinit();return null;};var w: usize = 0;for (opts.alpn) |p| {wire[w] = @intCast(p.len);w += 1;@memcpy(wire[w .. w + p.len], p);w += p.len;}const offer = allocator.create(AlpnOffer) catch {allocator.free(wire);ctx.deinit();return null;};offer.* = .{ .wire = wire };ctx.alpn = offer;set_alpn(ctx.ssl_ctx, &alpnSelect, offer);}}const cert_z = allocator.dupeZ(u8, cert_path) catch {ctx.deinit();return null;};defer allocator.free(cert_z);const key_z = allocator.dupeZ(u8, key_path) catch {ctx.deinit();return null;};defer allocator.free(key_z);if (ctx.fn_ssl_ctx_use_cert) |use_cert| {if (use_cert(ctx.ssl_ctx, cert_z.ptr) != 1) {logFmt("{s}: TLS: failed to load certificate: {s}\n", .{ opts.tag, cert_path });ctx.deinit();return null;}}if (ctx.fn_ssl_ctx_use_key) |use_key| {if (use_key(ctx.ssl_ctx, key_z.ptr, SSL_FILETYPE_PEM) != 1) {logFmt("{s}: TLS: failed to load private key: {s}\n", .{ opts.tag, key_path });ctx.deinit();return null;}}logFmt("{s}: TLS initialized\n", .{opts.tag});return ctx;}/// THE CLIENT HALF (mission 135, restored in 257). An SSL_CTX that VERIFIES:/// the system trust store is loaded, `opts.ca_file` adds an extra anchor for a/// self-signed fixture, and `connect` checks the hostname as well as the chain./// null = no usable OpenSSL, or a caFile that would not load — never a context/// that trusts less than asked, because "https that silently stopped verifying"/// is the one failure a caller cannot see.////// An SSL_CTX is thread-safe and expensive (it reads the trust store), so the/// callers keep one per distinct caFile and share it across worker threads.pub fn initClient(opts: Options) ?TlsContext {var ctx = TlsContext{ .is_client = true, .verify = opts.verify };if (!openOpenSsl(&ctx, opts.tag)) return null;const method_fn = loadSym(ctx.lib_ssl, TLS_client_method_fn, "TLS_client_method") orelse {logFmt("{s}: TLS: TLS_client_method not found\n", .{opts.tag});ctx.deinit();return null;};ctx.fn_ssl_connect = loadSym(ctx.lib_ssl, SSL_connect_fn, "SSL_connect");ctx.fn_ssl_ctrl = loadSym(ctx.lib_ssl, SSL_ctrl_fn, "SSL_ctrl");ctx.fn_ssl_set1_host = loadSym(ctx.lib_ssl, SSL_set1_host_fn, "SSL_set1_host");ctx.fn_ssl_get_verify_result = loadSym(ctx.lib_ssl, SSL_get_verify_result_fn, "SSL_get_verify_result");if (ctx.fn_ssl_ctx_new == null or ctx.fn_ssl_new == null orctx.fn_ssl_set_fd == null or ctx.fn_ssl_connect == null orctx.fn_ssl_read == null or ctx.fn_ssl_write == null){logFmt("{s}: TLS: missing required client SSL symbols\n", .{opts.tag});ctx.deinit();return null;}// Hostname checking is not optional when verification is on. Without// SSL_set1_host (OpenSSL < 1.1.0) a valid certificate for ANY host would// pass, which is not verification — so refuse rather than pretend.if (opts.verify and ctx.fn_ssl_set1_host == null) {logFmt("{s}: TLS: SSL_set1_host not found — this OpenSSL cannot check hostnames\n", .{opts.tag});ctx.deinit();return null;}ctx.ssl_ctx = ctx.fn_ssl_ctx_new.?(method_fn());if (ctx.ssl_ctx == null) {logFmt("{s}: TLS: SSL_CTX_new failed\n", .{opts.tag});ctx.deinit();return null;}if (loadSym(ctx.lib_ssl, SSL_CTX_set_default_verify_paths_fn, "SSL_CTX_set_default_verify_paths")) |defaults| {if (defaults(ctx.ssl_ctx) != 1 and opts.verify and opts.ca_file == null) {logFmt("{s}: TLS: the system trust store could not be loaded\n", .{opts.tag});ctx.deinit();return null;}}if (opts.ca_file) |ca| {const load_verify = loadSym(ctx.lib_ssl, SSL_CTX_load_verify_locations_fn, "SSL_CTX_load_verify_locations") orelse {logFmt("{s}: TLS: SSL_CTX_load_verify_locations not found\n", .{opts.tag});ctx.deinit();return null;};const ca_z = allocator.dupeZ(u8, ca) catch {ctx.deinit();return null;};defer allocator.free(ca_z);if (load_verify(ctx.ssl_ctx, ca_z.ptr, null) != 1) {logFmt("{s}: TLS: caFile could not be loaded: {s}\n", .{ opts.tag, ca });ctx.deinit();return null;}}if (loadSym(ctx.lib_ssl, SSL_CTX_set_verify_fn, "SSL_CTX_set_verify")) |set_verify| {set_verify(ctx.ssl_ctx, if (opts.verify) SSL_VERIFY_PEER else SSL_VERIFY_NONE, null);} else if (opts.verify) {logFmt("{s}: TLS: SSL_CTX_set_verify not found\n", .{opts.tag});ctx.deinit();return null;}logFmt("{s}: TLS client initialized (verify={s})\n", .{ opts.tag, if (opts.verify) "on" else "off" });return ctx;}/// Connect `fd` — already a live TCP socket, and for STARTTLS one that has/// already spoken plaintext — as a TLS CLIENT to `host`. SNI and the hostname/// to check are both `host`, which is why the caller passes the name and not/// just the socket. null = the handshake or the verification failed (logged).pub fn connect(self: *const TlsContext, fd: i32, host: []const u8, tag: []const u8) ?*SSL {if (!self.is_client) {logFmt("{s}: TLS: connect() on a server context\n", .{tag});return null;}const ssl = self.fn_ssl_new.?(self.ssl_ctx) orelse return null;_ = self.fn_ssl_set_fd.?(ssl, fd);// A name is only a name — an IP literal is not a valid SNI value, and// OpenSSL rejects it, so the dupeZ is worth doing once either way.const host_z = allocator.dupeZ(u8, host) catch {self.fn_ssl_free.?(ssl);return null;};defer allocator.free(host_z);if (self.fn_ssl_ctrl) |ctrl| {_ = ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, @constCast(@ptrCast(host_z.ptr)));}if (self.verify) {if (self.fn_ssl_set1_host.?(ssl, host_z.ptr) != 1) {logFmt("{s}: TLS: could not set the hostname to verify ({s})\n", .{ tag, host });self.fn_ssl_free.?(ssl);return null;}}while (true) {const ret = self.fn_ssl_connect.?(ssl);if (ret == 1) break;const err = self.getError(ssl, ret);if (err == SSL_ERROR_WANT_READ or err == SSL_ERROR_WANT_WRITE) continue;logFmt("{s}: TLS client handshake failed ret={d} err={d} host={s}\n", .{ tag, ret, err, host });self.fn_ssl_free.?(ssl);return null;}// SSL_connect succeeding is NOT the verdict: with SSL_VERIFY_PEER the// handshake already fails on a bad chain, but reading the result is the// documented way to be sure, and it is the only signal when verify is off// for a host that nevertheless presented something valid.if (self.verify) {if (self.fn_ssl_get_verify_result) |verify_result| {const rc = verify_result(ssl);if (rc != X509_V_OK) {logFmt("{s}: TLS: certificate verification failed (code {d}) for {s}\n", .{ tag, rc, host });self.shutdownAndFree(ssl);return null;}}}return ssl;}/// A new SSL object bound to `fd`. No handshake — the caller decides where that/// happens (this is the per-protocol half).pub fn newSSL(self: *const TlsContext, fd: i32) ?*SSL {const ssl = self.fn_ssl_new.?(self.ssl_ctx);if (ssl == null) return null;_ = self.fn_ssl_set_fd.?(ssl, fd);return ssl;}pub fn getError(self: *const TlsContext, ssl: *SSL, ret: isize) c_int {const f = self.fn_ssl_get_error orelse return 0;return f(ssl, @intCast(ret));}/// Drive SSL_accept to completion. On a BLOCKING socket this returns on the/// first call; on a non-blocking one it spins on WANT_READ/WANT_WRITE, which is/// what http2's accept path relies on.pub fn handshake(self: *const TlsContext, ssl: *SSL, tag: []const u8) bool {while (true) {const ret = self.fn_ssl_accept.?(ssl);if (ret == 1) return true;const err = self.getError(ssl, ret);if (err == SSL_ERROR_WANT_READ or err == SSL_ERROR_WANT_WRITE) continue;logFmt("{s}: TLS handshake failed ret={d} err={d}\n", .{ tag, ret, err });return false;}}/// newSSL + handshake, freeing the SSL object if the handshake fails.pub fn wrapConnection(self: *const TlsContext, fd: i32, tag: []const u8) ?*SSL {const ssl = self.newSSL(fd) orelse return null;if (!self.handshake(ssl, tag)) {self.fn_ssl_free.?(ssl);return null;}return ssl;}/// Raw SSL_read. <= 0 means "ask getError" — WANT_READ/WANT_WRITE is "nothing/// more right now", anything else is a dead connection.pub fn read(self: *const TlsContext, ssl: *SSL, buf: []u8) isize {return self.fn_ssl_read.?(ssl, buf.ptr, @intCast(@min(buf.len, std.math.maxInt(c_int))));}/// Raw SSL_write (one record's worth at most).pub fn write(self: *const TlsContext, ssl: *SSL, data: []const u8) isize {return self.fn_ssl_write.?(ssl, data.ptr, @intCast(@min(data.len, std.math.maxInt(c_int))));}/// Write everything, giving up on the first non-retryable error. Returns the/// number of bytes actually written.pub fn writeAll(self: *const TlsContext, ssl: *SSL, data: []const u8) usize {var written: usize = 0;while (written < data.len) {const ret = self.write(ssl, data[written..]);if (ret <= 0) return written;written += @intCast(ret);}return written;}pub fn shutdownAndFree(self: *const TlsContext, ssl: *SSL) void {if (self.fn_ssl_shutdown) |sd| _ = sd(ssl);self.fn_ssl_free.?(ssl);}/// Free WITHOUT the close_notify — for a connection whose handshake never/// completed (there is no session to shut down) or whose socket is already/// gone (writing a TLS record into a recycled fd number is worse than/// skipping the notify: mission 128 measured it corrupting a live peer).pub fn freeSSL(self: *const TlsContext, ssl: *SSL) void {self.fn_ssl_free.?(ssl);}pub fn deinit(self: *TlsContext) void {if (self.ssl_ctx != null) {if (self.fn_ssl_ctx_free) |free_fn| {free_fn(self.ssl_ctx);}self.ssl_ctx = null;}if (self.alpn) |offer| {allocator.free(offer.wire);allocator.destroy(offer);self.alpn = null;}if (self.lib_ssl != null) {_ = c_dlfcn.dlclose(self.lib_ssl);self.lib_ssl = null;}if (self.lib_crypto != null) {_ = c_dlfcn.dlclose(self.lib_crypto);self.lib_crypto = null;}}};
Branches
- mainmain branch
Latest commits
- 5602ff41gitoria: Hybriel master 190aa11d (fc838894 GC correctness, #127 mountKids by reference, #126, #48) — tracker README flat; gates 200/0, 46/0, 44/0mre
- e85eaf01gitoria: 069 round 2 — hybriel 1a096ad3 not adopted (Markdown SSR still grows); browser gate waits for the server-side logout before restartmre
- 09ce4f3fgitoria: mission 069 re-vendor hybriel 8efba065 stopped (big SSR pages grow + slow down); lambda audit clean; old vendor keptmre
- 3dc43108antcolony#40: mission references point to the moved missionsmre
- 8d9450fdantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 205d5fe4gitoria: Hybriel master ff51cf46; ssh keys/tokens no double rows (session sync); gates follow #20mre
- 9b27cb26gitoria#21: installable app (manifest, service worker, offline start page), own iconmre
- 68dcb603deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- e2deed6dgitoria#20: "Add code" only on the Code page of an empty repository, no collapsiblemre
- 8bb97ffddeploy.sh: never send .git or .gitignore to Byrodinmre
- fd981932State of 2026-09-27; bin/ no longer tracked (Hybriel commit is in README)mre
- 4a2d7125initial commitmre