╭ ssh_tui.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 //! the `ssh-tui` helper subcommand, plus the wire protocol it uses to talk 2 //! to the tui listener inside `haxy serve`. the protocol types and codecs 3 //! are shared between this file (which originates the messages) and 4 //! `serve_ssh_tui.zig` (which consumes them). 5 //! 6 //! the session begins with a text prelude: 7 //! 8 //! haxy-tui-v1 9 //! user-key=<ssh key fingerprint> 10 //! term=<value of $TERM> 11 //! width=<u16> 12 //! height=<u16> 13 //! <empty line> 14 //! 15 //! after the empty terminator line, the helper sends a stream of binary 16 //! frames. each frame is: 17 //! 18 //! 1 byte type 19 //! 4 bytes payload length (big-endian) 20 //! N bytes payload 21 //! 22 //! types are: 23 //! 0x01 data — raw stdin bytes from the user's terminal 24 //! 0x02 resize — payload is 4 bytes: u16 width, u16 height (big-endian) 25 //! 0x03 close — empty payload; clean session termination 26 //! 27 //! the reverse direction (serve → helper) is unframed: the server writes 28 //! ANSI bytes and the helper copies them straight to stdout. nothing on 29 //! that side needs out-of-band signaling. 30 31 const std = @import("std"); 32 33 pub const magic = "haxy-tui-v1"; 34 35 pub const max_user_key_len: usize = 256; 36 pub const max_term_len: usize = 64; 37 pub const max_data_frame_len: usize = 64 * 1024; 38 39 pub const Prelude = struct { 40 user_key: []const u8, 41 term: []const u8, 42 width: u16, 43 height: u16, 44 45 pub fn deinit(self: Prelude, allocator: std.mem.Allocator) void { 46 allocator.free(self.user_key); 47 allocator.free(self.term); 48 } 49 }; 50 51 pub fn writePrelude(writer: *std.Io.Writer, prelude: Prelude) !void { 52 try writer.print( 53 "{s}\nuser-key={s}\nterm={s}\nwidth={d}\nheight={d}\n\n", 54 .{ magic, prelude.user_key, prelude.term, prelude.width, prelude.height }, 55 ); 56 } 57 58 pub fn readPrelude(allocator: std.mem.Allocator, reader: *std.Io.Reader) !Prelude { 59 const magic_line = try takeLine(reader); 60 if (!std.mem.eql(u8, magic_line, magic)) return error.InvalidMagic; 61 62 const user_key_val = stripPrefix(try takeLine(reader), "user-key=") orelse return error.InvalidPrelude; 63 if (user_key_val.len > max_user_key_len) return error.InvalidPrelude; 64 const user_key = try allocator.dupe(u8, user_key_val); 65 errdefer allocator.free(user_key); 66 67 const term_val = stripPrefix(try takeLine(reader), "term=") orelse return error.InvalidPrelude; 68 if (term_val.len > max_term_len) return error.InvalidPrelude; 69 const term = try allocator.dupe(u8, term_val); 70 errdefer allocator.free(term); 71 72 const width_val = stripPrefix(try takeLine(reader), "width=") orelse return error.InvalidPrelude; 73 const width = std.fmt.parseInt(u16, width_val, 10) catch return error.InvalidPrelude; 74 75 const height_val = stripPrefix(try takeLine(reader), "height=") orelse return error.InvalidPrelude; 76 const height = std.fmt.parseInt(u16, height_val, 10) catch return error.InvalidPrelude; 77 78 const terminator = try takeLine(reader); 79 if (terminator.len != 0) return error.InvalidPrelude; 80 81 return .{ .user_key = user_key, .term = term, .width = width, .height = height }; 82 } 83 84 fn takeLine(reader: *std.Io.Reader) ![]const u8 { 85 return (try reader.takeDelimiter('\n')) orelse error.InvalidPrelude; 86 } 87 88 fn stripPrefix(line: []const u8, prefix: []const u8) ?[]const u8 { 89 if (!std.mem.startsWith(u8, line, prefix)) return null; 90 return line[prefix.len..]; 91 } 92 93 pub const FrameKind = enum(u8) { 94 data = 0x01, 95 resize = 0x02, 96 close = 0x03, 97 }; 98 99 pub const Frame = union(FrameKind) { 100 data: []u8, 101 resize: struct { width: u16, height: u16 }, 102 close, 103 104 pub fn deinit(self: Frame, allocator: std.mem.Allocator) void { 105 switch (self) { 106 .data => |bytes| allocator.free(bytes), 107 else => {}, 108 } 109 } 110 }; 111 112 pub fn writeDataFrame(writer: *std.Io.Writer, payload: []const u8) !void { 113 if (payload.len > max_data_frame_len) return error.FrameTooLarge; 114 try writeFrameHeader(writer, .data, @intCast(payload.len)); 115 try writer.writeAll(payload); 116 } 117 118 pub fn writeResizeFrame(writer: *std.Io.Writer, width: u16, height: u16) !void { 119 try writeFrameHeader(writer, .resize, 4); 120 try writer.writeInt(u16, width, .big); 121 try writer.writeInt(u16, height, .big); 122 } 123 124 pub fn writeCloseFrame(writer: *std.Io.Writer) !void { 125 try writeFrameHeader(writer, .close, 0); 126 } 127 128 fn writeFrameHeader(writer: *std.Io.Writer, kind: FrameKind, length: u32) !void { 129 try writer.writeByte(@intFromEnum(kind)); 130 try writer.writeInt(u32, length, .big); 131 } 132 133 pub fn readFrame(allocator: std.mem.Allocator, reader: *std.Io.Reader) !Frame { 134 const type_byte = try reader.takeByte(); 135 const length = try reader.takeInt(u32, .big); 136 const kind: FrameKind = switch (type_byte) { 137 @intFromEnum(FrameKind.data) => .data, 138 @intFromEnum(FrameKind.resize) => .resize, 139 @intFromEnum(FrameKind.close) => .close, 140 else => return error.InvalidFrameType, 141 }; 142 143 return switch (kind) { 144 .data => blk: { 145 if (length > max_data_frame_len) return error.FrameTooLarge; 146 const payload = try reader.readAlloc(allocator, length); 147 break :blk .{ .data = payload }; 148 }, 149 .resize => blk: { 150 if (length != 4) return error.InvalidResizeFrame; 151 const w = try reader.takeInt(u16, .big); 152 const h = try reader.takeInt(u16, .big); 153 break :blk .{ .resize = .{ .width = w, .height = h } }; 154 }, 155 .close => blk: { 156 if (length != 0) return error.InvalidCloseFrame; 157 break :blk .close; 158 }, 159 }; 160 } 161