#-- # ============================================================================= # Copyright (c) 2004,2005 Jamis Buck (jamis_buck@byu.edu) # All rights reserved. # # This source file is distributed as part of the Net::SSH Secure Shell Client # library for Ruby. This file (and the library as a whole) may be used only as # allowed by either the BSD license, or the Ruby license (or, by association # with the Ruby license, the GPL). See the "doc" subdirectory of the Net::SSH # distribution for the texts of these licenses. # ----------------------------------------------------------------------------- # net-ssh website : http://net-ssh.rubyforge.org # project website: http://rubyforge.org/projects/net-ssh # ============================================================================= #++ require 'thread' require 'net/ssh/errors' require 'net/ssh/transport/errors' module Net module SSH module Transport # The abstract parent of IncomingPacketStream and OutgoingPacketStream. It # represents the common interface of its subclasses. class PacketStream # the sequence number of the next packet to be processed. attr_reader :sequence_number # the setter for setting the socket to use for IO communication attr_writer :socket # Create a new packet stream. The given ciphers and hmacs are factories # that are used to initialize the cipher and mac attributes. def initialize( ciphers, hmacs ) @sequence_number = 0 @cipher = ciphers.get( "none" ) @hmac = hmacs.get( "none" ) end # Set the cipher and mac algorithms to the given arguments. def set_algorithms( cipher, mac ) @cipher, @hmac = cipher, mac end # Compute the mac for the given payload. def compute_hmac( payload ) @hmac.digest( [ @sequence_number, payload ].pack( "NA*" ) ) end # Increment the sequence number. This handles the (rare) case of a # sequence number overflowing a long integer, and resets it safely to 0 # (as required by the SSH2 protocol). def increment_sequence_number @sequence_number += 1 @sequence_number = 0 if @sequence_number > 0xFFFFFFFF end private :increment_sequence_number end # Handles the compression and encryption of outgoing packets. class OutgoingPacketStream < PacketStream # Create a new OutgoingPacketStream. def initialize( ciphers, hmacs, compressors ) super( ciphers, hmacs ) @compressor = compressors.fetch( "none" ) @mutex = Mutex.new end # Set the cipher, mac, and compressor to the given values. def set_algorithms( cipher, hmac, compressor ) super( cipher, hmac ) @compressor = compressor end # Send the given payload over the socket, after (possibly) compressing # and encrypting it. The payload is converted to a string (using #to_s) # before being manipulated. def send( payload ) @mutex.synchronize do # force the payload into a string payload = @compressor.compress( payload.to_s ) # the length of the packet, minus the padding actual_length = 4 + payload.length + 1 # compute the padding length padding_length = @cipher.block_size - ( actual_length % @cipher.block_size ) padding_length += @cipher.block_size if padding_length < 4 # compute the packet length (sans the length field itself) packet_length = payload.length + padding_length + 1 if packet_length < 16 padding_length += @cipher.block_size packet_length = payload.length + padding_length + 1 end padding = Array.new( padding_length ) { rand(256) }.pack("C*") unencrypted_data = [ packet_length, padding_length, payload, padding ].pack( "NCA*A*" ) mac = compute_hmac( unencrypted_data ) encrypted_data = @cipher.update( unencrypted_data ) << @cipher.final message = encrypted_data + mac @socket.send message, 0 increment_sequence_number end end end # Handles the decompression and dencryption of incoming packets. class IncomingPacketStream < PacketStream # A handle to the buffer factory to use when creating buffers attr_writer :buffers # A handle to the logger instance to use for writing log messages attr_writer :log # Create a new IncomingPacketStream. def initialize( ciphers, hmacs, decompressors ) super( ciphers, hmacs ) @decompressor = decompressors.fetch( "none" ) @mutex = Mutex.new end # Set the cipher, mac, and decompressor algorithms to the given values. def set_algorithms( cipher, mac, decompressor ) super( cipher, mac ) @decompressor = decompressor end # Retrieve the next packet from the string, after (possibly) decrypting # and decompressing it. The packet is returned as a reader buffer. def get @mutex.synchronize do # get the first block of data if @log.debug? @log.debug "reading #{@cipher.block_size} bytes from socket..." end data = read( @cipher.block_size ) # decipher it reader = @buffers.reader( @cipher.update( data ) ) # determine the packet length and how many bytes remain to be read packet_length = reader.read_long remaining_to_read = packet_length + 4 - @cipher.block_size if @log.debug? @log.debug "packet length(#{packet_length}) " + "remaining(#{remaining_to_read})" end # read the remainder of the packet and decrypt it. data = read( remaining_to_read ) reader.append @cipher.update( data ) reader.append @cipher.final padding_length = reader.read_byte payload = reader.read( packet_length - padding_length - 1 ) padding = reader.read( padding_length ) if padding_length > 0 # get the hmac from the tail of the packet (if one exists), and # then validate it. hmac = @hmac.mac_length > 0 ? @socket.recv( @hmac.mac_length ) : "" my_computed_hmac = compute_hmac( reader.content ) raise Net::SSH::Exception, "corrupted mac detected" if hmac != my_computed_hmac # decompress the payload payload = @decompressor.decompress( payload ) increment_sequence_number buffer = @buffers.reader( payload ) @log.debug "received: #{buffer.content.inspect}" if @log.debug? return buffer end end def read( length ) if IO === @socket data = "" while data.length < length break if @socket.closed? if ( IO.select([@socket],nil,nil,0.01) rescue nil ) data << @socket.sysread(length-data.length) end end else data = @socket.recv(length) end # if the data is less than expected, the socket was closed if data.nil? || data.length < length raise Net::SSH::Transport::Disconnect, "connection closed by remote host" end data end private :read end end end end