summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJamis Buck <jamis@37signals.com>2007-07-25 04:53:11 +0000
committerJamis Buck <jamis@37signals.com>2007-07-25 04:53:11 +0000
commitffc496b4c9746325618d34ec29fa98bdc49b9d5f (patch)
tree8ee1c020a93dd8c6d4f23fb1734747d32da8e2cb
parent57a2e22b0873f99e9d6d66026df30de1296645da (diff)
downloadnet-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.rb117
-rw-r--r--lib/net/ssh/connection/constants.rb27
-rw-r--r--lib/net/ssh/connection/session.rb122
-rw-r--r--lib/net/ssh/errors.rb2
-rw-r--r--lib/net/ssh/packet.rb14
-rw-r--r--lib/net/ssh/session.rb33
-rw-r--r--lib/net/ssh/transport/packet_stream.rb14
-rw-r--r--lib/net/ssh/transport/session.rb7
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