╭ 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 evt = @import("./event.zig"); 7 const web = @import("./web.zig"); 8 const serve_ssh_protocol = @import("./serve_ssh_protocol.zig"); 9 const serve_ssh = @import("./serve_ssh.zig"); 10 11 pub const Options = struct { 12 http_listen: []const u8 = "127.0.0.1:8080", 13 ssh_listen: []const u8 = "127.0.0.1:8022", 14 wui_listen: []const u8 = "127.0.0.1:8000", 15 data_dir: []const u8 = ".", 16 // test mode, used by the networking tests. it serves repos directly by 17 // their on-disk path instead of resolving them as <owner>/<repo> through 18 // the event store, giving the plain git-server behavior those tests rely on. 19 is_test: bool = false, 20 }; 21 22 const ListenAddress = struct { 23 host: []const u8, 24 port: u16, 25 }; 26 27 pub fn run( 28 comptime repo_kind: rp.RepoKind, 29 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 30 io: std.Io, 31 allocator: std.mem.Allocator, 32 cwd_path: []const u8, 33 options: Options, 34 err: *std.Io.Writer, 35 runnable: anytype, 36 ) !void { 37 // create the data dir 38 39 const data_dir_path = try std.fs.path.resolve(allocator, &.{ cwd_path, options.data_dir }); 40 defer allocator.free(data_dir_path); 41 42 const data_dir = try std.Io.Dir.cwd().createDirPathOpen(io, data_dir_path, .{}); 43 defer data_dir.close(io); 44 45 // create the repos dir 46 47 const repo_root_path = try std.fs.path.resolve(allocator, &.{ data_dir_path, "repos" }); 48 defer allocator.free(repo_root_path); 49 50 try std.Io.Dir.cwd().createDirPath(io, repo_root_path); 51 52 // the admin repo path is where the user/repo metadata events live 53 54 const admin_repo_path = try std.fs.path.resolve(allocator, &.{ data_dir_path, "admin" }); 55 defer allocator.free(admin_repo_path); 56 57 // create http listener 58 59 const http_listen_address = try parseListenAddress(options.http_listen); 60 const http_address = try std.Io.net.IpAddress.parseIp4(http_listen_address.host, http_listen_address.port); 61 var http_server = try http_address.listen(io, .{ .reuse_address = true }); 62 defer http_server.deinit(io); 63 64 // create ssh listener 65 66 const ssh_listen_address = try parseListenAddress(options.ssh_listen); 67 const ssh_address = try std.Io.net.IpAddress.parseIp4(ssh_listen_address.host, ssh_listen_address.port); 68 var ssh_server = try ssh_address.listen(io, .{ .reuse_address = true }); 69 defer ssh_server.deinit(io); 70 71 // load or generate the SSH host key 72 const host_key = try serve_ssh_protocol.HostKey.loadOrGenerate(io, allocator, data_dir_path); 73 74 // disk-backed store for web login sessions 75 const session_store = try web.SessionStore.init(io, data_dir); 76 defer session_store.deinit(); 77 78 // create wui listener 79 80 const wui_listen_address = try parseListenAddress(options.wui_listen); 81 const wui_address = try std.Io.net.IpAddress.parseIp4(wui_listen_address.host, wui_listen_address.port); 82 var wui_server = try wui_address.listen(io, .{ .reuse_address = true }); 83 defer wui_server.deinit(io); 84 85 // start task group 86 87 var tasks: std.Io.Group = .init; 88 defer tasks.cancel(io); 89 90 // run listeners 91 92 try err.print("serving HTTP on {s}, repo root {s}\n", .{ options.http_listen, repo_root_path }); 93 try err.flush(); 94 95 runHttpListener(repo_kind, any_repo_opts, io, allocator, repo_root_path, admin_repo_path, options.is_test, &http_server, &tasks, err); 96 97 try err.print("serving SSH on {s}\n", .{options.ssh_listen}); 98 try err.flush(); 99 100 const ssh_session_handler = serve_ssh.SessionHandler{ 101 .admin_repo_path = admin_repo_path, 102 .repo_root_path = repo_root_path, 103 .is_test = options.is_test, 104 }; 105 runSshListener(io, allocator, &host_key, &ssh_session_handler, &ssh_server, &tasks, err); 106 107 try err.print("serving web UI on http://{s}/\n", .{options.wui_listen}); 108 try err.flush(); 109 110 web.run(io, allocator, &wui_server, &tasks, admin_repo_path, session_store, err); 111 112 if (@TypeOf(runnable) != void) { 113 try runnable.run(); 114 } else { 115 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); 421 } 422 } 423 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 510 fn parseListenAddress(value: []const u8) !ListenAddress { 511 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidListenAddress; 512 if (colon == 0 or colon + 1 >= value.len) return error.InvalidListenAddress; 513 const port = try std.fmt.parseInt(u16, value[colon + 1 ..], 10); 514 return .{ .host = value[0..colon], .port = port }; 515 } 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); 523 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