1633 if (recipient_channel != ch.local_id) return error.UnknownChannel;
1634 const req_type = try takeStringField(allocator, &r, 64);
1635 defer allocator.free(req_type);
1636 const want_reply = (try r.takeByte()) != 0;
1637
1638 if (std.mem.eql(u8, req_type, "pty-req")) {
1639 // string TERM, uint32 width chars, uint32 height rows, uint32 width px,
1640 // uint32 height px, string modes
1641 const term = try takeStringField(allocator, &r, 64);
1642 defer allocator.free(term);
1643 const width = try r.takeInt(u32, .big);
1644 const height = try r.takeInt(u32, .big);
1645 ch.pty = .{ .width_cells = @intCast(@min(width, 0xFFFF)), .height_cells = @intCast(@min(height, 0xFFFF)) };
1646 try replyChannelRequest(io, allocator, sc_cipher, writer, ch, want_reply, true);
1647 return null;
1648 }
1649
1650 if (std.mem.eql(u8, req_type, "env")) {
1651 // ignore environment variables silently (we don't pass them anywhere)
1652 try replyChannelRequest(io, allocator, sc_cipher, writer, ch, want_reply, true);
1653 return null;
1654 }
1655
1656 if (std.mem.eql(u8, req_type, "window-change")) {
1657 // uint32 width cells, uint32 height rows, uint32 width px, uint32 height px
1658 const width = try r.takeInt(u32, .big);
1659 const height = try r.takeInt(u32, .big);
1660 if (ch.pty != null) {
1661 ch.pty = .{ .width_cells = @intCast(@min(width, 0xFFFF)), .height_cells = @intCast(@min(height, 0xFFFF)) };
1662 }
1663 // spec: window-change MUST NOT request a reply, but accept either
1664 try replyChannelRequest(io, allocator, sc_cipher, writer, ch, want_reply, true);
1665 return null;
1666 }
1667
1668 if (std.mem.eql(u8, req_type, "shell")) {
1669 try replyChannelRequest(io, allocator, sc_cipher, writer, ch, want_reply, true);
1670 return .shell;
1671 }
1672
1673 if (std.mem.eql(u8, req_type, "exec")) {
1674 // capture the command string for the handler to inspect
1675 const cmd = try takeStringField(allocator, &r, 4096);
1676 if (pending_exec.*) |old| allocator.free(old);
1677 pending_exec.* = cmd;
1678 try replyChannelRequest(io, allocator, sc_cipher, writer, ch, want_reply, true);
1679 return .exec;
1680 }
1681
1682 // unknown request kind — fail it
1683 try replyChannelRequest(io, allocator, sc_cipher, writer, ch, want_reply, false);
1684 return null;
1685 }
1686
1687 fn replyChannelRequest(
1688 io: std.Io,
1689 allocator: std.mem.Allocator,
1690 sc_cipher: *Cipher,
1691 writer: *std.Io.Writer,
1692 ch: *Channel,
1693 want_reply: bool,
1694 success: bool,
1695 ) !void {
1696 if (!want_reply) return;
1697 var reply: std.ArrayList(u8) = .empty;
1698 defer reply.deinit(allocator);
1699 try reply.append(allocator, if (success) SSH_MSG_CHANNEL_SUCCESS else SSH_MSG_CHANNEL_FAILURE);
1700 try writeU32(&reply, allocator, ch.remote_id);
1701 try sc_cipher.writePacket(io, allocator, writer, reply.items);
1702 }
1703
1704 fn handleWindowAdjust(ch: *Channel, packet: []const u8) !void {
1705 // byte SSH_MSG_CHANNEL_WINDOW_ADJUST
1706 // uint32 recipient_channel
1707 // uint32 bytes_to_add
1708 var r = std.Io.Reader.fixed(packet[1..]);
1709 const recipient_channel = try r.takeInt(u32, .big);
1710 if (recipient_channel != ch.local_id) return error.UnknownChannel;
1711 const add = try r.takeInt(u32, .big);
1712 ch.remote_window +|= add;
1713 }
1714
1715 fn discardChannelData(
1716 io: std.Io,
1717 allocator: std.mem.Allocator,
1718 sc_cipher: *Cipher,
1719 writer: *std.Io.Writer,
1720 ch: *Channel,
1721 packet: []const u8,
1722 extended: bool,
1723 ) !void {
1724 // byte SSH_MSG_CHANNEL_DATA/EXTENDED_DATA
1725 // uint32 recipient_channel
1726 // [if extended: uint32 type_code]
1727 // string data
1728 const payload = try parseChannelData(packet, extended, ch.local_id);
1729 const data_len: u32 = @intCast(payload.len);
1730
1731 if (data_len > ch.local_window) return error.WindowExceeded;
1732 ch.local_window -= data_len;
1733 try maybeRefillRecvWindow(io, allocator, sc_cipher, writer, ch);
1734 }
1735
1736 pub fn parseChannelData(packet: []const u8, extended: bool, expected_channel: u32) ![]const u8 {
1737 if (packet.len < 1) return error.EmptyPacket;
1738 var r = std.Io.Reader.fixed(packet[1..]);
1739 const recipient_channel = try r.takeInt(u32, .big);
1740 if (recipient_channel != expected_channel) return error.UnknownChannel;
1741 if (extended) {
1742 const data_type = try r.takeInt(u32, .big);
1743 if (data_type != SSH_EXTENDED_DATA_STDERR) return error.UnsupportedExtendedData;
1744 }
1745 const data_len = try r.takeInt(u32, .big);
1746 // catches both data_len > remaining (would otherwise be a bounds-check
1747 // panic on the slice) and trailing junk after the declared payload.
1748 if (data_len != r.buffered().len) return error.InvalidChannelData;
1749 return r.buffered();
1750 }
1751
1752 fn parseChannelId(packet: []const u8, expected_channel: u32) !void {
1753 if (packet.len < 1) return error.EmptyPacket;
1754 var r = std.Io.Reader.fixed(packet[1..]);
1755 const recipient_channel = try r.takeInt(u32, .big);
1756 if (recipient_channel != expected_channel) return error.UnknownChannel;
1757 if (r.buffered().len != 0) return error.InvalidChannelMessage;
1758 }
1759
1760 /// Send a CHANNEL_WINDOW_ADJUST if our receive window has dipped below half
1761 /// of the initial value. Used both for the pre-session discard path and by
1762 /// SessionCtx after it consumes a CHANNEL_DATA payload.
1763 fn maybeRefillRecvWindow(
1764 io: std.Io,
1765 allocator: std.mem.Allocator,
1766 sc_cipher: *Cipher,
1767 writer: *std.Io.Writer,
1768 ch: *Channel,
1769 ) !void {
1770 if (ch.local_window >= initial_recv_window / 2) return;
1771 const add = initial_recv_window - ch.local_window;
1772 ch.local_window += add;
1773 var adj: std.ArrayList(u8) = .empty;
1774 defer adj.deinit(allocator);
1775 try adj.append(allocator, SSH_MSG_CHANNEL_WINDOW_ADJUST);
1776 try writeU32(&adj, allocator, ch.remote_id);
1777 try writeU32(&adj, allocator, add);
1778 try sc_cipher.writePacket(io, allocator, writer, adj.items);
1779 }
1780
1781 pub fn writeU32(buf: *std.ArrayList(u8), allocator: std.mem.Allocator, value: u32) !void {
1782 var bytes: [4]u8 = undefined;
1783 std.mem.writeInt(u32, &bytes, value, .big);
1784 try buf.appendSlice(allocator, &bytes);
1785 }
1786