╭ ssh_helper.zig ───────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const builtin = @import("builtin"); 3 const xit = @import("xit"); 4 5 pub const Options = struct { 6 ssh_connect: []const u8, 7 service: ?[]const u8, 8 dir: ?[]const u8, 9 }; 10 11 const Service = enum { 12 upload_pack, 13 receive_pack, 14 }; 15 16 const ConnectAddress = struct { 17 host: []const u8, 18 port: u16, 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 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-helper-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 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 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 parseShellWords(allocator, original_command); 88 defer { 89 for (tokens.items) |token| allocator.free(token); 90 tokens.deinit(allocator); 91 } 92 if (tokens.items.len < 2) return error.InvalidSshCommand; 93 94 return .{ 95 .service = try parseService(tokens.items[0]), 96 .dir = try allocator.dupe(u8, tokens.items[1]), 97 }; 98 } 99 100 fn parseShellWords(allocator: std.mem.Allocator, command: []const u8) !std.ArrayList([]const u8) { 101 var tokens: std.ArrayList([]const u8) = .empty; 102 errdefer { 103 for (tokens.items) |token| allocator.free(token); 104 tokens.deinit(allocator); 105 } 106 107 var current: std.ArrayList(u8) = .empty; 108 defer current.deinit(allocator); 109 110 var quote: ?u8 = null; 111 var escaping = false; 112 var in_token = false; 113 114 for (command) |c| { 115 if (escaping) { 116 try current.append(allocator, c); 117 escaping = false; 118 in_token = true; 119 continue; 120 } 121 122 if (c == '\\' and quote != '\'') { 123 escaping = true; 124 in_token = true; 125 continue; 126 } 127 128 if (quote) |q| { 129 if (c == q) { 130 quote = null; 131 } else { 132 try current.append(allocator, c); 133 } 134 in_token = true; 135 continue; 136 } 137 138 if (c == '\'' or c == '"') { 139 quote = c; 140 in_token = true; 141 continue; 142 } 143 144 if (std.ascii.isWhitespace(c)) { 145 if (in_token) { 146 try tokens.append(allocator, try current.toOwnedSlice(allocator)); 147 current.clearRetainingCapacity(); 148 in_token = false; 149 } 150 continue; 151 } 152 153 try current.append(allocator, c); 154 in_token = true; 155 } 156 157 if (escaping or quote != null) return error.InvalidSshCommand; 158 if (in_token) { 159 try tokens.append(allocator, try current.toOwnedSlice(allocator)); 160 } 161 162 return tokens; 163 } 164 165 fn parseService(value: []const u8) !Service { 166 if (std.mem.eql(u8, value, "upload-pack") or std.mem.eql(u8, value, "git-upload-pack")) return .upload_pack; 167 if (std.mem.eql(u8, value, "receive-pack") or std.mem.eql(u8, value, "git-receive-pack")) return .receive_pack; 168 return error.InvalidSshCommand; 169 } 170 171 fn serviceName(service: Service) []const u8 { 172 return switch (service) { 173 .upload_pack => "upload-pack", 174 .receive_pack => "receive-pack", 175 }; 176 } 177 178 fn parseConnectAddress(value: []const u8) !ConnectAddress { 179 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidConnectAddress; 180 if (colon == 0 or colon + 1 >= value.len) return error.InvalidConnectAddress; 181 const port = try std.fmt.parseInt(u16, value[colon + 1 ..], 10); 182 return .{ .host = value[0..colon], .port = port }; 183 } 184 185 fn copy(reader: *std.Io.Reader, writer: *std.Io.Writer) !void { 186 var buffer: [4096]u8 = undefined; 187 while (true) { 188 const len = try reader.readSliceShort(&buffer); 189 if (len == 0) break; 190 try writer.writeAll(buffer[0..len]); 191 try writer.flush(); 192 } 193 } 194 195 fn copyFd(src: std.posix.fd_t, dst: std.posix.fd_t) !void { 196 var buffer: [4096]u8 = undefined; 197 while (true) { 198 const len = try std.posix.read(src, &buffer); 199 if (len == 0) break; 200 try writeAllFd(dst, buffer[0..len]); 201 } 202 } 203 204 fn writeAllFd(fd: std.posix.fd_t, bytes: []const u8) !void { 205 var written: usize = 0; 206 while (written < bytes.len) { 207 const rc = std.posix.system.write(fd, bytes[written..].ptr, bytes.len - written); 208 switch (std.posix.errno(rc)) { 209 .SUCCESS => written += @intCast(rc), 210 .INTR => continue, 211 .PIPE => return error.BrokenPipe, 212 else => return error.WriteFailed, 213 } 214 } 215 } 216