em-ssh 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +102 -0
- data/bin/em-ssh +102 -0
- data/bin/em-ssh-shell +73 -0
- data/lib/em-ssh/authentication-session.rb +38 -0
- data/lib/em-ssh/callbacks.rb +99 -0
- data/lib/em-ssh/connection.rb +277 -0
- data/lib/em-ssh/log.rb +30 -0
- data/lib/em-ssh/packet-stream.rb +154 -0
- data/lib/em-ssh/server-version.rb +37 -0
- data/lib/em-ssh/session.rb +51 -0
- data/lib/em-ssh/shell.rb +225 -0
- data/lib/em-ssh/version.rb +5 -0
- data/lib/em-ssh.rb +74 -0
- metadata +106 -0
data/README.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
#EM-SSH
|
2
|
+
Em-ssh is a net-ssh adapter for EventMachine. For the most part you can take any net-ssh code you have and run it in the EventMachine reactor.
|
3
|
+
|
4
|
+
Em-ssh is not associated with the Jamis Buck's [net-ssh](http://net-ssh.github.com/) library. Please report any bugs with em-ssh to [https://github.com/simulacre/em-ssh/issues](https://github.com/simulacre/em-ssh/issues)
|
5
|
+
##Installation
|
6
|
+
gem install em-ssh
|
7
|
+
|
8
|
+
##Synopsis
|
9
|
+
EM.run do
|
10
|
+
EM::Ssh.start(host, user, :password => password) do |ssh|
|
11
|
+
# capture all stderr and stdout output from a remote process
|
12
|
+
ssh.exec!('uname -a').tap {|r| puts "\nuname: #{r}"}
|
13
|
+
|
14
|
+
# capture only stdout matching a particular pattern
|
15
|
+
stdout = ""
|
16
|
+
ssh.exec!("ls -l /home") do |channel, stream, data|
|
17
|
+
stdout << data if stream == :stdout
|
18
|
+
end
|
19
|
+
puts "\n#{stdout}"
|
20
|
+
|
21
|
+
# run multiple processes in parallel to completion
|
22
|
+
ssh.exec('ping -c 1 www.google.com')
|
23
|
+
ssh.exec('ping -c 1 www.yahoo.com')
|
24
|
+
ssh.exec('ping -c 1 www.rakuten.co.jp')
|
25
|
+
|
26
|
+
#open a new channel and configure a minimal set of callbacks, then wait for the channel to finishes (closees).
|
27
|
+
channel = ssh.open_channel do |ch|
|
28
|
+
ch.exec "/usr/local/bin/ruby /path/to/file.rb" do |ch, success|
|
29
|
+
raise "could not execute command" unless success
|
30
|
+
|
31
|
+
# "on_data" is called when the process writes something to stdout
|
32
|
+
ch.on_data do |c, data|
|
33
|
+
$stdout.print data
|
34
|
+
end
|
35
|
+
|
36
|
+
# "on_extended_data" is called when the process writes something to stderr
|
37
|
+
ch.on_extended_data do |c, type, data|
|
38
|
+
$stderr.print data
|
39
|
+
end
|
40
|
+
|
41
|
+
ch.on_close { puts "done!" }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
channel.wait
|
46
|
+
|
47
|
+
ssh.close
|
48
|
+
EM.stop
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
See [http://net-ssh.github.com/ssh/v2/api/index.html](http://net-ssh.github.com/ssh/v2/api/index.html)
|
53
|
+
|
54
|
+
##Shell
|
55
|
+
|
56
|
+
Em-ssh provides an exepct-like shell abstraction layer on top of net-ssh in EM::Ssh::Shell
|
57
|
+
|
58
|
+
### Example
|
59
|
+
EM.run {
|
60
|
+
EM::Ssh::Shell.new(host, 'caleb', 'password') do |shell|
|
61
|
+
shell.wait_for(']$')
|
62
|
+
shell.send_and_wait('sudo su -', 'password for caleb: ')
|
63
|
+
shell.send_and_wait('password', ']$')
|
64
|
+
output = shell.send_and_wait('/etc/init.d/openvpn restart', ']$')
|
65
|
+
# ...
|
66
|
+
shell.send_and_wait('exit', ']$')
|
67
|
+
shell.send_data('exit')
|
68
|
+
end
|
69
|
+
}
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
## Other Examples
|
74
|
+
See bin/em-ssh for an example of a basic replacement for system ssh.
|
75
|
+
|
76
|
+
See bin/em-ssh-shell for a more complex example usage of Shell.
|
77
|
+
|
78
|
+
|
79
|
+
##Copyright
|
80
|
+
Copyright (c) 2011 Caleb Crane
|
81
|
+
|
82
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
83
|
+
a copy of this software and associated documentation files (the
|
84
|
+
"Software"), to deal in the Software without restriction, including
|
85
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
86
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
87
|
+
permit persons to whom the Software is furnished to do so, subject to
|
88
|
+
the following conditions:
|
89
|
+
|
90
|
+
The above copyright notice and this permission notice shall be
|
91
|
+
included in all copies or substantial portions of the Software.
|
92
|
+
|
93
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
94
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
95
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
96
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
97
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
98
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
99
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
100
|
+
|
101
|
+
|
102
|
+
Portions of this software are Copyright (c) 2008 Jamis Buck
|
data/bin/em-ssh
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This is really nothing more than a utility to test the em-ssh adapter.
|
3
|
+
# It's not meant to be used for anything else.
|
4
|
+
# It probably requires ruby 1.9.2-p180 as p190 tends to segfault when using Fibers.
|
5
|
+
require 'bundler/setup'
|
6
|
+
require 'termios'
|
7
|
+
require 'highline'
|
8
|
+
require 'em-ssh'
|
9
|
+
|
10
|
+
include EM::Ssh::Log
|
11
|
+
|
12
|
+
|
13
|
+
def bufferio( enable, io = $stdin )
|
14
|
+
raise "Termios library not found" unless defined?(::Termios)
|
15
|
+
attr = Termios::getattr( io )
|
16
|
+
enable ? (attr.c_lflag |= Termios::ICANON | Termios::ECHO) : (attr.c_lflag &= ~(Termios::ICANON|Termios::ECHO))
|
17
|
+
Termios::setattr( io, Termios::TCSANOW, attr )
|
18
|
+
end # def bufferio( enable, io = $stdin )
|
19
|
+
|
20
|
+
def abort(msg)
|
21
|
+
puts msg
|
22
|
+
Process.exit
|
23
|
+
end # abort(msg)
|
24
|
+
|
25
|
+
options = {}
|
26
|
+
opts = OptionParser.new
|
27
|
+
opts.banner += " [user:[password]@]host[:port]"
|
28
|
+
options[:port] = 22
|
29
|
+
opts.on('-u', '--user String', String) { |u| options[:user] = u }
|
30
|
+
opts.on('-p', '--password [String]', String) do |p|
|
31
|
+
options[:password] = p.nil? ? HighLine.new.ask("password: "){|q| q.echo = "*" } : p
|
32
|
+
end
|
33
|
+
opts.on('-v', '--verbose') do
|
34
|
+
EM::Ssh.logger.level = EM::Ssh.logger.level - 1 unless EM::Ssh.logger.level == 0
|
35
|
+
options[:verbose] = EM::Ssh.logger.level
|
36
|
+
end
|
37
|
+
opts.parse!
|
38
|
+
|
39
|
+
host = ARGV.shift
|
40
|
+
if host.nil?
|
41
|
+
host,options[:password] = options[:password], HighLine.new.ask("#{options[:password]}'s password: "){|q| q.echo = "*" }
|
42
|
+
end # host.nil?
|
43
|
+
abort("a host is required") if host.nil?
|
44
|
+
|
45
|
+
options[:user], host = *host.split('@') if host.include?('@')
|
46
|
+
options[:user], options[:password] = *options[:user].split(':') if options[:user].include?(':')
|
47
|
+
host, options[:port] = *host.split(':') if host.include?(':')
|
48
|
+
connected = false
|
49
|
+
|
50
|
+
|
51
|
+
module CInput
|
52
|
+
def shell=(shell)
|
53
|
+
@shell = shell
|
54
|
+
end # shell=(shell)
|
55
|
+
def initialize
|
56
|
+
bufferio(false, $stdin)
|
57
|
+
end # initialize
|
58
|
+
def unbind
|
59
|
+
bufferio(true, $stdin)
|
60
|
+
end # unbind
|
61
|
+
def notify_readable
|
62
|
+
@shell.send_data($stdin.read(1))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
|
68
|
+
EM.run do
|
69
|
+
EM::Ssh.start(host, options[:user], options) do |connection|
|
70
|
+
debug "**** connected: #{connection}"
|
71
|
+
connection.open_channel do |channel|
|
72
|
+
debug "**** channel: #{channel}"
|
73
|
+
channel.request_pty(options[:pty] || {}) do |pty,suc|
|
74
|
+
debug "***** pty: #{pty}; suc: #{suc}"
|
75
|
+
pty.send_channel_request("shell") do |shell,success|
|
76
|
+
raise ConnectionError, "Failed to create shell." unless success
|
77
|
+
debug "***** shell: #{shell}"
|
78
|
+
connected = true
|
79
|
+
|
80
|
+
shell.on_data { |c,d| $stdout.print d }
|
81
|
+
shell.on_extended_data { |c,data| $STDERR.print data }
|
82
|
+
shell.on_eof do
|
83
|
+
shell.close
|
84
|
+
EM.stop
|
85
|
+
end #
|
86
|
+
|
87
|
+
trap("SIGINT") { shell.send_data("\C-c") }
|
88
|
+
trap("SIGEXIT") do
|
89
|
+
shell.close
|
90
|
+
trap("SIGINT", "SIG_DFL")
|
91
|
+
end #
|
92
|
+
|
93
|
+
conn = EM.watch($stdin, CInput)
|
94
|
+
conn.shell = shell
|
95
|
+
conn.notify_readable = true
|
96
|
+
|
97
|
+
end # |shell,success|
|
98
|
+
end # |pty,suc|
|
99
|
+
end # |channel|
|
100
|
+
end # |connection|
|
101
|
+
end # EM.start
|
102
|
+
|
data/bin/em-ssh-shell
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This is really nothing more than a utility to test the em-ssh adapter.
|
3
|
+
# It's not meant to be used for anything else.
|
4
|
+
# It probably requires ruby 1.9.2-p180 as p190 tends to segfault when using Fibers.
|
5
|
+
require 'bundler/setup'
|
6
|
+
require 'termios'
|
7
|
+
require 'highline'
|
8
|
+
require 'em-ssh'
|
9
|
+
require 'em-ssh/shell'
|
10
|
+
|
11
|
+
include EM::Ssh::Log
|
12
|
+
|
13
|
+
def abort(msg)
|
14
|
+
puts msg
|
15
|
+
Process.exit
|
16
|
+
end # abort(msg)
|
17
|
+
|
18
|
+
options = {}
|
19
|
+
opts = OptionParser.new
|
20
|
+
opts.banner += " [user:[password]@]host[:port] wait_string command [command command ...]"
|
21
|
+
options[:port] = 22
|
22
|
+
opts.on('-u', '--user String', String) { |u| options[:user] = u }
|
23
|
+
opts.on('-p', '--password [String]', String) do |p|
|
24
|
+
options[:password] = p.nil? ? HighLine.new.ask("password: "){|q| q.echo = "*" } : p
|
25
|
+
end
|
26
|
+
opts.on('-v', '--verbose') do
|
27
|
+
EM::Ssh.logger.level = EM::Ssh.logger.level - 1 unless EM::Ssh.logger.level == 0
|
28
|
+
options[:verbose] = EM::Ssh.logger.level
|
29
|
+
end
|
30
|
+
opts.parse!
|
31
|
+
|
32
|
+
host = ARGV.shift
|
33
|
+
if host.nil?
|
34
|
+
host,options[:password] = options[:password], HighLine.new.ask("#{options[:password]}'s password: "){|q| q.echo = "*" }
|
35
|
+
end # host.nil?
|
36
|
+
abort("a host is required") if host.nil?
|
37
|
+
|
38
|
+
options[:user], host = *host.split('@') if host.include?('@')
|
39
|
+
options[:user], options[:password] = *options[:user].split(':') if options[:user].include?(':')
|
40
|
+
host, options[:port] = *host.split(':') if host.include?(':')
|
41
|
+
|
42
|
+
|
43
|
+
waitstr = ARGV.shift
|
44
|
+
commands = ARGV
|
45
|
+
abort("wait_string is required") if waitstr.nil?
|
46
|
+
abort("command is required") if commands.empty?
|
47
|
+
|
48
|
+
|
49
|
+
EM.run do
|
50
|
+
Fiber.new {
|
51
|
+
shell = EM::Ssh::Shell.new(host, options[:user], options[:password])
|
52
|
+
commands.each do |command|
|
53
|
+
mys = shell.split
|
54
|
+
mys.on(:closed) { info("#{mys} has closed") }
|
55
|
+
|
56
|
+
EM.next_tick do
|
57
|
+
Fiber.new {
|
58
|
+
debug("#{mys} waited for: '#{mys.wait_for(waitstr)}'")
|
59
|
+
mys.line_terminator = "\n"
|
60
|
+
debug("#{mys} send: #{command.inspect}")
|
61
|
+
puts "#{mys} result: '#{mys.send_and_wait(command, waitstr)}'"
|
62
|
+
mys.close
|
63
|
+
}.resume
|
64
|
+
end #
|
65
|
+
end # |command|
|
66
|
+
|
67
|
+
shell.on(:childless) do
|
68
|
+
info("#{shell}'s children all closed")
|
69
|
+
shell.close
|
70
|
+
EM.stop
|
71
|
+
end # :childless
|
72
|
+
}.resume
|
73
|
+
end # EM.run
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module EventMachine
|
2
|
+
class Ssh
|
3
|
+
class AuthenticationSession < Net::SSH::Authentication::Session
|
4
|
+
include Log
|
5
|
+
|
6
|
+
def authenticate(*args)
|
7
|
+
debug { "authenticate(#{args.join(", ")})" }
|
8
|
+
super(*args)
|
9
|
+
end # authenticate(*args)
|
10
|
+
|
11
|
+
# Returns once an acceptable auth packet is received.
|
12
|
+
def next_message
|
13
|
+
packet = transport.next_message
|
14
|
+
|
15
|
+
case packet.type
|
16
|
+
when USERAUTH_BANNER
|
17
|
+
info { packet[:message] }
|
18
|
+
transport.fire(:auth_banner, packet[:message])
|
19
|
+
return next_message
|
20
|
+
when USERAUTH_FAILURE
|
21
|
+
@allowed_auth_methods = packet[:authentications].split(/,/)
|
22
|
+
debug { "allowed methods: #{packet[:authentications]}" }
|
23
|
+
return packet
|
24
|
+
|
25
|
+
when USERAUTH_METHOD_RANGE, SERVICE_ACCEPT
|
26
|
+
return packet
|
27
|
+
|
28
|
+
when USERAUTH_SUCCESS
|
29
|
+
transport.hint :authenticated
|
30
|
+
return packet
|
31
|
+
|
32
|
+
else
|
33
|
+
raise SshError, "unexpected message #{packet.type} (#{packet})"
|
34
|
+
end
|
35
|
+
end # next_message
|
36
|
+
end # class::AuthenticationSession
|
37
|
+
end # module::Ssh
|
38
|
+
end # module::EventMachine
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module EventMachine
|
2
|
+
class Ssh
|
3
|
+
# A simple mixin enabling your objects to allow other objects to register callbacks and fire events.
|
4
|
+
# @example
|
5
|
+
# class Connection
|
6
|
+
# include Callbacks
|
7
|
+
# # ...
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# connection = Connection.new(...)
|
11
|
+
# cb = connection.on(:data) do |data|
|
12
|
+
# @version << data
|
13
|
+
# if @version[-1] == "\n"
|
14
|
+
# @version.chomp!
|
15
|
+
# raise SshError.new("incompatible SSH version `#{@version}'") unless @version.match(/^SSH-(1\.99|2\.0)-/)
|
16
|
+
# connection.send_data("#{PROTO_VERSION}\r\n")
|
17
|
+
# cb.cancel
|
18
|
+
# connection.fire(:version_negotiated)
|
19
|
+
# end # @header[-1] == "\n"
|
20
|
+
# end # |data|
|
21
|
+
module Callbacks
|
22
|
+
|
23
|
+
# @return [Hash] The registered callbacks
|
24
|
+
def callbacks
|
25
|
+
@clbks ||= {}
|
26
|
+
end # callbacks
|
27
|
+
|
28
|
+
|
29
|
+
# Signal that an event has occured.
|
30
|
+
# Each callback will receive whatever args are passed to fire, or the object that the event was fired upon.
|
31
|
+
# @param [Symbol] event
|
32
|
+
# @param [Objects] arguments to pass to all callbacks registered for the given event
|
33
|
+
# @param [Array] the results of each callback that was executed
|
34
|
+
def fire(event, *args)
|
35
|
+
#log.debug("#{self}.fire(#{event.inspect}, #{args})")
|
36
|
+
args = self if args.empty?
|
37
|
+
(callbacks[event] ||= []).clone.map { |cb| cb.call(*args) }
|
38
|
+
end # fire(event)
|
39
|
+
|
40
|
+
# Register a callback to be fired when a matching event occurs.
|
41
|
+
# The callback will be fired when the event occurs until it returns true.
|
42
|
+
# @param [Symbol] event
|
43
|
+
def on(event, &blk)
|
44
|
+
#log.debug("#{self}.on(#{event.inspect}, #{blk})")
|
45
|
+
if block_given?
|
46
|
+
raise "event (#{event.inspect}) must be a symbol when a block is given" unless event.is_a?(Symbol)
|
47
|
+
return Callback.new(self, event, &blk).tap{|cb| (callbacks[event] ||= []).push(cb) }
|
48
|
+
end # block_given?
|
49
|
+
|
50
|
+
raise "event (#{event.inspect}) must be a Callback when a block is not given" unless event.is_a?(Callback)
|
51
|
+
(callbacks[event] ||= []).push(event)
|
52
|
+
return event
|
53
|
+
end # on(event, &blk)
|
54
|
+
|
55
|
+
# Registers a callback that will be canceled after the first time it is called.
|
56
|
+
def on_next(event, &blk)
|
57
|
+
cb = on(event) do |*args|
|
58
|
+
cb.cancel
|
59
|
+
blk.call(*args)
|
60
|
+
end # |*args|
|
61
|
+
end # on_next(event, &blk)
|
62
|
+
|
63
|
+
|
64
|
+
class Callback
|
65
|
+
# The object that keeps this callback
|
66
|
+
attr_reader :obj
|
67
|
+
# [Sybmol] the name of the event
|
68
|
+
attr_reader :event
|
69
|
+
# The block to call when the event is fired
|
70
|
+
attr_reader :block
|
71
|
+
|
72
|
+
def initialize(obj, event, &blk)
|
73
|
+
raise ArgumentError.new("a block is required") unless block_given?
|
74
|
+
@obj = obj
|
75
|
+
@event = event
|
76
|
+
@block = blk
|
77
|
+
end # initialize(obj, event, &blk)
|
78
|
+
|
79
|
+
# Call the callback with optional arguments
|
80
|
+
def call(*args)
|
81
|
+
block.call(*args)
|
82
|
+
end # call(*args)
|
83
|
+
|
84
|
+
# Registers the callback with the object.
|
85
|
+
# This is useful if you cancel the callback at one point and want to re-enable it later on.
|
86
|
+
def register
|
87
|
+
@obj.on(self)
|
88
|
+
end # register
|
89
|
+
|
90
|
+
def cancel
|
91
|
+
raise "#{@obj} does not have any callbacks for #{@event.inspect}" unless @obj.respond_to?(:callbacks) && @obj.callbacks.respond_to?(:[]) && @obj.callbacks[@event].respond_to?(:delete)
|
92
|
+
@obj.callbacks[@event].delete(self)
|
93
|
+
self
|
94
|
+
end # cancel
|
95
|
+
end # class::Callback
|
96
|
+
|
97
|
+
end # module::Callbacks
|
98
|
+
end # class::Ssh
|
99
|
+
end # module::EventMachine
|
@@ -0,0 +1,277 @@
|
|
1
|
+
require 'em-ssh/callbacks'
|
2
|
+
module EventMachine
|
3
|
+
class Ssh
|
4
|
+
# EventMachine::Ssh::Connection is a EventMachine::Connection that emulates the Net::SSH transport layer. It ties
|
5
|
+
# itself into Net::SSH so that the EventMachine reactor loop can take the place of the Net::SSH event loop.
|
6
|
+
# Most of the methods here are only for compatibility with Net::SSH
|
7
|
+
class Connection < EventMachine::Connection
|
8
|
+
include Log
|
9
|
+
|
10
|
+
# Allows other objects to register callbacks with events that occur on a Ssh instance
|
11
|
+
include Callbacks
|
12
|
+
|
13
|
+
##
|
14
|
+
# Transport related
|
15
|
+
|
16
|
+
# @return [String] The host to connect to, as given to the constructor.
|
17
|
+
attr_reader :host
|
18
|
+
|
19
|
+
# @return [Fixnum] the port number (DEFAULT_PORT) to connect to, as given in the options to the constructor.
|
20
|
+
attr_reader :port
|
21
|
+
|
22
|
+
# @return [ServerVersion] The ServerVersion instance that encapsulates the negotiated protocol version.
|
23
|
+
attr_reader :server_version
|
24
|
+
|
25
|
+
# The Algorithms instance used to perform key exchanges.
|
26
|
+
attr_reader :algorithms
|
27
|
+
|
28
|
+
# The host-key verifier object used to verify host keys, to ensure that the connection is not being spoofed.
|
29
|
+
attr_reader :host_key_verifier
|
30
|
+
|
31
|
+
# The hash of options that were given to the object at initialization.
|
32
|
+
attr_reader :options
|
33
|
+
|
34
|
+
# @return [PacketStream] emulates a socket and ssh packetstream
|
35
|
+
attr_reader :socket
|
36
|
+
|
37
|
+
# @return [Boolean] true if the connection has been closed
|
38
|
+
def closed?
|
39
|
+
@closed == true
|
40
|
+
end
|
41
|
+
|
42
|
+
# Close the connection
|
43
|
+
def close
|
44
|
+
# #unbind will update @closed
|
45
|
+
close_connection
|
46
|
+
end
|
47
|
+
|
48
|
+
# Send a packet to the server
|
49
|
+
def send_message(message)
|
50
|
+
@socket.send_packet(message)
|
51
|
+
end
|
52
|
+
alias :enqueue_message :send_message
|
53
|
+
|
54
|
+
def next_message
|
55
|
+
return @queue.shift if @queue.any? && algorithms.allow?(@queue.first)
|
56
|
+
f = Fiber.current
|
57
|
+
cb = on(:packet) do |packet|
|
58
|
+
if @queue.any? && algorithms.allow?(@queue.first)
|
59
|
+
cb.cancel
|
60
|
+
f.resume(@queue.shift)
|
61
|
+
end
|
62
|
+
end # :packet
|
63
|
+
return Fiber.yield
|
64
|
+
end # next_message
|
65
|
+
|
66
|
+
# Returns a new service_request packet for the given service name, ready
|
67
|
+
# for sending to the server.
|
68
|
+
def service_request(service)
|
69
|
+
Net::SSH::Buffer.from(:byte, SERVICE_REQUEST, :string, service)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Requests a rekey operation, and simulates a block until the operation completes.
|
73
|
+
# If a rekey is already pending, this returns immediately, having no effect.
|
74
|
+
def rekey!
|
75
|
+
if !algorithms.pending?
|
76
|
+
f = Fiber.current
|
77
|
+
on_next(:algo_init) do
|
78
|
+
f.resume
|
79
|
+
end # :algo_init
|
80
|
+
algorithms.rekey!
|
81
|
+
return Fiber.yield
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns immediately if a rekey is already in process. Otherwise, if a
|
86
|
+
# rekey is needed (as indicated by the socket, see PacketStream#if_needs_rekey?)
|
87
|
+
# one is performed, causing this method to block until it completes.
|
88
|
+
def rekey_as_needed
|
89
|
+
return if algorithms.pending?
|
90
|
+
socket.if_needs_rekey? { rekey! }
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
##
|
96
|
+
# EventMachine callbacks
|
97
|
+
###
|
98
|
+
def post_init
|
99
|
+
@socket = PacketStream.new(self)
|
100
|
+
@data = @socket.input
|
101
|
+
end # post_init
|
102
|
+
|
103
|
+
# @return
|
104
|
+
def unbind
|
105
|
+
debug("#{self} is unbound")
|
106
|
+
@closed = true
|
107
|
+
end
|
108
|
+
|
109
|
+
def receive_data(data)
|
110
|
+
debug("read #{data.length} bytes")
|
111
|
+
@data.append(data)
|
112
|
+
fire(:data, data)
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
|
117
|
+
def initialize(options = {})
|
118
|
+
debug("#{self.class}.new(#{options})")
|
119
|
+
@host = options[:host]
|
120
|
+
@port = options[:port]
|
121
|
+
@password = options[:password]
|
122
|
+
@queue = []
|
123
|
+
@options = options
|
124
|
+
|
125
|
+
begin
|
126
|
+
on(:connected, &options[:callback]) if options[:callback]
|
127
|
+
@error_callback = lambda { |code| raise SshError.new(code) }
|
128
|
+
|
129
|
+
@host_key_verifier = select_host_key_verifier(options[:paranoid])
|
130
|
+
@server_version = ServerVersion.new(self)
|
131
|
+
on(:version_negotiated) do
|
132
|
+
@data.consume!(@server_version.header.length)
|
133
|
+
@algorithms = Net::SSH::Transport::Algorithms.new(self, options)
|
134
|
+
|
135
|
+
register_data_handler
|
136
|
+
|
137
|
+
on_next(:algo_init) do
|
138
|
+
auth = AuthenticationSession.new(self, options)
|
139
|
+
user = options.fetch(:user, user)
|
140
|
+
Fiber.new do
|
141
|
+
if auth.authenticate("ssh-connection", user, options[:password])
|
142
|
+
fire(:connected, Session.new(self, options))
|
143
|
+
else
|
144
|
+
close_connection
|
145
|
+
raise Net::SSH::AuthenticationFailed, user
|
146
|
+
end # auth.authenticate("ssh-connection", user, options[:password])
|
147
|
+
end.resume # Fiber
|
148
|
+
end # :algo_init
|
149
|
+
end # :version_negotiated
|
150
|
+
|
151
|
+
rescue Exception => e
|
152
|
+
log.fatal("caught an error during initialization: #{e}\n #{e.backtrace.join("\n ")}")
|
153
|
+
Process.exit
|
154
|
+
end # begin
|
155
|
+
self
|
156
|
+
end # initialize(options = {})
|
157
|
+
|
158
|
+
|
159
|
+
##
|
160
|
+
# Helpers required for compatibility with Net::SSH
|
161
|
+
##
|
162
|
+
|
163
|
+
# Returns the host (and possibly IP address) in a format compatible with
|
164
|
+
# SSH known-host files.
|
165
|
+
def host_as_string
|
166
|
+
@host_as_string ||= "#{host}".tap do |string|
|
167
|
+
string = "[#{string}]:#{port}" if port != DEFAULT_PORT
|
168
|
+
_, ip = Socket.unpack_sockaddr_in(get_peername)
|
169
|
+
if ip != host
|
170
|
+
string << "," << (port != DEFAULT_PORT ? "[#{ip}]:#{port}" : ip)
|
171
|
+
end # ip != host
|
172
|
+
end # |string|
|
173
|
+
end # host_as_string
|
174
|
+
|
175
|
+
alias :logger :log
|
176
|
+
|
177
|
+
|
178
|
+
# Configure's the packet stream's client state with the given set of
|
179
|
+
# options. This is typically used to define the cipher, compression, and
|
180
|
+
# hmac algorithms to use when sending packets to the server.
|
181
|
+
def configure_client(options={})
|
182
|
+
@socket.client.set(options)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Configure's the packet stream's server state with the given set of
|
186
|
+
# options. This is typically used to define the cipher, compression, and
|
187
|
+
# hmac algorithms to use when reading packets from the server.
|
188
|
+
def configure_server(options={})
|
189
|
+
@socket.server.set(options)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Sets a new hint for the packet stream, which the packet stream may use
|
193
|
+
# to change its behavior. (See PacketStream#hints).
|
194
|
+
def hint(which, value=true)
|
195
|
+
@socket.hints[which] = value
|
196
|
+
end
|
197
|
+
|
198
|
+
# Returns a new service_request packet for the given service name, ready
|
199
|
+
# for sending to the server.
|
200
|
+
def service_request(service)
|
201
|
+
Net::SSH::Buffer.from(:byte, SERVICE_REQUEST, :string, service)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Returns a hash of information about the peer (remote) side of the socket,
|
205
|
+
# including :ip, :port, :host, and :canonized (see #host_as_string).
|
206
|
+
def peer
|
207
|
+
@peer ||= {}.tap do |p|
|
208
|
+
_, ip = Socket.unpack_sockaddr_in(get_peername)
|
209
|
+
p[:ip] = ip
|
210
|
+
p[:port] = @port.to_i
|
211
|
+
p[:host] = @host
|
212
|
+
p[:canonized] = host_as_string
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
|
218
|
+
|
219
|
+
private
|
220
|
+
|
221
|
+
# Register the primary :data callback
|
222
|
+
# @return [Callback] the callback that was registered
|
223
|
+
def register_data_handler
|
224
|
+
on(:data) do |data|
|
225
|
+
# @todo instead of while use a EM.next_tick
|
226
|
+
while (packet = @socket.poll_next_packet)
|
227
|
+
case packet.type
|
228
|
+
when DISCONNECT
|
229
|
+
raise Net::SSH::Disconnect, "disconnected: #{packet[:description]} (#{packet[:reason_code]})"
|
230
|
+
when IGNORE
|
231
|
+
debug("IGNORE packet received: #{packet[:data].inspect}")
|
232
|
+
when UNIMPLEMENTED
|
233
|
+
log.warn("UNIMPLEMENTED: #{packet[:number]}")
|
234
|
+
when DEBUG
|
235
|
+
log.send((packet[:always_display] ? :fatal : :debug), packet[:message])
|
236
|
+
when KEXINIT
|
237
|
+
Fiber.new do
|
238
|
+
algorithms.accept_kexinit(packet)
|
239
|
+
fire(:algo_init) if algorithms.initialized?
|
240
|
+
end.resume
|
241
|
+
else
|
242
|
+
@queue.push(packet)
|
243
|
+
if algorithms.allow?(packet)
|
244
|
+
fire(:packet, packet)
|
245
|
+
fire(:session_packet, packet) if packet.type >= GLOBAL_REQUEST
|
246
|
+
end # algorithms.allow?(packet)
|
247
|
+
socket.consume!
|
248
|
+
end # packet.type
|
249
|
+
end # (packet = @socket.poll_next_packet)
|
250
|
+
end # |data|
|
251
|
+
end # register_data_handler
|
252
|
+
|
253
|
+
# Instantiates a new host-key verification class, based on the value of
|
254
|
+
# the parameter. When true or nil, the default Lenient verifier is
|
255
|
+
# returned. If it is false, the Null verifier is returned, and if it is
|
256
|
+
# :very, the Strict verifier is returned. If the argument happens to
|
257
|
+
# respond to :verify, it is returned directly. Otherwise, an exception
|
258
|
+
# is raised.
|
259
|
+
# Taken from Net::SSH::Session
|
260
|
+
def select_host_key_verifier(paranoid)
|
261
|
+
case paranoid
|
262
|
+
when true, nil then
|
263
|
+
Net::SSH::Verifiers::Lenient.new
|
264
|
+
when false then
|
265
|
+
Net::SSH::Verifiers::Null.new
|
266
|
+
when :very then
|
267
|
+
Net::SSH::Verifiers::Strict.new
|
268
|
+
else
|
269
|
+
paranoid.respond_to?(:verify) ? paranoid : (raise ArgumentError.new("argument to :paranoid is not valid: #{paranoid.inspect}"))
|
270
|
+
end # paranoid
|
271
|
+
end # select_host_key_verifier(paranoid)
|
272
|
+
|
273
|
+
end # class::Connection < EventMachine::Connection
|
274
|
+
end # module::Ssh
|
275
|
+
end # module::EventMachine
|
276
|
+
|
277
|
+
|
data/lib/em-ssh/log.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module EventMachine
|
2
|
+
class Ssh
|
3
|
+
module Log
|
4
|
+
# @return [Logger] the default logger
|
5
|
+
def log
|
6
|
+
EventMachine::Ssh.logger
|
7
|
+
end
|
8
|
+
|
9
|
+
def debug(msg = nil, &blk)
|
10
|
+
log.debug("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
|
11
|
+
end
|
12
|
+
|
13
|
+
def info(msg = nil, &blk)
|
14
|
+
log.info("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
|
15
|
+
end
|
16
|
+
|
17
|
+
def fatal(msg = nil, &blk)
|
18
|
+
log.fatal("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
|
19
|
+
end
|
20
|
+
|
21
|
+
def warn(msg = nil, &blk)
|
22
|
+
log.warn("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
|
23
|
+
end
|
24
|
+
|
25
|
+
def error(msg = nil, &blk)
|
26
|
+
log.error("#{self.class}".downcase.gsub("::",".") + " #{msg}", &blk)
|
27
|
+
end
|
28
|
+
end # module::Log
|
29
|
+
end # class::Ssh
|
30
|
+
end # module::EventMachine
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module EventMachine
|
2
|
+
class Ssh
|
3
|
+
class PacketStream
|
4
|
+
include Net::SSH::BufferedIo
|
5
|
+
include Log
|
6
|
+
|
7
|
+
|
8
|
+
# The map of "hints" that can be used to modify the behavior of the packet
|
9
|
+
# stream. For instance, when authentication succeeds, an "authenticated"
|
10
|
+
# hint is set, which is used to determine whether or not to compress the
|
11
|
+
# data when using the "delayed" compression algorithm.
|
12
|
+
attr_reader :hints
|
13
|
+
|
14
|
+
# The server state object, which encapsulates the algorithms used to interpret
|
15
|
+
# packets coming from the server.
|
16
|
+
attr_reader :server
|
17
|
+
|
18
|
+
# The client state object, which encapsulates the algorithms used to build
|
19
|
+
# packets to send to the server.
|
20
|
+
attr_reader :client
|
21
|
+
|
22
|
+
# The input stream
|
23
|
+
attr_reader :input
|
24
|
+
# The output stream
|
25
|
+
attr_reader :output
|
26
|
+
|
27
|
+
def initialize(connection)
|
28
|
+
@connection = connection
|
29
|
+
@input = Net::SSH::Buffer.new
|
30
|
+
@output = Net::SSH::Buffer.new
|
31
|
+
@hints = {}
|
32
|
+
@server = Net::SSH::Transport::State.new(self, :server)
|
33
|
+
@client = Net::SSH::Transport::State.new(self, :client)
|
34
|
+
@packet = nil
|
35
|
+
end # initialize(content="")
|
36
|
+
|
37
|
+
# Consumes n bytes from the buffer, where n is the current position
|
38
|
+
# unless otherwise specified. This is useful for removing data from the
|
39
|
+
# buffer that has previously been read, when you are expecting more data
|
40
|
+
# to be appended. It helps to keep the size of buffers down when they
|
41
|
+
# would otherwise tend to grow without bound.
|
42
|
+
#
|
43
|
+
# Returns the buffer object itself.
|
44
|
+
def consume!(*args)
|
45
|
+
input.consume!(*args)
|
46
|
+
end # consume!(*args)
|
47
|
+
|
48
|
+
# Tries to read the next packet. If there is insufficient data to read
|
49
|
+
# an entire packet, this returns immediately, otherwise the packet is
|
50
|
+
# read, post-processed according to the cipher, hmac, and compression
|
51
|
+
# algorithms specified in the server state object, and returned as a
|
52
|
+
# new Packet object.
|
53
|
+
# Copyright (c) 2008 Jamis Buck
|
54
|
+
def poll_next_packet
|
55
|
+
if @packet.nil?
|
56
|
+
minimum = server.block_size < 4 ? 4 : server.block_size
|
57
|
+
return nil if available < minimum
|
58
|
+
data = read_available(minimum)
|
59
|
+
# decipher it
|
60
|
+
@packet = Net::SSH::Buffer.new(server.update_cipher(data))
|
61
|
+
@packet_length = @packet.read_long
|
62
|
+
end
|
63
|
+
need = @packet_length + 4 - server.block_size
|
64
|
+
raise SshError, "padding error, need #{need} block #{server.block_size}" if need % server.block_size != 0
|
65
|
+
|
66
|
+
return nil if available < need + server.hmac.mac_length
|
67
|
+
|
68
|
+
if need > 0
|
69
|
+
# read the remainder of the packet and decrypt it.
|
70
|
+
data = read_available(need)
|
71
|
+
@packet.append(server.update_cipher(data))
|
72
|
+
end
|
73
|
+
|
74
|
+
# get the hmac from the tail of the packet (if one exists), and
|
75
|
+
# then validate it.
|
76
|
+
real_hmac = read_available(server.hmac.mac_length) || ""
|
77
|
+
|
78
|
+
@packet.append(server.final_cipher)
|
79
|
+
padding_length = @packet.read_byte
|
80
|
+
|
81
|
+
payload = @packet.read(@packet_length - padding_length - 1)
|
82
|
+
padding = @packet.read(padding_length) if padding_length > 0
|
83
|
+
|
84
|
+
my_computed_hmac = server.hmac.digest([server.sequence_number, @packet.content].pack("NA*"))
|
85
|
+
raise Net::SSH::Exception, "corrupted mac detected" if real_hmac != my_computed_hmac
|
86
|
+
|
87
|
+
# try to decompress the payload, in case compression is active
|
88
|
+
payload = server.decompress(payload)
|
89
|
+
|
90
|
+
log.debug("received packet nr #{server.sequence_number} type #{payload.getbyte(0)} len #{@packet_length}")
|
91
|
+
|
92
|
+
server.increment(@packet_length)
|
93
|
+
@packet = nil
|
94
|
+
|
95
|
+
return Net::SSH::Packet.new(payload)
|
96
|
+
end # poll_next_packet
|
97
|
+
|
98
|
+
# Copyright (c) 2008 Jamis Buck
|
99
|
+
def send_packet(payload)
|
100
|
+
# try to compress the packet
|
101
|
+
payload = client.compress(payload)
|
102
|
+
|
103
|
+
# the length of the packet, minus the padding
|
104
|
+
actual_length = 4 + payload.length + 1
|
105
|
+
|
106
|
+
# compute the padding length
|
107
|
+
padding_length = client.block_size - (actual_length % client.block_size)
|
108
|
+
padding_length += client.block_size if padding_length < 4
|
109
|
+
|
110
|
+
# compute the packet length (sans the length field itself)
|
111
|
+
packet_length = payload.length + padding_length + 1
|
112
|
+
if packet_length < 16
|
113
|
+
padding_length += client.block_size
|
114
|
+
packet_length = payload.length + padding_length + 1
|
115
|
+
end
|
116
|
+
|
117
|
+
padding = Array.new(padding_length) { rand(256) }.pack("C*")
|
118
|
+
|
119
|
+
unencrypted_data = [packet_length, padding_length, payload, padding].pack("NCA*A*")
|
120
|
+
mac = client.hmac.digest([client.sequence_number, unencrypted_data].pack("NA*"))
|
121
|
+
|
122
|
+
encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher
|
123
|
+
message = encrypted_data + mac
|
124
|
+
|
125
|
+
log.debug("queueing packet nr #{client.sequence_number} type #{payload.getbyte(0)} len #{packet_length}")
|
126
|
+
@connection.send_data(message)
|
127
|
+
log.debug("sent #{message.length} bytes")
|
128
|
+
client.increment(packet_length)
|
129
|
+
|
130
|
+
self
|
131
|
+
end # send_packet(payload)
|
132
|
+
|
133
|
+
# Performs any pending cleanup necessary on the IO and its associated
|
134
|
+
# state objects. (See State#cleanup).
|
135
|
+
def cleanup
|
136
|
+
client.cleanup
|
137
|
+
server.cleanup
|
138
|
+
end
|
139
|
+
|
140
|
+
# If the IO object requires a rekey operation (as indicated by either its
|
141
|
+
# client or server state objects, see State#needs_rekey?), this will
|
142
|
+
# yield. Otherwise, this does nothing.
|
143
|
+
# Copyright (c) 2008 Jamis Buck
|
144
|
+
def if_needs_rekey?
|
145
|
+
if client.needs_rekey? || server.needs_rekey?
|
146
|
+
yield
|
147
|
+
client.reset! if client.needs_rekey?
|
148
|
+
server.reset! if server.needs_rekey?
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end # class::PacketStream
|
153
|
+
end # class::Ssh
|
154
|
+
end # module::EventMachine
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module EventMachine
|
2
|
+
class Ssh
|
3
|
+
class ServerVersion
|
4
|
+
include Log
|
5
|
+
|
6
|
+
attr_reader :header
|
7
|
+
attr_reader :version
|
8
|
+
|
9
|
+
def initialize(connection)
|
10
|
+
debug("#{self}.new(#{connection})")
|
11
|
+
negotiate!(connection)
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def negotiate!(connection)
|
18
|
+
@version = ''
|
19
|
+
cb = connection.on(:data) do |data|
|
20
|
+
log.debug("#{self.class}.on(:data, #{data.inspect})")
|
21
|
+
@version << data
|
22
|
+
@header = @version.clone
|
23
|
+
if @version[-1] == "\n"
|
24
|
+
@version.chomp!
|
25
|
+
log.debug("server version: #{@version}")
|
26
|
+
raise SshError.new("incompatible SSH version `#{@version}'") unless @version.match(/^SSH-(1\.99|2\.0)-/)
|
27
|
+
log.debug("local version: #{Net::SSH::Transport::ServerVersion::PROTO_VERSION}")
|
28
|
+
connection.send_data("#{Net::SSH::Transport::ServerVersion::PROTO_VERSION}\r\n")
|
29
|
+
cb.cancel
|
30
|
+
connection.fire(:version_negotiated)
|
31
|
+
end # @header[-1] == "\n"
|
32
|
+
end # |data|
|
33
|
+
end
|
34
|
+
|
35
|
+
end # class::ServerVersion
|
36
|
+
end # module::Ssh
|
37
|
+
end # module::EventMachine
|
@@ -0,0 +1,51 @@
|
|
1
|
+
|
2
|
+
module EventMachine
|
3
|
+
class Ssh
|
4
|
+
class Session < Net::SSH::Connection::Session
|
5
|
+
include Log
|
6
|
+
|
7
|
+
def initialize(transport, options = {})
|
8
|
+
super(transport, options)
|
9
|
+
register_callbacks
|
10
|
+
end
|
11
|
+
|
12
|
+
# Override the default, blocking behavior of Net::SSH.
|
13
|
+
# Callers to loop will still wait, but not block the loop.
|
14
|
+
def loop(wait=nil, &block)
|
15
|
+
f = Fiber.current
|
16
|
+
l = proc do
|
17
|
+
block.call ? EM.next_tick(&l) : f.resume
|
18
|
+
end
|
19
|
+
EM.next_tick(&l)
|
20
|
+
return Fiber.yield
|
21
|
+
end
|
22
|
+
|
23
|
+
# Override the default, blocking behavior of Net::SSH
|
24
|
+
def process(wait=nil, &block)
|
25
|
+
return true
|
26
|
+
end
|
27
|
+
|
28
|
+
def send_message(msg)
|
29
|
+
transport.send_message(msg)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
|
36
|
+
def register_callbacks
|
37
|
+
transport.on(:packet) do |packet|
|
38
|
+
raise SshError, "unexpected response #{packet.type} (#{packet.inspect})" unless MAP.key?(packet.type)
|
39
|
+
send(MAP[packet.type], packet)
|
40
|
+
end # |packet|
|
41
|
+
|
42
|
+
chann_proc = proc do
|
43
|
+
channels.each { |id, channel| channel.process unless channel.closing? }
|
44
|
+
EM.next_tick(&chann_proc)
|
45
|
+
end
|
46
|
+
EM.next_tick(&chann_proc)
|
47
|
+
end # register_callbacks
|
48
|
+
|
49
|
+
end # class::Session
|
50
|
+
end # class::Ssh
|
51
|
+
end # module::EventMachine
|
data/lib/em-ssh/shell.rb
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'em-ssh'
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
class Ssh
|
5
|
+
# EM::Ssh::Shell encapsulates interaction with a user shell on an SSH server.
|
6
|
+
# @example Retrieve the output of ifconfig -a on a server
|
7
|
+
# EM.run{
|
8
|
+
# shell = EM::Ssh::Shell.new(host, user, password)
|
9
|
+
# shell.wait_for('@icaleb ~]$')
|
10
|
+
# interfaces = send_and_wait('/sbin/ifconfig -a', '@icaleb ~]$')
|
11
|
+
#
|
12
|
+
# Shells can be easily and quickly duplicated (#split) without the need to establish another connection.
|
13
|
+
# Shells provide :closed, :childless, and :split callbacks.
|
14
|
+
#
|
15
|
+
# @example Start another shell using the same connection
|
16
|
+
# shell.on(:childless) do
|
17
|
+
# info("#{shell}'s children all closed")
|
18
|
+
# shell.disconnect
|
19
|
+
# EM.stop
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# admin_shell = shell.split
|
23
|
+
# admin_shell.on(:closed) { warn("admin shell has closed") }
|
24
|
+
# admin_shell.send_and_wait('sudo su -', ']$')
|
25
|
+
class Shell
|
26
|
+
include Log
|
27
|
+
include Callbacks
|
28
|
+
|
29
|
+
# Global timeout for wait operations; can be overriden by :timeout option to new
|
30
|
+
TIMEOUT = 15
|
31
|
+
# @return [Net::SSH::Connection::Channel] The shell to which we can send_data
|
32
|
+
attr_reader :shell
|
33
|
+
# @return [Net::SSH::Connection]
|
34
|
+
attr_reader :connection
|
35
|
+
# @return [Boolean] Default (false) halt on timeout value - when true any timeouts will be halt the shell by raising an exception
|
36
|
+
attr_reader :halt_on_timeout
|
37
|
+
# @return [Hash] the options passed to initialize
|
38
|
+
attr_reader :options
|
39
|
+
# @return [Hash] the options to pass to connect automatically. They will be extracted from the opptions[:net_ssh] on initialization
|
40
|
+
attr_reader :connect_opts
|
41
|
+
|
42
|
+
# @return [String] the host to login to
|
43
|
+
attr_reader :host
|
44
|
+
# @return [String] The user to authenticate as
|
45
|
+
attr_reader :user
|
46
|
+
# @return [String] the password to authenticate with - can be nil
|
47
|
+
attr_reader :pass
|
48
|
+
# @return [Array] all shells that have been split off from this one.
|
49
|
+
attr_reader :children
|
50
|
+
# @return [Shell] the parent of this shell
|
51
|
+
attr_reader :parent
|
52
|
+
# @return [String] a string (\r\n) to append to every command
|
53
|
+
def line_terminator
|
54
|
+
@line_terminator ||= "\r\n"
|
55
|
+
end
|
56
|
+
# [String]
|
57
|
+
attr_writer :line_terminator
|
58
|
+
|
59
|
+
# Connect to an ssh server then start a user shell.
|
60
|
+
# @param [String] address
|
61
|
+
# @param [String] user
|
62
|
+
# @param [String, nil] pass by default publickey and password auth will be attempted
|
63
|
+
# @param [Hash] opts
|
64
|
+
# @option opts [Hash] :net_ssh options to pass to Net::SSH; see Net::SSH.start
|
65
|
+
# @option opts [Boolean] :halt_on_timeout (false)
|
66
|
+
# @option opts [Fixnum] :timeout (TIMEOUT) default timeout for all #wait_for and #send_wait calls.
|
67
|
+
def initialize(address, user, pass, opts = {}, &blk)
|
68
|
+
@halt_on_timeout = opts[:halt_on_timeout] || false
|
69
|
+
@timeout = opts[:timeout].is_a?(Fixnum) ? opts[:timeout] : TIMEOUT
|
70
|
+
@host = address
|
71
|
+
@user = user
|
72
|
+
@pass = pass
|
73
|
+
@options = opts
|
74
|
+
@connect_opts = {:password => pass, :port => 22, :auth_methods => ['publickey', 'password']}.merge(opts[:net_ssh] || {})
|
75
|
+
@connection = opts[:connection]
|
76
|
+
@parent = opts[:parent]
|
77
|
+
@children = []
|
78
|
+
|
79
|
+
block_given? ? open(&blk) : open
|
80
|
+
end
|
81
|
+
|
82
|
+
# Close the connection to the server and all child shells.
|
83
|
+
# Disconnected shells cannot be split.
|
84
|
+
def disconnect
|
85
|
+
close
|
86
|
+
connection.close
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [Boolean] true if the connection is still alive
|
90
|
+
def connected?
|
91
|
+
connection && !connection.closed?
|
92
|
+
end
|
93
|
+
|
94
|
+
# Close this shell and all children.
|
95
|
+
# Even when a shell is closed it is still connected to the server.
|
96
|
+
# Fires :closed event.
|
97
|
+
# @see Callbacks
|
98
|
+
def close
|
99
|
+
shell.close.tap{ debug("closing") } if shell.active?
|
100
|
+
@closed = true
|
101
|
+
children.each { |c| c.close }
|
102
|
+
fire(:closed)
|
103
|
+
end
|
104
|
+
|
105
|
+
# @return [Boolean] Has this shell been closed.
|
106
|
+
def closed?
|
107
|
+
@closed == true
|
108
|
+
end
|
109
|
+
|
110
|
+
# Send a string to the server and wait for a response containing a specified String or Regex.
|
111
|
+
# @param [String] send_str
|
112
|
+
# @return [String] all data in the buffer including the wait_str if it was found
|
113
|
+
def send_and_wait(send_str, wait_str = nil, opts = {})
|
114
|
+
raise ClosedChannel if closed?
|
115
|
+
debug("send_and_wait(#{send_str.inspect}, #{wait_str.inspect}, #{opts})")
|
116
|
+
send_data(send_str)
|
117
|
+
return wait_for(wait_str, opts)
|
118
|
+
end # send_and_wait(send_str, wait_str = nil, opts = {})
|
119
|
+
|
120
|
+
# Wait for the shell to send data containing the given string.
|
121
|
+
# @param [String, Regexp] strregex a string or regex to match the console output against.
|
122
|
+
# @param [Hash] opts
|
123
|
+
# @option opts [Fixnum] :timeout (Session::TIMEOUT) the maximum number of seconds to wait
|
124
|
+
# @option opts [Boolean] (false) :halt_on_timeout
|
125
|
+
# @return [String] the contents of the buffer
|
126
|
+
def wait_for(strregex, opts = { })
|
127
|
+
raise ClosedChannel if closed?
|
128
|
+
debug("wait_for(#{strregex}, #{opts})")
|
129
|
+
opts = { :timeout => @timeout, :halt_on_timeout => @halt_on_timeout }.merge(opts)
|
130
|
+
buffer = ''
|
131
|
+
found = nil
|
132
|
+
f = Fiber.current
|
133
|
+
|
134
|
+
timer = nil
|
135
|
+
timeout = proc do
|
136
|
+
shell.on_data {|c,d| }
|
137
|
+
# @todo fire an em errback
|
138
|
+
if opts[:halt_on_timeout]
|
139
|
+
raise TimeoutError("timeout while waiting for #{strregex.inspect}; received: #{buffer.inspect}")
|
140
|
+
else
|
141
|
+
warn("timeout while waiting for #{strregex.inspect}; received: #{buffer.inspect}")
|
142
|
+
end # opts[:halt_on_timeout]
|
143
|
+
end # timeout
|
144
|
+
|
145
|
+
shell.on_data do |ch,data|
|
146
|
+
buffer = "#{buffer}#{data}"
|
147
|
+
if strregex.is_a?(Regexp) ? buffer.match(strregex) : buffer.include?(strregex)
|
148
|
+
timer.respond_to?(:cancel) && timer.cancel
|
149
|
+
result = buffer.clone
|
150
|
+
shell.on_data {|c,d| }
|
151
|
+
f.resume(result)
|
152
|
+
end
|
153
|
+
end # |ch,data|
|
154
|
+
|
155
|
+
timer = EM::Timer.new(opts[:timeout], &timeout)
|
156
|
+
return Fiber.yield
|
157
|
+
end # wait_for(strregex, opts = { })
|
158
|
+
|
159
|
+
# Open a shell on the server.
|
160
|
+
# You generally don't need to call this.
|
161
|
+
# @return [self]
|
162
|
+
def open
|
163
|
+
f = Fiber.current
|
164
|
+
connect unless connected?
|
165
|
+
|
166
|
+
connection.open_channel do |channel|
|
167
|
+
debug "**** channel open: #{channel}"
|
168
|
+
channel.request_pty(options[:pty] || {}) do |pty,suc|
|
169
|
+
debug "***** pty open: #{pty}; suc: #{suc}"
|
170
|
+
pty.send_channel_request("shell") do |shell,success|
|
171
|
+
raise ConnectionError, "Failed to create shell." unless success
|
172
|
+
debug "***** shell open: #{shell}"
|
173
|
+
@shell = shell
|
174
|
+
f.resume(self)
|
175
|
+
end # |shell,success|
|
176
|
+
end # |pty,suc|
|
177
|
+
end # |channel|
|
178
|
+
|
179
|
+
return Fiber.yield
|
180
|
+
end # start
|
181
|
+
|
182
|
+
# Create a new shell using the same ssh connection.
|
183
|
+
# A connection will be established if this shell is not connected.
|
184
|
+
# If a block is provided the child will be closed after yielding.
|
185
|
+
# @yield [Shell] child
|
186
|
+
# @return [Shell] child
|
187
|
+
def split
|
188
|
+
connect unless connected?
|
189
|
+
child = self.class.new(host, user, pass, {:connection => connection, :parent => self}.merge(options))
|
190
|
+
child.line_terminator = line_terminator
|
191
|
+
children.push(child)
|
192
|
+
child.on(:closed) do
|
193
|
+
children.delete(child)
|
194
|
+
fire(:childless).tap{ info("fired :childless") } if children.empty?
|
195
|
+
end
|
196
|
+
fire(:split, child)
|
197
|
+
block_given? ? yield(child).tap { child.close } : child
|
198
|
+
end # split
|
199
|
+
|
200
|
+
# Connect to the server.
|
201
|
+
# Does not open the shell; use #open or #split
|
202
|
+
# You generally won't need to call this on your own.
|
203
|
+
def connect
|
204
|
+
f = Fiber.current
|
205
|
+
::EM::Ssh.start(host, user, connect_opts) do |connection|
|
206
|
+
@connection = connection
|
207
|
+
f.resume
|
208
|
+
end # |connection|
|
209
|
+
return Fiber.yield
|
210
|
+
end # connect
|
211
|
+
|
212
|
+
|
213
|
+
# Send data to the ssh server shell.
|
214
|
+
# You generally don't need to call this.
|
215
|
+
# @see #send_and_wait
|
216
|
+
# @param [String] d the data to send encoded as a string
|
217
|
+
def send_data(d)
|
218
|
+
#debug("send_data: #{d.inspect}#{line_terminator}")
|
219
|
+
shell.send_data("#{d}#{line_terminator}")
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
end # class::Shell
|
224
|
+
end # class::Ssh
|
225
|
+
end # module::EventMachine
|
data/lib/em-ssh.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
require 'fiber'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
require 'net/ssh'
|
7
|
+
require 'em-ssh/log'
|
8
|
+
|
9
|
+
module EventMachine
|
10
|
+
# @example
|
11
|
+
# EM::Ssh.start(host, user, :password => password) do |ssh|
|
12
|
+
# ssh.exec("hostname") do |ch,stream,data|
|
13
|
+
# puts "data: #{data}"
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
class Ssh
|
17
|
+
DEFAULT_PORT = 22
|
18
|
+
|
19
|
+
# Generic error tag
|
20
|
+
module Error; end
|
21
|
+
# Any class that inherits from SshError will be an Exception and include a Ssh::Error tag
|
22
|
+
class SshError < Net::SSH::Exception; include Error; end
|
23
|
+
class TimeoutError < Timeout::Error; include Error; end
|
24
|
+
class ClosedChannel < SshError; end
|
25
|
+
|
26
|
+
class << self
|
27
|
+
attr_writer :logger
|
28
|
+
# Creates a logger when necessary
|
29
|
+
# @return [Logger]
|
30
|
+
def logger(level = Logger::WARN)
|
31
|
+
@logger ||= ::Logger.new(STDERR).tap{ |l| l.level = level }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Connect to an ssh server
|
35
|
+
# @param [String] host
|
36
|
+
# @param [String] user
|
37
|
+
# @param [Hash] opts all options accepted by Net::SSH.start
|
38
|
+
# @yield [Session] an EventMachine compatible Net::SSH::Session
|
39
|
+
# @see http://net-ssh.github.com/ssh/v2/api/index.html
|
40
|
+
# @return [Session]
|
41
|
+
# @example
|
42
|
+
# EM::Ssh.start(host, user, options) do |connection|
|
43
|
+
# log.debug "**** connected: #{connection}"
|
44
|
+
# connection.open_channel do |channel|
|
45
|
+
# log.debug "**** channel: #{channel}"
|
46
|
+
# channel.request_pty(options[:pty] || {}) do |pty,suc|
|
47
|
+
def connect(host, user, opts = {}, &blk)
|
48
|
+
logger.debug("#{self}.connect(#{host}, #{user}, #{opts})")
|
49
|
+
options = { :host => host, :user => user, :port => DEFAULT_PORT, :callback => blk }.merge(opts)
|
50
|
+
EM.connect(options[:host], options[:port], Connection, options)
|
51
|
+
end
|
52
|
+
alias :start :connect
|
53
|
+
end # << self
|
54
|
+
|
55
|
+
# Pull in the constants from Net::SSH::[Transport, Connection and Authentication]
|
56
|
+
# and define them locally.
|
57
|
+
[:Transport, :Connection, :Authentication]
|
58
|
+
.map{ |sym| Net::SSH.const_get(sym).const_get(:Constants) }
|
59
|
+
.each do |mod|
|
60
|
+
mod.constants.each do |name|
|
61
|
+
const_set(name, mod.const_get(name))
|
62
|
+
end # |name|
|
63
|
+
end # |module|
|
64
|
+
|
65
|
+
end # class::Ssh
|
66
|
+
end # module::EventMachine
|
67
|
+
|
68
|
+
|
69
|
+
require 'em-ssh/callbacks'
|
70
|
+
require 'em-ssh/connection'
|
71
|
+
require 'em-ssh/server-version'
|
72
|
+
require 'em-ssh/packet-stream'
|
73
|
+
require 'em-ssh/authentication-session'
|
74
|
+
require 'em-ssh/session'
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em-ssh
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Caleb Crane
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-22 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: &70157793111680 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70157793111680
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: net-ssh
|
27
|
+
requirement: &70157793111020 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70157793111020
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: ruby-termios
|
38
|
+
requirement: &70157793110100 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70157793110100
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: highline
|
49
|
+
requirement: &70157793109680 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70157793109680
|
58
|
+
description: ''
|
59
|
+
email:
|
60
|
+
- em-ssh@simulacre.org
|
61
|
+
executables:
|
62
|
+
- em-ssh
|
63
|
+
- em-ssh-shell
|
64
|
+
extensions: []
|
65
|
+
extra_rdoc_files: []
|
66
|
+
files:
|
67
|
+
- lib/em-ssh/authentication-session.rb
|
68
|
+
- lib/em-ssh/callbacks.rb
|
69
|
+
- lib/em-ssh/connection.rb
|
70
|
+
- lib/em-ssh/log.rb
|
71
|
+
- lib/em-ssh/packet-stream.rb
|
72
|
+
- lib/em-ssh/server-version.rb
|
73
|
+
- lib/em-ssh/session.rb
|
74
|
+
- lib/em-ssh/shell.rb
|
75
|
+
- lib/em-ssh/version.rb
|
76
|
+
- lib/em-ssh.rb
|
77
|
+
- bin/em-ssh
|
78
|
+
- bin/em-ssh-shell
|
79
|
+
- README.md
|
80
|
+
homepage: http://github.com/simulacre/em-ssh
|
81
|
+
licenses:
|
82
|
+
- MIT
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: 1.3.6
|
99
|
+
requirements: []
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 1.8.10
|
102
|
+
signing_key:
|
103
|
+
specification_version: 3
|
104
|
+
summary: An EventMachine compatible net-ssh
|
105
|
+
test_files: []
|
106
|
+
has_rdoc:
|