╭ 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 if (any_repo_opts.hash) |hash_kind| { 166 var repo = try rp.Repo(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind)).open(io, allocator, .{ .path = repo_path }); 167 defer repo.deinit(io, allocator); 168 try runHttpBackend(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), &repo, io, allocator, &body_reader, http_server.out, request_method, handler, suffix, uri.query, content_type, has_remote_user, protocol_version); 169 } else { 170 var any_repo = try rp.AnyRepo(repo_kind, any_repo_opts).open(io, allocator, .{ .path = repo_path }); 171 defer any_repo.deinit(io, allocator); 172 switch (any_repo) { 173 inline else => |*repo| { 174 try runHttpBackend(repo.self_repo_kind, repo.self_repo_opts, repo, io, allocator, &body_reader, http_server.out, request_method, handler, suffix, uri.query, content_type, has_remote_user, protocol_version); 175 }, 176 } 177 } 178 } 179 180 fn runHttpBackend( 181 comptime repo_kind: rp.RepoKind, 182 comptime repo_opts: rp.RepoOpts(repo_kind), 183 repo: *rp.Repo(repo_kind, repo_opts), 184 io: std.Io, 185 allocator: std.mem.Allocator, 186 reader: *std.Io.Reader, 187 writer: *std.Io.Writer, 188 request_method: std.http.Method, 189 handler: xit.net_server_http_backend.HandlerKind, 190 suffix: []const u8, 191 query: ?std.Uri.Component, 192 content_type: []const u8, 193 has_remote_user: bool, 194 protocol_version: xit.net_server_common.ProtocolVersion, 195 ) !void { 196 try repo.httpBackend(io, allocator, reader, writer, .http, .{ 197 .request_method = request_method, 198 .handler = handler, 199 .suffix = suffix, 200 .query_string = if (query) |q| q.percent_encoded else "", 201 .content_type = content_type, 202 .has_remote_user = has_remote_user, 203 .protocol_version = protocol_version, 204 }); 205 } 206 207 fn findRoute(path: []const u8) ?struct { xit.net_server_http_backend.HandlerKind, []const u8 } { 208 for (&xit.net_server_http_backend.routes) |*route| { 209 if (std.mem.endsWith(u8, path, route.suffix)) { 210 return .{ route.handler, route.suffix }; 211 } 212 } 213 return null; 214 } 215 216 const SshService = enum { 217 upload_pack, 218 receive_pack, 219 }; 220 221 const SshRequest = struct { 222 service: SshService, 223 protocol_version: xit.net_server_common.ProtocolVersion, 224 repo: []const u8, 225 226 fn deinit(self: SshRequest, allocator: std.mem.Allocator) void { 227 allocator.free(self.repo); 228 } 229 }; 230 231 fn runSshListener( 232 comptime repo_kind: rp.RepoKind, 233 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 234 io: std.Io, 235 allocator: std.mem.Allocator, 236 project_root: []const u8, 237 ssh_listen: []const u8, 238 err: *std.Io.Writer, 239 ) !void { 240 const listen_address = try parseListenAddress(ssh_listen); 241 const address = try std.Io.net.IpAddress.parseIp4(listen_address.host, listen_address.port); 242 var net_server = try address.listen(io, .{ .reuse_address = true }); 243 defer net_server.deinit(io); 244 245 try err.print("serving SSH helper connections on {s}, project root {s}\n", .{ ssh_listen, project_root }); 246 try err.flush(); 247 248 while (true) { 249 const stream = net_server.accept(io) catch |accept_err| { 250 try err.print("ssh accept failed: {s}\n", .{@errorName(accept_err)}); 251 continue; 252 }; 253 defer stream.close(io); 254 255 handleSshConnection(repo_kind, any_repo_opts, io, allocator, project_root, stream) catch |request_err| { 256 try err.print("ssh request failed: {s}\n", .{@errorName(request_err)}); 257 try err.flush(); 258 }; 259 } 260 } 261 262 fn handleSshConnection( 263 comptime repo_kind: rp.RepoKind, 264 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 265 io: std.Io, 266 allocator: std.mem.Allocator, 267 project_root: []const u8, 268 stream: std.Io.net.Stream, 269 ) !void { 270 var send_buffer = [_]u8{0} ** any_repo_opts.net_buffer_size; 271 var recv_buffer = [_]u8{0} ** any_repo_opts.net_buffer_size; 272 var reader = stream.reader(io, &recv_buffer); 273 var writer = stream.writer(io, &send_buffer); 274 275 const request = try readSshRequest(allocator, &reader.interface); 276 defer request.deinit(allocator); 277 278 const repo_path = try resolveSshRepoPath(allocator, project_root, request.repo); 279 defer allocator.free(repo_path); 280 281 if (!isSubPath(project_root, repo_path)) return error.Forbidden; 282 283 if (any_repo_opts.hash) |hash_kind| { 284 var repo = try rp.Repo(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind)).open(io, allocator, .{ .path = repo_path }); 285 defer repo.deinit(io, allocator); 286 try runSshService(repo_kind, any_repo_opts.toRepoOptsWithHash(hash_kind), &repo, io, allocator, &reader.interface, &writer.interface, request); 287 } else { 288 var any_repo = try rp.AnyRepo(repo_kind, any_repo_opts).open(io, allocator, .{ .path = repo_path }); 289 defer any_repo.deinit(io, allocator); 290 switch (any_repo) { 291 inline else => |*repo| { 292 try runSshService(repo.self_repo_kind, repo.self_repo_opts, repo, io, allocator, &reader.interface, &writer.interface, request); 293 }, 294 } 295 } 296 } 297 298 fn runSshService( 299 comptime repo_kind: rp.RepoKind, 300 comptime repo_opts: rp.RepoOpts(repo_kind), 301 repo: *rp.Repo(repo_kind, repo_opts), 302 io: std.Io, 303 allocator: std.mem.Allocator, 304 reader: *std.Io.Reader, 305 writer: *std.Io.Writer, 306 request: SshRequest, 307 ) !void { 308 switch (request.service) { 309 .upload_pack => try repo.uploadPack(io, allocator, reader, writer, .{ 310 .protocol_version = request.protocol_version, 311 }), 312 .receive_pack => try repo.receivePack(io, allocator, reader, writer, .{ 313 .protocol_version = request.protocol_version, 314 }), 315 } 316 } 317 318 fn readSshRequest(allocator: std.mem.Allocator, reader: *std.Io.Reader) !SshRequest { 319 const magic = try readSshPreludeLine(reader); 320 if (!std.mem.eql(u8, magic, "haxy-ssh-helper-v1")) return error.InvalidSshMagic; 321 322 const service_line = try readSshPreludeLine(reader); 323 const service = parseSshService(stripPrefix(service_line, "service=") orelse return error.InvalidSshServiceLine) orelse return error.InvalidSshService; 324 325 const protocol_line = try readSshPreludeLine(reader); 326 const protocol_version = std.meta.stringToEnum( 327 xit.net_server_common.ProtocolVersion, 328 stripPrefix(protocol_line, "protocol=") orelse return error.InvalidSshProtocolLine, 329 ) orelse return error.InvalidSshProtocol; 330 331 const repo_length_line = try readSshPreludeLine(reader); 332 const repo_len = try std.fmt.parseInt(usize, stripPrefix(repo_length_line, "repo-length=") orelse return error.InvalidSshRepoLengthLine, 10); 333 334 const empty_line = try readSshPreludeLine(reader); 335 if (empty_line.len != 0) return error.InvalidSshPreludeTerminator; 336 337 const repo = try reader.readAlloc(allocator, repo_len); 338 errdefer allocator.free(repo); 339 340 return .{ 341 .service = service, 342 .protocol_version = protocol_version, 343 .repo = repo, 344 }; 345 } 346 347 fn readSshPreludeLine(reader: *std.Io.Reader) ![]const u8 { 348 return (try reader.takeDelimiter('\n')) orelse error.InvalidSshHelperRequest; 349 } 350 351 fn parseSshService(value: []const u8) ?SshService { 352 if (std.mem.eql(u8, value, "upload-pack")) return .upload_pack; 353 if (std.mem.eql(u8, value, "receive-pack")) return .receive_pack; 354 return null; 355 } 356 357 fn stripPrefix(value: []const u8, prefix: []const u8) ?[]const u8 { 358 if (!std.mem.startsWith(u8, value, prefix)) return null; 359 return value[prefix.len..]; 360 } 361 362 fn resolveSshRepoPath( 363 allocator: std.mem.Allocator, 364 project_root: []const u8, 365 repo: []const u8, 366 ) ![]const u8 { 367 if (std.fs.path.isAbsolute(repo)) { 368 return try std.fs.path.resolve(allocator, &.{repo}); 369 } 370 return try std.fs.path.resolve(allocator, &.{ project_root, repo }); 371 } 372 373 fn parseListenAddress(value: []const u8) !ListenAddress { 374 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidListenAddress; 375 if (colon == 0 or colon + 1 >= value.len) return error.InvalidListenAddress; 376 const port = try std.fmt.parseInt(u16, value[colon + 1 ..], 10); 377 return .{ .host = value[0..colon], .port = port }; 378 } 379 380 fn decodeAndValidateRepoPath(allocator: std.mem.Allocator, encoded: []const u8) ![]const u8 { 381 if (encoded.len == 0) return error.InvalidRepoPath; 382 383 const mutable = try allocator.dupe(u8, encoded); 384 errdefer allocator.free(mutable); 385 const decoded = std.Uri.percentDecodeInPlace(mutable); 386 387 var iter = std.mem.splitScalar(u8, decoded, '/'); 388 while (iter.next()) |segment| { 389 if (segment.len == 0 or std.mem.eql(u8, segment, ".") or std.mem.eql(u8, segment, "..")) { 390 return error.InvalidRepoPath; 391 } 392 } 393 394 return try allocator.realloc(mutable, decoded.len); 395 } 396 397 fn isSubPath(parent: []const u8, child: []const u8) bool { 398 if (std.mem.eql(u8, parent, std.fs.path.sep_str)) return std.fs.path.isAbsolute(child); 399 if (!std.mem.startsWith(u8, child, parent)) return false; 400 return child.len == parent.len or child[parent.len] == std.fs.path.sep; 401 } 402 403 fn normalizeMethod(method: std.http.Method) std.http.Method { 404 return if (method == .HEAD) .GET else method; 405 } 406 407 fn protocolVersionFromHeader(header: ?[]const u8) xit.net_server_common.ProtocolVersion { 408 const git_protocol = header orelse return .v0; 409 var version: xit.net_server_common.ProtocolVersion = .v0; 410 var iter = std.mem.splitScalar(u8, git_protocol, ':'); 411 while (iter.next()) |entry| { 412 const value = std.mem.trimStart(u8, entry, " "); 413 if (std.mem.startsWith(u8, value, "version=")) { 414 const v = value["version=".len..]; 415 if (std.mem.eql(u8, v, "2")) { 416 version = .v2; 417 } else if (std.mem.eql(u8, v, "1") and version != .v2) { 418 version = .v1; 419 } 420 } 421 } 422 return version; 423 } 424 425 fn findHeader(request: *std.http.Server.Request, name: []const u8) ?[]const u8 { 426 var it = request.iterateHeaders(); 427 while (it.next()) |header| { 428 if (std.ascii.eqlIgnoreCase(header.name, name)) return header.value; 429 } 430 return null; 431 } 432 433 fn writeSimpleResponse( 434 http_server: *std.http.Server, 435 code: u16, 436 message: []const u8, 437 content_type: []const u8, 438 body: []const u8, 439 ) !void { 440 try http_server.out.print( 441 "HTTP/1.1 {d} {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\n\r\n{s}", 442 .{ code, message, content_type, body.len, body }, 443 ); 444 } 445