require 'socket'
require 'net/ssh/errors'
require 'net/ssh/loggable'
require 'net/ssh/version'
require 'net/ssh/transport/algorithms'
require 'net/ssh/transport/constants'
require 'net/ssh/transport/packet_stream'
require 'net/ssh/transport/server_version'
require 'net/ssh/verifiers/accept_new_or_local_tunnel'
require 'net/ssh/verifiers/accept_new'
require 'net/ssh/verifiers/always'
require 'net/ssh/verifiers/never'
module Net
module SSH
module Transport
# The transport layer represents the lowest level of the SSH protocol, and
# implements basic message exchanging and protocol initialization. It will
# never be instantiated directly (unless you really know what you're about),
# but will instead be created for you automatically when you create a new
# SSH session via Net::SSH.start.
class Session
include Loggable
include Constants
# The standard port for the SSH protocol.
DEFAULT_PORT = 22
# The host to connect to, as given to the constructor.
attr_reader :host
# The port number to connect to, as given in the options to the constructor.
# If no port number was given, this will default to DEFAULT_PORT.
attr_reader :port
# The underlying socket object being used to communicate with the remote
# host.
attr_reader :socket
# The ServerVersion instance that encapsulates the negotiated protocol
# version.
attr_reader :server_version
# The Algorithms instance used to perform key exchanges.
attr_reader :algorithms
# The host-key verifier object used to verify host keys, to ensure that
# the connection is not being spoofed.
attr_reader :host_key_verifier
# The hash of options that were given to the object at initialization.
attr_reader :options
# Instantiates a new transport layer abstraction. This will block until
# the initial key exchange completes, leaving you with a ready-to-use
# transport session.
def initialize(host, options = {})
self.logger = options[:logger]
@host = host
@port = options[:port] || DEFAULT_PORT
@bind_address = options[:bind_address] || nil
@options = options
@socket =
if (factory = options[:proxy])
debug { "establishing connection to #{@host}:#{@port} through proxy" }
factory.open(@host, @port, options)
else
debug { "establishing connection to #{@host}:#{@port}" }
Socket.tcp(@host, @port, @bind_address, nil,
connect_timeout: options[:timeout])
end
@socket.extend(PacketStream)
@socket.logger = @logger
debug { "connection established" }
@queue = []
@host_key_verifier = select_host_key_verifier(options[:verify_host_key])
@server_version = ServerVersion.new(socket, logger, options[:timeout])
@algorithms = Algorithms.new(self, options)
@algorithms.start
wait { algorithms.initialized? }
rescue Errno::ETIMEDOUT
raise Net::SSH::ConnectionTimeout
end
def host_keys
@host_keys ||= begin
known_hosts = options.fetch(:known_hosts, KnownHosts)
known_hosts.search_for(options[:host_key_alias] || host_as_string, options)
end
end
# Returns the host (and possibly IP address) in a format compatible with
# SSH known-host files.
def host_as_string
@host_as_string ||= begin
string = "#{host}"
string = "[#{string}]:#{port}" if port != DEFAULT_PORT
peer_ip = socket.peer_ip
if peer_ip != Net::SSH::Transport::PacketStream::PROXY_COMMAND_HOST_IP &&
peer_ip != host
string2 = peer_ip
string2 = "[#{string2}]:#{port}" if port != DEFAULT_PORT
string << "," << string2
end
string
end
end
# Returns true if the underlying socket has been closed.
def closed?
socket.closed?
end
# Cleans up (see PacketStream#cleanup) and closes the underlying socket.
def close
socket.cleanup
socket.close
end
# Performs a "hard" shutdown of the connection. In general, this should
# never be done, but it might be necessary (in a rescue clause, for instance,
# when the connection needs to close but you don't know the status of the
# underlying protocol's state).
def shutdown!
error { "forcing connection closed" }
socket.close
end
# Returns a new service_request packet for the given service name, ready
# for sending to the server.
def service_request(service)
Net::SSH::Buffer.from(:byte, SERVICE_REQUEST, :string, service)
end
# Requests a rekey operation, and blocks until the operation completes.
# If a rekey is already pending, this returns immediately, having no
# effect.
def rekey!
if !algorithms.pending?
algorithms.rekey!
wait { algorithms.initialized? }
end
end
# Returns immediately if a rekey is already in process. Otherwise, if a
# rekey is needed (as indicated by the socket, see PacketStream#if_needs_rekey?)
# one is performed, causing this method to block until it completes.
def rekey_as_needed
return if algorithms.pending?
socket.if_needs_rekey? { rekey! }
end
# Returns a hash of information about the peer (remote) side of the socket,
# including :ip, :port, :host, and :canonized (see #host_as_string).
def peer
@peer ||= { ip: socket.peer_ip, port: @port.to_i, host: @host, canonized: host_as_string }
end
# Blocks until a new packet is available to be read, and returns that
# packet. See #poll_message.
def next_message
poll_message(:block)
end
# Tries to read the next packet from the socket. If mode is :nonblock (the
# default), this will not block and will return nil if there are no packets
# waiting to be read. Otherwise, this will block until a packet is
# available. Note that some packet types (DISCONNECT, IGNORE, UNIMPLEMENTED,
# DEBUG, and KEXINIT) are handled silently by this method, and will never
# be returned.
#
# If a key-exchange is in process and a disallowed packet type is
# received, it will be enqueued and otherwise ignored. When a key-exchange
# is not in process, and consume_queue is true, packets will be first
# read from the queue before the socket is queried.
def poll_message(mode = :nonblock, consume_queue = true)
loop do
return @queue.shift if consume_queue && @queue.any? && algorithms.allow?(@queue.first)
packet = socket.next_packet(mode, options[:timeout])
return nil if packet.nil?
case packet.type
when DISCONNECT
raise Net::SSH::Disconnect, "disconnected: #{packet[:description]} (#{packet[:reason_code]})"
when IGNORE
debug { "IGNORE packet received: #{packet[:data].inspect}" }
when UNIMPLEMENTED
lwarn { "UNIMPLEMENTED: #{packet[:number]}" }
when DEBUG
send(packet[:always_display] ? :fatal : :debug) { packet[:message] }
when KEXINIT
algorithms.accept_kexinit(packet)
else
return packet if algorithms.allow?(packet)
push(packet)
end
end
end
# Waits (blocks) until the given block returns true. If no block is given,
# this just waits long enough to see if there are any pending packets. Any
# packets read are enqueued (see #push).
def wait
loop do
break if block_given? && yield
message = poll_message(:nonblock, false)
push(message) if message
break if !block_given?
end
end
# Adds the given packet to the packet queue. If the queue is non-empty,
# #poll_message will return packets from the queue in the order they
# were received.
def push(packet)
@queue.push(packet)
end
# Sends the given message via the packet stream, blocking until the
# entire message has been sent.
def send_message(message)
socket.send_packet(message)
end
# Enqueues the given message, such that it will be sent at the earliest
# opportunity. This does not block, but returns immediately.
def enqueue_message(message)
socket.enqueue_packet(message)
end
# Configure's the packet stream's client state with the given set of
# options. This is typically used to define the cipher, compression, and
# hmac algorithms to use when sending packets to the server.
def configure_client(options = {})
socket.client.set(options)
end
# Configure's the packet stream's server state with the given set of
# options. This is typically used to define the cipher, compression, and
# hmac algorithms to use when reading packets from the server.
def configure_server(options = {})
socket.server.set(options)
end
# Sets a new hint for the packet stream, which the packet stream may use
# to change its behavior. (See PacketStream#hints).
def hint(which, value = true)
socket.hints[which] = value
end
public
# this method is primarily for use in tests
attr_reader :queue # :nodoc:
private
# Compatibility verifier which allows users to keep using
# custom verifier code without adding new :verify_signature
# method.
class CompatibleVerifier
def initialize(verifier)
@verifier = verifier
end
def verify(arguments)
@verifier.verify(arguments)
end
def verify_signature(&block)
yield
end
end
# Instantiates a new host-key verification class, based on the value of
# the parameter.
#
# Usually, the argument is a symbol like `:never` which corresponds to
# a verifier, like `::Net::SSH::Verifiers::Never`.
#
# - :never (very insecure)
# - :accept_new_or_local_tunnel (insecure)
# - :accept_new (insecure)
# - :always (secure)
#
# If the argument happens to respond to :verify and :verify_signature,
# it is returned directly. Otherwise, an exception is raised.
#
# Values false, true, and :very were deprecated in
# [#595](https://github.com/net-ssh/net-ssh/pull/595)
def select_host_key_verifier(verifier)
case verifier
when false
Kernel.warn('verify_host_key: false is deprecated, use :never')
Net::SSH::Verifiers::Never.new
when :never then
Net::SSH::Verifiers::Never.new
when true
Kernel.warn('verify_host_key: true is deprecated, use :accept_new_or_local_tunnel')
Net::SSH::Verifiers::AcceptNewOrLocalTunnel.new
when :accept_new_or_local_tunnel, nil then
Net::SSH::Verifiers::AcceptNewOrLocalTunnel.new
when :very
Kernel.warn('verify_host_key: :very is deprecated, use :accept_new')
Net::SSH::Verifiers::AcceptNew.new
when :accept_new then
Net::SSH::Verifiers::AcceptNew.new
when :secure then
Kernel.warn('verify_host_key: :secure is deprecated, use :always')
Net::SSH::Verifiers::Always.new
when :always then
Net::SSH::Verifiers::Always.new
else
if verifier.respond_to?(:verify)
if verifier.respond_to?(:verify_signature)
verifier
else
Kernel.warn("Warning: verifier without :verify_signature is deprecated")
CompatibleVerifier.new(verifier)
end
else
raise(
ArgumentError,
"Invalid argument to :verify_host_key (or deprecated " \
":paranoid): #{verifier.inspect}"
)
end
end
end
end
end
end
end