lib/hrr_rb_ssh/transport.rb
require 'hrr_rb_ssh/version'
require 'hrr_rb_ssh/error/closed_transport'
require 'hrr_rb_ssh/transport/constant'
require 'hrr_rb_ssh/transport/direction'
require 'hrr_rb_ssh/transport/sequence_number'
require 'hrr_rb_ssh/transport/sender'
require 'hrr_rb_ssh/transport/receiver'
require 'hrr_rb_ssh/transport/kex_algorithms'
require 'hrr_rb_ssh/transport/server_host_key_algorithm'
require 'hrr_rb_ssh/transport/encryption_algorithm'
require 'hrr_rb_ssh/transport/mac_algorithm'
require 'hrr_rb_ssh/transport/compression_algorithm'
module HrrRbSsh
class Transport
include Loggable
include Constant
attr_reader \
:io,
:mode,
:supported_encryption_algorithms,
:supported_server_host_key_algorithms,
:supported_kex_algorithms,
:supported_mac_algorithms,
:supported_compression_algorithms,
:preferred_encryption_algorithms,
:preferred_server_host_key_algorithms,
:preferred_kex_algorithms,
:preferred_mac_algorithms,
:preferred_compression_algorithms,
:incoming_sequence_number,
:outgoing_sequence_number,
:server_host_key_algorithm,
:incoming_encryption_algorithm,
:incoming_mac_algorithm,
:incoming_compression_algorithm,
:outgoing_encryption_algorithm,
:outgoing_mac_algorithm,
:outgoing_compression_algorithm,
:v_c,
:v_s,
:i_c,
:i_s,
:session_id
def initialize io, mode, options={}, logger: nil
self.logger = logger
@io = io
@mode = mode
@options = options
@closed = nil
@in_kex = false
@sender = Sender.new logger: logger
@receiver = Receiver.new logger: logger
@sender_monitor = Monitor.new
@receiver_monitor = Monitor.new
@local_version = (@options.delete('local_version') || "SSH-2.0-HrrRbSsh-#{VERSION}").encode(Encoding::ASCII_8BIT)
@remote_version = nil
@kex_algorithms = KexAlgorithms.new logger: logger
@incoming_sequence_number = SequenceNumber.new
@outgoing_sequence_number = SequenceNumber.new
@acceptable_services = Array.new
update_supported_algorithms
update_preferred_algorithms
initialize_local_algorithms
initialize_algorithms
end
def register_acceptable_service service_name
@acceptable_services.push service_name
end
def send payload
raise Error::ClosedTransport if @closed
@sender_monitor.synchronize do
begin
@sender.send self, payload
rescue IOError, SystemCallError => e
log_info { "#{e.message} (#{e.class})" }
close
raise Error::ClosedTransport
rescue => e
log_error { [e.backtrace[0], ": ", e.message, " (", e.class.to_s, ")\n\t", e.backtrace[1..-1].join("\n\t")].join }
close
raise Error::ClosedTransport
end
end
end
def receive
raise Error::ClosedTransport if @closed
@receiver_monitor.synchronize do
begin
payload = @receiver.receive self
case payload[0,1].unpack("C")[0]
when Messages::SSH_MSG_DISCONNECT::VALUE
log_info { "received disconnect message" }
message = Messages::SSH_MSG_DISCONNECT.new(logger: logger).decode payload
close
raise Error::ClosedTransport
when Messages::SSH_MSG_IGNORE::VALUE
log_info { "received ignore message" }
message = Messages::SSH_MSG_IGNORE.new(logger: logger).decode payload
receive
when Messages::SSH_MSG_UNIMPLEMENTED::VALUE
log_info { "received unimplemented message" }
message = Messages::SSH_MSG_UNIMPLEMENTED.new(logger: logger).decode payload
receive
when Messages::SSH_MSG_DEBUG::VALUE
log_info { "received debug message" }
message = Messages::SSH_MSG_DEBUG.new(logger: logger).decode payload
receive
when Messages::SSH_MSG_KEXINIT::VALUE
log_info { "received kexinit message" }
if @in_kex
payload
else
exchange_key payload
receive
end
else
payload
end
rescue Error::ClosedTransport
raise
rescue EOFError, IOError, SystemCallError => e
log_info { "#{e.message} (#{e.class})" }
close
raise Error::ClosedTransport
rescue => e
log_error { [e.backtrace[0], ": ", e.message, " (", e.class.to_s, ")\n\t", e.backtrace[1..-1].join("\n\t")].join }
close
raise Error::ClosedTransport
end
end
end
def start
log_info { "start transport" }
begin
exchange_version
exchange_key
case @mode
when Mode::SERVER
verify_service_request
when Mode::CLIENT
send_service_request
end
@closed = false
rescue Error::ClosedTransport
raise
rescue EOFError, IOError, SystemCallError => e
log_info { "#{e.message} (#{e.class})" }
close
raise Error::ClosedTransport
rescue => e
log_error { [e.backtrace[0], ": ", e.message, " (", e.class.to_s, ")\n\t", e.backtrace[1..-1].join("\n\t")].join }
close
raise Error::ClosedTransport
else
log_info { "transport started" }
end
end
def close
@sender_monitor.synchronize do
return if @closed
log_info { "close transport" }
begin
disconnect
@incoming_compression_algorithm.close
@outgoing_compression_algorithm.close
rescue => e
log_error { [e.backtrace[0], ": ", e.message, " (", e.class.to_s, ")\n\t", e.backtrace[1..-1].join("\n\t")].join }
ensure
@closed = true
log_info { "transport closed" }
end
end
end
def closed?
@closed
end
def disconnect
log_info { "disconnect transport" }
send_disconnect
log_info { "transport disconnected" }
end
def exchange_version
send_version
receive_version
update_version_strings
end
def exchange_key payload=nil
@in_kex = true
@sender_monitor.synchronize do
@receiver_monitor.synchronize do
send_kexinit
if payload
receive_kexinit payload
else
receive_kexinit receive
end
update_kex_and_server_host_key_algorithms
start_kex_algorithm
send_newkeys
receive_newkeys receive
update_encryption_mac_compression_algorithms
end
end
@in_kex = false
end
def start_kex_algorithm
@kex_algorithm.start self
end
def verify_service_request
service_request_message = receive_service_request
service_name = service_request_message[:'service name']
if @acceptable_services.include? service_name
send_service_accept service_name
else
close
end
end
def update_supported_algorithms
@supported_kex_algorithms = @kex_algorithms.list_supported
@supported_server_host_key_algorithms = ServerHostKeyAlgorithm.list_supported
@supported_encryption_algorithms = EncryptionAlgorithm.list_supported
@supported_mac_algorithms = MacAlgorithm.list_supported
@supported_compression_algorithms = CompressionAlgorithm.list_supported
end
def update_preferred_algorithms
@preferred_kex_algorithms = @options['transport_preferred_kex_algorithms'] || @kex_algorithms.list_preferred
@preferred_server_host_key_algorithms = @options['transport_preferred_server_host_key_algorithms'] || ServerHostKeyAlgorithm.list_preferred
@preferred_encryption_algorithms = @options['transport_preferred_encryption_algorithms'] || EncryptionAlgorithm.list_preferred
@preferred_mac_algorithms = @options['transport_preferred_mac_algorithms'] || MacAlgorithm.list_preferred
@preferred_compression_algorithms = @options['transport_preferred_compression_algorithms'] || CompressionAlgorithm.list_preferred
check_if_preferred_algorithms_are_supported
end
def check_if_preferred_algorithms_are_supported
[
['kex', @preferred_kex_algorithms, @supported_kex_algorithms ],
['server host key', @preferred_server_host_key_algorithms, @supported_server_host_key_algorithms],
['encryption', @preferred_encryption_algorithms, @supported_encryption_algorithms ],
['mac', @preferred_mac_algorithms, @supported_mac_algorithms ],
['compression', @preferred_compression_algorithms, @supported_compression_algorithms ],
].each{ |algorithm_name, list_preferred, list_supported|
list_preferred.each{ |a|
unless list_supported.include? a
raise ArgumentError, "#{algorithm_name} algorithm #{a} is not supported"
end
}
}
end
def initialize_local_algorithms
@local_kex_algorithms = @preferred_kex_algorithms
@local_server_host_key_algorithms = @preferred_server_host_key_algorithms
@local_encryption_algorithms_client_to_server = @preferred_encryption_algorithms
@local_encryption_algorithms_server_to_client = @preferred_encryption_algorithms
@local_mac_algorithms_client_to_server = @preferred_mac_algorithms
@local_mac_algorithms_server_to_client = @preferred_mac_algorithms
@local_compression_algorithms_client_to_server = @preferred_compression_algorithms
@local_compression_algorithms_server_to_client = @preferred_compression_algorithms
end
def initialize_algorithms
@incoming_encryption_algorithm = EncryptionAlgorithm['none'].new
@incoming_mac_algorithm = MacAlgorithm['none'].new
@incoming_compression_algorithm = CompressionAlgorithm['none'].new
@outgoing_encryption_algorithm = EncryptionAlgorithm['none'].new
@outgoing_mac_algorithm = MacAlgorithm['none'].new
@outgoing_compression_algorithm = CompressionAlgorithm['none'].new
end
def send_version
@io.write (@local_version + CR + LF)
end
def receive_version
str_io = StringIO.new
loop do
str_io.write @io.read(1)
if str_io.string[-2..-1] == "#{CR}#{LF}"
if str_io.string[0..3] == "SSH-"
@remote_version = str_io.string[0..-3].encode(Encoding::ASCII_8BIT)
log_info { "received remote version string: #{@remote_version}" }
break
else
log_info { "received message before remote version string: #{str_io.string}" }
str_io.rewind
str_io.truncate(0)
end
end
end
end
def update_version_strings
case @mode
when Mode::SERVER
@v_c = @remote_version
@v_s = @local_version
when Mode::CLIENT
@v_c = @local_version
@v_s = @remote_version
end
end
def send_disconnect
message = {
:'message number' => Messages::SSH_MSG_DISCONNECT::VALUE,
:'reason code' => Messages::SSH_MSG_DISCONNECT::ReasonCode::SSH_DISCONNECT_BY_APPLICATION,
:'description' => "disconnected by user",
:'language tag' => ""
}
payload = Messages::SSH_MSG_DISCONNECT.new(logger: logger).encode message
@sender_monitor.synchronize do
begin
@sender.send self, payload
rescue IOError, SystemCallError => e
log_info { "#{e.message} (#{e.class})" }
rescue => e
log_error { [e.backtrace[0], ": ", e.message, " (", e.class.to_s, ")\n\t", e.backtrace[1..-1].join("\n\t")].join }
end
end
end
def send_kexinit
message = {
:'message number' => Messages::SSH_MSG_KEXINIT::VALUE,
:'cookie (random byte)' => lambda { rand(0x01_00) },
:'kex_algorithms' => @local_kex_algorithms,
:'server_host_key_algorithms' => @local_server_host_key_algorithms,
:'encryption_algorithms_client_to_server' => @local_encryption_algorithms_client_to_server,
:'encryption_algorithms_server_to_client' => @local_encryption_algorithms_server_to_client,
:'mac_algorithms_client_to_server' => @local_mac_algorithms_client_to_server,
:'mac_algorithms_server_to_client' => @local_mac_algorithms_server_to_client,
:'compression_algorithms_client_to_server' => @local_compression_algorithms_client_to_server,
:'compression_algorithms_server_to_client' => @local_compression_algorithms_server_to_client,
:'languages_client_to_server' => [],
:'languages_server_to_client' => [],
:'first_kex_packet_follows' => false,
:'0 (reserved for future extension)' => 0,
}
payload = Messages::SSH_MSG_KEXINIT.new(logger: logger).encode message
send payload
case @mode
when Mode::SERVER
@i_s = payload
when Mode::CLIENT
@i_c = payload
end
end
def receive_kexinit payload
case @mode
when Mode::SERVER
@i_c = payload
when Mode::CLIENT
@i_s = payload
end
message = Messages::SSH_MSG_KEXINIT.new(logger: logger).decode payload
update_remote_algorithms message
end
def send_newkeys
message = {
:'message number' => Messages::SSH_MSG_NEWKEYS::VALUE,
}
payload = Messages::SSH_MSG_NEWKEYS.new(logger: logger).encode message
send payload
end
def receive_newkeys payload
message = Messages::SSH_MSG_NEWKEYS.new(logger: logger).decode payload
end
def send_service_request
message = {
:'message number' => Messages::SSH_MSG_SERVICE_REQUEST::VALUE,
:'service name' => 'ssh-userauth',
}
payload = Messages::SSH_MSG_SERVICE_REQUEST.new(logger: logger).encode message
send payload
payload = @receiver.receive self
message = Messages::SSH_MSG_SERVICE_ACCEPT.new(logger: logger).decode payload
end
def receive_service_request
payload = @receiver.receive self
message = Messages::SSH_MSG_SERVICE_REQUEST.new(logger: logger).decode payload
end
def send_service_accept service_name
message = {
:'message number' => Messages::SSH_MSG_SERVICE_ACCEPT::VALUE,
:'service name' => service_name,
}
payload = Messages::SSH_MSG_SERVICE_ACCEPT.new(logger: logger).encode message
send payload
end
def update_remote_algorithms message
@remote_kex_algorithms = message[:'kex_algorithms']
@remote_server_host_key_algorithms = message[:'server_host_key_algorithms']
@remote_encryption_algorithms_client_to_server = message[:'encryption_algorithms_client_to_server']
@remote_encryption_algorithms_server_to_client = message[:'encryption_algorithms_server_to_client']
@remote_mac_algorithms_client_to_server = message[:'mac_algorithms_client_to_server']
@remote_mac_algorithms_server_to_client = message[:'mac_algorithms_server_to_client']
@remote_compression_algorithms_client_to_server = message[:'compression_algorithms_client_to_server']
@remote_compression_algorithms_server_to_client = message[:'compression_algorithms_server_to_client']
end
def update_kex_and_server_host_key_algorithms
case @mode
when Mode::SERVER
kex_algorithm_name = @remote_kex_algorithms.find{ |a| @local_kex_algorithms.include? a } or raise
server_host_key_algorithm_name = @remote_server_host_key_algorithms.find{ |a| @local_server_host_key_algorithms.include? a } or raise
server_secret_host_key = @options.fetch('transport_server_secret_host_keys', {}).fetch(server_host_key_algorithm_name, nil)
when Mode::CLIENT
kex_algorithm_name = @local_kex_algorithms.find{ |a| @remote_kex_algorithms.include? a } or raise
server_host_key_algorithm_name = @local_server_host_key_algorithms.find{ |a| @remote_server_host_key_algorithms.include? a } or raise
server_secret_host_key = nil
end
@server_host_key_algorithm = ServerHostKeyAlgorithm[server_host_key_algorithm_name].new server_secret_host_key
@kex_algorithm = @kex_algorithms.instantiate(kex_algorithm_name)
end
def update_encryption_mac_compression_algorithms
@session_id ||= @kex_algorithm.hash(self)
update_encryption_algorithm
update_mac_algorithm
update_compression_algorithm
end
def update_encryption_algorithm
case @mode
when Mode::SERVER
encryption_algorithm_c_to_s_name = @remote_encryption_algorithms_client_to_server.find{ |a| @local_encryption_algorithms_client_to_server.include? a } or raise
encryption_algorithm_s_to_c_name = @remote_encryption_algorithms_server_to_client.find{ |a| @local_encryption_algorithms_server_to_client.include? a } or raise
incoming_encryption_algorithm_name = encryption_algorithm_c_to_s_name
outgoing_encryption_algorithm_name = encryption_algorithm_s_to_c_name
incoming_crpt_iv = @kex_algorithm.iv_c_to_s self, incoming_encryption_algorithm_name
outgoing_crpt_iv = @kex_algorithm.iv_s_to_c self, outgoing_encryption_algorithm_name
incoming_crpt_key = @kex_algorithm.key_c_to_s self, incoming_encryption_algorithm_name
outgoing_crpt_key = @kex_algorithm.key_s_to_c self, outgoing_encryption_algorithm_name
when Mode::CLIENT
encryption_algorithm_s_to_c_name = @local_encryption_algorithms_server_to_client.find{ |a| @remote_encryption_algorithms_server_to_client.include? a } or raise
encryption_algorithm_c_to_s_name = @local_encryption_algorithms_client_to_server.find{ |a| @remote_encryption_algorithms_client_to_server.include? a } or raise
incoming_encryption_algorithm_name = encryption_algorithm_s_to_c_name
outgoing_encryption_algorithm_name = encryption_algorithm_c_to_s_name
incoming_crpt_iv = @kex_algorithm.iv_s_to_c self, incoming_encryption_algorithm_name
outgoing_crpt_iv = @kex_algorithm.iv_c_to_s self, outgoing_encryption_algorithm_name
incoming_crpt_key = @kex_algorithm.key_s_to_c self, incoming_encryption_algorithm_name
outgoing_crpt_key = @kex_algorithm.key_c_to_s self, outgoing_encryption_algorithm_name
end
@incoming_encryption_algorithm = EncryptionAlgorithm[incoming_encryption_algorithm_name].new Direction::INCOMING, incoming_crpt_iv, incoming_crpt_key
@outgoing_encryption_algorithm = EncryptionAlgorithm[outgoing_encryption_algorithm_name].new Direction::OUTGOING, outgoing_crpt_iv, outgoing_crpt_key
end
def update_mac_algorithm
case @mode
when Mode::SERVER
mac_algorithm_c_to_s_name = @remote_mac_algorithms_client_to_server.find{ |a| @local_mac_algorithms_client_to_server.include? a } or raise
mac_algorithm_s_to_c_name = @remote_mac_algorithms_server_to_client.find{ |a| @local_mac_algorithms_server_to_client.include? a } or raise
incoming_mac_algorithm_name = mac_algorithm_c_to_s_name
outgoing_mac_algorithm_name = mac_algorithm_s_to_c_name
incoming_mac_key = @kex_algorithm.mac_c_to_s self, incoming_mac_algorithm_name
outgoing_mac_key = @kex_algorithm.mac_s_to_c self, outgoing_mac_algorithm_name
when Mode::CLIENT
mac_algorithm_s_to_c_name = @local_mac_algorithms_server_to_client.find{ |a| @remote_mac_algorithms_server_to_client.include? a } or raise
mac_algorithm_c_to_s_name = @local_mac_algorithms_client_to_server.find{ |a| @remote_mac_algorithms_client_to_server.include? a } or raise
incoming_mac_algorithm_name = mac_algorithm_s_to_c_name
outgoing_mac_algorithm_name = mac_algorithm_c_to_s_name
incoming_mac_key = @kex_algorithm.mac_s_to_c self, incoming_mac_algorithm_name
outgoing_mac_key = @kex_algorithm.mac_c_to_s self, outgoing_mac_algorithm_name
end
@incoming_mac_algorithm = MacAlgorithm[incoming_mac_algorithm_name].new incoming_mac_key
@outgoing_mac_algorithm = MacAlgorithm[outgoing_mac_algorithm_name].new outgoing_mac_key
end
def update_compression_algorithm
case @mode
when Mode::SERVER
compression_algorithm_c_to_s_name = @remote_compression_algorithms_client_to_server.find{ |a| @local_compression_algorithms_client_to_server.include? a } or raise
compression_algorithm_s_to_c_name = @remote_compression_algorithms_server_to_client.find{ |a| @local_compression_algorithms_server_to_client.include? a } or raise
incoming_compression_algorithm_name = compression_algorithm_c_to_s_name
outgoing_compression_algorithm_name = compression_algorithm_s_to_c_name
when Mode::CLIENT
compression_algorithm_s_to_c_name = @local_compression_algorithms_server_to_client.find{ |a| @remote_compression_algorithms_server_to_client.include? a } or raise
compression_algorithm_c_to_s_name = @local_compression_algorithms_client_to_server.find{ |a| @remote_compression_algorithms_client_to_server.include? a } or raise
incoming_compression_algorithm_name = compression_algorithm_s_to_c_name
outgoing_compression_algorithm_name = compression_algorithm_c_to_s_name
end
@incoming_compression_algorithm.close
@outgoing_compression_algorithm.close
@incoming_compression_algorithm = CompressionAlgorithm[incoming_compression_algorithm_name].new Direction::INCOMING
@outgoing_compression_algorithm = CompressionAlgorithm[outgoing_compression_algorithm_name].new Direction::OUTGOING
end
end
end