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