╭ serve.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const xit = @import("xit"); 3 const rp = xit.repo; 4 const hash = xit.hash; 5 6 pub const Options = struct { 7 http_listen: []const u8, 8 ssh_listen: ?[]const u8, 9 project_root: []const u8, 10 }; 11 12 const ListenAddress = struct { 13 host: []const u8, 14 port: u16, 15 }; 16 17 pub fn run( 18 comptime repo_kind: rp.RepoKind, 19 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 20 io: std.Io, 21 allocator: std.mem.Allocator, 22 cwd_path: []const u8, 23 options: Options, 24 err: *std.Io.Writer, 25 ) !void { 26 const project_root = try std.fs.path.resolve(allocator, &.{ cwd_path, options.project_root }); 27 defer allocator.free(project_root); 28 29 if (options.ssh_listen) |ssh_listen| { 30 const HttpContext = struct { 31 io: std.Io, 32 allocator: std.mem.Allocator, 33 project_root: []const u8, 34 http_listen: []const u8, 35 err: *std.Io.Writer, 36 37 fn run(ctx: @This()) !void { 38 try runHttpListener(repo_kind, any_repo_opts, ctx.io, ctx.allocator, ctx.project_root, ctx.http_listen, ctx.err); 39 } 40 }; 41 42 const http_thread = try std.Thread.spawn(.{}, HttpContext.run, .{HttpContext{ 43 .io = io, 44 .allocator = allocator, 45 .project_root = project_root, 46 .http_listen = options.http_listen, 47 .err = err, 48 }}); 49 defer http_thread.join(); 50 51 try runSshListener(repo_kind, any_repo_opts, io, allocator, project_root, ssh_listen, err); 52 } else { 53 try runHttpListener(repo_kind, any_repo_opts, io, allocator, project_root, options.http_listen, err); 54 } 55 } 56 57 fn runHttpListener( 58 comptime repo_kind: rp.RepoKind, 59 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 60 io: std.Io, 61 allocator: std.mem.Allocator, 62 project_root: []const u8, 63 http_listen: []const u8, 64 err: *std.Io.Writer, 65 ) !void { 66 const listen_address = try parseListenAddress(http_listen); 67 const address = try std.Io.net.IpAddress.parseIp4(listen_address.host, listen_address.port); 68 var net_server = try address.listen(io, .{ .reuse_address = true }); 69 defer net_server.deinit(io); 70 71 try err.print("serving HTTP on {s}, project root {s}\n", .{ http_listen, project_root }); 72 try err.flush(); 73 74 var send_buffer = [_]u8{0} ** any_repo_opts.net_buffer_size; 75 var recv_buffer = [_]u8{0} ** any_repo_opts.net_buffer_size; 76 77 while (true) { 78 const stream = net_server.accept(io) catch |accept_err| { 79 try err.print("accept failed: {s}\n", .{@errorName(accept_err)}); 80 continue; 81 }; 82 defer stream.close(io); 83 84 var conn_br = stream.reader(io, &recv_buffer); 85 var conn_bw = stream.writer(io, &send_buffer); 86 var http_server = std.http.Server.init(&conn_br.interface, &conn_bw.interface); 87 88 while (http_server.reader.state == .ready) { 89 var request = http_server.receiveHead() catch |receive_err| switch (receive_err) { 90 error.HttpConnectionClosing => break, 91 error.ReadFailed => break, 92 else => |e| return e, 93 }; 94 95 handleRequest(repo_kind, any_repo_opts, io, allocator, project_root, &http_server, &request) catch |request_err| { 96 try err.print("request failed: {s}\n", .{@errorName(request_err)}); 97 try err.flush(); 98 if (http_server.reader.state == .received_head) { 99 http_server.reader.state = .ready; 100 } 101 try writeSimpleResponse(&http_server, 500, "Internal Server Error", "text/plain", @errorName(request_err)); 102 }; 103 try http_server.out.flush(); 104 break; 105 } 106 } 107 } 108 109 fn handleRequest( 110 comptime repo_kind: rp.RepoKind, 111 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 112 io: std.Io, 113 allocator: std.mem.Allocator, 114 project_root: []const u8, 115 http_server: *std.http.Server, 116 request: *std.http.Server.Request, 117 ) !void { 118 const uri = try std.Uri.parseAfterScheme("", request.head.target); 119 const path = uri.path.percent_encoded; 120 if (path.len == 0 or path[0] != '/') { 121 try writeSimpleResponse(http_server, 400, "Bad Request", "text/plain", "bad path"); 122 return; 123 } 124 125 const handler, const suffix = findRoute(path) orelse { 126 if (http_server.reader.state == .received_head) { 127 http_server.reader.state = .ready; 128 } 129 try writeSimpleResponse(http_server, 404, "Not Found", "text/plain", "not found"); 130 return; 131 }; 132 133 const repo_rel_encoded = path[1 .. path.len - suffix.len]; 134 const repo_rel = try decodeAndValidateRepoPath(allocator, repo_rel_encoded); 135 defer allocator.free(repo_rel); 136 137 const repo_path = try std.fs.path.resolve(allocator, &.{ project_root, repo_rel }); 138 defer allocator.free(repo_path); 139 140 if (!isSubPath(project_root, repo_path)) { 141 if (http_server.reader.state == .received_head) { 142 http_server.reader.state = .ready; 143 } 144 try writeSimpleResponse(http_server, 403, "Forbidden", "text/plain", "forbidden"); 145 return; 146 } 147 148 const request_method = normalizeMethod(request.head.method); 149 const content_type = try allocator.dupe(u8, findHeader(request, "content-type") orelse ""); 150 defer allocator.free(content_type); 151 const has_remote_user = findHeader(request, "authorization") != null; 152 const protocol_version = protocolVersionFromHeader(findHeader(request, "git-protocol")); 153 154 const body = if (request.head.method == .POST) blk: { 155 const reader = try request.readerExpectContinue(&.{}); 156 break :blk try reader.allocRemaining(allocator, .unlimited); 157 } else try allocator.dupe(u8, ""); 158 defer allocator.free(body); 159 160 if (http_server.reader.state == .received_head) { 161 http_server.reader.state = .ready; 162 } 163 164 var body_reader = std.Io.Reader.fixed(body); 165 const create_if_missing = isReceivePack(handler, suffix, uri.query); 166 const http_backend_options = xit.net_server_http_backend.Options{ 167 .request_method = request_method, 168 .handler = handler, 169 .suffix = suffix, 170 .query_string = if (uri.query) |q| q.percent_encoded else "", 171 .content_type = content_type, 172 .has_remote_user = has_remote_user, 173 .protocol_version = protocol_version, 174 }; 175 176 if (any_repo_opts.hash) |hash_kind| { 177 var repo = try openRepo(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), io, allocator, repo_path, create_if_missing); 178 defer repo.deinit(io, allocator); 179 try repo.httpBackend(io, allocator, &body_reader, http_server.out, .http, http_backend_options); 180 } else { 181 var any_repo = rp.AnyRepo(repo_kind, any_repo_opts).open(io, allocator, .{ .path = repo_path }) catch |err| switch (err) { 182 error.RepoNotFound => { 183 if (!create_if_missing) return err; 184 185 var repo = try openRepo(repo_kind, any_repo_opts.toRepoOpts(), io, allocator, repo_path, true); 186 defer repo.deinit(io, allocator); 187 188 try repo.httpBackend(io, allocator, &body_reader, http_server.out, .http, http_backend_options); 189 return; 190 }, 191 else => |e| return e, 192 }; 193 defer any_repo.deinit(io, allocator); 194 195 switch (any_repo) { 196 inline else => |*repo| { 197 try repo.httpBackend(io, allocator, &body_reader, http_server.out, .http, http_backend_options); 198 }, 199 } 200 } 201 } 202 203 fn findRoute(path: []const u8) ?struct { xit.net_server_http_backend.HandlerKind, []const u8 } { 204 for (&xit.net_server_http_backend.routes) |*route| { 205 if (std.mem.endsWith(u8, path, route.suffix)) { 206 return .{ route.handler, route.suffix }; 207 } 208 } 209 return null; 210 } 211 212 const SshService = enum { 213 upload_pack, 214 receive_pack, 215 }; 216 217 const SshRequest = struct { 218 service: SshService, 219 protocol_version: xit.net_server_common.ProtocolVersion, 220 repo: []const u8, 221 222 fn deinit(self: SshRequest, allocator: std.mem.Allocator) void { 223 allocator.free(self.repo); 224 } 225 }; 226 227 fn runSshListener( 228 comptime repo_kind: rp.RepoKind, 229 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 230 io: std.Io, 231 allocator: std.mem.Allocator, 232 project_root: []const u8, 233 ssh_listen: []const u8, 234 err: *std.Io.Writer, 235 ) !void { 236 const listen_address = try parseListenAddress(ssh_listen); 237 const address = try std.Io.net.IpAddress.parseIp4(listen_address.host, listen_address.port); 238 var net_server = try address.listen(io, .{ .reuse_address = true }); 239 defer net_server.deinit(io); 240 241 try err.print("serving SSH helper connections on {s}, project root {s}\n", .{ ssh_listen, project_root }); 242 try err.flush(); 243 244 while (true) { 245 const stream = net_server.accept(io) catch |accept_err| { 246 try err.print("ssh accept failed: {s}\n", .{@errorName(accept_err)}); 247 continue; 248 }; 249 defer stream.close(io); 250 251 handleSshConnection(repo_kind, any_repo_opts, io, allocator, project_root, stream) catch |request_err| { 252 try err.print("ssh request failed: {s}\n", .{@errorName(request_err)}); 253 try err.flush(); 254 }; 255 } 256 } 257 258 fn handleSshConnection( 259 comptime repo_kind: rp.RepoKind, 260 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 261 io: std.Io, 262 allocator: std.mem.Allocator, 263 project_root: []const u8, 264 stream: std.Io.net.Stream, 265 ) !void { 266 var send_buffer = [_]u8{0} ** any_repo_opts.net_buffer_size; 267 var recv_buffer = [_]u8{0} ** any_repo_opts.net_buffer_size; 268 var reader = stream.reader(io, &recv_buffer); 269 var writer = stream.writer(io, &send_buffer); 270 271 const request = try readSshRequest(allocator, &reader.interface); 272 defer request.deinit(allocator); 273 274 const repo_path = try resolveSshRepoPath(allocator, project_root, request.repo); 275 defer allocator.free(repo_path); 276 277 if (!isSubPath(project_root, repo_path)) return error.Forbidden; 278 279 if (any_repo_opts.hash) |hash_kind| { 280 var repo = try openRepo(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), io, allocator, repo_path, request.service == .receive_pack); 281 defer repo.deinit(io, allocator); 282 try runSshService(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), &repo, io, allocator, &reader.interface, &writer.interface, request); 283 } else { 284 var any_repo = rp.AnyRepo(repo_kind, any_repo_opts).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) { 285 error.RepoNotFound => { 286 if (request.service != .receive_pack) return open_err; 287 288 var repo = try openRepo(repo_kind, any_repo_opts.toRepoOpts(), io, allocator, repo_path, true); 289 defer repo.deinit(io, allocator); 290 try runSshService(repo_kind, any_repo_opts.toRepoOpts(), &repo, io, allocator, &reader.interface, &writer.interface, request); 291 return; 292 }, 293 else => |e| return e, 294 }; 295 defer any_repo.deinit(io, allocator); 296 switch (any_repo) { 297 inline else => |*repo| { 298 try runSshService(repo.self_repo_kind, repo.self_repo_opts, repo, io, allocator, &reader.interface, &writer.interface, request); 299 }, 300 } 301 } 302 } 303 304 fn runSshService( 305 comptime repo_kind: rp.RepoKind, 306 comptime repo_opts: rp.RepoOpts(repo_kind), 307 repo: *rp.Repo(repo_kind, repo_opts), 308 io: std.Io, 309 allocator: std.mem.Allocator, 310 reader: *std.Io.Reader, 311 writer: *std.Io.Writer, 312 request: SshRequest, 313 ) !void { 314 switch (request.service) { 315 .upload_pack => try repo.uploadPack(io, allocator, reader, writer, .{ 316 .protocol_version = request.protocol_version, 317 }), 318 .receive_pack => try repo.receivePack(io, allocator, reader, writer, .{ 319 .protocol_version = request.protocol_version, 320 }), 321 } 322 } 323 324 fn openRepo( 325 comptime repo_kind: rp.RepoKind, 326 comptime repo_opts: rp.RepoOpts(repo_kind), 327 io: std.Io, 328 allocator: std.mem.Allocator, 329 repo_path: []const u8, 330 create_if_missing: bool, 331 ) !rp.Repo(repo_kind, repo_opts) { 332 return rp.Repo(repo_kind, repo_opts).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) { 333 error.RepoNotFound => { 334 if (!create_if_missing) return open_err; 335 336 var repo = try rp.Repo(repo_kind, repo_opts).init(io, allocator, .{ .path = repo_path }); 337 errdefer repo.deinit(io, allocator); 338 try repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); 339 try repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); 340 return repo; 341 }, 342 else => |e| return e, 343 }; 344 } 345 346 fn isReceivePack( 347 handler: xit.net_server_http_backend.HandlerKind, 348 suffix: []const u8, 349 query: ?std.Uri.Component, 350 ) bool { 351 return switch (handler) { 352 .run_service => std.mem.eql(u8, suffix, "/git-receive-pack"), 353 .get_info_refs => if (query) |q| 354 std.mem.startsWith(u8, q.percent_encoded, "service=git-receive-pack") 355 else 356 false, 357 }; 358 } 359 360 fn readSshRequest(allocator: std.mem.Allocator, reader: *std.Io.Reader) !SshRequest { 361 const magic = try readSshPreludeLine(reader); 362 if (!std.mem.eql(u8, magic, "haxy-ssh-helper-v1")) return error.InvalidSshMagic; 363 364 const service_line = try readSshPreludeLine(reader); 365 const service = parseSshService(stripPrefix(service_line, "service=") orelse return error.InvalidSshServiceLine) orelse return error.InvalidSshService; 366 367 const protocol_line = try readSshPreludeLine(reader); 368 const protocol_version = std.meta.stringToEnum( 369 xit.net_server_common.ProtocolVersion, 370 stripPrefix(protocol_line, "protocol=") orelse return error.InvalidSshProtocolLine, 371 ) orelse return error.InvalidSshProtocol; 372 373 const repo_length_line = try readSshPreludeLine(reader); 374 const repo_len = try std.fmt.parseInt(usize, stripPrefix(repo_length_line, "repo-length=") orelse return error.InvalidSshRepoLengthLine, 10); 375 376 const empty_line = try readSshPreludeLine(reader); 377 if (empty_line.len != 0) return error.InvalidSshPreludeTerminator; 378 379 const repo = try reader.readAlloc(allocator, repo_len); 380 errdefer allocator.free(repo); 381 382 return .{ 383 .service = service, 384 .protocol_version = protocol_version, 385 .repo = repo, 386 }; 387 } 388 389 fn readSshPreludeLine(reader: *std.Io.Reader) ![]const u8 { 390 return (try reader.takeDelimiter('\n')) orelse error.InvalidSshHelperRequest; 391 } 392 393 fn parseSshService(value: []const u8) ?SshService { 394 if (std.mem.eql(u8, value, "upload-pack")) return .upload_pack; 395 if (std.mem.eql(u8, value, "receive-pack")) return .receive_pack; 396 return null; 397 } 398 399 fn stripPrefix(value: []const u8, prefix: []const u8) ?[]const u8 { 400 if (!std.mem.startsWith(u8, value, prefix)) return null; 401 return value[prefix.len..]; 402 } 403 404 fn resolveSshRepoPath( 405 allocator: std.mem.Allocator, 406 project_root: []const u8, 407 repo: []const u8, 408 ) ![]const u8 { 409 if (std.fs.path.isAbsolute(repo)) { 410 return try std.fs.path.resolve(allocator, &.{repo}); 411 } 412 return try std.fs.path.resolve(allocator, &.{ project_root, repo }); 413 } 414 415 fn parseListenAddress(value: []const u8) !ListenAddress { 416 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidListenAddress; 417 if (colon == 0 or colon + 1 >= value.len) return error.InvalidListenAddress; 418 const port = try std.fmt.parseInt(u16, value[colon + 1 ..], 10); 419 return .{ .host = value[0..colon], .port = port }; 420 } 421 422 fn decodeAndValidateRepoPath(allocator: std.mem.Allocator, encoded: []const u8) ![]const u8 { 423 if (encoded.len == 0) return error.InvalidRepoPath; 424 425 const mutable = try allocator.dupe(u8, encoded); 426 errdefer allocator.free(mutable); 427 const decoded = std.Uri.percentDecodeInPlace(mutable); 428 429 var iter = std.mem.splitScalar(u8, decoded, '/'); 430 while (iter.next()) |segment| { 431 if (segment.len == 0 or std.mem.eql(u8, segment, ".") or std.mem.eql(u8, segment, "..")) { 432 return error.InvalidRepoPath; 433 } 434 } 435 436 return try allocator.realloc(mutable, decoded.len); 437 } 438 439 fn isSubPath(parent: []const u8, child: []const u8) bool { 440 if (std.mem.eql(u8, parent, std.fs.path.sep_str)) return std.fs.path.isAbsolute(child); 441 if (!std.mem.startsWith(u8, child, parent)) return false; 442 return child.len == parent.len or child[parent.len] == std.fs.path.sep; 443 } 444 445 fn normalizeMethod(method: std.http.Method) std.http.Method { 446 return if (method == .HEAD) .GET else method; 447 } 448 449 fn protocolVersionFromHeader(header: ?[]const u8) xit.net_server_common.ProtocolVersion { 450 const git_protocol = header orelse return .v0; 451 var version: xit.net_server_common.ProtocolVersion = .v0; 452 var iter = std.mem.splitScalar(u8, git_protocol, ':'); 453 while (iter.next()) |entry| { 454 const value = std.mem.trimStart(u8, entry, " "); 455 if (std.mem.startsWith(u8, value, "version=")) { 456 const v = value["version=".len..]; 457 if (std.mem.eql(u8, v, "2")) { 458 version = .v2; 459 } else if (std.mem.eql(u8, v, "1") and version != .v2) { 460 version = .v1; 461 } 462 } 463 } 464 return version; 465 } 466 467 fn findHeader(request: *std.http.Server.Request, name: []const u8) ?[]const u8 { 468 var it = request.iterateHeaders(); 469 while (it.next()) |header| { 470 if (std.ascii.eqlIgnoreCase(header.name, name)) return header.value; 471 } 472 return null; 473 } 474 475 fn writeSimpleResponse( 476 http_server: *std.http.Server, 477 code: u16, 478 message: []const u8, 479 content_type: []const u8, 480 body: []const u8, 481 ) !void { 482 try http_server.out.print( 483 "HTTP/1.1 {d} {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\n\r\n{s}", 484 .{ code, message, content_type, body.len, body }, 485 ); 486 } 487