╭ 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 const command = try resolveCommand(allocator, options, environ_map); 28 defer command.deinit(allocator); 29 30 const connect_address = try parseConnectAddress(options.ssh_connect); 31 const address = try std.Io.net.IpAddress.parseIp4(connect_address.host, connect_address.port); 32 const stream = try address.connect(io, .{ .mode = .stream }); 33 defer stream.close(io); 34 35 var send_buffer = [_]u8{0} ** 1024; 36 var recv_buffer = [_]u8{0} ** 1024; 37 var stream_writer = stream.writer(io, &send_buffer); 38 var stream_reader = stream.reader(io, &recv_buffer); 39 40 try stream_writer.interface.print( 41 "haxy-ssh-helper-v1\nservice={s}\nprotocol={s}\nrepo-length={d}\n\n", 42 .{ serviceName(command.service), @tagName(xit.net_server_common.detectProtocolVersion(environ_map)), command.dir.len }, 43 ); 44 try stream_writer.interface.writeAll(command.dir); 45 try stream_writer.interface.flush(); 46 47 if (builtin.os.tag != .linux) { 48 var stdin_buffer = [_]u8{0} ** 1024; 49 var stdin_reader = std.Io.File.stdin().reader(io, &stdin_buffer); 50 51 const CopyIn = struct { 52 io: std.Io, 53 stream: std.Io.net.Stream, 54 reader: *std.Io.Reader, 55 writer: *std.Io.Writer, 56 57 fn run(ctx: @This()) void { 58 copy(ctx.reader, ctx.writer) catch {}; 59 ctx.writer.flush() catch {}; 60 ctx.stream.shutdown(ctx.io, .send) catch {}; 61 } 62 }; 63 const stdin_thread = try std.Thread.spawn(.{}, CopyIn.run, .{CopyIn{ 64 .io = io, 65 .stream = stream, 66 .reader = &stdin_reader.interface, 67 .writer = &stream_writer.interface, 68 }}); 69 stdin_thread.detach(); 70 71 var stdout_buffer = [_]u8{0} ** 1024; 72 var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer); 73 try copy(&stream_reader.interface, &stdout_writer.interface); 74 try stdout_writer.interface.flush(); 75 } else { 76 const CopyIn = struct { 77 io: std.Io, 78 stream: std.Io.net.Stream, 79 80 fn run(ctx: @This()) void { 81 copyFd(std.posix.STDIN_FILENO, ctx.stream.socket.handle) catch {}; 82 ctx.stream.shutdown(ctx.io, .send) catch {}; 83 } 84 }; 85 const stdin_thread = try std.Thread.spawn(.{}, CopyIn.run, .{CopyIn{ .io = io, .stream = stream }}); 86 stdin_thread.detach(); 87 88 try copyFd(stream.socket.handle, std.posix.STDOUT_FILENO); 89 } 90 } 91 92 const Command = struct { 93 service: Service, 94 dir: []const u8, 95 96 fn deinit(self: Command, allocator: std.mem.Allocator) void { 97 allocator.free(self.dir); 98 } 99 }; 100 101 fn resolveCommand( 102 allocator: std.mem.Allocator, 103 options: Options, 104 environ_map: *std.process.Environ.Map, 105 ) !Command { 106 if (options.service) |service_name| { 107 const dir = options.dir orelse return error.InvalidSshCommand; 108 return .{ 109 .service = try parseService(service_name), 110 .dir = try allocator.dupe(u8, dir), 111 }; 112 } 113 114 const original_command = environ_map.get("SSH_ORIGINAL_COMMAND") orelse return error.InvalidSshCommand; 115 var tokens = try parseShellWords(allocator, original_command); 116 defer { 117 for (tokens.items) |token| allocator.free(token); 118 tokens.deinit(allocator); 119 } 120 if (tokens.items.len < 2) return error.InvalidSshCommand; 121 122 return .{ 123 .service = try parseService(tokens.items[0]), 124 .dir = try allocator.dupe(u8, tokens.items[1]), 125 }; 126 } 127 128 fn parseShellWords(allocator: std.mem.Allocator, command: []const u8) !std.ArrayList([]const u8) { 129 var tokens: std.ArrayList([]const u8) = .empty; 130 errdefer { 131 for (tokens.items) |token| allocator.free(token); 132 tokens.deinit(allocator); 133 } 134 135 var current: std.ArrayList(u8) = .empty; 136 defer current.deinit(allocator); 137 138 var quote: ?u8 = null; 139 var escaping = false; 140 var in_token = false; 141 142 for (command) |c| { 143 if (escaping) { 144 try current.append(allocator, c); 145 escaping = false; 146 in_token = true; 147 continue; 148 } 149 150 if (c == '\\' and quote != '\'') { 151 escaping = true; 152 in_token = true; 153 continue; 154 } 155 156 if (quote) |q| { 157 if (c == q) { 158 quote = null; 159 } else { 160 try current.append(allocator, c); 161 } 162 in_token = true; 163 continue; 164 } 165 166 if (c == '\'' or c == '"') { 167 quote = c; 168 in_token = true; 169 continue; 170 } 171 172 if (std.ascii.isWhitespace(c)) { 173 if (in_token) { 174 try tokens.append(allocator, try current.toOwnedSlice(allocator)); 175 current.clearRetainingCapacity(); 176 in_token = false; 177 } 178 continue; 179 } 180 181 try current.append(allocator, c); 182 in_token = true; 183 } 184 185 if (escaping or quote != null) return error.InvalidSshCommand; 186 if (in_token) { 187 try tokens.append(allocator, try current.toOwnedSlice(allocator)); 188 } 189 190 return tokens; 191 } 192 193 fn parseService(value: []const u8) !Service { 194 if (std.mem.eql(u8, value, "upload-pack") or std.mem.eql(u8, value, "git-upload-pack")) return .upload_pack; 195 if (std.mem.eql(u8, value, "receive-pack") or std.mem.eql(u8, value, "git-receive-pack")) return .receive_pack; 196 return error.InvalidSshCommand; 197 } 198 199 fn serviceName(service: Service) []const u8 { 200 return switch (service) { 201 .upload_pack => "upload-pack", 202 .receive_pack => "receive-pack", 203 }; 204 } 205 206 fn parseConnectAddress(value: []const u8) !ConnectAddress { 207 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidConnectAddress; 208 if (colon == 0 or colon + 1 >= value.len) return error.InvalidConnectAddress; 209 const port = try std.fmt.parseInt(u16, value[colon + 1 ..], 10); 210 return .{ .host = value[0..colon], .port = port }; 211 } 212 213 fn copy(reader: *std.Io.Reader, writer: *std.Io.Writer) !void { 214 var buffer: [4096]u8 = undefined; 215 while (true) { 216 const len = try reader.readSliceShort(&buffer); 217 if (len == 0) break; 218 try writer.writeAll(buffer[0..len]); 219 try writer.flush(); 220 } 221 } 222 223 fn copyFd(src: std.posix.fd_t, dst: std.posix.fd_t) !void { 224 var buffer: [4096]u8 = undefined; 225 while (true) { 226 const len = try std.posix.read(src, &buffer); 227 if (len == 0) break; 228 try writeAllFd(dst, buffer[0..len]); 229 } 230 } 231 232 fn writeAllFd(fd: std.posix.fd_t, bytes: []const u8) !void { 233 var written: usize = 0; 234 while (written < bytes.len) { 235 while (true) { 236 const rc = std.os.linux.write(fd, bytes[written..].ptr, bytes.len - written); 237 switch (std.os.linux.errno(rc)) { 238 .SUCCESS => { 239 written += @intCast(rc); 240 break; 241 }, 242 .INTR => continue, 243 .PIPE => return error.BrokenPipe, 244 else => return error.WriteFailed, 245 } 246 } 247 } 248 } 249