╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────╮ │src/serve.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │23 options: Options, │ │24 err: *std.Io.Writer, │ │25 ) !void { │ │25 - const listen_address = try parseListenAddress(options.http_listen); │ │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 │ │30 - const project_root = try std.fs.path.resolve(allocator, &.{ cwd_path, options.project_root }); │ │31 - defer allocator.free(project_root); │ │32 - │ │33 - try err.print("serving HTTP on {s}, project root {s}\n", .{ options.http_listen, project_root });│ │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; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───╮ │src/test.zig│ ╰────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │14 try testFetch(.xit, .xit, .{ .wire = .http }, 3005, true, io, allocator); │ │15 if (.windows != builtin.os.tag) { │ │16 try testFetch(.xit, .xit, .{ .wire = .ssh }, 3006, false, io, allocator);│ │17 + try testFetch(.xit, .xit, .{ .wire = .ssh }, 3007, true, io, allocator); │ │18 } │ │19 } │ │20 │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │25 try testPush(.xit, .xit, .{ .wire = .http }, 3011, true, io, allocator); │ │26 if (.windows != builtin.os.tag) { │ │27 try testPush(.xit, .xit, .{ .wire = .ssh }, 3012, false, io, allocator);│ │28 + try testPush(.xit, .xit, .{ .wire = .ssh }, 3013, true, io, allocator); │ │29 } │ │30 } │ │31 │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │32 test "clone small" { │ │33 const io = std.testing.io; │ │34 const allocator = std.testing.allocator; │ │33 - try testClone(.xit, .xit, .{ .wire = .http }, 3016, false, false, io, allocator); │ │34 - try testClone(.xit, .xit, .{ .wire = .http }, 3017, false, true, io, allocator); │ │35 + try testClone(.xit, .xit, .{ .wire = .http }, 3020, false, false, io, allocator); │ │36 + try testClone(.xit, .xit, .{ .wire = .http }, 3021, false, true, io, allocator); │ │37 if (.windows != builtin.os.tag) { │ │36 - try testClone(.xit, .xit, .{ .wire = .ssh }, 3018, false, false, io, allocator);│ │38 + try testClone(.xit, .xit, .{ .wire = .ssh }, 3022, false, false, io, allocator);│ │39 + try testClone(.xit, .xit, .{ .wire = .ssh }, 3023, false, true, io, allocator); │ │40 } │ │41 } │ │42 │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │43 test "clone small subprocess" { │ │44 const io = std.testing.io; │ │45 const allocator = std.testing.allocator; │ │43 - try testClone(.git, .xit, .{ .wire = .http }, 3031, true, false, io, allocator); │ │44 - try testClone(.git, .xit, .{ .wire = .http }, 3032, true, true, io, allocator); │ │46 + try testClone(.git, .xit, .{ .wire = .http }, 3030, true, false, io, allocator); │ │47 + try testClone(.git, .xit, .{ .wire = .http }, 3031, true, true, io, allocator); │ │48 if (.windows != builtin.os.tag) { │ │46 - try testClone(.git, .xit, .{ .wire = .ssh }, 3033, true, false, io, allocator);│ │49 + try testClone(.git, .xit, .{ .wire = .ssh }, 3032, true, false, io, allocator);│ │50 + try testClone(.git, .xit, .{ .wire = .ssh }, 3033, true, true, io, allocator); │ │51 } │ │52 } │ │53 │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │54 test "fetch large" { │ │55 const io = std.testing.io; │ │56 const allocator = std.testing.allocator; │ │53 - try testFetchLarge(.git, .git, .{ .wire = .http }, 3019, false, false, io, allocator); │ │57 + try testFetchLarge(.git, .git, .{ .wire = .http }, 3040, false, false, io, allocator); │ │58 if (.windows != builtin.os.tag) { │ │55 - try testFetchLarge(.git, .git, .{ .wire = .ssh }, 3021, false, false, io, allocator);│ │59 + try testFetchLarge(.git, .git, .{ .wire = .ssh }, 3041, false, false, io, allocator);│ │60 } │ │61 } │ │62 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │63 test "fetch large subprocess" { │ │64 const io = std.testing.io; │ │65 const allocator = std.testing.allocator; │ │62 - try testFetchLarge(.git, .xit, .{ .wire = .http }, 3022, true, false, io, allocator); │ │63 - try testFetchLarge(.git, .xit, .{ .wire = .http }, 3023, true, true, io, allocator); │ │66 + try testFetchLarge(.git, .xit, .{ .wire = .http }, 3050, true, false, io, allocator); │ │67 + try testFetchLarge(.git, .xit, .{ .wire = .http }, 3051, true, true, io, allocator); │ │68 if (.windows != builtin.os.tag) { │ │65 - try testFetchLarge(.git, .xit, .{ .wire = .ssh }, 3024, true, false, io, allocator);│ │69 + try testFetchLarge(.git, .xit, .{ .wire = .ssh }, 3052, true, false, io, allocator);│ │70 + try testFetchLarge(.git, .xit, .{ .wire = .ssh }, 3053, true, true, io, allocator); │ │71 } │ │72 } │ │73 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │74 test "push large" { │ │75 const io = std.testing.io; │ │76 const allocator = std.testing.allocator; │ │72 - try testPushLarge(.git, .git, .{ .wire = .http }, 3025, false, false, io, allocator); │ │77 + try testPushLarge(.git, .git, .{ .wire = .http }, 3060, false, false, io, allocator); │ │78 if (.windows != builtin.os.tag) { │ │74 - try testPushLarge(.git, .git, .{ .wire = .ssh }, 3027, false, false, io, allocator);│ │79 + try testPushLarge(.git, .git, .{ .wire = .ssh }, 3061, false, false, io, allocator);│ │80 } │ │81 } │ │82 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯