hrr_rb_ssh 0.3.0.pre3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5758ec1e3457a1aaa41ec25677bd85711eb176fa9478d786c70c885e2206c58
4
- data.tar.gz: 37e411ed657bceca996dcaf71c4b36c706984738005dce48043649a55d6ba022
3
+ metadata.gz: 5069234076e5c4f106d9ef733fd232fd0df99497fc64b948e9014d74a688f608
4
+ data.tar.gz: 0f88f74186bf0c86aa0dd6e76770d0e028231105bb06bc4ea5870f97841194d3
5
5
  SHA512:
6
- metadata.gz: a2ebfcb6fad617c0c8201ff30815e03d28e9e688f43fedbb14e3dd67d83426f3d9c06145392c1d84f4bb3e16d7daf78adf83fc57ba1d152136070b74cb0b93ba
7
- data.tar.gz: 9dcc34deaca035e461c98a9891f4e1d0876fcdf379e08438cc01e97e9965526dbf7a6306f0bbd87b090ce815e079a7f407aca3a3c9c5138c4d4ac5707b24be3e
6
+ metadata.gz: 5937b53c91ce899fbc3b0d0b97de9c336ad14fecda882ca24f6e9fc3184ec6b07123ae78bc7fdf3a11560ad3b268cb23277453d3f1ced91a5b1fb5054e3106f3
7
+ data.tar.gz: c757a877562b34ca17a8015fc9829ad6babfa89dbb197b451a0a23a1c372bc2ba1ba0ce77e2756da6b63157384be7774d15dd569b8b349ed898186e8f8f1818d
data/README.md CHANGED
@@ -5,9 +5,9 @@
5
5
  [![Test Coverage](https://api.codeclimate.com/v1/badges/f5dfdb97d72f24ca5939/test_coverage)](https://codeclimate.com/github/hirura/hrr_rb_ssh/test_coverage)
6
6
  [![Gem Version](https://badge.fury.io/rb/hrr_rb_ssh.svg)](https://badge.fury.io/rb/hrr_rb_ssh)
7
7
 
8
- hrr_rb_ssh is a pure Ruby SSH 2.0 server implementation.
8
+ hrr_rb_ssh is a pure Ruby SSH 2.0 server and client implementation.
9
9
 
10
- With hrr_rb_ssh, it is possible to write an SSH server easily, and also possible to write an original server side application on secure connection provided by SSH protocol.
10
+ With hrr_rb_ssh, it is possible to write an SSH server easily, and also possible to write an original server side application on secure connection provided by SSH protocol. And it supports to write SSH client as well.
11
11
 
12
12
  ## Table of Contents
13
13
 
@@ -31,6 +31,7 @@ With hrr_rb_ssh, it is possible to write an SSH server easily, and also possible
31
31
  - [Custom request handlers](#custom-request-handlers)
32
32
  - [Defining preferred algorithms (optional)](#defining-preferred-algorithms-optional)
33
33
  - [Hiding and/or simulating local SSH version](#hiding-and-or-simulating-local-ssh-version)
34
+ - [Writing SSH client (Experimental)](#writing-ssh-client-experimental)
34
35
  - [Demo](#demo)
35
36
  - [Supported Features](#supported-features)
36
37
  - [Connection layer](#connection-layer)
@@ -420,9 +421,66 @@ options['local_version'] = "SSH-2.0-OpenSSH"
420
421
 
421
422
  Please note that the beginning of the string must be `SSH-2.0-`. Otherwise SSH 2.0 remote peer cannot continue negotiation with the local peer.
422
423
 
424
+ ### Writing SSH client (Experimental)
425
+
426
+ #### Requiring `hrr_rb_ssh` library
427
+
428
+ First of all, `hrr_rb_ssh` library needs to be loaded.
429
+
430
+ ```ruby
431
+ require 'hrr_rb_ssh'
432
+ ```
433
+
434
+ #### Starting SSH connection
435
+
436
+ The client mode can be started with `HrrRbSsh::Client.start`. The method takes `address` and `options` arguments. The `address` is the target host address that the SSH client connects to. And the `options` contains various parameters for the SSH connection. At least `username` key must be set in the `options`. Also at least one of `password`, `publickey`, or `keyboard-interactive` needs to be set for authentication instead of authenticators that are used in server mode. Also as similar to server mode, it is possible to specify preferred transport algorithms and preferred authentication methods with the same keywords.
437
+
438
+ ```ruby
439
+ address = 'remotehost'
440
+ options = {
441
+ port: 22,
442
+ username: 'user1',
443
+ password: 'password1',
444
+ publickey: ['ssh-rsa', "/home/user1/.ssh/id_rsa")],
445
+ authentication_preferred_authentication_methods = ['publickey', 'password'],
446
+ }
447
+ HrrRbSsh::Client.start(address, options) do |conn|
448
+ # Do something here
449
+ # For instance: conn.exec "command"
450
+ end
451
+ ```
452
+
453
+ #### Executing remote commands
454
+
455
+ There are some methods supported in client mode. The methods works as a receiver of `conn` block variable.
456
+
457
+ ##### exec method
458
+
459
+ The `exec` and `exec!` methods execute command on a remote host. Both takes a command argument that is executed in the remote host. And they can take optional `pty` and `env` arguments. When `pty: true` is set, then the command will be executed on a pseudo-TTY. When `env: {'key' => 'value'}` is set, then the environmental variables are set before the command is executed.
460
+
461
+ The `exec!` method returns `[stdout, stderr]` outputs. Once the command is executed and the outputs are completed, then the method returns the value.
462
+
463
+ On the other hand, `exec` method takes block like the below example and returns exit status of the command. When the command is executed and the outputs and reading them are finished, then `io_out` and `io_err` return EOF.
464
+
465
+ ```ruby
466
+ conn.exec "command" do |io_in, io_out, io_err|
467
+ # Do something here
468
+ end
469
+ ```
470
+
471
+ ##### shell method
472
+
473
+ The `shell` method provides a shell access on a remote host. As similar to `exec` method, it takes block and its block variable is also `io_in, io_out, io_err`. `shell` is always on pseudo-TTY, so it doesn't take `pty` optional argument. It takes `env` optional argument. Exiting shell will leads `io_out` and `io_err` EOF.
474
+
475
+ ```ruby
476
+ conn.shell do |io_in, io_out, io_err|
477
+ # Do something here
478
+ end
479
+ ```
480
+
423
481
  ### Demo
424
482
 
425
- The `demo/server.rb` shows a good example on how to use the hrr_rb_ssh library in SSH server mode.
483
+ The `demo/server.rb` shows a good example on how to use the hrr_rb_ssh library in SSH server mode. And the `demo/client.rb` shows an example on how to use the hrr_rb_ssh library in SSH client mode.
426
484
 
427
485
  ## Supported Features
428
486
 
data/demo/client.rb ADDED
@@ -0,0 +1,58 @@
1
+ # coding: utf-8
2
+ # vim: et ts=2 sw=2
3
+
4
+ require 'logger'
5
+
6
+ begin
7
+ require 'hrr_rb_ssh'
8
+ rescue LoadError
9
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
10
+ require 'hrr_rb_ssh'
11
+ end
12
+
13
+ logger = Logger.new STDOUT
14
+ logger.level = Logger::INFO
15
+ logger.level = Logger::DEBUG
16
+ HrrRbSsh::Logger.initialize logger
17
+
18
+ address = 'localhost'
19
+ options = {
20
+ port: 10022,
21
+ username: 'user1',
22
+ password: 'password1',
23
+ publickey: ['ssh-rsa', "/home/user1/.ssh/id_rsa")],
24
+ keyboard_interactive: [
25
+ 'password1',
26
+ #'password2' # when keyboard-interactive authentication requires 2nd response
27
+ ],
28
+ }
29
+ HrrRbSsh::Client.start(address, options){ |conn|
30
+ puts conn.exec!('ls -l') # => [out, err]
31
+
32
+ puts conn.exec!('ls -l', pty: true) # => [out, err] # "ls -l" command will be run on PTY
33
+
34
+ conn.exec('ls -l', pty: true){ |io_in, io_out, io_err| # => exit status
35
+ while true
36
+ begin
37
+ print io_out.readpartial(10240)
38
+ rescue EOFError
39
+ break
40
+ end
41
+ end
42
+ }
43
+
44
+ conn.shell{ |io_in, io_out, io_err| # => exit status
45
+ t = Thread.new {
46
+ while true
47
+ begin
48
+ print io_out.readpartial(10240)
49
+ rescue EOFError
50
+ break
51
+ end
52
+ end
53
+ }
54
+ io_in.puts "ls -l"
55
+ io_in.puts "exit"
56
+ t.join
57
+ }
58
+ }
data/hrr_rb_ssh.gemspec CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.name = "hrr_rb_ssh"
10
10
  spec.version = HrrRbSsh::VERSION
11
11
  spec.license = 'Apache-2.0'
12
- spec.summary = %q{Pure Ruby SSH 2.0 server implementation}
13
- spec.description = %q{Pure Ruby SSH 2.0 server implementation}
12
+ spec.summary = %q{Pure Ruby SSH 2.0 server and client implementation}
13
+ spec.description = %q{Pure Ruby SSH 2.0 server and client implementation}
14
14
  spec.authors = ["hirura"]
15
15
  spec.email = ["hirura@gmail.com"]
16
16
  spec.homepage = "https://github.com/hirura/hrr_rb_ssh"
@@ -13,6 +13,7 @@ module HrrRbSsh
13
13
  def initialize transport, options, variables, authentication_methods
14
14
  @logger = Logger.new(self.class.name)
15
15
  @transport = transport
16
+ @options = options
16
17
  @authenticator = options.fetch( 'authentication_keyboard_interactive_authenticator', Authenticator.new { false } )
17
18
  @variables = variables
18
19
  @authentication_methods = authentication_methods
@@ -26,6 +27,39 @@ module HrrRbSsh
26
27
  context = Context.new(@transport, username, submethods, @variables, @authentication_methods)
27
28
  @authenticator.authenticate context
28
29
  end
30
+
31
+ def request_authentication username, service_name
32
+ message = {
33
+ :'message number' => Message::SSH_MSG_USERAUTH_REQUEST::VALUE,
34
+ :"user name" => username,
35
+ :"service name" => service_name,
36
+ :"method name" => NAME,
37
+ :"language tag" => "",
38
+ :'submethods' => "",
39
+ }
40
+ payload = Message::SSH_MSG_USERAUTH_REQUEST.encode message
41
+ @transport.send payload
42
+
43
+ payload = @transport.receive
44
+ case payload[0,1].unpack("C")[0]
45
+ when Message::SSH_MSG_USERAUTH_INFO_REQUEST::VALUE
46
+ message = Message::SSH_MSG_USERAUTH_INFO_REQUEST.decode payload
47
+ num_responses = @options['client_authentication_keyboard_interactive'].size
48
+ message = {
49
+ :'message number' => Message::SSH_MSG_USERAUTH_INFO_RESPONSE::VALUE,
50
+ :'num-responses' => num_responses,
51
+ }
52
+ message_responses = @options['client_authentication_keyboard_interactive'].map.with_index{ |response, i|
53
+ {:"response[#{i+1}]" => response}
54
+ }.inject(Hash.new){ |a, b| a.merge(b) }
55
+ message.update(message_responses)
56
+ payload = Message::SSH_MSG_USERAUTH_INFO_RESPONSE.encode message
57
+ @transport.send payload
58
+ @transport.receive
59
+ else
60
+ payload
61
+ end
62
+ end
29
63
  end
30
64
  end
31
65
  end
@@ -12,6 +12,7 @@ module HrrRbSsh
12
12
 
13
13
  def initialize transport, options, variables, authentication_methods
14
14
  @logger = Logger.new(self.class.name)
15
+ @transport = transport
15
16
  @authenticator = options.fetch( 'authentication_none_authenticator', Authenticator.new { false } )
16
17
  @variables = variables
17
18
  @authentication_methods = authentication_methods
@@ -23,6 +24,18 @@ module HrrRbSsh
23
24
  context = Context.new(userauth_request_message[:'user name'], @variables, @authentication_methods)
24
25
  @authenticator.authenticate context
25
26
  end
27
+
28
+ def request_authentication username, service_name
29
+ message = {
30
+ :'message number' => Message::SSH_MSG_USERAUTH_REQUEST::VALUE,
31
+ :"user name" => username,
32
+ :"service name" => service_name,
33
+ :"method name" => NAME,
34
+ }
35
+ payload = Message::SSH_MSG_USERAUTH_REQUEST.encode message
36
+ @transport.send payload
37
+ payload = @transport.receive
38
+ end
26
39
  end
27
40
  end
28
41
  end
@@ -12,7 +12,9 @@ module HrrRbSsh
12
12
 
13
13
  def initialize transport, options, variables, authentication_methods
14
14
  @logger = Logger.new(self.class.name)
15
+ @transport = transport
15
16
  @authenticator = options.fetch( 'authentication_password_authenticator', Authenticator.new { false } )
17
+ @options = options
16
18
  @variables = variables
17
19
  @authentication_methods = authentication_methods
18
20
  end
@@ -25,6 +27,22 @@ module HrrRbSsh
25
27
  context = Context.new(username, password, @variables, @authentication_methods)
26
28
  @authenticator.authenticate context
27
29
  end
30
+
31
+ def request_authentication username, service_name
32
+ password = @options['client_authentication_password']
33
+ message = {
34
+ :'message number' => Message::SSH_MSG_USERAUTH_REQUEST::VALUE,
35
+ :"user name" => username,
36
+ :"service name" => service_name,
37
+ :"method name" => NAME,
38
+ :"FALSE" => false,
39
+ :"plaintext password" => password,
40
+ }
41
+ payload = Message::SSH_MSG_USERAUTH_REQUEST.encode message
42
+ @transport.send payload
43
+
44
+ payload = @transport.receive
45
+ end
28
46
  end
29
47
  end
30
48
  end
@@ -44,6 +44,28 @@ module HrrRbSsh
44
44
  false
45
45
  end
46
46
  end
47
+
48
+ def generate_public_key_blob secret_key
49
+ publickey = HrrRbSsh::Algorithm::Publickey[self.class::NAME].new secret_key
50
+ publickey.to_public_key_blob
51
+ end
52
+
53
+ def generate_signature session_id, username, service_name, method_name, secret_key
54
+ publickey = HrrRbSsh::Algorithm::Publickey[self.class::NAME].new secret_key
55
+ publickey_blob = publickey.to_public_key_blob
56
+ signature_blob_h = {
57
+ :'session identifier' => session_id,
58
+ :'message number' => Message::SSH_MSG_USERAUTH_REQUEST::VALUE,
59
+ :'user name' => username,
60
+ :'service name' => service_name,
61
+ :'method name' => method_name,
62
+ :'with signature' => true,
63
+ :'public key algorithm name' => self.class::NAME,
64
+ :'public key blob' => publickey_blob
65
+ }
66
+ signature_blob = SignatureBlob.encode signature_blob_h
67
+ publickey.sign signature_blob
68
+ end
47
69
  end
48
70
  end
49
71
  end
@@ -12,6 +12,8 @@ module HrrRbSsh
12
12
 
13
13
  def initialize transport, options, variables, authentication_methods
14
14
  @logger = Logger.new(self.class.name)
15
+ @transport = transport
16
+ @options = options
15
17
  @session_id = options['session id']
16
18
  @authenticator = options.fetch( 'authentication_publickey_authenticator', Authenticator.new { false } )
17
19
  @variables = variables
@@ -45,6 +47,53 @@ module HrrRbSsh
45
47
  }
46
48
  payload = Message::SSH_MSG_USERAUTH_PK_OK.encode message
47
49
  end
50
+
51
+ def request_authentication username, service_name
52
+ public_key_algorithm_name, secret_key = @options['client_authentication_publickey']
53
+ send_request_without_signature username, service_name, public_key_algorithm_name, secret_key
54
+ payload = @transport.receive
55
+ case payload[0,1].unpack("C")[0]
56
+ when Message::SSH_MSG_USERAUTH_PK_OK::VALUE
57
+ send_request_with_signature username, service_name, public_key_algorithm_name, secret_key
58
+ @transport.receive
59
+ else
60
+ payload
61
+ end
62
+ end
63
+
64
+ def send_request_without_signature username, service_name, public_key_algorithm_name, secret_key
65
+ algorithm = Algorithm[public_key_algorithm_name].new
66
+ public_key_blob = algorithm.generate_public_key_blob(secret_key)
67
+ message = {
68
+ :'message number' => Message::SSH_MSG_USERAUTH_REQUEST::VALUE,
69
+ :"user name" => username,
70
+ :"service name" => service_name,
71
+ :"method name" => NAME,
72
+ :"with signature" => false,
73
+ :'public key algorithm name' => public_key_algorithm_name,
74
+ :'public key blob' => public_key_blob,
75
+ }
76
+ payload = Message::SSH_MSG_USERAUTH_REQUEST.encode message
77
+ @transport.send payload
78
+ end
79
+
80
+ def send_request_with_signature username, service_name, public_key_algorithm_name, secret_key
81
+ algorithm = Algorithm[public_key_algorithm_name].new
82
+ public_key_blob = algorithm.generate_public_key_blob(secret_key)
83
+ signature = algorithm.generate_signature(@session_id, username, service_name, 'publickey', secret_key)
84
+ message = {
85
+ :'message number' => Message::SSH_MSG_USERAUTH_REQUEST::VALUE,
86
+ :"user name" => username,
87
+ :"service name" => service_name,
88
+ :"method name" => NAME,
89
+ :"with signature" => true,
90
+ :'public key algorithm name' => public_key_algorithm_name,
91
+ :'public key blob' => public_key_blob,
92
+ :'signature' => signature,
93
+ }
94
+ payload = Message::SSH_MSG_USERAUTH_REQUEST.encode message
95
+ @transport.send payload
96
+ end
48
97
  end
49
98
  end
50
99
  end
@@ -12,8 +12,9 @@ module HrrRbSsh
12
12
  class Authentication
13
13
  include Constant
14
14
 
15
- def initialize transport, options={}
15
+ def initialize transport, mode, options={}
16
16
  @transport = transport
17
+ @mode = mode
17
18
  @options = options
18
19
 
19
20
  @logger = Logger.new self.class.name
@@ -70,6 +71,15 @@ module HrrRbSsh
70
71
  end
71
72
 
72
73
  def authenticate
74
+ case @mode
75
+ when Mode::SERVER
76
+ respond_to_authentication
77
+ when Mode::CLIENT
78
+ request_authentication
79
+ end
80
+ end
81
+
82
+ def respond_to_authentication
73
83
  authentication_methods = (@options['authentication_preferred_authentication_methods'].dup rescue nil) || Method.list_preferred # rescue nil.dup for Ruby version < 2.4
74
84
  @logger.info { "preferred authentication methods: #{authentication_methods}" }
75
85
  loop do
@@ -116,6 +126,42 @@ module HrrRbSsh
116
126
  end
117
127
  end
118
128
 
129
+ def request_authentication
130
+ authentication_methods = (@options['authentication_preferred_authentication_methods'].dup rescue nil) || Method.list_preferred # rescue nil.dup for Ruby version < 2.4
131
+ @logger.info { "preferred authentication methods: #{authentication_methods}" }
132
+ next_method_name = "none"
133
+ @logger.info { "authentication request begins with none method" }
134
+ loop do
135
+ @logger.info { "authentication method: #{next_method_name}" }
136
+ method = Method[next_method_name].new(@transport, {'session id' => @transport.session_id}.merge(@options), @variables, authentication_methods)
137
+ payload = method.request_authentication @options['username'], "ssh-connection"
138
+ case payload[0,1].unpack("C")[0]
139
+ when Message::SSH_MSG_USERAUTH_SUCCESS::VALUE
140
+ @logger.info { "verified" }
141
+ @username = @options['username']
142
+ @closed = false
143
+ break
144
+ when Message::SSH_MSG_USERAUTH_FAILURE::VALUE
145
+ message = Message::SSH_MSG_USERAUTH_FAILURE.decode payload
146
+ partial_success = message[:'partial success']
147
+ if partial_success
148
+ @logger.info { "partially verified" }
149
+ end
150
+ authentication_methods_that_can_continue = message[:'authentications that can continue']
151
+ @logger.debug { "authentication methods that can continue: #{authentication_methods_that_can_continue}" }
152
+ next_method_name = authentication_methods.find{ |local_m| authentication_methods_that_can_continue.find{ |remote_m| local_m == remote_m } }
153
+ if next_method_name
154
+ authentication_methods.delete next_method_name
155
+ @logger.info { "continue" }
156
+ else
157
+ @logger.info { "no more available authentication methods" }
158
+ @closed = true
159
+ raise "failed authentication"
160
+ end
161
+ end
162
+ end
163
+ end
164
+
119
165
  def send_userauth_failure authentication_methods, partial_success
120
166
  message = {
121
167
  :'message number' => Message::SSH_MSG_USERAUTH_FAILURE::VALUE,
@@ -0,0 +1,198 @@
1
+ # coding: utf-8
2
+ # vim: et ts=2 sw=2
3
+
4
+ require 'socket'
5
+ require 'stringio'
6
+ require 'hrr_rb_ssh/logger'
7
+ require 'hrr_rb_ssh/mode'
8
+ require 'hrr_rb_ssh/transport'
9
+ require 'hrr_rb_ssh/authentication'
10
+ require 'hrr_rb_ssh/connection'
11
+
12
+ module HrrRbSsh
13
+ class Client
14
+ def self.start address, options={}
15
+ client = self.new address, options
16
+ client.start
17
+ if block_given?
18
+ begin
19
+ yield client
20
+ ensure
21
+ client.close unless client.closed?
22
+ end
23
+ else
24
+ client
25
+ end
26
+ end
27
+
28
+ def initialize address, tmp_options={}
29
+ @logger = Logger.new self.class.name
30
+ @closed = true
31
+ options = initialize_options tmp_options
32
+ io = TCPSocket.new address, options['port']
33
+ transport = HrrRbSsh::Transport.new io, HrrRbSsh::Mode::CLIENT, options
34
+ authentication = HrrRbSsh::Authentication.new transport, HrrRbSsh::Mode::CLIENT, options
35
+ @connection = HrrRbSsh::Connection.new authentication, HrrRbSsh::Mode::CLIENT, options
36
+ end
37
+
38
+ def initialize_options tmp_options
39
+ tmp_options = Hash[tmp_options.map{|k, v| [k.to_s, v]}]
40
+ options = {}
41
+ options['port'] = tmp_options['port'] || 22
42
+ options['username'] = tmp_options['username']
43
+ options['authentication_preferred_authentication_methods'] = tmp_options['authentication_preferred_authentication_methods']
44
+ options['client_authentication_password'] = tmp_options['password']
45
+ options['client_authentication_publickey'] = tmp_options['publickey']
46
+ options['client_authentication_keyboard_interactive'] = tmp_options['keyboard_interactive']
47
+ options['transport_preferred_encryption_algorithms'] = tmp_options['transport_preferred_encryption_algorithms']
48
+ options['transport_preferred_server_host_key_algorithms'] = tmp_options['transport_preferred_server_host_key_algorithms']
49
+ options['transport_preferred_kex_algorithms'] = tmp_options['transport_preferred_kex_algorithms']
50
+ options['transport_preferred_mac_algorithms'] = tmp_options['transport_preferred_mac_algorithms']
51
+ options['transport_preferred_compression_algorithms'] = tmp_options['transport_preferred_compression_algorithms']
52
+ options
53
+ end
54
+
55
+ def start
56
+ @connection.start foreground: false
57
+ @closed = false
58
+ end
59
+
60
+ def loop
61
+ @connection.loop
62
+ end
63
+
64
+ def closed?
65
+ @closed
66
+ end
67
+
68
+ def close
69
+ @logger.debug { "closing client" }
70
+ @closed = true
71
+ @connection.close
72
+ @logger.debug { "client closed" }
73
+ end
74
+
75
+ def exec! command, pty: false, env: {}
76
+ @logger.debug { "start exec!: #{command}" }
77
+ out_buf = StringIO.new
78
+ err_buf = StringIO.new
79
+ begin
80
+ @logger.info { "Opning channel" }
81
+ channel = @connection.request_channel_open "session"
82
+ @logger.info { "Channel opened" }
83
+ if pty
84
+ channel.send_channel_request_pty_req 'xterm', 80, 24, 580, 336, ''
85
+ end
86
+ env.each{ |variable_name, variable_value|
87
+ channel.send_channel_request_env variable_name, variable_value
88
+ }
89
+ channel.send_channel_request_exec command
90
+ out_t = Thread.new {
91
+ while true
92
+ begin
93
+ out_buf.write channel.io[1].readpartial(10240)
94
+ rescue
95
+ break
96
+ end
97
+ end
98
+ }
99
+ err_t = Thread.new {
100
+ while true
101
+ begin
102
+ err_buf.write channel.io[2].readpartial(10240)
103
+ rescue
104
+ break
105
+ end
106
+ end
107
+ }
108
+ [out_t, err_t].each{ |t|
109
+ begin
110
+ t.join
111
+ rescue => e
112
+ @logger.warn { [e.backtrace[0], ": ", e.message, " (", e.class.to_s, ")\n\t", e.backtrace[1..-1].join("\n\t")].join }
113
+ end
114
+ }
115
+ rescue => e
116
+ @logger.error { "Failed opening channel" }
117
+ @logger.error { [e.backtrace[0], ": ", e.message, " (", e.class.to_s, ")\n\t", e.backtrace[1..-1].join("\n\t")].join }
118
+ raise "Error in exec!"
119
+ ensure
120
+ if channel
121
+ @logger.info { "closing channel IOs" }
122
+ channel.io.each{ |io| io.close rescue nil }
123
+ @logger.info { "channel IOs closed" }
124
+ @logger.info { "closing channel" }
125
+ @logger.info { "wait until threads closed in channel" }
126
+ channel.wait_until_closed
127
+ channel.close
128
+ @logger.info { "channel closed" }
129
+ end
130
+ end
131
+ [out_buf.string, err_buf.string]
132
+ end
133
+
134
+ def exec command, pty: false, env: {}
135
+ @logger.debug { "start exec: #{command}" }
136
+ begin
137
+ @logger.info { "Opning channel" }
138
+ channel = @connection.request_channel_open "session"
139
+ @logger.info { "Channel opened" }
140
+ if pty
141
+ channel.send_channel_request_pty_req 'xterm', 80, 24, 580, 336, ''
142
+ end
143
+ env.each{ |variable_name, variable_value|
144
+ channel.send_channel_request_env variable_name, variable_value
145
+ }
146
+ channel.send_channel_request_exec command
147
+ yield channel.io
148
+ rescue => e
149
+ @logger.error { "Failed opening channel" }
150
+ @logger.error { [e.backtrace[0], ": ", e.message, " (", e.class.to_s, ")\n\t", e.backtrace[1..-1].join("\n\t")].join }
151
+ raise "Error in shell"
152
+ ensure
153
+ if channel
154
+ @logger.info { "closing channel IOs" }
155
+ channel.io.each{ |io| io.close rescue nil }
156
+ @logger.info { "channel IOs closed" }
157
+ @logger.info { "closing channel" }
158
+ @logger.info { "wait until threads closed in channel" }
159
+ channel.wait_until_closed
160
+ channel.close
161
+ @logger.info { "channel closed" }
162
+ end
163
+ end
164
+ channel_exit_status = channel.exit_status rescue nil
165
+ end
166
+
167
+ def shell env: {}
168
+ @logger.debug { "start shell" }
169
+ begin
170
+ @logger.info { "Opning channel" }
171
+ channel = @connection.request_channel_open "session"
172
+ @logger.info { "Channel opened" }
173
+ channel.send_channel_request_pty_req 'xterm', 80, 24, 580, 336, ''
174
+ env.each{ |variable_name, variable_value|
175
+ channel.send_channel_request_env variable_name, variable_value
176
+ }
177
+ channel.send_channel_request_shell
178
+ yield channel.io
179
+ rescue => e
180
+ @logger.error { "Failed opening channel" }
181
+ @logger.error { [e.backtrace[0], ": ", e.message, " (", e.class.to_s, ")\n\t", e.backtrace[1..-1].join("\n\t")].join }
182
+ raise "Error in shell"
183
+ ensure
184
+ if channel
185
+ @logger.info { "closing channel IOs" }
186
+ channel.io.each{ |io| io.close rescue nil }
187
+ @logger.info { "channel IOs closed" }
188
+ @logger.info { "closing channel" }
189
+ @logger.info { "wait until threads closed in channel" }
190
+ channel.wait_until_closed
191
+ channel.close
192
+ @logger.info { "channel closed" }
193
+ end
194
+ end
195
+ channel_exit_status = channel.exit_status rescue nil
196
+ end
197
+ end
198
+ end
@@ -31,6 +31,9 @@ module HrrRbSsh
31
31
  if @sender_thread_finished && @receiver_thread_finished
32
32
  @logger.info { "closing direct-tcpip" }
33
33
  @socket.close
34
+ @logger.info { "closing channel IOs" }
35
+ @channel.io.each{ |io| io.close rescue nil }
36
+ @logger.info { "channel IOs closed" }
34
37
  @channel.close from=:channel_type_instance
35
38
  @logger.info { "direct-tcpip closed" }
36
39
  end
@@ -47,15 +50,15 @@ module HrrRbSsh
47
50
  @channel.io[1].write s.readpartial(10240)
48
51
  rescue EOFError
49
52
  @logger.info { "socket is EOF" }
50
- @channel.io[1].close
53
+ @channel.io[1].close rescue nil
51
54
  break
52
55
  rescue IOError
53
56
  @logger.info { "socket is closed" }
54
- @channel.io[1].close
57
+ @channel.io[1].close rescue nil
55
58
  break
56
59
  rescue => e
57
60
  @logger.error { [e.backtrace[0], ": ", e.message, " (", e.class.to_s, ")\n\t", e.backtrace[1..-1].join("\n\t")].join }
58
- @channel.io[1].close
61
+ @channel.io[1].close rescue nil
59
62
  break
60
63
  end
61
64
  end