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 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
|
#--
# =============================================================================
# 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 'needle'
require 'net/ssh/errors'
module Net
module SSH
# Encapsulates a single session (connection) to a server via SSH.
class Session
# The dependency-injection registry used by this session.
attr_reader :registry
# The name of the host that this session is connected to.
attr_reader :host
# The hash of options that were used to establish this session.
attr_reader :options
# The underlying connection
attr_reader :connection
# Create a new SSH session. This method polymorphically accepts a
# variable number of parameters, as follows:
#
# * 1 parameter: must be the hostname to connect to.
# * 2 parameters: must be the hostname, and either the port (as an
# integer) or the username to connect as.
# * 3 parameters: must be the hostname, and either the port (as an
# integer) and username, or the username and the password.
# * 4 parameters: must be the hostname, port, username, and password.
#
# Any scenario above that omits the username assumes that the USER
# environment variable is set to the user's name. Any scenario above that
# omits the password assumes that the user will log in without a password
# (ie, using a public key). Any scenario above that omits the port number
# assumes a port number of 22 (the default for SSH).
#
# Any of the above scenarios may also accept a Hash as the last
# parameter, specifying a list of additional options to be used to
# initialize the session. (See Net::SSH::Session.add_options).
#
# Alternatively, named parameters may be used, in which case the first
# parameter is positional and is always the host to connect to, following
# which you may specify any of the following named parameters (as
# symbols):
#
# * :port
# * :username
# * :password
#
# Any additional parameters are treated as options that configure how the
# connection behaves.
#
# Allowed options are:
#
# * :keys (the list of filenames identifying the user's keys)
# * :host_keys (the list of filenames identifying the host's keys)
# * :auth_methods (a list of authentication methods to use)
# * :crypto_backend (defaults to :ossl, and specifies the cryptography
# backend to use)
# * :registry_options (a hash of options to use when creating the
# registry)
# * :container (the registry to use. If not specified, a new registry
# will be created)
# * :verbose (how verbose the logging output should be. Defaults to
# :warn).
# * :log (the name of the file, or the IO object, to which messages will
# be logged. Defaults to STDERR.)
# * :forward_agent (true or false, whether or not to forward requests
# for the authentication agent. Defaults to false.)
# * :paranoid (either false, in which case server fingerprints are not
# verified, true, in which case they are verified and mismatches result
# in a warning and a prompt, or an object responding to :allow?, which
# will be invoked and should return true or false for whether or not
# to allow the connection. Defaults to true.)
#
# Also, any options recognized by Net::SSH::Transport::Session may be
# given, and will be passed through to initialize the transport session.
#
# If a block is given to this method, then it is called with the new
# session object. The session object is then closed when the block
# terminates. If a block is not given, then the session object is
# returned (and must be closed explicitly).
def initialize( *args )
@open = false
@agent_forwarded = false
process_arguments( *args )
@registry.define do |b|
b.crypto_backend { @crypto_backend }
b.transport_host { @host }
b.transport_options { @options }
b.userauth_keys { @keys }
b.userauth_host_keys { @host_keys }
b.userauth_method_order { @auth_methods }
b.host_key_verifier { @host_key_verifier }
# Register myself with the registry, so that other services may
# access me.
b.session( :pipeline => [] ) { self }
b.prompter do
require 'net/ssh/util/prompter'
Net::SSH::Util::Prompter.new
end
b.require 'net/ssh/transport/services', "Net::SSH::Transport"
b.require 'net/ssh/connection/services', "Net::SSH::Connection"
b.require 'net/ssh/userauth/services', "Net::SSH::UserAuth"
b.require 'net/ssh/service/services', "Net::SSH::Service"
end
userauth = @registry[:userauth][:driver]
if userauth.authenticate( "ssh-connection", @username, @password )
@open = true
@connection = @registry[:connection][:driver]
if block_given?
yield self
close
end
else
@registry[:transport][:session].close
raise AuthenticationFailed, @username
end
end
# Closes the session, if it is open. If it is not open, this does
# nothing.
def close
@registry[:transport][:session].close if @open
@open = false
end
# Returns +true+ if the session is currently open.
def open?
@open
end
# Opens a new communication channel over the current connection. This
# returns immediately. The block will be invoked when then the channel
# has been opened. (See Net::SSH::Connection::Driver#open_channel).
def open_channel( type="session", data=nil, &block )
sanity_check
channel = @connection.open_channel( type, data )
channel.on_confirm_open(&block)
# If we have an agent, and agent-forwarding is enabled, set up
# the forwarding. Do this once only, after the first channel
# is opened.
if @forward_agent && @registry[:userauth].agent
unless @agent_forwarded
agentforward.request
@agent_forwarded = true
end
end
channel
end
# Enters the main communication loop. This processes events occuring over
# the channel. If a block is given, the loop will continue for as long
# as the block returns +true+. Otherwise, the loop continues until there
# are no more open channels. (See Net::SSH::Connection::Driver#loop).
def loop( &block )
sanity_check
@connection.loop(&block)
end
# Provides convenient access to services that have been registered with
# the session, such as "process" and "forward".
#
# Usage:
#
# session.forward.local(...)
def method_missing( sym, *args, &block )
if args.empty? && block.nil? && @registry[:services].has_key?( sym )
return @registry[:services][ sym ]
else
super
end
end
# Processes the argument list, determining the meaning of each argument
# and allowing polymorphic argument lists. (See #initialize).
def process_arguments( *args )
@options = {}
@username = ENV['USER'] || ENV['USERNAME']
raise ArgumentError,
"you must specify the host to connect to" if args.length < 1
@host = args.shift
# support for both named arguments, and positional arguments...
if args.length == 1 && args[0].is_a?( Hash ) &&
( args[0][:username] || args[0][:password] ||
args[0][:port] || args[0][:options] )
# then
@username = args[0][:username] || @username
@password = args[0][:password]
@options.update args.shift
else
@options[ :port ] = args.shift if args.first.is_a? Numeric
if args.first.nil? || args.first.is_a?( String )
@username = args.shift || @username
end
if args.first.nil? || args.first.is_a?( String )
@password = args.shift
end
@options.update args.shift if args.first.is_a?( Hash )
end
if !args.empty?
raise ArgumentError, "extra parameters detected: #{args.inspect}"
elsif @username.nil?
raise ArgumentError, "no username was given and none could be inferred from the environment"
end
@keys = @options[ :keys ]
@host_keys = @options[ :host_keys ]
@auth_methods = @options[ :auth_methods ]
@forward_agent = @options[ :forward_agent ] || false
@crypto_backend = @options.fetch( :crypto_backend, :ossl )
@host_key_verifier = host_key_verifier_from(@options.fetch(:paranoid, true))
verbose = @options.fetch( :verbose, :warn )
log = @options.fetch( :log, STDERR )
@registry_options = @options.fetch( :registry_options, {} )
@registry_options[ :logs ] ||= {}
@registry_options[ :logs ][ :default_level ] = verbose
if log.is_a? IO
@registry_options[ :logs ][ :device ] ||= log
else
@registry_options[ :logs ][ :filename ] ||= log
end
@registry = @options[ :container ] ||
Needle::Registry.new( @registry_options )
[ :keys, :host_keys, :auth_methods, :username, :password,
:crypto_backend, :registry_options, :container, :log, :verbose,
:forward_agent, :paranoid
].each do |i|
@options.delete i
end
@options.freeze
end
private :process_arguments
def host_key_verifier_from(paranoia)
case paranoia
when true then
require 'net/ssh/lenient-host-key-verifier'
Net::SSH::LenientHostKeyVerifier.new
when false then
require 'net/ssh/null-host-key-verifier'
Net::SSH::NullHostKeyVerifier.new
when :very then
require 'net/ssh/host-key-verifier'
Net::SSH::HostKeyVerifier.new
else
if paranoia.respond_to?(:verify)
paranoia
else
raise ArgumentError, "argument to :paranoid is not valid: #{paranoia.inspect}"
end
end
end
private :host_key_verifier_from
# Make sure we're in an acceptible state.
def sanity_check
raise Net::SSH::Exception, "session not open" unless @open
end
private :sanity_check
end
end
end
|