diff options
author | Jamis Buck <jamis@37signals.com> | 2007-08-17 03:52:30 +0000 |
---|---|---|
committer | Jamis Buck <jamis@37signals.com> | 2007-08-17 03:52:30 +0000 |
commit | 5338ecc6eb3023e72d78a510199c152527e07116 (patch) | |
tree | bb8ce5338c9a4d1446e19af14aefc8b436a52fb8 | |
parent | a27b7c5d1f6616019cda4fb1d579fa8e7780971b (diff) | |
download | net-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-- | Rakefile | 2 | ||||
-rw-r--r-- | lib/net/ssh/authentication/agent.rb | 25 | ||||
-rw-r--r-- | lib/net/ssh/authentication/key_manager.rb | 16 | ||||
-rw-r--r-- | lib/net/ssh/authentication/methods/abstract.rb | 20 | ||||
-rw-r--r-- | lib/net/ssh/authentication/methods/hostbased.rb | 4 | ||||
-rw-r--r-- | lib/net/ssh/authentication/methods/password.rb | 6 | ||||
-rw-r--r-- | lib/net/ssh/authentication/methods/publickey.rb | 9 | ||||
-rw-r--r-- | lib/net/ssh/authentication/session.rb | 20 | ||||
-rw-r--r-- | lib/net/ssh/buffer.rb | 12 | ||||
-rw-r--r-- | lib/net/ssh/buffered_io.rb | 53 | ||||
-rw-r--r-- | lib/net/ssh/connection/channel.rb | 326 |
11 files changed, 366 insertions, 127 deletions
@@ -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 |