╭ 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_tui = @import("./serve_ssh_tui.zig"); 8 9 pub const Options = struct { 10 http_listen: []const u8 = "127.0.0.1:8080", 11 ssh_listen: []const u8 = "127.0.0.1:8081", 12 tui_listen: []const u8 = "127.0.0.1:8082", 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 // web.zig opens it per request to render the server-side TUI 49 50 const admin_repo_path = try std.fs.path.resolve(allocator, &.{ data_dir_path, "admin" }); 51 defer allocator.free(admin_repo_path); 52 53 // create http listener 54 55 const http_listen_address = try parseListenAddress(options.http_listen); 56 const http_address = try std.Io.net.IpAddress.parseIp4(http_listen_address.host, http_listen_address.port); 57 var http_server = try http_address.listen(io, .{ .reuse_address = true }); 58 defer http_server.deinit(io); 59 60 // create ssh listener 61 62 const ssh_listen_address = try parseListenAddress(options.ssh_listen); 63 const ssh_address = try std.Io.net.IpAddress.parseIp4(ssh_listen_address.host, ssh_listen_address.port); 64 var ssh_server = try ssh_address.listen(io, .{ .reuse_address = true }); 65 defer ssh_server.deinit(io); 66 67 // create tui listener 68 69 const tui_listen_address = try parseListenAddress(options.tui_listen); 70 const tui_address = try std.Io.net.IpAddress.parseIp4(tui_listen_address.host, tui_listen_address.port); 71 var tui_server = try tui_address.listen(io, .{ .reuse_address = true }); 72 defer tui_server.deinit(io); 73 74 // create wui listener 75 76 const wui_listen_address = try parseListenAddress(options.wui_listen); 77 const wui_address = try std.Io.net.IpAddress.parseIp4(wui_listen_address.host, wui_listen_address.port); 78 var wui_server = try wui_address.listen(io, .{ .reuse_address = true }); 79 defer wui_server.deinit(io); 80 81 // start task group 82 83 var tasks: std.Io.Group = .init; 84 defer tasks.cancel(io); 85 86 // run listeners 87 88 try err.print("serving HTTP on {s}, repo root {s}\n", .{ options.http_listen, repo_root_path }); 89 try err.flush(); 90 91 runHttpListener(repo_kind, any_repo_opts, io, allocator, repo_root_path, &http_server, &tasks, err); 92 93 try err.print("serving SSH helper connections on {s}, repo root {s}\n", .{ options.ssh_listen, repo_root_path }); 94 try err.flush(); 95 96 runSshListener(repo_kind, any_repo_opts, io, allocator, repo_root_path, &ssh_server, &tasks, err); 97 98 try err.print("serving TUI sessions on {s}\n", .{options.tui_listen}); 99 try err.flush(); 100 101 runTuiListener(io, allocator, admin_repo_path, &tui_server, &tasks, err); 102 103 try err.print("serving web UI on http://{s}/\n", .{options.wui_listen}); 104 try err.flush(); 105 106 web.run(io, allocator, &wui_server, &tasks, admin_repo_path, err); 107 108 if (@TypeOf(runnable) != void) { 109 try runnable.run(); 110 } else { 111 try tasks.await(io); 112 } 113 } 114 115 fn runHttpListener( 116 comptime repo_kind: rp.RepoKind, 117 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 118 io: std.Io, 119 allocator: std.mem.Allocator, 120 repo_root_path: []const u8, 121 net_server: *std.Io.net.Server, 122 tasks: *std.Io.Group, 123 err: *std.Io.Writer, 124 ) void { 125 const Listener = struct { 126 io: std.Io, 127 allocator: std.mem.Allocator, 128 repo_root_path: []const u8, 129 net_server: *std.Io.net.Server, 130 tasks: *std.Io.Group, 131 err: *std.Io.Writer, 132 133 fn run(ctx: @This()) void { 134 while (true) { 135 const stream = ctx.net_server.accept(ctx.io) catch |accept_err| { 136 if (accept_err == error.Canceled) return; 137 logError(ctx.err, "accept failed: {s}\n", .{@errorName(accept_err)}); 138 continue; 139 }; 140 141 const Connection = struct { 142 io: std.Io, 143 allocator: std.mem.Allocator, 144 repo_root_path: []const u8, 145 stream: std.Io.net.Stream, 146 err: *std.Io.Writer, 147 148 fn run(conn: @This()) void { 149 defer conn.stream.close(conn.io); 150 handleHttpConnection(repo_kind, any_repo_opts, conn.io, conn.allocator, conn.repo_root_path, conn.stream, conn.err) catch |request_err| { 151 logError(conn.err, "connection failed: {s}\n", .{@errorName(request_err)}); 152 }; 153 } 154 }; 155 156 ctx.tasks.async(ctx.io, Connection.run, .{Connection{ 157 .io = ctx.io, 158 .allocator = ctx.allocator, 159 .repo_root_path = ctx.repo_root_path, 160 .stream = stream, 161 .err = ctx.err, 162 }}); 163 } 164 } 165 }; 166 167 tasks.async(io, Listener.run, .{Listener{ 168 .io = io, 169 .allocator = allocator, 170 .repo_root_path = repo_root_path, 171 .net_server = net_server, 172 .tasks = tasks, 173 .err = err, 174 }}); 175 } 176 177 fn handleHttpConnection( 178 comptime repo_kind: rp.RepoKind, 179 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 180 io: std.Io, 181 allocator: std.mem.Allocator, 182 repo_root_path: []const u8, 183 stream: std.Io.net.Stream, 184 err: *std.Io.Writer, 185 ) !void { 186 var send_buffer = [_]u8{0} ** any_repo_opts.net_buffer_size; 187 var recv_buffer = [_]u8{0} ** any_repo_opts.net_buffer_size; 188 var conn_br = stream.reader(io, &recv_buffer); 189 var conn_bw = stream.writer(io, &send_buffer); 190 var http_server = std.http.Server.init(&conn_br.interface, &conn_bw.interface); 191 192 while (http_server.reader.state == .ready) { 193 var request = http_server.receiveHead() catch |receive_err| switch (receive_err) { 194 error.HttpConnectionClosing => break, 195 error.ReadFailed => break, 196 else => |e| return e, 197 }; 198 199 handleHttpGitRequest(repo_kind, any_repo_opts, io, allocator, repo_root_path, &http_server, &request) catch |request_err| { 200 try err.print("request failed: {s}\n", .{@errorName(request_err)}); 201 try err.flush(); 202 if (http_server.reader.state == .received_head) { 203 http_server.reader.state = .ready; 204 } 205 try writeSimpleResponse(&http_server, 500, "Internal Server Error", "text/plain", @errorName(request_err)); 206 }; 207 try http_server.out.flush(); 208 break; 209 } 210 } 211 212 fn handleHttpGitRequest( 213 comptime repo_kind: rp.RepoKind, 214 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 215 io: std.Io, 216 allocator: std.mem.Allocator, 217 repo_root_path: []const u8, 218 http_server: *std.http.Server, 219 request: *std.http.Server.Request, 220 ) !void { 221 const uri = try std.Uri.parseAfterScheme("", request.head.target); 222 const path = uri.path.percent_encoded; 223 if (path.len == 0 or path[0] != '/') { 224 try writeSimpleResponse(http_server, 400, "Bad Request", "text/plain", "bad path"); 225 return; 226 } 227 228 const handler, const suffix = findRoute(path) orelse { 229 if (http_server.reader.state == .received_head) { 230 http_server.reader.state = .ready; 231 } 232 try writeSimpleResponse(http_server, 404, "Not Found", "text/plain", "not found"); 233 return; 234 }; 235 236 const repo_rel_encoded = path[1 .. path.len - suffix.len]; 237 const repo_rel = try decodeAndValidateRepoPath(allocator, repo_rel_encoded); 238 defer allocator.free(repo_rel); 239 240 const repo_path = try std.fs.path.resolve(allocator, &.{ repo_root_path, repo_rel }); 241 defer allocator.free(repo_path); 242 243 if (!isSubPath(repo_root_path, repo_path)) { 244 if (http_server.reader.state == .received_head) { 245 http_server.reader.state = .ready; 246 } 247 try writeSimpleResponse(http_server, 403, "Forbidden", "text/plain", "forbidden"); 248 return; 249 } 250 251 const request_method = normalizeMethod(request.head.method); 252 const content_type = try allocator.dupe(u8, findHeader(request, "content-type") orelse ""); 253 defer allocator.free(content_type); 254 const has_remote_user = findHeader(request, "authorization") != null; 255 const protocol_version = protocolVersionFromHeader(findHeader(request, "git-protocol")); 256 257 const body = if (request.head.method == .POST) blk: { 258 const reader = try request.readerExpectContinue(&.{}); 259 break :blk try reader.allocRemaining(allocator, .unlimited); 260 } else try allocator.dupe(u8, ""); 261 defer allocator.free(body); 262 263 if (http_server.reader.state == .received_head) { 264 http_server.reader.state = .ready; 265 } 266 267 var body_reader = std.Io.Reader.fixed(body); 268 const create_if_missing = isReceivePack(handler, suffix, uri.query); 269 const http_backend_options = xit.net_server_http_backend.Options{ 270 .request_method = request_method, 271 .handler = handler, 272 .suffix = suffix, 273 .query_string = if (uri.query) |q| q.percent_encoded else "", 274 .content_type = content_type, 275 .has_remote_user = has_remote_user, 276 .protocol_version = protocol_version, 277 }; 278 279 try openRepoAndServe(repo_kind, any_repo_opts, io, allocator, repo_path, create_if_missing, HttpGitService{ 280 .body_reader = &body_reader, 281 .writer = http_server.out, 282 .options = http_backend_options, 283 }); 284 } 285 286 fn findRoute(path: []const u8) ?struct { xit.net_server_http_backend.HandlerKind, []const u8 } { 287 for (&xit.net_server_http_backend.routes) |*route| { 288 if (std.mem.endsWith(u8, path, route.suffix)) { 289 return .{ route.handler, route.suffix }; 290 } 291 } 292 return null; 293 } 294 295 const SshService = enum { 296 upload_pack, 297 receive_pack, 298 }; 299 300 const SshRequest = struct { 301 service: SshService, 302 protocol_version: xit.net_server_common.ProtocolVersion, 303 repo: []const u8, 304 305 fn deinit(self: SshRequest, allocator: std.mem.Allocator) void { 306 allocator.free(self.repo); 307 } 308 }; 309 310 fn runSshListener( 311 comptime repo_kind: rp.RepoKind, 312 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 313 io: std.Io, 314 allocator: std.mem.Allocator, 315 repo_root_path: []const u8, 316 net_server: *std.Io.net.Server, 317 tasks: *std.Io.Group, 318 err: *std.Io.Writer, 319 ) void { 320 const Listener = struct { 321 io: std.Io, 322 allocator: std.mem.Allocator, 323 repo_root_path: []const u8, 324 net_server: *std.Io.net.Server, 325 tasks: *std.Io.Group, 326 err: *std.Io.Writer, 327 328 fn run(ctx: @This()) void { 329 while (true) { 330 const stream = ctx.net_server.accept(ctx.io) catch |accept_err| { 331 if (accept_err == error.Canceled) return; 332 logError(ctx.err, "ssh accept failed: {s}\n", .{@errorName(accept_err)}); 333 continue; 334 }; 335 336 const Connection = struct { 337 io: std.Io, 338 allocator: std.mem.Allocator, 339 repo_root_path: []const u8, 340 stream: std.Io.net.Stream, 341 err: *std.Io.Writer, 342 343 fn run(conn: @This()) void { 344 defer conn.stream.close(conn.io); 345 handleSshGitConnection(repo_kind, any_repo_opts, conn.io, conn.allocator, conn.repo_root_path, conn.stream) catch |request_err| { 346 logError(conn.err, "ssh request failed: {s}\n", .{@errorName(request_err)}); 347 }; 348 } 349 }; 350 351 ctx.tasks.async(ctx.io, Connection.run, .{Connection{ 352 .io = ctx.io, 353 .allocator = ctx.allocator, 354 .repo_root_path = ctx.repo_root_path, 355 .stream = stream, 356 .err = ctx.err, 357 }}); 358 } 359 } 360 }; 361 362 tasks.async(io, Listener.run, .{Listener{ 363 .io = io, 364 .allocator = allocator, 365 .repo_root_path = repo_root_path, 366 .net_server = net_server, 367 .tasks = tasks, 368 .err = err, 369 }}); 370 } 371 372 fn runTuiListener( 373 io: std.Io, 374 allocator: std.mem.Allocator, 375 admin_repo_path: []const u8, 376 net_server: *std.Io.net.Server, 377 tasks: *std.Io.Group, 378 err: *std.Io.Writer, 379 ) void { 380 const Listener = struct { 381 io: std.Io, 382 allocator: std.mem.Allocator, 383 admin_repo_path: []const u8, 384 net_server: *std.Io.net.Server, 385 tasks: *std.Io.Group, 386 err: *std.Io.Writer, 387 388 fn run(ctx: @This()) void { 389 while (true) { 390 const stream = ctx.net_server.accept(ctx.io) catch |accept_err| { 391 if (accept_err == error.Canceled) return; 392 logError(ctx.err, "tui accept failed: {s}\n", .{@errorName(accept_err)}); 393 continue; 394 }; 395 396 const Connection = struct { 397 io: std.Io, 398 allocator: std.mem.Allocator, 399 admin_repo_path: []const u8, 400 stream: std.Io.net.Stream, 401 err: *std.Io.Writer, 402 403 fn run(conn: @This()) void { 404 defer conn.stream.close(conn.io); 405 serve_ssh_tui.handleConnection(conn.io, conn.allocator, conn.stream, conn.admin_repo_path) catch |session_err| { 406 logError(conn.err, "tui session failed: {s}\n", .{@errorName(session_err)}); 407 }; 408 } 409 }; 410 411 ctx.tasks.async(ctx.io, Connection.run, .{Connection{ 412 .io = ctx.io, 413 .allocator = ctx.allocator, 414 .admin_repo_path = ctx.admin_repo_path, 415 .stream = stream, 416 .err = ctx.err, 417 }}); 418 } 419 } 420 }; 421 422 tasks.async(io, Listener.run, .{Listener{ 423 .io = io, 424 .allocator = allocator, 425 .admin_repo_path = admin_repo_path, 426 .net_server = net_server, 427 .tasks = tasks, 428 .err = err, 429 }}); 430 } 431 432 fn handleSshGitConnection( 433 comptime repo_kind: rp.RepoKind, 434 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 435 io: std.Io, 436 allocator: std.mem.Allocator, 437 repo_root_path: []const u8, 438 stream: std.Io.net.Stream, 439 ) !void { 440 var send_buffer = [_]u8{0} ** any_repo_opts.net_buffer_size; 441 var recv_buffer = [_]u8{0} ** any_repo_opts.net_buffer_size; 442 var reader = stream.reader(io, &recv_buffer); 443 var writer = stream.writer(io, &send_buffer); 444 445 const request = try readSshRequest(allocator, &reader.interface); 446 defer request.deinit(allocator); 447 448 const repo_path = try resolveSshRepoPath(allocator, repo_root_path, request.repo); 449 defer allocator.free(repo_path); 450 451 if (!isSubPath(repo_root_path, repo_path)) return error.Forbidden; 452 453 try openRepoAndServe(repo_kind, any_repo_opts, io, allocator, repo_path, request.service == .receive_pack, SshGitService{ 454 .reader = &reader.interface, 455 .writer = &writer.interface, 456 .request = request, 457 }); 458 } 459 460 fn openRepoAndServe( 461 comptime repo_kind: rp.RepoKind, 462 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 463 io: std.Io, 464 allocator: std.mem.Allocator, 465 repo_path: []const u8, 466 create_if_missing: bool, 467 service: anytype, 468 ) !void { 469 if (any_repo_opts.hash) |hash_kind| { 470 var repo = try openRepo(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), io, allocator, repo_path, create_if_missing); 471 defer repo.deinit(io, allocator); 472 try service.serve(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), &repo, io, allocator); 473 } else { 474 var any_repo = rp.AnyRepo(repo_kind, any_repo_opts).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) { 475 error.RepoNotFound => { 476 if (!create_if_missing) return open_err; 477 478 var repo = try openRepo(repo_kind, any_repo_opts.toRepoOpts(), io, allocator, repo_path, true); 479 defer repo.deinit(io, allocator); 480 try service.serve(repo_kind, any_repo_opts.toRepoOpts(), &repo, io, allocator); 481 return; 482 }, 483 else => |e| return e, 484 }; 485 defer any_repo.deinit(io, allocator); 486 487 switch (any_repo) { 488 inline else => |*repo| { 489 try service.serve(repo.self_repo_kind, repo.self_repo_opts, repo, io, allocator); 490 }, 491 } 492 } 493 } 494 495 fn openRepo( 496 comptime repo_kind: rp.RepoKind, 497 comptime repo_opts: rp.RepoOpts(repo_kind), 498 io: std.Io, 499 allocator: std.mem.Allocator, 500 repo_path: []const u8, 501 create_if_missing: bool, 502 ) !rp.Repo(repo_kind, repo_opts) { 503 return rp.Repo(repo_kind, repo_opts).open(io, allocator, .{ .path = repo_path }) catch |open_err| switch (open_err) { 504 error.RepoNotFound => { 505 if (!create_if_missing) return open_err; 506 507 var repo = try rp.Repo(repo_kind, repo_opts).init(io, allocator, .{ .path = repo_path }); 508 errdefer repo.deinit(io, allocator); 509 try repo.addConfig(io, allocator, .{ .name = "http.receivepack", .value = "true" }); 510 try repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); 511 return repo; 512 }, 513 else => |e| return e, 514 }; 515 } 516 517 const HttpGitService = struct { 518 body_reader: *std.Io.Reader, 519 writer: *std.Io.Writer, 520 options: xit.net_server_http_backend.Options, 521 522 fn serve( 523 self: @This(), 524 comptime repo_kind: rp.RepoKind, 525 comptime repo_opts: rp.RepoOpts(repo_kind), 526 repo: *rp.Repo(repo_kind, repo_opts), 527 io: std.Io, 528 allocator: std.mem.Allocator, 529 ) !void { 530 try repo.httpBackend(io, allocator, self.body_reader, self.writer, .http, self.options); 531 } 532 }; 533 534 const SshGitService = struct { 535 reader: *std.Io.Reader, 536 writer: *std.Io.Writer, 537 request: SshRequest, 538 539 fn serve( 540 self: @This(), 541 comptime repo_kind: rp.RepoKind, 542 comptime repo_opts: rp.RepoOpts(repo_kind), 543 repo: *rp.Repo(repo_kind, repo_opts), 544 io: std.Io, 545 allocator: std.mem.Allocator, 546 ) !void { 547 switch (self.request.service) { 548 .upload_pack => try repo.uploadPack(io, allocator, self.reader, self.writer, .{ 549 .protocol_version = self.request.protocol_version, 550 }), 551 .receive_pack => try repo.receivePack(io, allocator, self.reader, self.writer, .{ 552 .protocol_version = self.request.protocol_version, 553 }), 554 } 555 } 556 }; 557 558 fn isReceivePack( 559 handler: xit.net_server_http_backend.HandlerKind, 560 suffix: []const u8, 561 query: ?std.Uri.Component, 562 ) bool { 563 return switch (handler) { 564 .run_service => std.mem.eql(u8, suffix, "/git-receive-pack"), 565 .get_info_refs => if (query) |q| 566 std.mem.startsWith(u8, q.percent_encoded, "service=git-receive-pack") 567 else 568 false, 569 }; 570 } 571 572 fn readSshRequest(allocator: std.mem.Allocator, reader: *std.Io.Reader) !SshRequest { 573 const magic = try readSshPreludeLine(reader); 574 if (!std.mem.eql(u8, magic, "haxy-ssh-git-v1")) return error.InvalidSshMagic; 575 576 const service_line = try readSshPreludeLine(reader); 577 const service = parseSshService(stripPrefix(service_line, "service=") orelse return error.InvalidSshServiceLine) orelse return error.InvalidSshService; 578 579 const protocol_line = try readSshPreludeLine(reader); 580 const protocol_version = std.meta.stringToEnum( 581 xit.net_server_common.ProtocolVersion, 582 stripPrefix(protocol_line, "protocol=") orelse return error.InvalidSshProtocolLine, 583 ) orelse return error.InvalidSshProtocol; 584 585 const repo_length_line = try readSshPreludeLine(reader); 586 const repo_len = try std.fmt.parseInt(usize, stripPrefix(repo_length_line, "repo-length=") orelse return error.InvalidSshRepoLengthLine, 10); 587 588 const empty_line = try readSshPreludeLine(reader); 589 if (empty_line.len != 0) return error.InvalidSshPreludeTerminator; 590 591 const repo = try reader.readAlloc(allocator, repo_len); 592 errdefer allocator.free(repo); 593 594 return .{ 595 .service = service, 596 .protocol_version = protocol_version, 597 .repo = repo, 598 }; 599 } 600 601 fn readSshPreludeLine(reader: *std.Io.Reader) ![]const u8 { 602 return (try reader.takeDelimiter('\n')) orelse error.InvalidSshHelperRequest; 603 } 604 605 fn parseSshService(value: []const u8) ?SshService { 606 if (std.mem.eql(u8, value, "upload-pack")) return .upload_pack; 607 if (std.mem.eql(u8, value, "receive-pack")) return .receive_pack; 608 return null; 609 } 610 611 fn stripPrefix(value: []const u8, prefix: []const u8) ?[]const u8 { 612 if (!std.mem.startsWith(u8, value, prefix)) return null; 613 return value[prefix.len..]; 614 } 615 616 fn resolveSshRepoPath( 617 allocator: std.mem.Allocator, 618 repo_root_path: []const u8, 619 repo: []const u8, 620 ) ![]const u8 { 621 if (std.fs.path.isAbsolute(repo)) { 622 return try std.fs.path.resolve(allocator, &.{repo}); 623 } 624 return try std.fs.path.resolve(allocator, &.{ repo_root_path, repo }); 625 } 626 627 fn parseListenAddress(value: []const u8) !ListenAddress { 628 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidListenAddress; 629 if (colon == 0 or colon + 1 >= value.len) return error.InvalidListenAddress; 630 const port = try std.fmt.parseInt(u16, value[colon + 1 ..], 10); 631 return .{ .host = value[0..colon], .port = port }; 632 } 633 634 fn decodeAndValidateRepoPath(allocator: std.mem.Allocator, encoded: []const u8) ![]const u8 { 635 if (encoded.len == 0) return error.InvalidRepoPath; 636 637 const mutable = try allocator.dupe(u8, encoded); 638 errdefer allocator.free(mutable); 639 const decoded = std.Uri.percentDecodeInPlace(mutable); 640 641 var iter = std.mem.splitScalar(u8, decoded, '/'); 642 while (iter.next()) |segment| { 643 if (segment.len == 0 or std.mem.eql(u8, segment, ".") or std.mem.eql(u8, segment, "..")) { 644 return error.InvalidRepoPath; 645 } 646 } 647 648 return try allocator.realloc(mutable, decoded.len); 649 } 650 651 fn isSubPath(parent: []const u8, child: []const u8) bool { 652 if (std.mem.eql(u8, parent, std.fs.path.sep_str)) return std.fs.path.isAbsolute(child); 653 if (!std.mem.startsWith(u8, child, parent)) return false; 654 return child.len == parent.len or child[parent.len] == std.fs.path.sep; 655 } 656 657 fn normalizeMethod(method: std.http.Method) std.http.Method { 658 return if (method == .HEAD) .GET else method; 659 } 660 661 fn protocolVersionFromHeader(header: ?[]const u8) xit.net_server_common.ProtocolVersion { 662 const git_protocol = header orelse return .v0; 663 var version: xit.net_server_common.ProtocolVersion = .v0; 664 var iter = std.mem.splitScalar(u8, git_protocol, ':'); 665 while (iter.next()) |entry| { 666 const value = std.mem.trimStart(u8, entry, " "); 667 if (std.mem.startsWith(u8, value, "version=")) { 668 const v = value["version=".len..]; 669 if (std.mem.eql(u8, v, "2")) { 670 version = .v2; 671 } else if (std.mem.eql(u8, v, "1") and version != .v2) { 672 version = .v1; 673 } 674 } 675 } 676 return version; 677 } 678 679 fn findHeader(request: *std.http.Server.Request, name: []const u8) ?[]const u8 { 680 var it = request.iterateHeaders(); 681 while (it.next()) |header| { 682 if (std.ascii.eqlIgnoreCase(header.name, name)) return header.value; 683 } 684 return null; 685 } 686 687 fn writeSimpleResponse( 688 http_server: *std.http.Server, 689 code: u16, 690 message: []const u8, 691 content_type: []const u8, 692 body: []const u8, 693 ) !void { 694 try http_server.out.print( 695 "HTTP/1.1 {d} {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\n\r\n{s}", 696 .{ code, message, content_type, body.len, body }, 697 ); 698 } 699 700 fn logError(err: *std.Io.Writer, comptime fmt: []const u8, args: anytype) void { 701 err.print(fmt, args) catch return; 702 err.flush() catch {}; 703 } 704