╭ ssh_git.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const builtin = @import("builtin"); 3 const xit = @import("xit"); 4 const ssh = @import("./ssh.zig"); 5 6 pub const Options = struct { 7 ssh_connect: []const u8 = blk: { 8 const srv = @import("./serve.zig"); 9 const opts: srv.Options = .{}; 10 break :blk opts.ssh_listen; 11 }, 12 service: ?[]const u8 = null, 13 dir: ?[]const u8 = null, 14 }; 15 16 const Service = enum { 17 upload_pack, 18 receive_pack, 19 }; 20 21 pub fn run( 22 io: std.Io, 23 allocator: std.mem.Allocator, 24 options: Options, 25 environ_map: *std.process.Environ.Map, 26 ) !void { 27 if (builtin.os.tag == .windows) { 28 return error.WindowsNotSupported; 29 } 30 31 const command = try resolveCommand(allocator, options, environ_map); 32 defer command.deinit(allocator); 33 34 const connect_address = try ssh.parseConnectAddress(options.ssh_connect); 35 const address = try std.Io.net.IpAddress.parseIp4(connect_address.host, connect_address.port); 36 const stream = try address.connect(io, .{ .mode = .stream }); 37 defer stream.close(io); 38 39 var send_buffer = [_]u8{0} ** 1024; 40 var stream_writer = stream.writer(io, &send_buffer); 41 42 try stream_writer.interface.print( 43 "haxy-ssh-git-v1\nservice={s}\nprotocol={s}\nrepo-length={d}\n\n", 44 .{ serviceName(command.service), @tagName(xit.net_server_common.detectProtocolVersion(environ_map)), command.dir.len }, 45 ); 46 try stream_writer.interface.writeAll(command.dir); 47 try stream_writer.interface.flush(); 48 49 const CopyIn = struct { 50 io: std.Io, 51 stream: std.Io.net.Stream, 52 53 fn run(ctx: @This()) void { 54 ssh.copyFd(std.posix.STDIN_FILENO, ctx.stream.socket.handle) catch {}; 55 ctx.stream.shutdown(ctx.io, .send) catch {}; 56 } 57 }; 58 const stdin_thread = try std.Thread.spawn(.{}, CopyIn.run, .{CopyIn{ .io = io, .stream = stream }}); 59 stdin_thread.detach(); 60 61 try ssh.copyFd(stream.socket.handle, std.posix.STDOUT_FILENO); 62 } 63 64 const Command = struct { 65 service: Service, 66 dir: []const u8, 67 68 fn deinit(self: Command, allocator: std.mem.Allocator) void { 69 allocator.free(self.dir); 70 } 71 }; 72 73 fn resolveCommand( 74 allocator: std.mem.Allocator, 75 options: Options, 76 environ_map: *std.process.Environ.Map, 77 ) !Command { 78 if (options.service) |service_name| { 79 const dir = options.dir orelse return error.InvalidSshCommand; 80 return .{ 81 .service = try parseService(service_name), 82 .dir = try allocator.dupe(u8, dir), 83 }; 84 } 85 86 const original_command = environ_map.get("SSH_ORIGINAL_COMMAND") orelse return error.InvalidSshCommand; 87 var tokens = try std.process.Args.IteratorGeneral(.{ .single_quotes = true }).init(allocator, original_command); 88 defer tokens.deinit(); 89 90 const service_name = tokens.next() orelse return error.InvalidSshCommand; 91 const dir = tokens.next() orelse return error.InvalidSshCommand; 92 93 return .{ 94 .service = try parseService(service_name), 95 .dir = try allocator.dupe(u8, dir), 96 }; 97 } 98 99 fn parseService(value: []const u8) !Service { 100 if (std.mem.eql(u8, value, "upload-pack") or std.mem.eql(u8, value, "git-upload-pack")) return .upload_pack; 101 if (std.mem.eql(u8, value, "receive-pack") or std.mem.eql(u8, value, "git-receive-pack")) return .receive_pack; 102 return error.InvalidSshCommand; 103 } 104 105 fn serviceName(service: Service) []const u8 { 106 return switch (service) { 107 .upload_pack => "upload-pack", 108 .receive_pack => "receive-pack", 109 }; 110 } 111