diff options
| author | Jamis Buck <jamis@37signals.com> | 2007-07-25 04:53:11 +0000 |
|---|---|---|
| committer | Jamis Buck <jamis@37signals.com> | 2007-07-25 04:53:11 +0000 |
| commit | ffc496b4c9746325618d34ec29fa98bdc49b9d5f (patch) | |
| tree | 8ee1c020a93dd8c6d4f23fb1734747d32da8e2cb | |
| parent | 57a2e22b0873f99e9d6d66026df30de1296645da (diff) | |
| download | net-ssh-ffc496b4c9746325618d34ec29fa98bdc49b9d5f.tar.gz | |
basic connection protocol support
git-svn-id: http://svn.jamisbuck.org/net-ssh/branches/v2@120 1d2a57f2-1ded-0310-ad52-83097a15a5de
| -rw-r--r-- | lib/net/ssh/connection/channel.rb | 117 | ||||
| -rw-r--r-- | lib/net/ssh/connection/constants.rb | 27 | ||||
| -rw-r--r-- | lib/net/ssh/connection/session.rb | 122 | ||||
| -rw-r--r-- | lib/net/ssh/errors.rb | 2 | ||||
| -rw-r--r-- | lib/net/ssh/packet.rb | 14 | ||||
| -rw-r--r-- | lib/net/ssh/session.rb | 33 | ||||
| -rw-r--r-- | lib/net/ssh/transport/packet_stream.rb | 14 | ||||
| -rw-r--r-- | lib/net/ssh/transport/session.rb | 7 |
8 files changed, 326 insertions, 10 deletions
diff --git a/lib/net/ssh/connection/channel.rb b/lib/net/ssh/connection/channel.rb new file mode 100644 index 0000000..dfbd71f --- /dev/null +++ b/lib/net/ssh/connection/channel.rb @@ -0,0 +1,117 @@ +require 'net/ssh/loggable' +require 'net/ssh/connection/constants' + +module Net; module SSH; module Connection + + class Channel + include Constants, Loggable + + attr_reader :local_id + attr_reader :remote_id + attr_reader :type + attr_reader :connection + + attr_reader :local_maximum_packet_size + attr_reader :local_maximum_window_size + attr_reader :remote_maximum_packet_size + attr_reader :remote_maximum_window_size + + attr_reader :local_window_size + attr_reader :remote_window_size + + attr_reader :output + + def initialize(connection, type, local_id, &on_confirm_open) + self.logger = connection.logger + + @connection = connection + @type = type + @local_id = local_id + + @local_maximum_packet_size = 0x0FFFF + @local_window_size = @local_maximum_window_size = 0x1FFFF + + @on_confirm_open = on_confirm_open + + @output = Buffer.new + + @on_data = nil + end + + def exec(command, want_reply=false) + connection.send_message(channel_request("exec", command, want_reply)) + end + + def send_data(data) + output.append(data) + end + + def important? + true + end + + def enqueue_pending_output + return unless remote_id + + 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 + end + end + + def on_data(&block) + @on_data = block + end + + def channel_request(request_name, data, want_reply=false) + Buffer.from(:byte, CHANNEL_REQUEST, + :long, remote_id, :string, request_name, + :bool, want_reply, :string, data) + 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 + @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 + + def do_request(request, want_reply, data) + # ... + end + + def do_data(data) + update_local_window_size(data.length) + @on_data.call(data) if @on_data + end + + def do_eof + end + + def do_close + end + + private + + def update_local_window_size(size) + @local_window_size -= size + if local_window_size < local_maximum_window_size/2 + connection.send_message(Buffer.from(:byte, CHANNEL_WINDOW_ADJUST, + :long, remote_id, :long, 0x20000)) + @local_window_size += 0x20000 + @local_maximum_window_size += 0x20000 + end + end + end + +end; end; end
\ No newline at end of file diff --git a/lib/net/ssh/connection/constants.rb b/lib/net/ssh/connection/constants.rb new file mode 100644 index 0000000..cbe876d --- /dev/null +++ b/lib/net/ssh/connection/constants.rb @@ -0,0 +1,27 @@ +module Net; module SSH; module Connection + + module Constants + + # Connection protocol generic messages + + GLOBAL_REQUEST = 80 + REQUEST_SUCCESS = 81 + REQUEST_FAILURE = 82 + + # Channel related messages + + CHANNEL_OPEN = 90 + CHANNEL_OPEN_CONFIRMATION = 91 + CHANNEL_OPEN_FAILURE = 92 + CHANNEL_WINDOW_ADJUST = 93 + CHANNEL_DATA = 94 + CHANNEL_EXTENDED_DATA = 95 + CHANNEL_EOF = 96 + CHANNEL_CLOSE = 97 + CHANNEL_REQUEST = 98 + CHANNEL_SUCCESS = 99 + CHANNEL_FAILURE = 100 + + end + +end; end end
\ No newline at end of file diff --git a/lib/net/ssh/connection/session.rb b/lib/net/ssh/connection/session.rb new file mode 100644 index 0000000..f117eaa --- /dev/null +++ b/lib/net/ssh/connection/session.rb @@ -0,0 +1,122 @@ +require 'net/ssh/loggable' +require 'net/ssh/connection/channel' +require 'net/ssh/connection/constants' + +module Net; module SSH; module Connection + + class Session + include Constants, Loggable + + MAP = Constants.constants.inject({}) do |memo, name| + memo[const_get(name)] = name.downcase.to_sym + memo + end + + attr_reader :transport + attr_reader :channels + + def initialize(transport, options={}) + self.logger = transport.logger + + @transport = transport + + @next_channel_id = 0 + @channels = {} + end + + # preserve a reference to Kernel#loop + alias :loop_forever :loop + + def loop(&block) + running = block || Proc.new { channels.any? { |id,ch| ch.important? } } + + while running.call + process + end + end + + def process + dispatch_incoming_packets + + channels.each { |id, channel| channel.enqueue_pending_output } + + readers = [transport.socket] + writers = [] + writers << transport.socket if transport.socket.pending_write? + + readers, writers, errors = IO.select(readers, writers, nil, 0) + + if readers + transport.socket.fill if readers.include?(transport.socket) + end + + if writers + transport.socket.send_queue if writers.include?(transport.socket) + end + end + + def open_channel(type, &on_confirm) + local_id = @next_channel_id += 1 + channel = Channel.new(self, type, local_id, &on_confirm) + + msg = Buffer.from(:byte, CHANNEL_OPEN, :string, type, :long, local_id, + :long, channel.local_maximum_window_size, + :long, channel.local_maximum_packet_size) + send_message(msg) + + channels[local_id] = channel + end + + def send_message(message) + transport.socket.enqueue_packet(message) + end + + private + + def dispatch_incoming_packets + while packet = transport.poll_message + unless MAP.key?(packet.type) + raise Net::SSH::Exception, "unexpected response #{packet.type} (#{packet.inspect})" + end + + send(MAP[packet.type], packet) + end + end + + def channel_open_confirmation(packet) + trace { "channel_open_confirmation: #{packet[:local_id]} #{packet[:remote_id]} #{packet[:window_size]} #{packet[:packet_size]}" } + channels[packet[:local_id]].do_open_confirmation(packet[:remote_id], packet[:window_size], packet[:packet_size]) + end + + def channel_window_adjust(packet) + trace { "channel_window_adjust: #{packet[:local_id]} +#{packet[:extra_bytes]}" } + channels[packet[:local_id]].do_window_adjust(packet[:extra_bytes]) + end + + def channel_request(packet) + trace { "channel_request: #{packet[:local_id]} #{packet[:request]} #{packet[:want_reply]}" } + channels[packet[:local_id]].do_request(packet[:request], packet[:want_reply], packet[:request_data]) + end + + def channel_data(packet) + trace { "channel_data: #{packet[:local_id]} #{packet[:data].length}b" } + channels[packet[:local_id]].do_data(packet[:data]) + end + + def channel_eof(packet) + trace { "channel_eof: #{packet[:local_id]}" } + channels[packet[:local_id]].do_eof + end + + def channel_close(packet) + trace { "channel_close: #{packet[:local_id]}" } + + channel = channels[packet[:local_id]] + send_message(Buffer.from(:byte, CHANNEL_CLOSE, :long, channel.remote_id)) + + channels.delete(packet[:local_id]) + channel.do_close + end + end + +end; end; end
\ No newline at end of file diff --git a/lib/net/ssh/errors.rb b/lib/net/ssh/errors.rb index a4fd23c..956b951 100644 --- a/lib/net/ssh/errors.rb +++ b/lib/net/ssh/errors.rb @@ -2,4 +2,6 @@ module Net; module SSH class Exception < ::RuntimeError; end class AuthenticationFailed < Exception; end + + class Disconnect < Exception; end end; end
\ No newline at end of file diff --git a/lib/net/ssh/packet.rb b/lib/net/ssh/packet.rb index dfe3465..312dc97 100644 --- a/lib/net/ssh/packet.rb +++ b/lib/net/ssh/packet.rb @@ -1,6 +1,7 @@ require 'net/ssh/buffer' require 'net/ssh/transport/constants' require 'net/ssh/authentication/constants' +require 'net/ssh/connection/constants' module Net; module SSH class Packet < Buffer @@ -14,6 +15,13 @@ module Net; module SSH register Authentication::Constants::USERAUTH_BANNER, [:message, :string], [:language, :string] register Authentication::Constants::USERAUTH_FAILURE, [:authentications, :string], [:partial_success, :bool] + register Connection::Constants::CHANNEL_OPEN_CONFIRMATION, [:local_id, :long], [:remote_id, :long], [:window_size, :long], [:packet_size, :long] + register Connection::Constants::CHANNEL_WINDOW_ADJUST, [:local_id, :long], [:extra_bytes, :long] + register Connection::Constants::CHANNEL_DATA, [:local_id, :long], [:data, :string] + register Connection::Constants::CHANNEL_EOF, [:local_id, :long] + register Connection::Constants::CHANNEL_CLOSE, [:local_id, :long] + register Connection::Constants::CHANNEL_REQUEST, [:local_id, :long], [:request, :string], [:want_reply, :bool], [:request_data, :buffer] + def initialize(payload) @instantiated = false @named_elements = {} @@ -30,7 +38,11 @@ module Net; module SSH @instantiated = true definitions.each do |name, datatype| - @named_elements[name.to_sym] = send("read_#{datatype}") + @named_elements[name.to_sym] = if datatype == :buffer + remainder_as_buffer + else + send("read_#{datatype}") + end end self diff --git a/lib/net/ssh/session.rb b/lib/net/ssh/session.rb new file mode 100644 index 0000000..ac4408a --- /dev/null +++ b/lib/net/ssh/session.rb @@ -0,0 +1,33 @@ +require 'net/ssh/errors' +require 'net/ssh/loggable' +require 'net/ssh/transport/session' +require 'net/ssh/authentication/session' +require 'net/ssh/connection/session' + +module Net; module SSH + + class Session + include Loggable + + attr_reader :transport + attr_reader :connection + + def initialize(host, options={}) + self.logger = options[:logger] + + @transport = Transport::Session.new(host, options) + + auth = Authentication::Session.new(@transport, options) + if auth.authenticate("ssh-connection", options[:username], options[:password]) + @connection = Connection::Session.new(@transport, options) + else + raise AuthenticationFailed, options[:username] + end + end + + def close + @transport.close + end + end + +end; end
\ No newline at end of file diff --git a/lib/net/ssh/transport/packet_stream.rb b/lib/net/ssh/transport/packet_stream.rb index dbf4461..7b45a09 100644 --- a/lib/net/ssh/transport/packet_stream.rb +++ b/lib/net/ssh/transport/packet_stream.rb @@ -77,8 +77,12 @@ module Net; module SSH; module Transport return data.length end + def pending_write? + output.length > 0 + end + def send_queue - if output.length > 0 + if pending_write? sent = send(output.to_s, 0) trace { "sent #{sent} bytes" } output.consume!(sent) @@ -87,7 +91,7 @@ module Net; module SSH; module Transport def wait_for_queue send_queue - while output.length > 0 + while pending_write? result = IO.select(nil, [self]) or next next unless result[1].any? send_queue @@ -100,6 +104,8 @@ module Net; module SSH; module Transport end def enqueue_packet(payload) + payload = payload.to_s + # the length of the packet, minus the padding actual_length = 4 + payload.length + 1 @@ -123,7 +129,7 @@ module Net; module SSH; module Transport encrypted_data = client_cipher.update(unencrypted_data) << client_cipher.final message = encrypted_data + mac - trace { "queueing packet ##{@client_sequence_number} len #{packet_length}" } + trace { "queueing packet nr #{@client_sequence_number} type #{payload[0]} len #{packet_length}" } output.append(message) @client_sequence_number += 1 @@ -176,7 +182,7 @@ module Net; module SSH; module Transport my_computed_hmac = server_hmac.digest([server_sequence_number, @packet.content].pack("NA*")) raise Net::SSH::Exception, "corrupted mac detected" if real_hmac != my_computed_hmac - trace { "received packet ##{@server_sequence_number} len #{@packet_length}" } + trace { "received packet nr #{@server_sequence_number} type #{payload[0]} len #{@packet_length}" } @server_sequence_number += 1 @server_sequence_number = 0 if @server_sequence_number > 0xFFFFFFFF diff --git a/lib/net/ssh/transport/session.rb b/lib/net/ssh/transport/session.rb index 253c2f4..835677f 100644 --- a/lib/net/ssh/transport/session.rb +++ b/lib/net/ssh/transport/session.rb @@ -1,6 +1,7 @@ require 'socket' require 'timeout' +require 'net/ssh/errors' require 'net/ssh/loggable' require 'net/ssh/version' require 'net/ssh/transport/algorithms' @@ -62,14 +63,12 @@ module Net; module SSH; module Transport packet = socket.next_packet(mode) return nil if packet.nil? - trace { "got packet type #{packet.type} len #{packet.length}" } - case packet.type when DISCONNECT reason_code = packet.read_long description = packet.read_string language = packet.read_string - raise Net::SSH::Transport::Disconnect, "disconnected: #{description} (#{reason_code})" + raise Net::SSH::Disconnect, "disconnected: #{description} (#{reason_code})" when IGNORE trace { "IGNORE packet recieved: #{packet.read_string.inspect}" } @@ -91,8 +90,6 @@ module Net; module SSH; module Transport end def send_message(message) - message = message.to_s - trace { "sending packet type #{message[0]} len #{message.length}" } socket.send_packet(message) end end |
