summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJamis Buck <jamis@37signals.com>2007-08-17 03:52:30 +0000
committerJamis Buck <jamis@37signals.com>2007-08-17 03:52:30 +0000
commit5338ecc6eb3023e72d78a510199c152527e07116 (patch)
treebb8ce5338c9a4d1446e19af14aefc8b436a52fb8
parenta27b7c5d1f6616019cda4fb1d579fa8e7780971b (diff)
downloadnet-ssh-5338ecc6eb3023e72d78a510199c152527e07116.tar.gz
Starting to document everything
git-svn-id: http://svn.jamisbuck.org/net-ssh/branches/v2@190 1d2a57f2-1ded-0310-ad52-83097a15a5de
-rw-r--r--Rakefile2
-rw-r--r--lib/net/ssh/authentication/agent.rb25
-rw-r--r--lib/net/ssh/authentication/key_manager.rb16
-rw-r--r--lib/net/ssh/authentication/methods/abstract.rb20
-rw-r--r--lib/net/ssh/authentication/methods/hostbased.rb4
-rw-r--r--lib/net/ssh/authentication/methods/password.rb6
-rw-r--r--lib/net/ssh/authentication/methods/publickey.rb9
-rw-r--r--lib/net/ssh/authentication/session.rb20
-rw-r--r--lib/net/ssh/buffer.rb12
-rw-r--r--lib/net/ssh/buffered_io.rb53
-rw-r--r--lib/net/ssh/connection/channel.rb326
11 files changed, 366 insertions, 127 deletions
diff --git a/Rakefile b/Rakefile
index 136be50..0566177 100644
--- a/Rakefile
+++ b/Rakefile
@@ -191,7 +191,7 @@ Rake::RDocTask.new( :rdoc_core ) do |rdoc|
rdoc.rdoc_dir = rdoc_dir
rdoc.title = "Net::SSH -- An SSH client in, and for, Ruby"
rdoc.options += %w(--line-numbers --inline-source --main README)
- rdoc.rdoc_files.include 'README'
+ #rdoc.rdoc_files.include 'README'
rdoc.rdoc_files.include 'lib/**/*.rb'
if can_require( "rdoc/generators/template/html/jamis" )
diff --git a/lib/net/ssh/authentication/agent.rb b/lib/net/ssh/authentication/agent.rb
index a042b9e..6f7fc79 100644
--- a/lib/net/ssh/authentication/agent.rb
+++ b/lib/net/ssh/authentication/agent.rb
@@ -10,6 +10,7 @@ module Net; module SSH; module Authentication
# A trivial exception class for representing agent-specific errors.
class AgentError < Net::SSH::Exception; end
+ # An exception for indicating that the SSH agent is not available.
class AgentNotAvailable < AgentError; end
# This class implements a simple client for the ssh-agent protocol. It
@@ -21,6 +22,8 @@ module Net; module SSH; module Authentication
class Agent
include Loggable
+ # A simple module for extending keys, to allow comments to be specified
+ # for them.
module Comment
attr_accessor :comment
end
@@ -39,8 +42,11 @@ module Net; module SSH; module Authentication
SSH_AGENT_RSA_IDENTITIES_ANSWER = 2
SSH_AGENT_FAILURE = 5
+ # The underlying socket being used to communicate with the SSH agent.
attr_reader :socket
+ # Instantiates a new agent object, connects to a running SSH agent,
+ # negotiates the agent protocol version, and returns the agent object.
def self.connect(logger=nil)
agent = new(logger)
agent.connect!
@@ -48,6 +54,8 @@ module Net; module SSH; module Authentication
agent
end
+ # Creates a new Agent object, using the optional logger instance to
+ # report status.
def initialize(logger=nil)
self.logger = logger
end
@@ -118,16 +126,17 @@ module Net; module SSH; module Authentication
return reply.read_string
end
- def agent_socket_factory
- if File::ALT_SEPARATOR
- Pageant::Socket
- else
- UNIXSocket
- end
- end
-
private
+ # Returns the agent socket factory to use.
+ def agent_socket_factory
+ if File::ALT_SEPARATOR
+ Pageant::Socket
+ else
+ UNIXSocket
+ end
+ end
+
# Send a new packet of the given type, with the associated data.
def send_packet(type, *args)
buffer = Buffer.from(*args)
diff --git a/lib/net/ssh/authentication/key_manager.rb b/lib/net/ssh/authentication/key_manager.rb
index 93e77b7..a20eac9 100644
--- a/lib/net/ssh/authentication/key_manager.rb
+++ b/lib/net/ssh/authentication/key_manager.rb
@@ -66,7 +66,8 @@ module Net
# reconnected. This method simply allows the client connection to be
# closed when it will not be used in the immediate future.
def finish
- close_agent
+ @agent.close if @agent
+ @agent = nil
end
# Returns an array of identities (public keys) known to this manager.
@@ -140,10 +141,13 @@ module Net
# attempt will be made to use the ssh-agent. If false, any existing
# connection to an agent is closed and the agent will not be used.
def use_agent=(use_agent)
- close_agent if !use_agent
+ finish if !use_agent
@use_agent = use_agent
end
+ # Returns an Agent instance to use for communicating with an SSH
+ # agent process. Returns nil if use of an SSH agent has been disabled,
+ # or if the agent is otherwise not available.
def agent
return unless use_agent?
@agent ||= Agent.connect(logger)
@@ -151,14 +155,6 @@ module Net
@use_agent = false
nil
end
-
- private
-
- # Closes any open connection to an ssh-agent.
- def close_agent
- @agent.close if @agent
- @agent = nil
- end
end
end
diff --git a/lib/net/ssh/authentication/methods/abstract.rb b/lib/net/ssh/authentication/methods/abstract.rb
index 1e34b44..339c53c 100644
--- a/lib/net/ssh/authentication/methods/abstract.rb
+++ b/lib/net/ssh/authentication/methods/abstract.rb
@@ -5,11 +5,19 @@ require 'net/ssh/authentication/constants'
module Net; module SSH; module Authentication; module Methods
+ # The base class of all user authentication methods. It provides a few
+ # bits of common functionality.
class Abstract
include Constants, Loggable
- attr_reader :session, :key_manager
+ # The authentication session object
+ attr_reader :session
+ # The key manager object. Not all authentication methods will require
+ # this.
+ attr_reader :key_manager
+
+ # Instantiates a new authentication method.
def initialize(session, options={})
@session = session
@key_manager = options[:key_manager]
@@ -17,14 +25,21 @@ module Net; module SSH; module Authentication; module Methods
self.logger = session.logger
end
+ # Returns the session-id, as generated during the first key exchange of
+ # an SSH connection.
def session_id
session.transport.algorithms.session_id
end
+ # Sends a message via the underlying transport layer abstraction. This
+ # will block until the message is completely sent.
def send_message(msg)
session.transport.send_message(msg)
end
+ # Creates a new USERAUTH_REQUEST packet. The extra arguments on the end
+ # must be either boolean values or strings, and are tacked onto the end
+ # of the packet. The new packet is returned, ready for sending.
def userauth_request(username, next_service, auth_method, *others)
buffer = Net::SSH::Buffer.from(:byte, USERAUTH_REQUEST,
:string, username, :string, next_service, :string, auth_method)
@@ -39,6 +54,7 @@ module Net; module SSH; module Authentication; module Methods
buffer
end
-end
+
+ end
end; end; end; end \ No newline at end of file
diff --git a/lib/net/ssh/authentication/methods/hostbased.rb b/lib/net/ssh/authentication/methods/hostbased.rb
index d02364b..0145116 100644
--- a/lib/net/ssh/authentication/methods/hostbased.rb
+++ b/lib/net/ssh/authentication/methods/hostbased.rb
@@ -9,7 +9,8 @@ module Net
class Hostbased < Abstract
include Constants
- # Attempts to perform host-based authorization of the user.
+ # Attempts to perform host-based authorization of the user by trying
+ # all known keys.
def authenticate(next_service, username, password=nil)
return false unless key_manager
@@ -23,6 +24,7 @@ module Net
private
+ # Returns the hostname as reported by the underlying socket.
def hostname
session.transport.socket.client_name
end
diff --git a/lib/net/ssh/authentication/methods/password.rb b/lib/net/ssh/authentication/methods/password.rb
index 19aa673..03c1cae 100644
--- a/lib/net/ssh/authentication/methods/password.rb
+++ b/lib/net/ssh/authentication/methods/password.rb
@@ -8,9 +8,9 @@ module Net
# Implements the "password" SSH authentication method.
class Password < Abstract
- # Attempt to authenticate the given user for the given service. The
- # data hash must specify a <tt>:password</tt> value, otherwise this
- # will always return false.
+ # Attempt to authenticate the given user for the given service. If
+ # the password parameter is nil, this will never do anything except
+ # return false.
def authenticate(next_service, username, password=nil)
return false unless password
diff --git a/lib/net/ssh/authentication/methods/publickey.rb b/lib/net/ssh/authentication/methods/publickey.rb
index f57b7ca..1ac217e 100644
--- a/lib/net/ssh/authentication/methods/publickey.rb
+++ b/lib/net/ssh/authentication/methods/publickey.rb
@@ -11,9 +11,8 @@ module Net
class Publickey < Abstract
# Attempts to perform public-key authentication for the given
# username, trying each identity known to the key manager. If any of
- # them succeed, returns +true+, otherwise returns +false+. The data
- # hash must contain a UserKeyManager instance under the
- # <tt>:key_manager</tt> key.
+ # them succeed, returns +true+, otherwise returns +false+. This
+ # requires the presence of a key manager.
def authenticate(next_service, username, password=nil)
return false unless key_manager
@@ -26,8 +25,8 @@ module Net
private
- # Builds a Net::SSH::Util::WriterBuffer that contains the request
- # formatted for sending a public-key request to the server.
+ # Builds a packet that contains the request formatted for sending
+ # a public-key request to the server.
def build_request(pub_key, username, next_service, has_sig)
blob = Net::SSH::Buffer.new
blob.write_key pub_key
diff --git a/lib/net/ssh/authentication/session.rb b/lib/net/ssh/authentication/session.rb
index 56ab965..2315cb7 100644
--- a/lib/net/ssh/authentication/session.rb
+++ b/lib/net/ssh/authentication/session.rb
@@ -8,16 +8,26 @@ require 'net/ssh/authentication/methods/password'
require 'net/ssh/authentication/methods/keyboard_interactive'
module Net; module SSH; module Authentication
+
+ # Represents an authentication session. It manages the authentication of
+ # a user over an established connection (the "transport" object).
class Session
include Transport::Constants, Constants, Loggable
- # transport session
+ # transport layer abstraction
attr_reader :transport
+ # the list of authentication methods to try
attr_reader :auth_methods
+
+ # the list of authentication methods that are allowed
attr_reader :allowed_auth_methods
+
+ # a hash of options, given at construction time
attr_reader :options
+ # Instantiates a new Authentication::Session object over the given
+ # transport layer abstraction.
def initialize(transport, options={})
self.logger = transport.logger
@transport = transport
@@ -28,6 +38,9 @@ module Net; module SSH; module Authentication
@allowed_auth_methods = @auth_methods
end
+ # Attempts to authenticate the given user, in preparation for the next
+ # service request. Returns true if an authentication method succeeds in
+ # authenticating the user, and false otherwise.
def authenticate(next_service, username, password=nil)
trace { "beginning authentication of `#{username}'" }
@@ -55,6 +68,9 @@ module Net; module SSH; module Authentication
key_manager.finish if key_manager
end
+ # Blocks until a packet is received. It silently handles USERAUTH_BANNER
+ # packets, and will raise an error if any packet is received that is not
+ # valid during user authentication.
def next_message
loop do
packet = transport.next_message
@@ -82,6 +98,8 @@ module Net; module SSH; module Authentication
end
end
+ # Blocks until a packet is received, and returns it if it is of the given
+ # type. If it is not, an exception is raised.
def expect_message(type)
message = next_message
unless message.type == type
diff --git a/lib/net/ssh/buffer.rb b/lib/net/ssh/buffer.rb
index b5dcb39..423a5d7 100644
--- a/lib/net/ssh/buffer.rb
+++ b/lib/net/ssh/buffer.rb
@@ -2,6 +2,16 @@ require 'net/ssh/transport/openssl'
module Net; module SSH
class Buffer
+ # This is a convenience method for creating and populating a new buffer
+ # from a single command. The arguments must be even in length, with the
+ # first of each pair of arguments being a symbol naming the type of the
+ # data that follows. If the type is :raw, the value is written directly
+ # to the hash.
+ #
+ # Example:
+ #
+ # b = Buffer.from(:byte, 1, :string, "hello", :raw, "\1\2\3\4")
+ # #-> "\1\0\0\0\5hello\1\2\3\4"
def self.from(*args)
raise ArgumentError, "odd number of arguments given" unless args.length % 2 == 0
@@ -202,6 +212,8 @@ module Net; module SSH
return key
end
+ # Reads the next string from the buffer, and returns a new Buffer
+ # object that wraps it.
def read_buffer
Buffer.new(read_string)
end
diff --git a/lib/net/ssh/buffered_io.rb b/lib/net/ssh/buffered_io.rb
index 0c2b576..617e3ff 100644
--- a/lib/net/ssh/buffered_io.rb
+++ b/lib/net/ssh/buffered_io.rb
@@ -1,52 +1,65 @@
-require 'thread'
require 'net/ssh/buffer'
require 'net/ssh/loggable'
module Net; module SSH
+ # This module is used to extend sockets and other IO objects, to allow
+ # them to be buffered for both read and write. This abstraction makes it
+ # quite easy to write a select-based event loop (see Connection::Session).
module BufferedIo
include Loggable
- def self.extended(object)
+ # Called when the #extend is called on an object, with this module as the
+ # argument. It ensures that the modules instance variables are all properly
+ # initialized.
+ def self.extended(object) #:nodoc:
object.__send__(:initialize_buffered_io)
end
+ # Tries to consume up to +n+ bytes of data from the underlying IO object,
+ # and adds the data to the input buffer. It returns the number of bytes
+ # read.
def fill(n=8192)
- input_mutex.synchronize do
- input.consume!
- data = recv(n)
- trace { "read #{data.length} bytes" }
- input.append(data)
- return data.length
- end
+ input.consume!
+ data = recv(n)
+ trace { "read #{data.length} bytes" }
+ input.append(data)
+ return data.length
end
+ # Read up to +length+ bytes from the input buffer.
def read_available(length)
- input_mutex.synchronize { input.read(length) }
+ input.read(length)
end
+ # Returns the number of bytes available to be read from the input buffer,
+ # via #read_available.
def available
- input_mutex.synchronize { input.available }
+ input.available
end
+ # Enqueues data in the output buffer, to be written when #send_pending
+ # is called.
def enqueue(data)
- output_mutex.synchronize { output.append(data) }
+ output.append(data)
end
+ # Returns +true+ if there is data waiting in the output buffer, and
+ # +false+ otherwise.
def pending_write?
- output_mutex.synchronize { output.length > 0 }
+ output.length > 0
end
+ # Sends as much of the pending output as possible.
def send_pending
- output_mutex.synchronize do
- if output.length > 0
- sent = send(output.to_s, 0)
- trace { "sent #{sent} bytes" }
- output.consume!(sent)
- end
+ if output.length > 0
+ sent = send(output.to_s, 0)
+ trace { "sent #{sent} bytes" }
+ output.consume!(sent)
end
end
+ # Blocks until the output buffer is empty.
def wait_for_pending_sends
send_pending
while output.length > 0
@@ -73,8 +86,6 @@ module Net; module SSH
def initialize_buffered_io
@input = Net::SSH::Buffer.new
@output = Net::SSH::Buffer.new
- @input_mutex = Mutex.new
- @output_mutex = Mutex.new
end
end
diff --git a/lib/net/ssh/connection/channel.rb b/lib/net/ssh/connection/channel.rb
index fbaffe9..9fd5905 100644
--- a/lib/net/ssh/connection/channel.rb
+++ b/lib/net/ssh/connection/channel.rb
@@ -4,27 +4,75 @@ require 'net/ssh/connection/term'
module Net; module SSH; module Connection
+ # The channel abstraction. Multiple "channels" can be multiplexed onto a
+ # single SSH channel, each operating independently and seemingly in parallel.
+ # This class represents a single such channel. Most operations performed
+ # with the Net::SSH library will involve using one or more channels.
+ #
+ # Channels are intended to be used asynchronously. You request that one be
+ # opened (via Connection::Session#open_channel), and when it is opened, your
+ # callback is invoked. Then, you set various other callbacks on the newly
+ # opened channel, which are called in response to the corresponding events.
+ # Programming with Net::SSH works best if you think of your programs as
+ # state machines. Complex programs are best implemented as objects that
+ # wrap a channel. See Net::SCP and Net::SFTP for examples.
class Channel
include Constants, Loggable
+ # The local id for this channel, assigned by the Connection::Session instance.
attr_reader :local_id
+
+ # The remote id for this channel, assigned by the remote host.
attr_reader :remote_id
+
+ # The type of this channel, usually "session".
attr_reader :type
+
+ # The underlying Connection::Session instance that supports this channel.
attr_reader :connection
+ # The maximum packet size that the local host can receive.
attr_reader :local_maximum_packet_size
+
+ # The maximum amount of data that the local end of this channel can
+ # receive. This is a total, not per-packet.
attr_reader :local_maximum_window_size
+
+ # The maximum packet size that the remote host can receive.
attr_reader :remote_maximum_packet_size
+
+ # The maximum amount of data that the remote end of this channel can
+ # receive. This is a total, not per-packet.
attr_reader :remote_maximum_window_size
+ # This is the remaining window size on the local end of this channel. When
+ # this reaches zero, no more data can be received.
attr_reader :local_window_size
+
+ # This is the remaining window size on the remote end of this channel. When
+ # this reaches zero, no more data can be sent.
attr_reader :remote_window_size
- attr_reader :output
+ # A hash of properties for this channel. These can be used to store state
+ # information about this channel. See also #[] and #[]=.
attr_reader :properties
- attr_reader :pending_requests
-
+ # The output buffer for this channel. Data written to the channel is
+ # enqueued here, to be written as CHANNEL_DATA packets during each pass of
+ # the event loop. See Connection::Session#process and #enqueue_pending_output.
+ attr_reader :output #:nodoc:
+
+ # The list of pending requests. Each time a request is sent which requires
+ # a reply, the corresponding callback is pushed onto this queue. As responses
+ # arrive, they are shifted off the front and handled.
+ attr_reader :pending_requests #:nodoc:
+
+ # Instantiates a new channel on the given connection, of the given type,
+ # and with the given id. If a block is given, it will be remembered until
+ # the channel is confirmed open by the server, and will be invoked at
+ # that time (see #do_open_confirmation).
+ #
+ # This also sets the default maximum packet size and maximum window size.
def initialize(connection, type, local_id, &on_confirm_open)
self.logger = connection.logger
@@ -47,22 +95,36 @@ module Net; module SSH; module Connection
@closing = false
end
+ # A shortcut for accessing properties of the channel (see #properties).
def [](name)
@properties[name]
end
+ # A shortcut for setting properties of the channel (see #properties).
def []=(name, value)
@properties[name] = value
end
+ # Syntactic sugar for executing a command. Sends a channel request asking
+ # that the given command be invoked. If the block is given, it will be
+ # called when the server responds. The first parameter will be the
+ # channel, and the second will be true or false, indicating whether the
+ # request succeeded or not. In this case, success means that the command
+ # is being executed, not that it has completed, and failure means that the
+ # command altogether failed to be executed.
def exec(command, &block)
send_channel_request("exec", :string, command, &block)
end
+ # Syntactic sugar for requesting that a subsystem be started. Subsystems
+ # are a way for other protocols (like SFTP) to be run, using SSH as
+ # the transport. Generally, you'll never need to call this directly unless
+ # you are the implementor of a subsystem.
def subsystem(subsystem, &block)
send_channel_request("subsystem", :string, subsystem, &block)
end
+ # A hash of the valid PTY options.
VALID_PTY_OPTIONS = { :term=>"xterm",
:chars_wide=>80,
:chars_high=>24,
@@ -70,6 +132,9 @@ module Net; module SSH; module Connection
:pixels_high=>480,
:modes=>{} }
+ # Requests that a pty be made available for this channel. This is useful
+ # when you want to invoke and interact with some kind of screen-based
+ # program (e.g., vim, or some menuing system).
def request_pty(opts={}, &block)
extra = opts.keys - VALID_PTY_OPTIONS.keys
raise ArgumentError, "invalid option(s) to request_pty: #{extra.inspect}" if extra.any?
@@ -89,14 +154,24 @@ module Net; module SSH; module Connection
:string, modes, &block)
end
+ # Appends the given data to the channel's output buffer, preparatory to
+ # being packaged up and sent to the remote server as channel data.
def send_data(data)
- output.append(data)
+ output.append(data.to_s)
end
+ # Returns true if the channel is currently closing, but not actually
+ # closed. A channel is closing when, for instance, #close has been
+ # invoked, but the server has not yet responded with a CHANNEL_CLOSE
+ # packet of its own.
def closing?
@closing
end
+ # Requests that the channel be closed. If the channel is already closing,
+ # this does nothing, nor does it do anything if the channel has not yet
+ # been confirmed open (see #do_open_confirmation). Otherwise, it sends a
+ # CHANNEL_CLOSE message and marks the channel as closing.
def close
return if @closing
if remote_id
@@ -105,43 +180,79 @@ module Net; module SSH; module Connection
end
end
+ # If an #on_process handler has been set up, this will cause it to be
+ # invoked (passing the channel itself as an argument). It also causes all
+ # pending output to be enqueued as CHANNEL_DATA packets (see #enqueue_pending_output).
def process
@on_process.call(self) if @on_process
enqueue_pending_output
end
-
- def enqueue_pending_output
- return unless remote_id
- while output.length > 0
- length = output.length
- length = remote_window_size if length > remote_window_size
- length = remote_maximum_packet_size if length > remote_maximum_packet_size
+ # Registers callback to be invoked when data packets are received by the
+ # channel. The callback is called with the channel as the first argument,
+ # and the data as the second.
+ def on_data(&block)
+ old, @on_data = @on_data, block
+ old
+ end
- if length > 0
- connection.send_message(Buffer.from(:byte, CHANNEL_DATA, :long, remote_id, :string, output.read(length)))
- output.consume!
- @remote_window_size -= length
- else
- break
- end
- end
+ # Registers callback to be invoked when extended data packets are received
+ # by the channel. The callback is called with the channel as the first
+ # argument, the data type (as an integer) as the second, and the data as
+ # the third. Extended data is almost exclusively used to send STDERR data.
+ def on_extended_data(&block)
+ old, @on_extended_data = @on_extended_data, block
+ old
end
- %w(data extended_data process close eof).each do |callback|
- class_eval(<<-CODE, __FILE__, __LINE__+1)
- def on_#{callback}(&block)
- old, @on_#{callback} = @on_#{callback}, block
- old
- end
- CODE
+ # Registers a callback to be invoked for each pass of the event loop for
+ # this channel. There are no guarantees on timeliness in the event loop,
+ # but it will be called roughly once for each packet received by the
+ # connection (not the channel). This callback is invoked with the channel
+ # as the sole argument.
+ def on_process(&block)
+ old, @on_process = @on_process, block
+ old
end
+ # Registers a callback to be invoked when the server acknowledges that a
+ # channel is closed. This is invoked with the channel as the sole argument.
+ def on_close(&block)
+ old, @on_close = @on_close, block
+ old
+ end
+
+ # Registers a callback to be invoked when the server indicates that no more
+ # data will be sent to the channel (although the channel can still send
+ # data to the server). The channel is the sole argument to the callback.
+ def on_eof(&block)
+ old, @on_eof = @on_eof, block
+ old
+ end
+
+ # Registers a callback to be invoked when a channel request of the given
+ # type is received. The callback will receive the channel as the first
+ # argument, and the associated data as the second. By default, if the request
+ # wants a reply, Net::SSH will send a CHANNEL_SUCCESS response for any
+ # request that was handled by a registered callback, and CHANNEL_FAILURE
+ # for any that wasn't, but if you want your registered callback to result
+ # in a CHANNEL_FAILURE response, just raise ChannelRequestFailed.
def on_request(type, &block)
old, @on_request[type] = @on_request[type], block
old
end
+ # Sends a new channel request with the given name. The extra +data+
+ # parameter must either be empty, or consist of an even number of
+ # arguments. See Net::SSH::Buffer.from for a description of their format.
+ # If a block is given, it is registered as a callback for a pending
+ # request, and the packet will be flagged so that the server knows a
+ # reply is required. If no block is given, the server will send no
+ # response to this request. Responses, where required, will cause the
+ # callback to be invoked with the channel as the first argument, and
+ # either true or false as the second, depending on whether the request
+ # succeeded or not. The meaning of "success" and "failure" in this context
+ # is dependent on the specific request that was sent.
def send_channel_request(request_name, *data, &callback)
msg = Buffer.from(:byte, CHANNEL_REQUEST,
:long, remote_id, :string, request_name,
@@ -150,71 +261,136 @@ module Net; module SSH; module Connection
pending_requests << callback if callback
end
- def do_open_confirmation(remote_id, max_window, max_packet)
- @remote_id = remote_id
- @remote_window_size = @remote_maximum_window_size = max_window
- @remote_maximum_packet_size = max_packet
- connection.forward.agent(self) if connection.options[:forward_agent] && type == "session"
- @on_confirm_open.call(self) if @on_confirm_open
- end
-
- def do_window_adjust(bytes)
- @remote_maximum_window_size += bytes
- @remote_window_size += bytes
- end
+ public # these methods are public, but for internal use only
+
+ # Enqueues pending output at the connection as CHANNEL_DATA packets. This
+ # does nothing if the channel has not yet been confirmed open (see
+ # #do_open_confirmation). This is called automatically by #process, which
+ # is called from the event loop (Connection::Session#process). You will
+ # generally not need to invoke it directly.
+ def enqueue_pending_output #:nodoc:
+ return unless remote_id
+
+ while output.length > 0
+ length = output.length
+ length = remote_window_size if length > remote_window_size
+ length = remote_maximum_packet_size if length > remote_maximum_packet_size
+
+ if length > 0
+ connection.send_message(Buffer.from(:byte, CHANNEL_DATA, :long, remote_id, :string, output.read(length)))
+ output.consume!
+ @remote_window_size -= length
+ else
+ break
+ end
+ end
+ end
- def do_request(request, want_reply, data)
- result = true
+ # Invoked when the server confirms that a channel has been opened.
+ # The remote_id is the id of the channel as assigned by the remote host,
+ # and max_window and max_packet are the maximum window and maximum
+ # packet sizes, respectively. If an open-confirmation callback was
+ # given when the channel was created, it is invoked at this time with
+ # the channel itself as the sole argument.
+ def do_open_confirmation(remote_id, max_window, max_packet) #:nodoc:
+ @remote_id = remote_id
+ @remote_window_size = @remote_maximum_window_size = max_window
+ @remote_maximum_packet_size = max_packet
+ connection.forward.agent(self) if connection.options[:forward_agent] && type == "session"
+ @on_confirm_open.call(self) if @on_confirm_open
+ end
- begin
- callback = @on_request[request] or raise ChannelRequestFailed
- callback.call(self, data)
- rescue ChannelRequestFailed
- result = false
+ # Invoked when the server sends a CHANNEL_WINDOW_ADJUST packet, and
+ # causes the remote window size to be adjusted upwards by the given
+ # number of bytes. This has the effect of allowing more data to be sent
+ # from the local end to the remote end of the channel.
+ def do_window_adjust(bytes) #:nodoc:
+ @remote_maximum_window_size += bytes
+ @remote_window_size += bytes
end
- if want_reply
- msg = Buffer.from(:byte, result ? CHANNEL_SUCCESS : CHANNEL_FAILURE, :long, remote_id)
- connection.send_message(msg)
+ # Invoked when the server sends a channel request. If any #on_request
+ # callback has been registered for the specific type of this request,
+ # it is invoked. If +want_reply+ is true, a packet will be sent of
+ # either CHANNEL_SUCCESS or CHANNEL_FAILURE type. If there was no callback
+ # to handle the request, CHANNEL_FAILURE will be sent. Otherwise,
+ # CHANNEL_SUCCESS, unless the callback raised ChannelRequestFailed. The
+ # callback should accept the channel as the first argument, and the
+ # request-specific data as the second.
+ def do_request(request, want_reply, data) #:nodoc:
+ result = true
+
+ begin
+ callback = @on_request[request] or raise ChannelRequestFailed
+ callback.call(self, data)
+ rescue ChannelRequestFailed
+ result = false
+ end
+
+ if want_reply
+ msg = Buffer.from(:byte, result ? CHANNEL_SUCCESS : CHANNEL_FAILURE, :long, remote_id)
+ connection.send_message(msg)
+ end
end
- end
- def do_data(data)
- update_local_window_size(data.length)
- @on_data.call(self, data) if @on_data
- end
+ # Invokes the #on_data callback when the server sends data to the
+ # channel. This will reduce the available window size on the local end,
+ # but does not actually throttle requests that come in illegally when
+ # the window size is too small. The callback is invoked with the channel
+ # as the first argument, and the data as the second.
+ def do_data(data) #:nodoc:
+ update_local_window_size(data.length)
+ @on_data.call(self, data) if @on_data
+ end
- def do_extended_data(type, data)
- update_local_window_size(data.length)
- @on_extended_data.call(self, type, data) if @on_extended_data
- end
+ # Invokes the #on_extended_data callback when the server sends
+ # extended data to the channel. This will reduce the available window
+ # size on the local end. The callback is invoked with the channel,
+ # type, and data.
+ def do_extended_data(type, data)
+ update_local_window_size(data.length)
+ @on_extended_data.call(self, type, data) if @on_extended_data
+ end
- def do_eof
- @on_eof.call(self) if @on_eof
- end
+ # Invokes the #on_eof callback when the server indicates that no
+ # further data is forthcoming. The callback is invoked with the channel
+ # as the argument.
+ def do_eof
+ @on_eof.call(self) if @on_eof
+ end
- def do_close
- @on_close.call(self) if @on_close
- end
+ # Invokes the #on_close callback when the server closes a channel.
+ # The channel is the only argument.
+ def do_close
+ @on_close.call(self) if @on_close
+ end
- def do_failure
- if callback = pending_requests.shift
- callback.call(self, false)
- else
- error { "channel failure recieved with no pending request to handle it (bug?)" }
+ # Invokes the next pending request callback with +false+ as the second
+ # argument.
+ def do_failure
+ if callback = pending_requests.shift
+ callback.call(self, false)
+ else
+ error { "channel failure recieved with no pending request to handle it (bug?)" }
+ end
end
- end
- def do_success
- if callback = pending_requests.shift
- callback.call(self, true)
- else
- error { "channel success recieved with no pending request to handle it (bug?)" }
+ # Invokes the next pending request callback with +true+ as the second
+ # argument.
+ def do_success
+ if callback = pending_requests.shift
+ callback.call(self, true)
+ else
+ error { "channel success recieved with no pending request to handle it (bug?)" }
+ end
end
- end
private
+ # Updates the local window size by the given amount. If the window
+ # size drops to less than half of the local maximum (an arbitrary
+ # threshold), a CHANNEL_WINDOW_ADJUST message will be sent to the
+ # server telling it that the window size has grown.
def update_local_window_size(size)
@local_window_size -= size
if local_window_size < local_maximum_window_size/2