╭ changed ────╮ │src/serve.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────╮ │1 const std = @import("std"); │ │2 const xit = @import("xit"); │ │3 const rp = xit.repo; │ │4 -const hash = xit.hash; │ │5 -const ui = @import("./ui.zig"); │ │6 -const evt = @import("./event.zig"); │ │4 const web = @import("./web.zig"); │ │5 const serve_ssh_protocol = @import("./serve_ssh_protocol.zig");│ │6 const serve_ssh = @import("./serve_ssh.zig"); │ ╰──────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────╮ │ 7 +const serve_http = @import("./serve_http.zig");│ │ 8 │ │ 9 pub const Options = struct { │ │10 http_listen: []const u8 = "127.0.0.1:8080",│ ╰───────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │90 try err.print("serving HTTP on {s}, repo root {s}\n", .{ options.http_listen, repo_root_path }); │ │91 try err.flush(); │ │92 │ │95 - runHttpListener(repo_kind, any_repo_opts, io, allocator, repo_root_path, admin_repo_path, options.is_test, &http_server, &tasks, err); │ │93 + serve_http.runListener(repo_kind, any_repo_opts, io, allocator, repo_root_path, admin_repo_path, options.is_test, &http_server, &tasks, err);│ │94 │ │95 try err.print("serving SSH on {s}\n", .{options.ssh_listen}); │ │96 try err.flush(); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │100 .repo_root_path = repo_root_path, │ │101 .is_test = options.is_test, │ │102 }; │ │105 - runSshListener(io, allocator, &host_key, &ssh_session_handler, &ssh_server, &tasks, err); │ │103 + serve_ssh.runListener(io, allocator, &host_key, &ssh_session_handler, &ssh_server, &tasks, err);│ │104 │ │105 try err.print("serving web UI on http://{s}/\n", .{options.wui_listen}); │ │106 try err.flush(); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │111 try runnable.run(); │ │112 } else { │ │113 try tasks.await(io); │ │116 - } │ │117 -} │ │118 - │ │119 -fn runHttpListener( │ │120 - comptime repo_kind: rp.RepoKind, │ │121 - comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), │ │122 - io: std.Io, │ │123 - allocator: std.mem.Allocator, │ │124 - repo_root_path: []const u8, │ │125 - admin_repo_path: []const u8, │ │126 - is_test: bool, │ │127 - net_server: *std.Io.net.Server, │ │128 - tasks: *std.Io.Group, │ │129 - err: *std.Io.Writer, │ │130 -) void { │ │131 - const Listener = struct { │ │132 - io: std.Io, │ │133 - allocator: std.mem.Allocator, │ │134 - repo_root_path: []const u8, │ │135 - admin_repo_path: []const u8, │ │136 - is_test: bool, │ │137 - net_server: *std.Io.net.Server, │ │138 - tasks: *std.Io.Group, │ │139 - err: *std.Io.Writer, │ │140 - │ │141 - fn run(ctx: @This()) void { │ │142 - while (true) { │ │143 - const stream = ctx.net_server.accept(ctx.io) catch |accept_err| { │ │144 - if (accept_err == error.Canceled) return; │ │145 - logError(ctx.err, "accept failed: {s}\n", .{@errorName(accept_err)}); │ │146 - continue; │ │147 - }; │ │148 - │ │149 - const Connection = struct { │ │150 - io: std.Io, │ │151 - allocator: std.mem.Allocator, │ │152 - repo_root_path: []const u8, │ │153 - admin_repo_path: []const u8, │ │154 - is_test: bool, │ │155 - stream: std.Io.net.Stream, │ │156 - err: *std.Io.Writer, │ │157 - │ │158 - fn run(conn: @This()) void { │ │159 - defer conn.stream.close(conn.io); │ │160 - handleHttpConnection(repo_kind, any_repo_opts, conn.io, conn.allocator, conn.repo_root_path, conn.admin_repo_path, conn.is_test, conn.stream, conn.err) catch |request_err| {│ │161 - logError(conn.err, "connection failed: {s}\n", .{@errorName(request_err)}); │ │162 - }; │ │163 - } │ │164 - }; │ │165 - │ │166 - ctx.tasks.async(ctx.io, Connection.run, .{Connection{ │ │167 - .io = ctx.io, │ │168 - .allocator = ctx.allocator, │ │169 - .repo_root_path = ctx.repo_root_path, │ │170 - .admin_repo_path = ctx.admin_repo_path, │ │171 - .is_test = ctx.is_test, │ │172 - .stream = stream, │ │173 - .err = ctx.err, │ │174 - }}); │ │175 - } │ │176 - } │ │177 - }; │ │178 - │ │179 - tasks.async(io, Listener.run, .{Listener{ │ │180 - .io = io, │ │181 - .allocator = allocator, │ │182 - .repo_root_path = repo_root_path, │ │183 - .admin_repo_path = admin_repo_path, │ │184 - .is_test = is_test, │ │185 - .net_server = net_server, │ │186 - .tasks = tasks, │ │187 - .err = err, │ │188 - }}); │ │189 -} │ │190 - │ │191 -fn handleHttpConnection( │ │192 - comptime repo_kind: rp.RepoKind, │ │193 - comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), │ │194 - io: std.Io, │ │195 - allocator: std.mem.Allocator, │ │196 - repo_root_path: []const u8, │ │197 - admin_repo_path: []const u8, │ │198 - is_test: bool, │ │199 - stream: std.Io.net.Stream, │ │200 - err: *std.Io.Writer, │ │201 -) !void { │ │202 - var send_buffer = [_]u8{0} ** any_repo_opts.net_buffer_size; │ │203 - var recv_buffer = [_]u8{0} ** any_repo_opts.net_buffer_size; │ │204 - var conn_br = stream.reader(io, &recv_buffer); │ │205 - var conn_bw = stream.writer(io, &send_buffer); │ │206 - var http_server = std.http.Server.init(&conn_br.interface, &conn_bw.interface); │ │207 - │ │208 - while (http_server.reader.state == .ready) { │ │209 - var request = http_server.receiveHead() catch |receive_err| switch (receive_err) { │ │210 - error.HttpConnectionClosing => break, │ │211 - error.ReadFailed => break, │ │212 - else => |e| return e, │ │213 - }; │ │214 - │ │215 - handleHttpGitRequest(repo_kind, any_repo_opts, io, allocator, repo_root_path, admin_repo_path, is_test, &http_server, &request) catch |request_err| { │ │216 - try err.print("request failed: {s}\n", .{@errorName(request_err)}); │ │217 - try err.flush(); │ │218 - if (http_server.reader.state == .received_head) { │ │219 - http_server.reader.state = .ready; │ │220 - } │ │221 - try writeSimpleResponse(&http_server, 500, "Internal Server Error", "text/plain", @errorName(request_err)); │ │222 - }; │ │223 - try http_server.out.flush(); │ │224 - break; │ │225 - } │ │226 -} │ │227 - │ │228 -fn handleHttpGitRequest( │ │229 - comptime repo_kind: rp.RepoKind, │ │230 - comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), │ │231 - io: std.Io, │ │232 - allocator: std.mem.Allocator, │ │233 - repo_root_path: []const u8, │ │234 - admin_repo_path: []const u8, │ │235 - is_test: bool, │ │236 - http_server: *std.http.Server, │ │237 - request: *std.http.Server.Request, │ │238 -) !void { │ │239 - const uri = try std.Uri.parseAfterScheme("", request.head.target); │ │240 - const path = uri.path.percent_encoded; │ │241 - if (path.len == 0 or path[0] != '/') { │ │242 - try writeSimpleResponse(http_server, 400, "Bad Request", "text/plain", "bad path"); │ │243 - return; │ │244 - } │ │245 - │ │246 - const handler, const suffix = findRoute(path) orelse { │ │247 - if (http_server.reader.state == .received_head) { │ │248 - http_server.reader.state = .ready; │ │249 - } │ │250 - try writeSimpleResponse(http_server, 404, "Not Found", "text/plain", "not found"); │ │251 - return; │ │252 - }; │ │253 - │ │254 - const repo_rel_encoded = path[1 .. path.len - suffix.len]; │ │255 - const repo_rel = try decodeAndValidateRepoPath(allocator, repo_rel_encoded); │ │256 - defer allocator.free(repo_rel); │ │257 - │ │258 - const request_method = normalizeMethod(request.head.method); │ │259 - const content_type = try allocator.dupe(u8, findHeader(request, "content-type") orelse ""); │ │260 - defer allocator.free(content_type); │ │261 - const has_remote_user = findHeader(request, "authorization") != null; │ │262 - const protocol_version = protocolVersionFromHeader(findHeader(request, "git-protocol")); │ │263 - │ │264 - const body = if (request.head.method == .POST) blk: { │ │265 - const reader = try request.readerExpectContinue(&.{}); │ │266 - break :blk try reader.allocRemaining(allocator, .unlimited); │ │267 - } else try allocator.dupe(u8, ""); │ │268 - defer allocator.free(body); │ │269 - │ │270 - if (http_server.reader.state == .received_head) { │ │271 - http_server.reader.state = .ready; │ │272 - } │ │273 - │ │274 - const create_if_missing = isReceivePack(handler, suffix, uri.query); │ │275 - │ │276 - // derive the on-disk repo path. normally resolve <owner>/<repo> through the │ │277 - // event store and serve its event-id dir; in test mode serve the path directly │ │278 - const repo_path = if (is_test) blk: { │ │279 - const direct_path = try std.fs.path.resolve(allocator, &.{ repo_root_path, repo_rel }); │ │280 - if (!isSubPath(repo_root_path, direct_path)) { │ │281 - allocator.free(direct_path); │ │282 - try writeSimpleResponse(http_server, 403, "Forbidden", "text/plain", "forbidden"); │ │283 - return; │ │284 - } │ │285 - break :blk direct_path; │ │286 - } else blk: { │ │287 - const owner_repo = evt.parseOwnerRepoPath(repo_rel) orelse { │ │288 - try writeSimpleResponse(http_server, 400, "Bad Request", "text/plain", "repo path must be <owner>/<repo>"); │ │289 - return; │ │290 - }; │ │291 - const event_id_hex = (try evt.resolveOrCreateRepo(io, allocator, admin_repo_path, owner_repo.owner, owner_repo.name, create_if_missing)) orelse return error.RepoNotFound; │ │292 - break :blk try std.fs.path.join(allocator, &.{ repo_root_path, &event_id_hex }); │ │293 - }; │ │294 - defer allocator.free(repo_path); │ │295 - │ │296 - var body_reader = std.Io.Reader.fixed(body); │ │297 - const http_backend_options = xit.net_server_http_backend.Options{ │ │298 - .request_method = request_method, │ │299 - .handler = handler, │ │300 - .suffix = suffix, │ │301 - .query_string = if (uri.query) |q| q.percent_encoded else "", │ │302 - .content_type = content_type, │ │303 - .has_remote_user = has_remote_user, │ │304 - .protocol_version = protocol_version, │ │305 - }; │ │306 - │ │307 - try openRepoAndServe(repo_kind, any_repo_opts, io, allocator, repo_path, create_if_missing, HttpGitService{ │ │308 - .body_reader = &body_reader, │ │309 - .writer = http_server.out, │ │310 - .options = http_backend_options, │ │311 - }); │ │312 -} │ │313 - │ │314 -fn findRoute(path: []const u8) ?struct { xit.net_server_http_backend.HandlerKind, []const u8 } { │ │315 - for (&xit.net_server_http_backend.routes) |*route| { │ │316 - if (std.mem.endsWith(u8, path, route.suffix)) { │ │317 - return .{ route.handler, route.suffix }; │ │318 - } │ │319 - } │ │320 - return null; │ │321 -} │ │322 - │ │323 -fn runSshListener( │ │324 - io: std.Io, │ │325 - allocator: std.mem.Allocator, │ │326 - host_key: *const serve_ssh_protocol.HostKey, │ │327 - session_handler: *const serve_ssh.SessionHandler, │ │328 - net_server: *std.Io.net.Server, │ │329 - tasks: *std.Io.Group, │ │330 - err: *std.Io.Writer, │ │331 -) void { │ │332 - const Listener = struct { │ │333 - io: std.Io, │ │334 - allocator: std.mem.Allocator, │ │335 - host_key: *const serve_ssh_protocol.HostKey, │ │336 - session_handler: *const serve_ssh.SessionHandler, │ │337 - net_server: *std.Io.net.Server, │ │338 - tasks: *std.Io.Group, │ │339 - err: *std.Io.Writer, │ │340 - │ │341 - fn run(ctx: @This()) void { │ │342 - while (true) { │ │343 - const stream = ctx.net_server.accept(ctx.io) catch |accept_err| { │ │344 - if (accept_err == error.Canceled) return; │ │345 - logError(ctx.err, "ssh accept failed: {s}\n", .{@errorName(accept_err)}); │ │346 - continue; │ │347 - }; │ │348 - │ │349 - const Connection = struct { │ │350 - io: std.Io, │ │351 - allocator: std.mem.Allocator, │ │352 - host_key: *const serve_ssh_protocol.HostKey, │ │353 - session_handler: *const serve_ssh.SessionHandler, │ │354 - stream: std.Io.net.Stream, │ │355 - err: *std.Io.Writer, │ │356 - │ │357 - fn run(conn: @This()) void { │ │358 - defer conn.stream.close(conn.io); │ │359 - var recv_buf: [4096]u8 = undefined; │ │360 - var send_buf: [4096]u8 = undefined; │ │361 - var stream_reader = conn.stream.reader(conn.io, &recv_buf); │ │362 - var stream_writer = conn.stream.writer(conn.io, &send_buf); │ │363 - serve_ssh_protocol.handleConnection( │ │364 - conn.io, │ │365 - conn.allocator, │ │366 - &stream_reader.interface, │ │367 - &stream_writer.interface, │ │368 - conn.host_key, │ │369 - conn.session_handler, │ │370 - ) catch |session_err| { │ │371 - logError(conn.err, "ssh session failed: {s}\n", .{@errorName(session_err)}); │ │372 - }; │ │373 - } │ │374 - }; │ │375 - │ │376 - ctx.tasks.async(ctx.io, Connection.run, .{Connection{ │ │377 - .io = ctx.io, │ │378 - .allocator = ctx.allocator, │ │379 - .host_key = ctx.host_key, │ │380 - .session_handler = ctx.session_handler, │ │381 - .stream = stream, │ │382 - .err = ctx.err, │ │383 - }}); │ │384 - } │ │385 - } │ │386 - }; │ │387 - │ │388 - tasks.async(io, Listener.run, .{Listener{ │ │389 - .io = io, │ │390 - .allocator = allocator, │ │391 - .host_key = host_key, │ │392 - .session_handler = session_handler, │ │393 - .net_server = net_server, │ │394 - .tasks = tasks, │ │395 - .err = err, │ │396 - }}); │ │397 -} │ │398 - │ │399 -fn openRepoAndServe( │ │400 - comptime repo_kind: rp.RepoKind, │ │401 - comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), │ │402 - io: std.Io, │ │403 - allocator: std.mem.Allocator, │ │404 - repo_path: []const u8, │ │405 - create_if_missing: bool, │ │406 - service: anytype, │ │407 -) !void { │ │408 - // serve the existing on-disk repo at its event-id directory │ │409 - if (try serveIfExists(repo_kind, any_repo_opts, io, allocator, repo_path, service)) return; │ │410 - if (!create_if_missing) return error.RepoNotFound; │ │411 - │ │412 - // create the on-disk repo for the just-minted event and serve the push │ │413 - if (any_repo_opts.hash) |hash_kind| { │ │414 - var repo = try openRepo(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), io, allocator, repo_path, true); │ │415 - defer repo.deinit(io, allocator); │ │416 - try service.serve(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), &repo, io, allocator); │ │417 - } else { │ │418 - var repo = try openRepo(repo_kind, any_repo_opts.toRepoOpts(), io, allocator, repo_path, true); │ │419 - defer repo.deinit(io, allocator); │ │420 - try service.serve(repo_kind, any_repo_opts.toRepoOpts(), &repo, io, allocator); │ │114 } │ │115 } │ │116 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │424 -// serve an existing repo at `repo_path`, or return false if none is there │ │425 -fn serveIfExists( │ │426 - comptime repo_kind: rp.RepoKind, │ │427 - comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), │ │428 - io: std.Io, │ │429 - allocator: std.mem.Allocator, │ │430 - repo_path: []const u8, │ │431 - service: anytype, │ │432 -) !bool { │ │433 - // a bare open() creates the directory while probing for the repo, so only │ │434 - // attempt it when the path already exists │ │435 - std.Io.Dir.accessAbsolute(io, repo_path, .{}) catch return false; │ │436 - │ │437 - if (any_repo_opts.hash) |hash_kind| { │ │438 - var repo = rp.Repo(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind)).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) {│ │439 - error.RepoNotFound => return false, │ │440 - else => |e| return e, │ │441 - }; │ │442 - defer repo.deinit(io, allocator); │ │443 - try service.serve(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), &repo, io, allocator); │ │444 - } else { │ │445 - var any_repo = rp.AnyRepo(repo_kind, any_repo_opts).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) { │ │446 - error.RepoNotFound => return false, │ │447 - else => |e| return e, │ │448 - }; │ │449 - defer any_repo.deinit(io, allocator); │ │450 - switch (any_repo) { │ │451 - inline else => |*repo| try service.serve(repo.self_repo_kind, repo.self_repo_opts, repo, io, allocator), │ │452 - } │ │453 - } │ │454 - return true; │ │455 -} │ │456 - │ │457 -fn openRepo( │ │458 - comptime repo_kind: rp.RepoKind, │ │459 - comptime repo_opts: rp.RepoOpts(repo_kind), │ │460 - io: std.Io, │ │461 - allocator: std.mem.Allocator, │ │462 - repo_path: []const u8, │ │463 - create_if_missing: bool, │ │464 -) !rp.Repo(repo_kind, repo_opts) { │ │465 - return rp.Repo(repo_kind, repo_opts).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) { │ │466 - error.RepoNotFound => { │ │467 - if (!create_if_missing) return open_err; │ │468 - │ │469 - var repo = try rp.Repo(repo_kind, repo_opts).init(io, allocator, .{ .path = repo_path }); │ │470 - errdefer repo.deinit(io, allocator); │ │471 - try repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); │ │472 - try repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); │ │473 - return repo; │ │474 - }, │ │475 - else => |e| return e, │ │476 - }; │ │477 -} │ │478 - │ │479 -const HttpGitService = struct { │ │480 - body_reader: *std.Io.Reader, │ │481 - writer: *std.Io.Writer, │ │482 - options: xit.net_server_http_backend.Options, │ │483 - │ │484 - fn serve( │ │485 - self: @This(), │ │486 - comptime repo_kind: rp.RepoKind, │ │487 - comptime repo_opts: rp.RepoOpts(repo_kind), │ │488 - repo: *rp.Repo(repo_kind, repo_opts), │ │489 - io: std.Io, │ │490 - allocator: std.mem.Allocator, │ │491 - ) !void { │ │492 - try repo.httpBackend(io, allocator, self.body_reader, self.writer, .http, self.options); │ │493 - } │ │494 -}; │ │495 - │ │496 -fn isReceivePack( │ │497 - handler: xit.net_server_http_backend.HandlerKind, │ │498 - suffix: []const u8, │ │499 - query: ?std.Uri.Component, │ │500 -) bool { │ │501 - return switch (handler) { │ │502 - .run_service => std.mem.eql(u8, suffix, "/git-receive-pack"), │ │503 - .get_info_refs => if (query) |q| │ │504 - std.mem.startsWith(u8, q.percent_encoded, "service=git-receive-pack") │ │505 - else │ │506 - false, │ │507 - }; │ │508 -} │ │509 - │ │117 fn parseListenAddress(value: []const u8) !ListenAddress { │ │118 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidListenAddress; │ │119 if (colon == 0 or colon + 1 >= value.len) return error.InvalidListenAddress; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │120 const port = try std.fmt.parseInt(u16, value[colon + 1 ..], 10); │ │121 return .{ .host = value[0..colon], .port = port }; │ │122 } │ │516 - │ │517 -fn decodeAndValidateRepoPath(allocator: std.mem.Allocator, encoded: []const u8) ![]const u8 { │ │518 - if (encoded.len == 0) return error.InvalidRepoPath; │ │519 - │ │520 - const mutable = try allocator.dupe(u8, encoded); │ │521 - errdefer allocator.free(mutable); │ │522 - const decoded = std.Uri.percentDecodeInPlace(mutable); │ │123 │ │524 - var iter = std.fs.path.componentIterator(decoded); │ │525 - while (iter.next()) |component| { │ │526 - if (component.name.len == 0 or std.mem.eql(u8, component.name, ".") or std.mem.eql(u8, component.name, "..")) {│ │527 - return error.InvalidRepoPath; │ │528 - } │ │529 - } │ │530 - │ │531 - return try allocator.realloc(mutable, decoded.len); │ │532 -} │ │533 - │ │534 -fn isSubPath(parent: []const u8, child: []const u8) bool { │ │535 - if (std.mem.eql(u8, parent, std.fs.path.sep_str)) return std.fs.path.isAbsolute(child); │ │536 - if (!std.mem.startsWith(u8, child, parent)) return false; │ │537 - return child.len == parent.len or child[parent.len] == std.fs.path.sep; │ │538 -} │ │539 - │ │540 -fn normalizeMethod(method: std.http.Method) std.http.Method { │ │541 - return if (method == .HEAD) .GET else method; │ │542 -} │ │543 - │ │544 -fn protocolVersionFromHeader(header: ?[]const u8) xit.net_server_common.ProtocolVersion { │ │545 - const git_protocol = header orelse return .v0; │ │546 - var version: xit.net_server_common.ProtocolVersion = .v0; │ │547 - var iter = std.mem.splitScalar(u8, git_protocol, ':'); │ │548 - while (iter.next()) |entry| { │ │549 - const value = std.mem.trimStart(u8, entry, " "); │ │550 - if (std.mem.startsWith(u8, value, "version=")) { │ │551 - const v = value["version=".len..]; │ │552 - if (std.mem.eql(u8, v, "2")) { │ │553 - version = .v2; │ │554 - } else if (std.mem.eql(u8, v, "1") and version != .v2) { │ │555 - version = .v1; │ │556 - } │ │557 - } │ │558 - } │ │559 - return version; │ │560 -} │ │561 - │ │562 -fn findHeader(request: *std.http.Server.Request, name: []const u8) ?[]const u8 { │ │563 - var it = request.iterateHeaders(); │ │564 - while (it.next()) |header| { │ │565 - if (std.ascii.eqlIgnoreCase(header.name, name)) return header.value; │ │566 - } │ │567 - return null; │ │568 -} │ │569 - │ │570 -fn writeSimpleResponse( │ │571 - http_server: *std.http.Server, │ │572 - code: u16, │ │573 - message: []const u8, │ │574 - content_type: []const u8, │ │575 - body: []const u8, │ │576 -) !void { │ │577 - try http_server.out.print( │ │578 - "HTTP/1.1 {d} {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\n\r\n{s}", │ │579 - .{ code, message, content_type, body.len, body }, │ │580 - ); │ │581 -} │ │582 - │ │583 -fn logError(err: *std.Io.Writer, comptime fmt: []const u8, args: anytype) void { │ │584 - err.print(fmt, args) catch return; │ │585 - err.flush() catch {}; │ │586 -} │ │587 - │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯