gitoriaLog in with ident

notes

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit3eff126d3eff126dnotes#3: installable app (manifest + own icon/favicon; notes' own sw.js kept)mre3eff126d/plugins/http/tls_common.zig

25.5 KB

  1. // Shared TLS layer — OpenSSL via dlopen, compiled into each HTTP plugin at build time.
  2. // Not a standalone plugin .so, but a Zig module imported by hl:http1 and hl:http2
  3. // (the same shape as http_common.zig and ws_common.zig).
  4. //
  5. // Mission 121 EXTRACTED this file. Both http1 and http2 carried their own copy of
  6. // the same dlopen dance — http1's since the plugin was written, http2's since
  7. // mission 061 added ALPN on top of it. D8's rule applies here exactly as it does to
  8. // WS framing: the transport-agnostic half is shared, the per-protocol half stays in
  9. // the plugin. What is per-protocol about TLS is TWO things and nothing else:
  10. // • the ALPN list a server offers ("h2" for http2, "http/1.1" for http1)
  11. // • WHERE the handshake happens (http2 does it on the accept thread before the
  12. // socket goes back to non-blocking; http1 does it in the I/O worker)
  13. // Both are parameters here, so there is one implementation of the dlopen, the
  14. // SSL_CTX, the cert/key load, the ALPN callback and the read/write/shutdown paths.
  15. //
  16. // No compile-time dependency on OpenSSL: the library is resolved at runtime and a
  17. // server whose host has no libssl falls back to plaintext (the caller decides).
  18. //
  19. // NOT hl:http3. Its `Quic` struct dlopens the same library but through a DIFFERENT
  20. // OpenSSL surface — `OSSL_QUIC_server_method`, `SSL_new_listener`/`SSL_listen`,
  21. // `SSL_read_ex`/`SSL_write_ex`, event-driven, no `SSL_accept` and no socket fd —
  22. // so it is not a copy of this and folding it in would be a rewrite, not an
  23. // extraction. It stays where it is.
  24. const std = @import("std");
  25. const c_dlfcn = @cImport({
  26. @cInclude("dlfcn.h");
  27. });
  28. const linux = std.os.linux;
  29. // The plugins that use this module both run per-request allocations across threads,
  30. // so the thread-safe production allocator is the only correct choice here too.
  31. const allocator = std.heap.smp_allocator;
  32. // Direct syscall for stderr — std.debug.print pulls in std.Progress, whose global
  33. // state is ABI-incompatible when a .so is loaded into a differently-built binary.
  34. fn logMsg(msg: []const u8) void {
  35. _ = linux.write(2, msg.ptr, msg.len);
  36. }
  37. fn logFmt(comptime fmt: []const u8, args: anytype) void {
  38. var buf: [512]u8 = undefined;
  39. const s = std.fmt.bufPrint(&buf, fmt, args) catch return;
  40. logMsg(s);
  41. }
  42. pub const SSL_CTX = opaque {};
  43. pub const SSL = opaque {};
  44. pub const SSL_METHOD = opaque {};
  45. // OpenSSL function pointer types
  46. const SSL_library_init_fn = *const fn () callconv(.c) c_int;
  47. const SSL_load_error_strings_fn = *const fn () callconv(.c) void;
  48. const TLS_server_method_fn = *const fn () callconv(.c) ?*const SSL_METHOD;
  49. const SSL_CTX_new_fn = *const fn (?*const SSL_METHOD) callconv(.c) ?*SSL_CTX;
  50. const SSL_CTX_free_fn = *const fn (?*SSL_CTX) callconv(.c) void;
  51. const SSL_CTX_use_certificate_chain_file_fn = *const fn (?*SSL_CTX, [*:0]const u8) callconv(.c) c_int;
  52. const SSL_CTX_use_PrivateKey_file_fn = *const fn (?*SSL_CTX, [*:0]const u8, c_int) callconv(.c) c_int;
  53. const SSL_CTX_set_alpn_select_cb_fn = *const fn (?*SSL_CTX, ?AlpnSelectCallback, ?*anyopaque) callconv(.c) void;
  54. const SSL_new_fn = *const fn (?*SSL_CTX) callconv(.c) ?*SSL;
  55. const SSL_free_fn = *const fn (?*SSL) callconv(.c) void;
  56. const SSL_set_fd_fn = *const fn (?*SSL, c_int) callconv(.c) c_int;
  57. const SSL_accept_fn = *const fn (?*SSL) callconv(.c) c_int;
  58. const SSL_read_fn = *const fn (?*SSL, [*]u8, c_int) callconv(.c) c_int;
  59. const SSL_write_fn = *const fn (?*SSL, [*]const u8, c_int) callconv(.c) c_int;
  60. const SSL_shutdown_fn = *const fn (?*SSL) callconv(.c) c_int;
  61. const SSL_get_error_fn = *const fn (?*SSL, c_int) callconv(.c) c_int;
  62. const OPENSSL_init_ssl_fn = *const fn (u64, ?*anyopaque) callconv(.c) c_int;
  63. // ── The CLIENT half's symbols (hl:fetch, hl:smtp) ────────────────────────────
  64. // RESTORED 2026-08-29 (mission 257): `initClient`/`connect` and everything they
  65. // resolve were lost in the 2026-08-20 recovery replay, which nothing noticed
  66. // because neither plugin that calls them was in `native/build.zig`.
  67. const TLS_client_method_fn = *const fn () callconv(.c) ?*const SSL_METHOD;
  68. const SSL_CTX_set_default_verify_paths_fn = *const fn (?*SSL_CTX) callconv(.c) c_int;
  69. const SSL_CTX_load_verify_locations_fn = *const fn (?*SSL_CTX, ?[*:0]const u8, ?[*:0]const u8) callconv(.c) c_int;
  70. const SSL_CTX_set_verify_fn = *const fn (?*SSL_CTX, c_int, ?*anyopaque) callconv(.c) void;
  71. const SSL_connect_fn = *const fn (?*SSL) callconv(.c) c_int;
  72. const SSL_get_verify_result_fn = *const fn (?*SSL) callconv(.c) c_long;
  73. /// SNI goes through the generic control entry point: there is no exported
  74. /// `SSL_set_tlsext_host_name` — it is a macro over `SSL_ctrl` in the headers.
  75. const SSL_ctrl_fn = *const fn (?*SSL, c_int, c_long, ?*anyopaque) callconv(.c) c_long;
  76. const SSL_set1_host_fn = *const fn (?*SSL, [*:0]const u8) callconv(.c) c_int;
  77. pub const SSL_VERIFY_NONE: c_int = 0;
  78. pub const SSL_VERIFY_PEER: c_int = 1;
  79. pub const X509_V_OK: c_long = 0;
  80. const SSL_CTRL_SET_TLSEXT_HOSTNAME: c_int = 55;
  81. const TLSEXT_NAMETYPE_host_name: c_long = 0;
  82. pub const SSL_FILETYPE_PEM: c_int = 1;
  83. pub const SSL_ERROR_WANT_READ: c_int = 2;
  84. pub const SSL_ERROR_WANT_WRITE: c_int = 3;
  85. pub const SSL_ERROR_ZERO_RETURN: c_int = 6;
  86. pub const SSL_TLSEXT_ERR_OK: c_int = 0;
  87. pub const SSL_TLSEXT_ERR_NOACK: c_int = 2;
  88. const AlpnSelectCallback = *const fn (?*SSL, [*c][*c]const u8, [*c]u8, [*c]const u8, c_uint, ?*anyopaque) callconv(.c) c_int;
  89. /// The server's ALPN offer in wire format (each protocol length-prefixed), heap
  90. /// allocated because OpenSSL keeps the callback's user_data pointer for the life of
  91. /// the SSL_CTX and a TlsContext is returned BY VALUE (a pointer to it would dangle).
  92. const AlpnOffer = struct {
  93. wire: []u8,
  94. };
  95. /// RFC 7301 selection with SERVER preference: the first protocol this server offers
  96. /// that the client also listed wins. No overlap → NOACK, which leaves the connection
  97. /// without a negotiated protocol rather than failing the handshake (what http2 did
  98. /// before the extraction, and what http1 wants anyway — a browser that omits ALPN
  99. /// still speaks HTTP/1.1 over the socket).
  100. 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 {
  101. _ = ssl;
  102. const offer: *const AlpnOffer = @ptrCast(@alignCast(user_data orelse return SSL_TLSEXT_ERR_NOACK));
  103. var si: usize = 0;
  104. while (si < offer.wire.len) {
  105. const slen = offer.wire[si];
  106. si += 1;
  107. if (si + slen > offer.wire.len) break;
  108. const ours = offer.wire[si .. si + slen];
  109. var ci: usize = 0;
  110. while (ci < inlen) {
  111. const clen = in[ci];
  112. ci += 1;
  113. if (ci + clen > inlen) break;
  114. if (clen == slen and std.mem.eql(u8, in[ci .. ci + clen], ours)) {
  115. out.* = in + ci;
  116. outlen.* = clen;
  117. return SSL_TLSEXT_ERR_OK;
  118. }
  119. ci += clen;
  120. }
  121. si += slen;
  122. }
  123. return SSL_TLSEXT_ERR_NOACK;
  124. }
  125. /// The per-protocol half, as data.
  126. pub const Options = struct {
  127. /// Protocols this server offers over ALPN, in preference order. Empty = no ALPN
  128. /// callback is registered at all (byte-identical to a server that never had one).
  129. alpn: []const []const u8 = &.{},
  130. /// Prefix for this plugin's log lines ("http1" / "http2" / "fetch" / "smtp").
  131. tag: []const u8 = "tls",
  132. // ── client-side only ──
  133. /// An EXTRA trust anchor on top of the system store, for a self-signed test
  134. /// fixture. null = the system store alone. Never a replacement: a caFile that
  135. /// fails to load fails the whole context rather than silently trusting less.
  136. ca_file: ?[]const u8 = null,
  137. /// Verify the peer's chain AND its hostname. hl:fetch never turns this off —
  138. /// it does not pass the field at all. hl:smtp exposes it, because a mail host
  139. /// with a self-signed certificate and no way to name its CA file is a real
  140. /// configuration, and a deliberate one.
  141. verify: bool = true,
  142. };
  143. pub const TlsContext = struct {
  144. ssl_ctx: ?*SSL_CTX = null,
  145. lib_ssl: ?*anyopaque = null,
  146. lib_crypto: ?*anyopaque = null,
  147. alpn: ?*AlpnOffer = null,
  148. fn_ssl_ctx_new: ?SSL_CTX_new_fn = null,
  149. fn_ssl_ctx_free: ?SSL_CTX_free_fn = null,
  150. fn_ssl_ctx_use_cert: ?SSL_CTX_use_certificate_chain_file_fn = null,
  151. fn_ssl_ctx_use_key: ?SSL_CTX_use_PrivateKey_file_fn = null,
  152. fn_ssl_ctx_set_alpn: ?SSL_CTX_set_alpn_select_cb_fn = null,
  153. fn_ssl_new: ?SSL_new_fn = null,
  154. fn_ssl_free: ?SSL_free_fn = null,
  155. fn_ssl_set_fd: ?SSL_set_fd_fn = null,
  156. fn_ssl_accept: ?SSL_accept_fn = null,
  157. fn_ssl_read: ?SSL_read_fn = null,
  158. fn_ssl_write: ?SSL_write_fn = null,
  159. fn_ssl_shutdown: ?SSL_shutdown_fn = null,
  160. fn_ssl_get_error: ?SSL_get_error_fn = null,
  161. // client half
  162. fn_ssl_connect: ?SSL_connect_fn = null,
  163. fn_ssl_ctrl: ?SSL_ctrl_fn = null,
  164. fn_ssl_set1_host: ?SSL_set1_host_fn = null,
  165. fn_ssl_get_verify_result: ?SSL_get_verify_result_fn = null,
  166. /// What this context was built for. `connect` refuses on a server context and
  167. /// `wrapConnection` on a client one, rather than calling a null function
  168. /// pointer somewhere deep inside OpenSSL.
  169. is_client: bool = false,
  170. /// Whether `connect` enforces the chain and the hostname (client only).
  171. verify: bool = true,
  172. fn loadSym(lib: ?*anyopaque, comptime T: type, name: [*:0]const u8) ?T {
  173. const sym = c_dlfcn.dlsym(lib, name) orelse return null;
  174. return @ptrCast(sym);
  175. }
  176. /// dlopen libssl+libcrypto, run OpenSSL's own initialiser, and resolve every
  177. /// symbol BOTH halves use. false = there is no usable OpenSSL on this host
  178. /// (already logged, and anything opened is already closed again).
  179. fn openOpenSsl(ctx: *TlsContext, tag: []const u8) bool {
  180. const ssl_paths = [_][*:0]const u8{ "libssl.so.3", "libssl.so.1.1", "libssl.so" };
  181. const crypto_paths = [_][*:0]const u8{ "libcrypto.so.3", "libcrypto.so.1.1", "libcrypto.so" };
  182. for (ssl_paths) |path| {
  183. ctx.lib_ssl = c_dlfcn.dlopen(path, c_dlfcn.RTLD_NOW | c_dlfcn.RTLD_LOCAL);
  184. if (ctx.lib_ssl != null) break;
  185. }
  186. if (ctx.lib_ssl == null) {
  187. logFmt("{s}: TLS: failed to load libssl.so\n", .{tag});
  188. return false;
  189. }
  190. for (crypto_paths) |path| {
  191. ctx.lib_crypto = c_dlfcn.dlopen(path, c_dlfcn.RTLD_NOW | c_dlfcn.RTLD_LOCAL);
  192. if (ctx.lib_crypto != null) break;
  193. }
  194. if (ctx.lib_crypto == null) {
  195. logFmt("{s}: TLS: failed to load libcrypto.so\n", .{tag});
  196. _ = c_dlfcn.dlclose(ctx.lib_ssl);
  197. ctx.lib_ssl = null;
  198. return false;
  199. }
  200. // OPENSSL_init_ssl first (OpenSSL 1.1+), SSL_library_init as the fallback.
  201. if (loadSym(ctx.lib_ssl, OPENSSL_init_ssl_fn, "OPENSSL_init_ssl")) |init_fn| {
  202. _ = init_fn(0, null);
  203. } else if (loadSym(ctx.lib_ssl, SSL_library_init_fn, "SSL_library_init")) |lib_init| {
  204. _ = lib_init();
  205. if (loadSym(ctx.lib_ssl, SSL_load_error_strings_fn, "SSL_load_error_strings")) |load_err| {
  206. load_err();
  207. }
  208. }
  209. ctx.fn_ssl_ctx_new = loadSym(ctx.lib_ssl, SSL_CTX_new_fn, "SSL_CTX_new");
  210. ctx.fn_ssl_ctx_free = loadSym(ctx.lib_ssl, SSL_CTX_free_fn, "SSL_CTX_free");
  211. ctx.fn_ssl_new = loadSym(ctx.lib_ssl, SSL_new_fn, "SSL_new");
  212. ctx.fn_ssl_free = loadSym(ctx.lib_ssl, SSL_free_fn, "SSL_free");
  213. ctx.fn_ssl_set_fd = loadSym(ctx.lib_ssl, SSL_set_fd_fn, "SSL_set_fd");
  214. ctx.fn_ssl_read = loadSym(ctx.lib_ssl, SSL_read_fn, "SSL_read");
  215. ctx.fn_ssl_write = loadSym(ctx.lib_ssl, SSL_write_fn, "SSL_write");
  216. ctx.fn_ssl_shutdown = loadSym(ctx.lib_ssl, SSL_shutdown_fn, "SSL_shutdown");
  217. ctx.fn_ssl_get_error = loadSym(ctx.lib_ssl, SSL_get_error_fn, "SSL_get_error");
  218. return true;
  219. }
  220. /// Resolve OpenSSL, build an SSL_CTX, load cert+key, register the ALPN offer.
  221. /// null = no TLS on this host or a bad cert/key; the caller falls back to plaintext.
  222. pub fn init(cert_path: []const u8, key_path: []const u8, opts: Options) ?TlsContext {
  223. var ctx = TlsContext{};
  224. if (!openOpenSsl(&ctx, opts.tag)) return null;
  225. const method_fn = loadSym(ctx.lib_ssl, TLS_server_method_fn, "TLS_server_method") orelse {
  226. logFmt("{s}: TLS: TLS_server_method not found\n", .{opts.tag});
  227. ctx.deinit();
  228. return null;
  229. };
  230. ctx.fn_ssl_ctx_use_cert = loadSym(ctx.lib_ssl, SSL_CTX_use_certificate_chain_file_fn, "SSL_CTX_use_certificate_chain_file");
  231. ctx.fn_ssl_ctx_use_key = loadSym(ctx.lib_ssl, SSL_CTX_use_PrivateKey_file_fn, "SSL_CTX_use_PrivateKey_file");
  232. ctx.fn_ssl_ctx_set_alpn = loadSym(ctx.lib_ssl, SSL_CTX_set_alpn_select_cb_fn, "SSL_CTX_set_alpn_select_cb");
  233. ctx.fn_ssl_accept = loadSym(ctx.lib_ssl, SSL_accept_fn, "SSL_accept");
  234. if (ctx.fn_ssl_ctx_new == null or ctx.fn_ssl_new == null or
  235. ctx.fn_ssl_set_fd == null or ctx.fn_ssl_accept == null or
  236. ctx.fn_ssl_read == null or ctx.fn_ssl_write == null)
  237. {
  238. logFmt("{s}: TLS: missing required SSL symbols\n", .{opts.tag});
  239. ctx.deinit();
  240. return null;
  241. }
  242. const method = method_fn();
  243. ctx.ssl_ctx = ctx.fn_ssl_ctx_new.?(method);
  244. if (ctx.ssl_ctx == null) {
  245. logFmt("{s}: TLS: SSL_CTX_new failed\n", .{opts.tag});
  246. ctx.deinit();
  247. return null;
  248. }
  249. if (opts.alpn.len > 0) {
  250. if (ctx.fn_ssl_ctx_set_alpn) |set_alpn| {
  251. var total: usize = 0;
  252. for (opts.alpn) |p| total += 1 + p.len;
  253. const wire = allocator.alloc(u8, total) catch {
  254. ctx.deinit();
  255. return null;
  256. };
  257. var w: usize = 0;
  258. for (opts.alpn) |p| {
  259. wire[w] = @intCast(p.len);
  260. w += 1;
  261. @memcpy(wire[w .. w + p.len], p);
  262. w += p.len;
  263. }
  264. const offer = allocator.create(AlpnOffer) catch {
  265. allocator.free(wire);
  266. ctx.deinit();
  267. return null;
  268. };
  269. offer.* = .{ .wire = wire };
  270. ctx.alpn = offer;
  271. set_alpn(ctx.ssl_ctx, &alpnSelect, offer);
  272. }
  273. }
  274. const cert_z = allocator.dupeZ(u8, cert_path) catch {
  275. ctx.deinit();
  276. return null;
  277. };
  278. defer allocator.free(cert_z);
  279. const key_z = allocator.dupeZ(u8, key_path) catch {
  280. ctx.deinit();
  281. return null;
  282. };
  283. defer allocator.free(key_z);
  284. if (ctx.fn_ssl_ctx_use_cert) |use_cert| {
  285. if (use_cert(ctx.ssl_ctx, cert_z.ptr) != 1) {
  286. logFmt("{s}: TLS: failed to load certificate: {s}\n", .{ opts.tag, cert_path });
  287. ctx.deinit();
  288. return null;
  289. }
  290. }
  291. if (ctx.fn_ssl_ctx_use_key) |use_key| {
  292. if (use_key(ctx.ssl_ctx, key_z.ptr, SSL_FILETYPE_PEM) != 1) {
  293. logFmt("{s}: TLS: failed to load private key: {s}\n", .{ opts.tag, key_path });
  294. ctx.deinit();
  295. return null;
  296. }
  297. }
  298. logFmt("{s}: TLS initialized\n", .{opts.tag});
  299. return ctx;
  300. }
  301. /// THE CLIENT HALF (mission 135, restored in 257). An SSL_CTX that VERIFIES:
  302. /// the system trust store is loaded, `opts.ca_file` adds an extra anchor for a
  303. /// self-signed fixture, and `connect` checks the hostname as well as the chain.
  304. /// null = no usable OpenSSL, or a caFile that would not load — never a context
  305. /// that trusts less than asked, because "https that silently stopped verifying"
  306. /// is the one failure a caller cannot see.
  307. ///
  308. /// An SSL_CTX is thread-safe and expensive (it reads the trust store), so the
  309. /// callers keep one per distinct caFile and share it across worker threads.
  310. pub fn initClient(opts: Options) ?TlsContext {
  311. var ctx = TlsContext{ .is_client = true, .verify = opts.verify };
  312. if (!openOpenSsl(&ctx, opts.tag)) return null;
  313. const method_fn = loadSym(ctx.lib_ssl, TLS_client_method_fn, "TLS_client_method") orelse {
  314. logFmt("{s}: TLS: TLS_client_method not found\n", .{opts.tag});
  315. ctx.deinit();
  316. return null;
  317. };
  318. ctx.fn_ssl_connect = loadSym(ctx.lib_ssl, SSL_connect_fn, "SSL_connect");
  319. ctx.fn_ssl_ctrl = loadSym(ctx.lib_ssl, SSL_ctrl_fn, "SSL_ctrl");
  320. ctx.fn_ssl_set1_host = loadSym(ctx.lib_ssl, SSL_set1_host_fn, "SSL_set1_host");
  321. ctx.fn_ssl_get_verify_result = loadSym(ctx.lib_ssl, SSL_get_verify_result_fn, "SSL_get_verify_result");
  322. if (ctx.fn_ssl_ctx_new == null or ctx.fn_ssl_new == null or
  323. ctx.fn_ssl_set_fd == null or ctx.fn_ssl_connect == null or
  324. ctx.fn_ssl_read == null or ctx.fn_ssl_write == null)
  325. {
  326. logFmt("{s}: TLS: missing required client SSL symbols\n", .{opts.tag});
  327. ctx.deinit();
  328. return null;
  329. }
  330. // Hostname checking is not optional when verification is on. Without
  331. // SSL_set1_host (OpenSSL < 1.1.0) a valid certificate for ANY host would
  332. // pass, which is not verification — so refuse rather than pretend.
  333. if (opts.verify and ctx.fn_ssl_set1_host == null) {
  334. logFmt("{s}: TLS: SSL_set1_host not found — this OpenSSL cannot check hostnames\n", .{opts.tag});
  335. ctx.deinit();
  336. return null;
  337. }
  338. ctx.ssl_ctx = ctx.fn_ssl_ctx_new.?(method_fn());
  339. if (ctx.ssl_ctx == null) {
  340. logFmt("{s}: TLS: SSL_CTX_new failed\n", .{opts.tag});
  341. ctx.deinit();
  342. return null;
  343. }
  344. if (loadSym(ctx.lib_ssl, SSL_CTX_set_default_verify_paths_fn, "SSL_CTX_set_default_verify_paths")) |defaults| {
  345. if (defaults(ctx.ssl_ctx) != 1 and opts.verify and opts.ca_file == null) {
  346. logFmt("{s}: TLS: the system trust store could not be loaded\n", .{opts.tag});
  347. ctx.deinit();
  348. return null;
  349. }
  350. }
  351. if (opts.ca_file) |ca| {
  352. const load_verify = loadSym(ctx.lib_ssl, SSL_CTX_load_verify_locations_fn, "SSL_CTX_load_verify_locations") orelse {
  353. logFmt("{s}: TLS: SSL_CTX_load_verify_locations not found\n", .{opts.tag});
  354. ctx.deinit();
  355. return null;
  356. };
  357. const ca_z = allocator.dupeZ(u8, ca) catch {
  358. ctx.deinit();
  359. return null;
  360. };
  361. defer allocator.free(ca_z);
  362. if (load_verify(ctx.ssl_ctx, ca_z.ptr, null) != 1) {
  363. logFmt("{s}: TLS: caFile could not be loaded: {s}\n", .{ opts.tag, ca });
  364. ctx.deinit();
  365. return null;
  366. }
  367. }
  368. if (loadSym(ctx.lib_ssl, SSL_CTX_set_verify_fn, "SSL_CTX_set_verify")) |set_verify| {
  369. set_verify(ctx.ssl_ctx, if (opts.verify) SSL_VERIFY_PEER else SSL_VERIFY_NONE, null);
  370. } else if (opts.verify) {
  371. logFmt("{s}: TLS: SSL_CTX_set_verify not found\n", .{opts.tag});
  372. ctx.deinit();
  373. return null;
  374. }
  375. logFmt("{s}: TLS client initialized (verify={s})\n", .{ opts.tag, if (opts.verify) "on" else "off" });
  376. return ctx;
  377. }
  378. /// Connect `fd` — already a live TCP socket, and for STARTTLS one that has
  379. /// already spoken plaintext — as a TLS CLIENT to `host`. SNI and the hostname
  380. /// to check are both `host`, which is why the caller passes the name and not
  381. /// just the socket. null = the handshake or the verification failed (logged).
  382. pub fn connect(self: *const TlsContext, fd: i32, host: []const u8, tag: []const u8) ?*SSL {
  383. if (!self.is_client) {
  384. logFmt("{s}: TLS: connect() on a server context\n", .{tag});
  385. return null;
  386. }
  387. const ssl = self.fn_ssl_new.?(self.ssl_ctx) orelse return null;
  388. _ = self.fn_ssl_set_fd.?(ssl, fd);
  389. // A name is only a name — an IP literal is not a valid SNI value, and
  390. // OpenSSL rejects it, so the dupeZ is worth doing once either way.
  391. const host_z = allocator.dupeZ(u8, host) catch {
  392. self.fn_ssl_free.?(ssl);
  393. return null;
  394. };
  395. defer allocator.free(host_z);
  396. if (self.fn_ssl_ctrl) |ctrl| {
  397. _ = ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, @constCast(@ptrCast(host_z.ptr)));
  398. }
  399. if (self.verify) {
  400. if (self.fn_ssl_set1_host.?(ssl, host_z.ptr) != 1) {
  401. logFmt("{s}: TLS: could not set the hostname to verify ({s})\n", .{ tag, host });
  402. self.fn_ssl_free.?(ssl);
  403. return null;
  404. }
  405. }
  406. while (true) {
  407. const ret = self.fn_ssl_connect.?(ssl);
  408. if (ret == 1) break;
  409. const err = self.getError(ssl, ret);
  410. if (err == SSL_ERROR_WANT_READ or err == SSL_ERROR_WANT_WRITE) continue;
  411. logFmt("{s}: TLS client handshake failed ret={d} err={d} host={s}\n", .{ tag, ret, err, host });
  412. self.fn_ssl_free.?(ssl);
  413. return null;
  414. }
  415. // SSL_connect succeeding is NOT the verdict: with SSL_VERIFY_PEER the
  416. // handshake already fails on a bad chain, but reading the result is the
  417. // documented way to be sure, and it is the only signal when verify is off
  418. // for a host that nevertheless presented something valid.
  419. if (self.verify) {
  420. if (self.fn_ssl_get_verify_result) |verify_result| {
  421. const rc = verify_result(ssl);
  422. if (rc != X509_V_OK) {
  423. logFmt("{s}: TLS: certificate verification failed (code {d}) for {s}\n", .{ tag, rc, host });
  424. self.shutdownAndFree(ssl);
  425. return null;
  426. }
  427. }
  428. }
  429. return ssl;
  430. }
  431. /// A new SSL object bound to `fd`. No handshake — the caller decides where that
  432. /// happens (this is the per-protocol half).
  433. pub fn newSSL(self: *const TlsContext, fd: i32) ?*SSL {
  434. const ssl = self.fn_ssl_new.?(self.ssl_ctx);
  435. if (ssl == null) return null;
  436. _ = self.fn_ssl_set_fd.?(ssl, fd);
  437. return ssl;
  438. }
  439. pub fn getError(self: *const TlsContext, ssl: *SSL, ret: isize) c_int {
  440. const f = self.fn_ssl_get_error orelse return 0;
  441. return f(ssl, @intCast(ret));
  442. }
  443. /// Drive SSL_accept to completion. On a BLOCKING socket this returns on the
  444. /// first call; on a non-blocking one it spins on WANT_READ/WANT_WRITE, which is
  445. /// what http2's accept path relies on.
  446. pub fn handshake(self: *const TlsContext, ssl: *SSL, tag: []const u8) bool {
  447. while (true) {
  448. const ret = self.fn_ssl_accept.?(ssl);
  449. if (ret == 1) return true;
  450. const err = self.getError(ssl, ret);
  451. if (err == SSL_ERROR_WANT_READ or err == SSL_ERROR_WANT_WRITE) continue;
  452. logFmt("{s}: TLS handshake failed ret={d} err={d}\n", .{ tag, ret, err });
  453. return false;
  454. }
  455. }
  456. /// newSSL + handshake, freeing the SSL object if the handshake fails.
  457. pub fn wrapConnection(self: *const TlsContext, fd: i32, tag: []const u8) ?*SSL {
  458. const ssl = self.newSSL(fd) orelse return null;
  459. if (!self.handshake(ssl, tag)) {
  460. self.fn_ssl_free.?(ssl);
  461. return null;
  462. }
  463. return ssl;
  464. }
  465. /// Raw SSL_read. <= 0 means "ask getError" — WANT_READ/WANT_WRITE is "nothing
  466. /// more right now", anything else is a dead connection.
  467. pub fn read(self: *const TlsContext, ssl: *SSL, buf: []u8) isize {
  468. return self.fn_ssl_read.?(ssl, buf.ptr, @intCast(@min(buf.len, std.math.maxInt(c_int))));
  469. }
  470. /// Raw SSL_write (one record's worth at most).
  471. pub fn write(self: *const TlsContext, ssl: *SSL, data: []const u8) isize {
  472. return self.fn_ssl_write.?(ssl, data.ptr, @intCast(@min(data.len, std.math.maxInt(c_int))));
  473. }
  474. /// Write everything, giving up on the first non-retryable error. Returns the
  475. /// number of bytes actually written.
  476. pub fn writeAll(self: *const TlsContext, ssl: *SSL, data: []const u8) usize {
  477. var written: usize = 0;
  478. while (written < data.len) {
  479. const ret = self.write(ssl, data[written..]);
  480. if (ret <= 0) return written;
  481. written += @intCast(ret);
  482. }
  483. return written;
  484. }
  485. pub fn shutdownAndFree(self: *const TlsContext, ssl: *SSL) void {
  486. if (self.fn_ssl_shutdown) |sd| _ = sd(ssl);
  487. self.fn_ssl_free.?(ssl);
  488. }
  489. /// Free WITHOUT the close_notify — for a connection whose handshake never
  490. /// completed (there is no session to shut down) or whose socket is already
  491. /// gone (writing a TLS record into a recycled fd number is worse than
  492. /// skipping the notify: mission 128 measured it corrupting a live peer).
  493. pub fn freeSSL(self: *const TlsContext, ssl: *SSL) void {
  494. self.fn_ssl_free.?(ssl);
  495. }
  496. pub fn deinit(self: *TlsContext) void {
  497. if (self.ssl_ctx != null) {
  498. if (self.fn_ssl_ctx_free) |free_fn| {
  499. free_fn(self.ssl_ctx);
  500. }
  501. self.ssl_ctx = null;
  502. }
  503. if (self.alpn) |offer| {
  504. allocator.free(offer.wire);
  505. allocator.destroy(offer);
  506. self.alpn = null;
  507. }
  508. if (self.lib_ssl != null) {
  509. _ = c_dlfcn.dlclose(self.lib_ssl);
  510. self.lib_ssl = null;
  511. }
  512. if (self.lib_crypto != null) {
  513. _ = c_dlfcn.dlclose(self.lib_crypto);
  514. self.lib_crypto = null;
  515. }
  516. }
  517. };

Branches

Latest commits

  • 3eff126dnotes#3: installable app (manifest + own icon/favicon; notes' own sw.js kept)mre
  • 9883c540deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • eee693b8deploy.sh: never send .git or .gitignore to Byrodinmre
  • c8904061State of 2026-09-27, before the move to gitoriamre