╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ ssh.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
679 fn init(io: std.Io, pipe: *Pipe, buf: []u8) PipeReader { 680 return .{ 681 .interface = .{ 682 .vtable = &.{ .readVec = readVec, .stream = stream }, 683 .buffer = buf, 684 .seek = 0, 685 .end = 0, 686 }, 687 .io = io, 688 .pipe = pipe, 689 }; 690 } 691 692 fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { 693 // tests don't drive any code path that calls stream, but the vtable 694 // requires a non-default impl. read into a small staging buffer and 695 // forward, so the implementation is correct if it ever fires. 696 const self: *PipeReader = @fieldParentPtr("interface", r); 697 var stage: [256]u8 = undefined; 698 const cap = limit.minInt(stage.len); 699 if (cap == 0) return 0; 700 const n = self.pipe.get(self.io, stage[0..cap], 1) catch |err| switch (err) { 701 error.Closed => return error.EndOfStream, 702 error.Canceled => return error.ReadFailed, 703 }; 704 return try w.write(stage[0..n]); 705 } 706 707 fn readVec(r: *std.Io.Reader, data: [][]u8) std.Io.Reader.Error!usize { 708 const self: *PipeReader = @fieldParentPtr("interface", r); 709 // vtable contract: if data[0] is empty, fill r.buffer[r.end..] instead. 710 const dest = if (data.len == 0 or data[0].len == 0) r.buffer[r.end..] else data[0]; 711 if (dest.len == 0) return 0; 712 const n = self.pipe.get(self.io, dest, 1) catch |err| switch (err) { 713 error.Closed => return error.EndOfStream, 714 error.Canceled => return error.ReadFailed, 715 }; 716 if (data.len == 0 or data[0].len == 0) { 717 r.end += n; 718 return 0; 719 } 720 return n; 721 } 722 }; 723 724 const PipeWriter = struct { 725 interface: std.Io.Writer, 726 io: std.Io, 727 pipe: *Pipe, 728 729 fn init(io: std.Io, pipe: *Pipe, buf: []u8) PipeWriter { 730 return .{ 731 .interface = .{ 732 .vtable = &.{ .drain = drain }, 733 .buffer = buf, 734 }, 735 .io = io, 736 .pipe = pipe, 737 }; 738 } 739 740 fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { 741 const self: *PipeWriter = @fieldParentPtr("interface", w); 742 const buffered = w.buffered(); 743 if (buffered.len > 0) self.pipe.putAll(self.io, buffered) catch return error.WriteFailed; 744 745 var extra: usize = 0; 746 if (data.len > 1) { 747 for (data[0 .. data.len - 1]) |chunk| { 748 if (chunk.len == 0) continue; 749 self.pipe.putAll(self.io, chunk) catch return error.WriteFailed; 750 extra += chunk.len; 751 } 752 } 753 if (data.len > 0) { 754 const pattern = data[data.len - 1]; 755 if (pattern.len > 0) { 756 var i: usize = 0; 757 while (i < splat) : (i += 1) { 758 self.pipe.putAll(self.io, pattern) catch return error.WriteFailed; 759 extra += pattern.len; 760 } 761 } 762 } 763 return w.consume(buffered.len + extra); 764 } 765 }; 766 767 // runs proto.handleConnection in a concurrent task (spawned via std.Io.concurrent). 768 // also acts as the session handler — captures the authenticated key's 769 // fingerprint and returns immediately, leaving runChannelLayer's wrap-up to 770 // send exit-status / EOF / CLOSE. records any error so the test can assert 771 // on it after awaiting the future. 772 const ServerTask = struct { 773 io: std.Io, 774 allocator: std.mem.Allocator, 775 host_key: *const proto.HostKey, 776 reader: *std.Io.Reader, 777 writer: *std.Io.Writer, 778 outgoing: *Pipe, // closed on exit so the client's reads return EOF 779 captured_fp: [proto.fingerprint_len]u8 = undefined, 780 captured_exec_buf: [256]u8 = undefined, 781 captured_exec_len: usize = 0, 782 result: ?anyerror = null, 783 784 pub fn handleSession(self: *ServerTask, sess: *proto.SessionCtx, request: proto.Request) !void { 785 @memcpy(&self.captured_fp, &sess.fingerprint); 786 switch (request) { 787 .exec => |cmd| { 788 // copy out of the protocol's stack-scoped buffer so the 789 // test can assert on it after the handler returns. 790 @memcpy(self.captured_exec_buf[0..cmd.len], cmd); 791 self.captured_exec_len = cmd.len; 792 }, 793 .shell => {}, 794 } 795 } 796 797 fn run(self: *ServerTask) void { 798 defer self.outgoing.close(self.io); 799 proto.handleConnection( 800 self.io, 801 self.allocator, 802 self.reader, 803 self.writer, 804 self.host_key, 805 self, 806 ) catch |e| { 807 self.result = e; 808 }; 809 } 810 }; 811 812 // deterministic keys so failures don't depend on RNG state. 813 814 fn newHostKey() !proto.HostKey { 815 const seed = [_]u8{0xAA} ** Ed25519.KeyPair.seed_length; 816 return .{ .keypair = try Ed25519.KeyPair.generateDeterministic(seed) }; 817 } 818 819 fn newUserKeypair() !Ed25519.KeyPair { 820 const seed = [_]u8{0xBB} ** Ed25519.KeyPair.seed_length; 821 return try Ed25519.KeyPair.generateDeterministic(seed); 822 } 823 824 fn newClientEphemeral() !X25519.KeyPair { 825 const seed = [_]u8{0xCC} ** X25519.seed_length; 826 return try X25519.KeyPair.generateDeterministic(seed); 827 } 828