A module that builds additional functionality onto the Net::SSH::BufferedIo module. It adds SSH encryption, compression, and packet validation, as per the SSH2 protocol. It also adds an abstraction for polling packets, to allow for both blocking and non-blocking reads.
Methods
public class
public instance
- available_for_read?
- cleanup
- client_name
- enqueue_packet
- if_needs_rekey?
- next_packet
- peer_ip
- send_packet
protected instance
Included modules
Attributes
client | [R] | The client state object, which encapsulates the algorithms used to build packets to send to the server. |
hints | [R] | The map of “hints” that can be used to modify the behavior of the packet stream. For instance, when authentication succeeds, an “authenticated” hint is set, which is used to determine whether or not to compress the data when using the “delayed” compression algorithm. |
server | [R] | The server state object, which encapsulates the algorithms used to interpret packets coming from the server. |
Public class methods
# File lib/net/ssh/transport/packet_stream.rb, line 19 19: def self.extended(object) 20: object.__send__(:initialize_ssh) 21: end
Public instance methods
Returns true if the IO is available for reading, and false otherwise.
# File lib/net/ssh/transport/packet_stream.rb, line 72 72: def available_for_read? 73: result = Net::SSH::Compat.io_select([self], nil, nil, 0) 74: result && result.first.any? 75: end
Performs any pending cleanup necessary on the IO and its associated state objects. (See State#cleanup).
# File lib/net/ssh/transport/packet_stream.rb, line 155 155: def cleanup 156: client.cleanup 157: server.cleanup 158: end
The name of the client (local) end of the socket, as reported by the socket.
# File lib/net/ssh/transport/packet_stream.rb, line 39 39: def client_name 40: @client_name ||= begin 41: sockaddr = getsockname 42: begin 43: Socket.getnameinfo(sockaddr, Socket::NI_NAMEREQD).first 44: rescue 45: begin 46: Socket.getnameinfo(sockaddr).first 47: rescue 48: begin 49: Socket.gethostbyname(Socket.gethostname).first 50: rescue 51: lwarn { "the client ipaddr/name could not be determined" } 52: "unknown" 53: end 54: end 55: end 56: end 57: end
Enqueues a packet to be sent, but does not immediately send the packet. The given payload is pre-processed according to the algorithms specified in the client state (compression, cipher, and hmac).
# File lib/net/ssh/transport/packet_stream.rb, line 118 118: def enqueue_packet(payload) 119: # try to compress the packet 120: payload = client.compress(payload) 121: 122: # the length of the packet, minus the padding 123: actual_length = 4 + payload.length + 1 124: 125: # compute the padding length 126: padding_length = client.block_size - (actual_length % client.block_size) 127: padding_length += client.block_size if padding_length < 4 128: 129: # compute the packet length (sans the length field itself) 130: packet_length = payload.length + padding_length + 1 131: 132: if packet_length < 16 133: padding_length += client.block_size 134: packet_length = payload.length + padding_length + 1 135: end 136: 137: padding = Array.new(padding_length) { rand(256) }.pack("C*") 138: 139: unencrypted_data = [packet_length, padding_length, payload, padding].pack("NCA*A*") 140: mac = client.hmac.digest([client.sequence_number, unencrypted_data].pack("NA*")) 141: 142: encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher 143: message = encrypted_data + mac 144: 145: debug { "queueing packet nr #{client.sequence_number} type #{payload.getbyte(0)} len #{packet_length}" } 146: enqueue(message) 147: 148: client.increment(packet_length) 149: 150: self 151: end
If the IO object requires a rekey operation (as indicated by either its client or server state objects, see State#needs_rekey?), this will yield. Otherwise, this does nothing.
# File lib/net/ssh/transport/packet_stream.rb, line 163 163: def if_needs_rekey? 164: if client.needs_rekey? || server.needs_rekey? 165: yield 166: client.reset! if client.needs_rekey? 167: server.reset! if server.needs_rekey? 168: end 169: end
Returns the next full packet. If the mode parameter is :nonblock (the default), then this will return immediately, whether a packet is available or not, and will return nil if there is no packet ready to be returned. If the mode parameter is :block, then this method will block until a packet is available.
# File lib/net/ssh/transport/packet_stream.rb, line 82 82: def next_packet(mode=:nonblock) 83: case mode 84: when :nonblock then 85: fill if available_for_read? 86: poll_next_packet 87: 88: when :block then 89: loop do 90: packet = poll_next_packet 91: return packet if packet 92: 93: loop do 94: result = Net::SSH::Compat.io_select([self]) or next 95: break if result.first.any? 96: end 97: 98: if fill <= 0 99: raise Net::SSH::Disconnect, "connection closed by remote host" 100: end 101: end 102: 103: else 104: raise ArgumentError, "expected :block or :nonblock, got #{mode.inspect}" 105: end 106: end
The IP address of the peer (remote) end of the socket, as reported by the socket.
# File lib/net/ssh/transport/packet_stream.rb, line 61 61: def peer_ip 62: @peer_ip ||= 63: if respond_to?(:getpeername) 64: addr = getpeername 65: Socket.getnameinfo(addr, Socket::NI_NUMERICHOST | Socket::NI_NUMERICSERV).first 66: else 67: "<no hostip for proxy command>" 68: end 69: end
Enqueues a packet to be sent, and blocks until the entire packet is sent.
# File lib/net/ssh/transport/packet_stream.rb, line 110 110: def send_packet(payload) 111: enqueue_packet(payload) 112: wait_for_pending_sends 113: end
Protected instance methods
Called when this module is used to extend an object. It initializes the states and generally prepares the object for use as a packet stream.
# File lib/net/ssh/transport/packet_stream.rb, line 175 175: def initialize_ssh 176: @hints = {} 177: @server = State.new(self, :server) 178: @client = State.new(self, :client) 179: @packet = nil 180: initialize_buffered_io 181: end
Tries to read the next packet. If there is insufficient data to read an entire packet, this returns immediately, otherwise the packet is read, post-processed according to the cipher, hmac, and compression algorithms specified in the server state object, and returned as a new Packet object.
# File lib/net/ssh/transport/packet_stream.rb, line 188 188: def poll_next_packet 189: if @packet.nil? 190: minimum = server.block_size < 4 ? 4 : server.block_size 191: return nil if available < minimum 192: data = read_available(minimum) 193: 194: # decipher it 195: @packet = Net::SSH::Buffer.new(server.update_cipher(data)) 196: @packet_length = @packet.read_long 197: end 198: 199: need = @packet_length + 4 - server.block_size 200: raise Net::SSH::Exception, "padding error, need #{need} block #{server.block_size}" if need % server.block_size != 0 201: 202: return nil if available < need + server.hmac.mac_length 203: 204: if need > 0 205: # read the remainder of the packet and decrypt it. 206: data = read_available(need) 207: @packet.append(server.update_cipher(data)) 208: end 209: 210: # get the hmac from the tail of the packet (if one exists), and 211: # then validate it. 212: real_hmac = read_available(server.hmac.mac_length) || "" 213: 214: @packet.append(server.final_cipher) 215: padding_length = @packet.read_byte 216: 217: payload = @packet.read(@packet_length - padding_length - 1) 218: padding = @packet.read(padding_length) if padding_length > 0 219: 220: my_computed_hmac = server.hmac.digest([server.sequence_number, @packet.content].pack("NA*")) 221: raise Net::SSH::Exception, "corrupted mac detected" if real_hmac != my_computed_hmac 222: 223: # try to decompress the payload, in case compression is active 224: payload = server.decompress(payload) 225: 226: debug { "received packet nr #{server.sequence_number} type #{payload.getbyte(0)} len #{@packet_length}" } 227: 228: server.increment(@packet_length) 229: @packet = nil 230: 231: return Packet.new(payload) 232: end