1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
|
#--
# =============================================================================
# Copyright (c) 2004,2005 Jamis Buck (jamis@37signals.com)
# 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 )
# get the hmac from the tail of the packet (if one exists), and
# then validate it.
hmac = @hmac.mac_length > 0 ? read( @hmac.mac_length ) : ""
reader.append @cipher.update( data ) unless data.empty?
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
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.read(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
|