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 +4 -4
- data/README.md +61 -3
- data/demo/client.rb +58 -0
- data/hrr_rb_ssh.gemspec +2 -2
- data/lib/hrr_rb_ssh/authentication/method/keyboard_interactive.rb +34 -0
- data/lib/hrr_rb_ssh/authentication/method/none.rb +13 -0
- data/lib/hrr_rb_ssh/authentication/method/password.rb +18 -0
- data/lib/hrr_rb_ssh/authentication/method/publickey/algorithm/functionable.rb +22 -0
- data/lib/hrr_rb_ssh/authentication/method/publickey.rb +49 -0
- data/lib/hrr_rb_ssh/authentication.rb +47 -1
- data/lib/hrr_rb_ssh/client.rb +198 -0
- data/lib/hrr_rb_ssh/connection/channel/channel_type/direct_tcpip.rb +6 -3
- data/lib/hrr_rb_ssh/connection/channel/channel_type/forwarded_tcpip.rb +6 -3
- data/lib/hrr_rb_ssh/connection/channel/channel_type/session.rb +7 -1
- data/lib/hrr_rb_ssh/connection/channel.rb +308 -79
- data/lib/hrr_rb_ssh/connection.rb +99 -38
- data/lib/hrr_rb_ssh/logger.rb +5 -5
- data/lib/hrr_rb_ssh/server.rb +3 -3
- data/lib/hrr_rb_ssh/transport/kex_algorithm/diffie_hellman.rb +37 -32
- data/lib/hrr_rb_ssh/transport/kex_algorithm/diffie_hellman_group_exchange.rb +80 -46
- data/lib/hrr_rb_ssh/transport/kex_algorithm/elliptic_curve_diffie_hellman.rb +37 -32
- data/lib/hrr_rb_ssh/transport.rb +46 -10
- data/lib/hrr_rb_ssh/version.rb +1 -1
- data/lib/hrr_rb_ssh.rb +1 -0
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5069234076e5c4f106d9ef733fd232fd0df99497fc64b948e9014d74a688f608
|
4
|
+
data.tar.gz: 0f88f74186bf0c86aa0dd6e76770d0e028231105bb06bc4ea5870f97841194d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|