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