╭──────────╮ │← previous│ ╰──────────╯ ╭ added ─────────╮ │src/test/ssh.zig│ ╰────────────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭──────────────────────────────────────╮ │3 //! out safely. │ │4 │ │5 const std = @import("std"); │ │6 -const builtin = @import("builtin");│ │6 const hx = @import("haxy"); │ │7 const srv = hx.serve; │ │8 const evt = hx.event; │ ╰──────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │191 │ │192 // start the server │ │193 │ │195 - var launch_sshd = false; │ │194 + var cli = false; │ │195 │ │196 var arg_it = try init.minimal.args.iterateAllocator(allocator);│ │197 defer arg_it.deinit(); │ ╰────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────╮ │198 _ = arg_it.skip(); │ │199 while (arg_it.next()) |arg| { │ │201 - if (std.mem.eql(u8, "--sshd", arg)) {│ │202 - launch_sshd = true; │ │200 + if (std.mem.eql(u8, "--cli", arg)) { │ │201 + cli = true; │ │202 } │ │203 } │ │204 │ ╰──────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │205 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); │ │206 defer allocator.free(server_path); │ │207 │ │209 - if (launch_sshd) { │ │208 + if (cli) { │ │209 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); │ │210 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); │ │212 - const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface, .environ_map = init.environ_map }; │ │213 - │ │214 - const port = 2222; │ │215 - │ │216 - // create priv host key │ │217 - const host_key_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/host_key", .{}); │ │218 - defer host_key_file.close(io); │ │219 - try host_key_file.writeStreamingAll(io, │ │220 - \\-----BEGIN OPENSSH PRIVATE KEY----- │ │221 - \\b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS │ │222 - \\1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQS1ppUfk8n7yvVKEgz3tXjt4q76VGuj │ │223 - \\LcQlRwmogzovV40LLcX0aTObZlQaLWfzJMNpCa/ztMpQlr86nsarE4lEAAAAqLe43zK3uN │ │224 - \\8yAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLWmlR+TyfvK9UoS │ │225 - \\DPe1eO3irvpUa6MtxCVHCaiDOi9XjQstxfRpM5tmVBotZ/Mkw2kJr/O0ylCWvzqexqsTiU │ │226 - \\QAAAAgQ+LCk30ZNJxb2Da5JL+QOFWCMf7bgXCWcEzhEGGvFWYAAAALcmFkYXJAcm9hcmsB │ │227 - \\AgMEBQ== │ │228 - \\-----END OPENSSH PRIVATE KEY----- │ │229 - \\ │ │230 - ); │ │231 - if (.windows != builtin.os.tag) { │ │232 - try host_key_file.setPermissions(io, @enumFromInt(0o600)); │ │233 - } │ │234 - │ │235 - // create priv client key │ │236 - const priv_key_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/key", .{}); │ │237 - defer priv_key_file.close(io); │ │238 - try priv_key_file.writeStreamingAll(io, │ │239 - \\-----BEGIN OPENSSH PRIVATE KEY----- │ │240 - \\b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW │ │241 - \\QyNTUxOQAAACCniLPJiaooAWecvOCeAjoJwCSeWxzysvpTNkpYjF22JgAAAJA+7hikPu4Y │ │242 - \\pAAAAAtzc2gtZWQyNTUxOQAAACCniLPJiaooAWecvOCeAjoJwCSeWxzysvpTNkpYjF22Jg │ │243 - \\AAAEDVlopOMnKt/7by/IA8VZvQXUS/O6VLkixOqnnahUdPCKeIs8mJqigBZ5y84J4COgnA │ │244 - \\JJ5bHPKy+lM2SliMXbYmAAAAC3JhZGFyQHJvYXJrAQI= │ │245 - \\-----END OPENSSH PRIVATE KEY----- │ │246 - \\ │ │247 - ); │ │248 - if (.windows != builtin.os.tag) { │ │249 - try priv_key_file.setPermissions(io, @enumFromInt(0o600)); │ │250 - } │ │251 - │ │252 - // create pub key │ │253 - const pub_key_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/key.pub", .{}); │ │254 - defer pub_key_file.close(io); │ │255 - try pub_key_file.writeStreamingAll(io, │ │256 - \\ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark │ │257 - \\ │ │258 - ); │ │259 - if (.windows != builtin.os.tag) { │ │260 - try pub_key_file.setPermissions(io, @enumFromInt(0o600)); │ │261 - } │ │262 - │ │263 - const haxy_bin_path = try std.fs.path.join(allocator, &.{ cwd_path, "zig-out", "bin", "haxy" }); │ │264 - defer allocator.free(haxy_bin_path); │ │265 - │ │266 - // dispatch.sh — sshd's forced command. picks between ssh-tui (for │ │267 - // interactive sessions) and ssh-git (for `git clone`/`git push`) by │ │268 - // checking whether sshd populated $SSH_ORIGINAL_COMMAND │ │269 - const dispatch_contents = try std.fmt.allocPrint( │ │270 - allocator, │ │271 - "#!/bin/sh\n" ++ │ │272 - "if [ -z \"$SSH_ORIGINAL_COMMAND\" ]; then\n" ++ │ │273 - " exec {s} ssh-tui --tui-connect 127.0.0.1:8082 --user-key test-user\n" ++ │ │274 - "else\n" ++ │ │275 - " exec {s} ssh-git\n" ++ │ │276 - "fi\n", │ │277 - .{ haxy_bin_path, haxy_bin_path }, │ │278 - ); │ │279 - defer allocator.free(dispatch_contents); │ │280 - │ │281 - // scope the file handle so the close happens before sshd ever spawns │ │282 - // a shell that tries to exec it — otherwise Linux raises ETXTBSY. │ │283 - { │ │284 - const dispatch_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/dispatch.sh", .{}); │ │285 - defer dispatch_file.close(io); │ │286 - try dispatch_file.writeStreamingAll(io, dispatch_contents); │ │287 - if (.windows != builtin.os.tag) { │ │288 - try dispatch_file.setPermissions(io, @enumFromInt(0o755)); │ │289 - } │ │290 - } │ │291 - │ │292 - // create authorized_keys file. forced command runs dispatch.sh so a │ │293 - // single key can drive both the TUI and git operations. │ │294 - const dispatch_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "dispatch.sh" }); │ │295 - defer allocator.free(dispatch_path); │ │296 - │ │297 - const auth_keys_contents = try std.fmt.allocPrint( │ │298 - allocator, │ │299 - "command=\"{s}\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKeIs8mJqigBZ5y84J4COgnAJJ5bHPKy+lM2SliMXbYm radar@roark\n", │ │300 - .{dispatch_path}, │ │301 - ); │ │302 - defer allocator.free(auth_keys_contents); │ │303 - │ │304 - const auth_keys_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/authorized_keys", .{}); │ │305 - defer auth_keys_file.close(io); │ │306 - try auth_keys_file.writeStreamingAll(io, auth_keys_contents); │ │307 - if (.windows != builtin.os.tag) { │ │308 - try auth_keys_file.setPermissions(io, @enumFromInt(0o600)); │ │309 - } │ │310 - │ │311 - // create known_hosts file │ │312 - const known_hosts_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/known_hosts", .{}); │ │313 - defer known_hosts_file.close(io); │ │314 - const port_str = std.fmt.comptimePrint("{}", .{port}); │ │315 - try known_hosts_file.writeStreamingAll(io, "[localhost]:" ++ port_str ++ " ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLWmlR+TyfvK9UoSDPe1eO3irvpUa6MtxCVHCaiDOi9XjQstxfRpM5tmVBotZ/Mkw2kJr/O0ylCWvzqexqsTiUQ=");│ │316 - if (.windows != builtin.os.tag) { │ │317 - try known_hosts_file.setPermissions(io, @enumFromInt(0o600)); │ │318 - } │ │319 - │ │320 - // create sshd_config file │ │321 - const sshd_config_str = blk: { │ │322 - // SetEnv PATH=... allows us to propagate the test process's PATH │ │323 - // to the spawned login shell (in sshd). │ │324 - // │ │325 - // without it, shells that don't auto-resource PATH on startup (e.g. nushell │ │326 - // on NixOS) through /etc/set-environment will fail to find │ │327 - // git-upload-pack / git-receive-pack. │ │328 - const base_config = │ │329 - \\AuthenticationMethods publickey │ │330 - \\PubkeyAuthentication yes │ │331 - \\PasswordAuthentication no │ │332 - \\StrictModes no │ │333 - //SetEnv PATH={s} -- if we find $PATH defined. │ │334 - ; │ │335 - var env_map = try std.process.Environ.createMap(init.minimal.environ, allocator); │ │336 - defer env_map.deinit(); │ │337 - │ │338 - const config_str = if (env_map.get("PATH")) |path_str| │ │339 - try std.fmt.allocPrint(allocator, "{s}\n" ++ "SetEnv PATH={s}\n", .{ base_config, path_str }) │ │340 - else │ │341 - try std.fmt.allocPrint(allocator, "{s}", .{base_config}); │ │342 - │ │343 - break :blk config_str; │ │344 - }; │ │345 - defer allocator.free(sshd_config_str); │ │346 - │ │347 - const sshd_config_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/sshd_config", .{}); │ │348 - defer sshd_config_file.close(io); │ │349 - try sshd_config_file.writeStreamingAll(io, sshd_config_str); │ │350 - if (.windows != builtin.os.tag) { │ │351 - try sshd_config_file.setPermissions(io, @enumFromInt(0o600)); │ │352 - } │ │353 - │ │354 - // create sshd.sh contents │ │355 - const host_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "host_key" }); │ │356 - defer allocator.free(host_key_path); │ │357 - const auth_keys_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "authorized_keys" }); │ │358 - defer allocator.free(auth_keys_path); │ │359 - const sshd_contents = try std.fmt.allocPrint( │ │360 - allocator, │ │361 - "#!/bin/sh\nexec $(which sshd) -p {} -f sshd_config -h \"{s}\" -D -e -o AuthorizedKeysFile=\"{s}\" -o PidFile=none", │ │362 - .{ port, host_key_path, auth_keys_path }, │ │363 - ); │ │364 - defer allocator.free(sshd_contents); │ │365 - │ │366 - // if path has a space char, it fucks up sshd │ │367 - try std.testing.expect(null == std.mem.indexOfScalar(u8, auth_keys_path, ' ')); │ │211 + const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; │ │212 │ │369 - // create sshd.sh │ │370 - { │ │371 - const sshd_file = try std.Io.Dir.cwd().createFile(io, temp_dir_name ++ "/sshd.sh", .{}); │ │372 - defer sshd_file.close(io); │ │373 - try sshd_file.writeStreamingAll(io, sshd_contents); │ │374 - if (.windows != builtin.os.tag) { │ │375 - try sshd_file.setPermissions(io, .executable_file); │ │376 - } │ │377 - } │ │378 - │ │379 - // build a copy-pasteable ssh command line for the user │ │380 - const ssh_key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); │ │381 - defer allocator.free(ssh_key_path); │ │382 - const known_hosts_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "known_hosts" }); │ │383 - defer allocator.free(known_hosts_path); │ │384 - │ │385 - const ssh_command = try std.fmt.allocPrint( │ │386 - allocator, │ │387 - "ssh -p {} -i {s} -o UserKnownHostsFile={s} -o IdentitiesOnly=yes localhost", │ │388 - .{ port, ssh_key_path, known_hosts_path }, │ │389 - ); │ │390 - defer allocator.free(ssh_command); │ │391 - │ │392 - // git uses GIT_SSH_COMMAND to override the ssh invocation it'd │ │393 - // normally do. uses scp-style `host:path` rather than ssh://host/path │ │394 - // so the path stays relative — ssh://host/test would send /test │ │395 - // (absolute), which haxy serve rejects as outside the repo root. │ │396 - const git_command = try std.fmt.allocPrint( │ │397 - allocator, │ │398 - "GIT_SSH_COMMAND='ssh -p {} -i {s} -o UserKnownHostsFile={s} -o IdentitiesOnly=yes' git push localhost:test master", │ │399 - .{ port, ssh_key_path, known_hosts_path }, │ │400 - ); │ │401 - defer allocator.free(git_command); │ │402 - │ │213 const Runnable = struct { │ │214 io: std.Io, │ │405 - ssh_command: []const u8, │ │406 - git_command: []const u8, │ │215 │ │216 pub fn run(self: @This()) !void { │ │217 std.debug.print( │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │218 \\ │ │411 - \\open the TUI from another terminal with: │ │219 + \\connect to the TUI with: │ │220 + \\ ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost│ │221 \\ │ │413 - \\ {s} │ │414 - \\ │ │415 - \\create a git repo and push it over ssh with: │ │416 - \\ │ │222 + \\create a git repo and push over SSH: │ │223 \\ mkdir -p temp-try/client/test │ │224 \\ cd temp-try/client/test │ │225 \\ git init │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │226 \\ echo "hello" > hello.txt │ │227 \\ git add hello.txt │ │228 \\ git commit -m "let there be light" │ │423 - \\ {s} │ │229 + \\ GIT_SSH_COMMAND='ssh -p 8022 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' git push localhost:test HEAD:master│ │230 \\ │ │425 - \\press enter to stop. │ │231 + \\to quit, press enter. │ │232 \\ │ │427 - \\ │ │428 - , .{ self.ssh_command, self.git_command }); │ │429 - │ │430 - // launch sshd │ │431 - var process = try std.process.spawn(self.io, .{ │ │432 - .argv = &.{"./sshd.sh"}, │ │433 - .cwd = .{ .path = temp_dir_name }, │ │434 - .stdin = .pipe, │ │435 - .stdout = .inherit, │ │436 - .stderr = .inherit, │ │437 - }); │ │438 - defer process.kill(self.io); │ │439 - │ │233 + , .{}); │ │234 + // portable stdin read via std.Io — equivalent to the │ │235 + // std.posix.read(STDIN_FILENO, ...) that doesn't compile │ │236 + // on windows. │ │237 var buf: [1]u8 = undefined; │ │441 - _ = std.posix.read(std.posix.STDIN_FILENO, &buf) catch {}; │ │238 + var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); │ │239 + _ = stdin_reader.interface.takeByte() catch {}; │ │240 } │ │241 }; │ │242 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │243 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ │ │244 .data_dir = server_path, │ │447 - }, run_opts.err, Runnable{ .io = io, .ssh_command = ssh_command, .git_command = git_command }); │ │245 + }, run_opts.err, Runnable{ .io = io }); │ │246 } else { │ │247 var null_writer = std.Io.Writer.Discarding.init(&.{}); │ │450 - const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer, .environ_map = init.environ_map };│ │248 + const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; │ │249 │ │250 const Runnable = struct { │ │251 io: std.Io, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │54 │ │55 // route a single decoded key into the widget tree, handling 'q'-to-quit and│ │56 // mouse-click focus traversal along the way. shared between the tty event │ │57 -// loop (ui.run) and the SSH session event loop in serve_ssh_tui.zig. │ │57 +// loop (ui.run) and the SSH session event loop in serve_ssh.zig. │ │58 // `terminal` is whatever terminal type the caller is driving — it must │ │59 // expose requestQuit(). │ │60 pub fn inputKey(root: *Widget, key: inp.Key, terminal: anytype) !void { │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭ added ──────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭──────╮ │next →│ ╰──────╯