net-ssh 2.9.2 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (138) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.gitignore +6 -0
  4. data/.rubocop.yml +5 -0
  5. data/.rubocop_todo.yml +1129 -0
  6. data/.travis.yml +41 -5
  7. data/CHANGES.txt +133 -1
  8. data/Gemfile +13 -0
  9. data/Gemfile.norbnacl +10 -0
  10. data/Gemfile.norbnacl.lock +41 -0
  11. data/ISSUE_TEMPLATE.md +30 -0
  12. data/README.rdoc +26 -81
  13. data/Rakefile +63 -45
  14. data/appveyor.yml +51 -0
  15. data/lib/net/ssh/authentication/agent.rb +174 -14
  16. data/lib/net/ssh/authentication/ed25519.rb +137 -0
  17. data/lib/net/ssh/authentication/ed25519_loader.rb +21 -0
  18. data/lib/net/ssh/authentication/key_manager.rb +36 -30
  19. data/lib/net/ssh/authentication/methods/abstract.rb +4 -0
  20. data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +16 -9
  21. data/lib/net/ssh/authentication/methods/password.rb +17 -4
  22. data/lib/net/ssh/authentication/pageant.rb +166 -45
  23. data/lib/net/ssh/authentication/session.rb +3 -2
  24. data/lib/net/ssh/buffer.rb +49 -10
  25. data/lib/net/ssh/buffered_io.rb +17 -12
  26. data/lib/net/ssh/config.rb +39 -8
  27. data/lib/net/ssh/connection/channel.rb +42 -20
  28. data/lib/net/ssh/connection/event_loop.rb +114 -0
  29. data/lib/net/ssh/connection/keepalive.rb +2 -2
  30. data/lib/net/ssh/connection/session.rb +120 -34
  31. data/lib/net/ssh/errors.rb +6 -6
  32. data/lib/net/ssh/key_factory.rb +49 -43
  33. data/lib/net/ssh/known_hosts.rb +49 -3
  34. data/lib/net/ssh/prompt.rb +47 -78
  35. data/lib/net/ssh/proxy/command.rb +31 -5
  36. data/lib/net/ssh/proxy/http.rb +15 -11
  37. data/lib/net/ssh/proxy/https.rb +49 -0
  38. data/lib/net/ssh/proxy/socks4.rb +2 -1
  39. data/lib/net/ssh/proxy/socks5.rb +3 -2
  40. data/lib/net/ssh/ruby_compat.rb +2 -29
  41. data/lib/net/ssh/service/forward.rb +2 -2
  42. data/lib/net/ssh/test/channel.rb +7 -0
  43. data/lib/net/ssh/test/extensions.rb +17 -0
  44. data/lib/net/ssh/test/kex.rb +4 -4
  45. data/lib/net/ssh/test/packet.rb +18 -2
  46. data/lib/net/ssh/test/script.rb +16 -2
  47. data/lib/net/ssh/test/socket.rb +1 -1
  48. data/lib/net/ssh/test.rb +5 -5
  49. data/lib/net/ssh/transport/algorithms.rb +92 -75
  50. data/lib/net/ssh/transport/cipher_factory.rb +19 -26
  51. data/lib/net/ssh/transport/ctr.rb +7 -9
  52. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +20 -9
  53. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +5 -3
  54. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb +1 -1
  55. data/lib/net/ssh/transport/key_expander.rb +1 -0
  56. data/lib/net/ssh/transport/openssl.rb +1 -1
  57. data/lib/net/ssh/transport/packet_stream.rb +11 -3
  58. data/lib/net/ssh/transport/server_version.rb +13 -6
  59. data/lib/net/ssh/transport/session.rb +20 -10
  60. data/lib/net/ssh/transport/state.rb +1 -1
  61. data/lib/net/ssh/verifiers/secure.rb +8 -10
  62. data/lib/net/ssh/version.rb +4 -4
  63. data/lib/net/ssh.rb +62 -14
  64. data/net-ssh-public_cert.pem +19 -18
  65. data/net-ssh.gemspec +34 -194
  66. data/support/arcfour_check.rb +1 -1
  67. data/support/ssh_tunnel_bug.rb +1 -1
  68. data.tar.gz.sig +0 -0
  69. metadata +125 -109
  70. metadata.gz.sig +0 -0
  71. data/Rudyfile +0 -96
  72. data/lib/net/ssh/authentication/agent/java_pageant.rb +0 -85
  73. data/lib/net/ssh/authentication/agent/socket.rb +0 -178
  74. data/setup.rb +0 -1585
  75. data/test/README.txt +0 -47
  76. data/test/authentication/methods/common.rb +0 -28
  77. data/test/authentication/methods/test_abstract.rb +0 -51
  78. data/test/authentication/methods/test_hostbased.rb +0 -114
  79. data/test/authentication/methods/test_keyboard_interactive.rb +0 -100
  80. data/test/authentication/methods/test_none.rb +0 -41
  81. data/test/authentication/methods/test_password.rb +0 -95
  82. data/test/authentication/methods/test_publickey.rb +0 -148
  83. data/test/authentication/test_agent.rb +0 -224
  84. data/test/authentication/test_key_manager.rb +0 -227
  85. data/test/authentication/test_session.rb +0 -107
  86. data/test/common.rb +0 -108
  87. data/test/configs/auth_off +0 -5
  88. data/test/configs/auth_on +0 -4
  89. data/test/configs/empty +0 -0
  90. data/test/configs/eqsign +0 -3
  91. data/test/configs/exact_match +0 -8
  92. data/test/configs/host_plus +0 -10
  93. data/test/configs/multihost +0 -4
  94. data/test/configs/negative_match +0 -6
  95. data/test/configs/nohost +0 -19
  96. data/test/configs/numeric_host +0 -4
  97. data/test/configs/send_env +0 -2
  98. data/test/configs/substitutes +0 -8
  99. data/test/configs/wild_cards +0 -14
  100. data/test/connection/test_channel.rb +0 -467
  101. data/test/connection/test_session.rb +0 -543
  102. data/test/known_hosts/github +0 -1
  103. data/test/manual/test_forward.rb +0 -285
  104. data/test/manual/test_pageant.rb +0 -37
  105. data/test/start/test_connection.rb +0 -53
  106. data/test/start/test_options.rb +0 -43
  107. data/test/start/test_transport.rb +0 -28
  108. data/test/test_all.rb +0 -11
  109. data/test/test_buffer.rb +0 -433
  110. data/test/test_buffered_io.rb +0 -63
  111. data/test/test_config.rb +0 -221
  112. data/test/test_key_factory.rb +0 -191
  113. data/test/test_known_hosts.rb +0 -13
  114. data/test/transport/hmac/test_md5.rb +0 -41
  115. data/test/transport/hmac/test_md5_96.rb +0 -27
  116. data/test/transport/hmac/test_none.rb +0 -34
  117. data/test/transport/hmac/test_ripemd160.rb +0 -36
  118. data/test/transport/hmac/test_sha1.rb +0 -36
  119. data/test/transport/hmac/test_sha1_96.rb +0 -27
  120. data/test/transport/hmac/test_sha2_256.rb +0 -37
  121. data/test/transport/hmac/test_sha2_256_96.rb +0 -27
  122. data/test/transport/hmac/test_sha2_512.rb +0 -37
  123. data/test/transport/hmac/test_sha2_512_96.rb +0 -27
  124. data/test/transport/kex/test_diffie_hellman_group14_sha1.rb +0 -13
  125. data/test/transport/kex/test_diffie_hellman_group1_sha1.rb +0 -146
  126. data/test/transport/kex/test_diffie_hellman_group_exchange_sha1.rb +0 -92
  127. data/test/transport/kex/test_diffie_hellman_group_exchange_sha256.rb +0 -34
  128. data/test/transport/kex/test_ecdh_sha2_nistp256.rb +0 -161
  129. data/test/transport/kex/test_ecdh_sha2_nistp384.rb +0 -38
  130. data/test/transport/kex/test_ecdh_sha2_nistp521.rb +0 -38
  131. data/test/transport/test_algorithms.rb +0 -324
  132. data/test/transport/test_cipher_factory.rb +0 -443
  133. data/test/transport/test_hmac.rb +0 -34
  134. data/test/transport/test_identity_cipher.rb +0 -40
  135. data/test/transport/test_packet_stream.rb +0 -1761
  136. data/test/transport/test_server_version.rb +0 -78
  137. data/test/transport/test_session.rb +0 -331
  138. data/test/transport/test_state.rb +0 -181
@@ -1,6 +1,8 @@
1
1
  require 'net/ssh/transport/openssl'
2
2
  require 'net/ssh/prompt'
3
3
 
4
+ require 'net/ssh/authentication/ed25519_loader'
5
+
4
6
  module Net; module SSH
5
7
 
6
8
  # A factory class for returning new Key classes. It is used for obtaining
@@ -21,11 +23,10 @@ module Net; module SSH
21
23
  }
22
24
  if defined?(OpenSSL::PKey::EC)
23
25
  MAP["ecdsa"] = OpenSSL::PKey::EC
26
+ MAP["ed25519"] = Net::SSH::Authentication::ED25519::PrivKey if defined? Net::SSH::Authentication::ED25519
24
27
  end
25
28
 
26
29
  class <<self
27
- include Prompt
28
-
29
30
  # Fetch an OpenSSL key instance by its SSH name. It will be a new,
30
31
  # empty key of the given type.
31
32
  def get(name)
@@ -36,61 +37,43 @@ module Net; module SSH
36
37
  # whether the file describes an RSA or DSA key, and will load it
37
38
  # appropriately. The new key is returned. If the key itself is
38
39
  # encrypted (requiring a passphrase to use), the user will be
39
- # prompted to enter their password unless passphrase works.
40
- def load_private_key(filename, passphrase=nil, ask_passphrase=true)
40
+ # prompted to enter their password unless passphrase works.
41
+ def load_private_key(filename, passphrase=nil, ask_passphrase=true, prompt=Prompt.default)
41
42
  data = File.read(File.expand_path(filename))
42
- load_data_private_key(data, passphrase, ask_passphrase, filename)
43
+ load_data_private_key(data, passphrase, ask_passphrase, filename, prompt)
43
44
  end
44
45
 
45
46
  # Loads a private key. It will correctly determine
46
47
  # whether the file describes an RSA or DSA key, and will load it
47
48
  # appropriately. The new key is returned. If the key itself is
48
49
  # encrypted (requiring a passphrase to use), the user will be
49
- # prompted to enter their password unless passphrase works.
50
- def load_data_private_key(data, passphrase=nil, ask_passphrase=true, filename="")
51
- if OpenSSL::PKey.respond_to?(:read)
52
- pkey_read = true
53
- error_class = ArgumentError
54
- else
55
- pkey_read = false
56
- if data.match(/-----BEGIN DSA PRIVATE KEY-----/)
57
- key_type = OpenSSL::PKey::DSA
58
- error_class = OpenSSL::PKey::DSAError
59
- elsif data.match(/-----BEGIN RSA PRIVATE KEY-----/)
60
- key_type = OpenSSL::PKey::RSA
61
- error_class = OpenSSL::PKey::RSAError
62
- elsif data.match(/-----BEGIN EC PRIVATE KEY-----/) && defined?(OpenSSL::PKey::EC)
63
- key_type = OpenSSL::PKey::EC
64
- error_class = OpenSSL::PKey::ECError
65
- elsif data.match(/-----BEGIN (.+) PRIVATE KEY-----/)
66
- raise OpenSSL::PKey::PKeyError, "not a supported key type '#{$1}'"
67
- else
68
- raise OpenSSL::PKey::PKeyError, "not a private key (#{filename})"
69
- end
70
- end
50
+ # prompted to enter their password unless passphrase works.
51
+ def load_data_private_key(data, passphrase=nil, ask_passphrase=true, filename="", prompt=Prompt.default)
52
+ key_read, error_classes = classify_key(data, filename)
71
53
 
72
54
  encrypted_key = data.match(/ENCRYPTED/)
73
55
  tries = 0
74
56
 
75
- begin
76
- if pkey_read
77
- return OpenSSL::PKey.read(data, passphrase || 'invalid')
78
- else
79
- return key_type.new(data, passphrase || 'invalid')
80
- end
81
- rescue error_class
82
- if encrypted_key && ask_passphrase
83
- tries += 1
84
- if tries <= 3
85
- passphrase = prompt("Enter passphrase for #{filename}:", false)
86
- retry
57
+ prompter = nil
58
+ result =
59
+ begin
60
+ key_read[data, passphrase || 'invalid']
61
+ rescue *error_classes
62
+ if encrypted_key && ask_passphrase
63
+ tries += 1
64
+ if tries <= 3
65
+ prompter ||= prompt.start(type: 'private_key', filename: filename, sha: Digest::SHA256.digest(data))
66
+ passphrase = prompter.ask("Enter passphrase for #{filename}:", false)
67
+ retry
68
+ else
69
+ raise
70
+ end
87
71
  else
88
72
  raise
89
73
  end
90
- else
91
- raise
92
74
  end
93
- end
75
+ prompter.success if prompter
76
+ result
94
77
  end
95
78
 
96
79
  # Loads a public key from a file. It will correctly determine whether
@@ -110,7 +93,7 @@ module Net; module SSH
110
93
  blob = nil
111
94
  begin
112
95
  blob = fields.shift
113
- end while !blob.nil? && !/^(ssh-(rsa|dss)|ecdsa-sha2-nistp\d+)$/.match(blob)
96
+ end while !blob.nil? && !/^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp\d+)$/.match(blob)
114
97
  blob = fields.shift
115
98
 
116
99
  raise Net::SSH::Exception, "public key at #{filename} is not valid" if blob.nil?
@@ -119,6 +102,29 @@ module Net; module SSH
119
102
  reader = Net::SSH::Buffer.new(blob)
120
103
  reader.read_key or raise OpenSSL::PKey::PKeyError, "not a public key #{filename.inspect}"
121
104
  end
105
+
106
+ private
107
+
108
+ # Determine whether the file describes an RSA or DSA key, and return how load it
109
+ # appropriately.
110
+ def classify_key(data, filename)
111
+ if data.match(/-----BEGIN OPENSSH PRIVATE KEY-----/)
112
+ Net::SSH::Authentication::ED25519Loader.raiseUnlessLoaded("OpenSSH keys only supported if ED25519 is available")
113
+ return ->(key_data, passphrase) { Net::SSH::Authentication::ED25519::PrivKey.read(key_data, passphrase) }, [ArgumentError]
114
+ elsif OpenSSL::PKey.respond_to?(:read)
115
+ return ->(key_data, passphrase) { OpenSSL::PKey.read(key_data, passphrase) }, [ArgumentError, OpenSSL::PKey::PKeyError]
116
+ elsif data.match(/-----BEGIN DSA PRIVATE KEY-----/)
117
+ return ->(key_data, passphrase) { OpenSSL::PKey::DSA.new(key_data, passphrase) }, [OpenSSL::PKey::DSAError]
118
+ elsif data.match(/-----BEGIN RSA PRIVATE KEY-----/)
119
+ return ->(key_data, passphrase) { OpenSSL::PKey::RSA.new(key_data, passphrase) }, [OpenSSL::PKey::RSAError]
120
+ elsif data.match(/-----BEGIN EC PRIVATE KEY-----/) && defined?(OpenSSL::PKey::EC)
121
+ return ->(key_data, passphrase) { OpenSSL::PKey::EC.new(key_data, passphrase) }, [OpenSSL::PKey::ECError]
122
+ elsif data.match(/-----BEGIN (.+) PRIVATE KEY-----/)
123
+ raise OpenSSL::PKey::PKeyError, "not a supported key type '#{$1}'"
124
+ else
125
+ raise OpenSSL::PKey::PKeyError, "not a private key (#{filename})"
126
+ end
127
+ end
122
128
  end
123
129
 
124
130
  end
@@ -1,8 +1,37 @@
1
1
  require 'strscan'
2
+ require 'openssl'
3
+ require 'base64'
2
4
  require 'net/ssh/buffer'
3
5
 
4
6
  module Net; module SSH
5
7
 
8
+ # Represents the result of a search in known hosts
9
+ # see search_for
10
+ class HostKeys
11
+ include Enumerable
12
+ attr_reader :host
13
+
14
+ def initialize(host_keys, host, known_hosts, options = {})
15
+ @host_keys = host_keys
16
+ @host = host
17
+ @known_hosts = known_hosts
18
+ @options = options
19
+ end
20
+
21
+ def add_host_key(key)
22
+ @known_hosts.add(@host, key, @options)
23
+ @host_keys.push(key)
24
+ end
25
+
26
+ def each(&block)
27
+ @host_keys.each(&block)
28
+ end
29
+
30
+ def empty?
31
+ @host_keys.empty?
32
+ end
33
+ end
34
+
6
35
  # Searches an OpenSSH-style known-host file for a given host, and returns all
7
36
  # matching keys. This is used to implement host-key verification, as well as
8
37
  # to determine what key a user prefers to use for a given host.
@@ -24,9 +53,9 @@ module Net; module SSH
24
53
  class <<self
25
54
 
26
55
  # Searches all known host files (see KnownHosts.hostfiles) for all keys
27
- # of the given host. Returns an array of keys found.
56
+ # of the given host. Returns an enumerable of keys found.
28
57
  def search_for(host, options={})
29
- search_in(hostfiles(options), host)
58
+ HostKeys.new(search_in(hostfiles(options), host), host, self, options)
30
59
  end
31
60
 
32
61
  # Search for all known keys for the given host, in every file given in
@@ -111,7 +140,9 @@ module Net; module SSH
111
140
  next if scanner.match?(/$|#/)
112
141
 
113
142
  hostlist = scanner.scan(/\S+/).split(/,/)
114
- next unless entries.all? { |entry| hostlist.include?(entry) }
143
+ found = entries.all? { |entry| hostlist.include?(entry) } ||
144
+ known_host_hash?(hostlist, entries, scanner)
145
+ next unless found
115
146
 
116
147
  scanner.skip(/\s*/)
117
148
  type = scanner.scan(/\S+/)
@@ -127,6 +158,21 @@ module Net; module SSH
127
158
  keys
128
159
  end
129
160
 
161
+ # Indicates whether one of the entries matches an hostname that has been
162
+ # stored as a HMAC-SHA1 hash in the known hosts.
163
+ def known_host_hash?(hostlist, entries, scanner)
164
+ if hostlist.size == 1 && hostlist.first =~ /\A\|1(\|.+){2}\z/
165
+ chunks = hostlist.first.split(/\|/)
166
+ salt = Base64.decode64(chunks[2])
167
+ digest = OpenSSL::Digest.new('sha1')
168
+ entries.each do |entry|
169
+ hmac = OpenSSL::HMAC.digest(digest, salt, entry)
170
+ return true if Base64.encode64(hmac).chomp == chunks[3]
171
+ end
172
+ end
173
+ false
174
+ end
175
+
130
176
  # Tries to append an entry to the current source file for the given host
131
177
  # and key. If it is unable to (because the file is not writable, for
132
178
  # instance), an exception will be raised.
@@ -1,93 +1,62 @@
1
- module Net; module SSH
2
-
3
- # A basic prompt module that can be mixed into other objects. If HighLine is
4
- # installed, it will be used to display prompts and read input from the
5
- # user. Otherwise, the termios library will be used. If neither HighLine
6
- # nor termios is installed, a simple prompt that echos text in the clear
7
- # will be used.
1
+ require 'io/console'
8
2
 
9
- module PromptMethods
3
+ module Net; module SSH
10
4
 
11
- # Defines the prompt method to use if the Highline library is installed.
12
- module Highline
13
- # Uses Highline#ask to present a prompt and accept input. If +echo+ is
14
- # +false+, the characters entered by the user will not be echoed to the
15
- # screen.
16
- def prompt(prompt, echo=true)
17
- @highline ||= ::HighLine.new
18
- @highline.ask(prompt + " ") { |q| q.echo = echo }
19
- end
5
+ # Default prompt implementation, called for asking password from user.
6
+ # It will never be instantiated directly, but will instead be created for
7
+ # you automatically.
8
+ #
9
+ # A custom prompt objects can implement caching, or different UI. The prompt
10
+ # object should implemnted a start method, which should return something implementing
11
+ # ask and success. Net::SSH uses it like:
12
+ #
13
+ # prompter = options[:password_prompt].start({type:'password'})
14
+ # while !ok && max_retries < 3
15
+ # user = prompter.ask("user: ", {}, true)
16
+ # password = prompter.ask("password: ", {}, false)
17
+ # ok = send(user, password)
18
+ # prompter.sucess if ok
19
+ # end
20
+ #
21
+ class Prompt
22
+ # factory
23
+ def self.default(options = {})
24
+ @default ||= new(options)
20
25
  end
21
26
 
22
- # Defines the prompt method to use if the Termios library is installed.
23
- module Termios
24
- # Displays the prompt to $stdout. If +echo+ is false, the Termios
25
- # library will be used to disable keystroke echoing for the duration of
26
- # this method.
27
- def prompt(prompt, echo=true)
28
- $stdout.print(prompt)
29
- $stdout.flush
27
+ def initialize(options = {}); end
30
28
 
31
- set_echo(false) unless echo
32
- $stdin.gets.chomp
33
- ensure
34
- if !echo
35
- set_echo(true)
36
- $stdout.puts
29
+ # default prompt object implementation. More sophisticated implemenetations
30
+ # might implement caching.
31
+ class Prompter
32
+ def initialize(info)
33
+ if info[:type] == 'keyboard-interactive'
34
+ $stdout.puts(info[:name]) unless info[:name].empty?
35
+ $stdout.puts(info[:instruction]) unless info[:instruction].empty?
37
36
  end
38
37
  end
39
38
 
40
- private
41
-
42
- # Enables or disables keystroke echoing using the Termios library.
43
- def set_echo(enable)
44
- term = ::Termios.getattr($stdin)
45
-
46
- if enable
47
- term.c_lflag |= (::Termios::ECHO | ::Termios::ICANON)
48
- else
49
- term.c_lflag &= ~::Termios::ECHO
50
- end
51
-
52
- ::Termios.setattr($stdin, ::Termios::TCSANOW, term)
53
- end
54
- end
55
-
56
- # Defines the prompt method to use when neither Highline nor Termios are
57
- # installed.
58
- module Clear
59
- # Displays the prompt to $stdout and pulls the response from $stdin.
60
- # Text is always echoed in the clear, regardless of the +echo+ setting.
61
- # The first time a prompt is given and +echo+ is false, a warning will
62
- # be written to $stderr recommending that either Highline or Termios
63
- # be installed.
64
- def prompt(prompt, echo=true)
65
- @seen_warning ||= false
66
- if !echo && !@seen_warning
67
- $stderr.puts "Text will be echoed in the clear. Please install the HighLine or Termios libraries to suppress echoed text."
68
- @seen_warning = true
69
- end
70
-
39
+ # ask input from user, a prompter might ask for multiple inputs
40
+ # (like user and password) in a single session.
41
+ def ask(prompt, echo=true)
71
42
  $stdout.print(prompt)
72
43
  $stdout.flush
73
- $stdin.gets.chomp
44
+ ret = $stdin.noecho(&:gets).chomp
45
+ $stdout.print("\n")
46
+ ret
74
47
  end
48
+
49
+ # success method will be called when the password was accepted
50
+ # It's a good time to save password asked to a cache.
51
+ def success; end
75
52
  end
76
- end
77
53
 
78
- # Try to load Highline and Termios in turn, selecting the corresponding
79
- # PromptMethods module to use. If neither are available, choose PromptMethods::Clear.
80
- Prompt = begin
81
- require 'highline'
82
- HighLine.track_eof = false
83
- PromptMethods::Highline
84
- rescue LoadError
85
- begin
86
- require 'termios'
87
- PromptMethods::Termios
88
- rescue LoadError
89
- PromptMethods::Clear
90
- end
54
+ # start password session. Multiple questions might be asked multiple times
55
+ # on the returned object. Info hash tries to uniquely identify the password
56
+ # session, so caching implementations can save passwords properly.
57
+ def start(info)
58
+ Prompter.new(info)
91
59
  end
60
+ end
92
61
 
93
- end; end
62
+ end; end
@@ -1,4 +1,5 @@
1
1
  require 'socket'
2
+ require 'rubygems'
2
3
  require 'net/ssh/proxy/errors'
3
4
  require 'net/ssh/ruby_compat'
4
5
 
@@ -66,13 +67,38 @@ module Net; module SSH; module Proxy
66
67
  raise ConnectError, "#{e}: #{command_line}"
67
68
  end
68
69
  @command_line = command_line
69
- class << io
70
- def send(data, flag)
71
- write_nonblock(data)
70
+ if Gem.win_platform?
71
+ # read_nonblock and write_nonblock are not available on Windows
72
+ # pipe. Use sysread and syswrite as a replacement works.
73
+ def io.send(data, flag)
74
+ syswrite(data)
72
75
  end
73
76
 
74
- def recv(size)
75
- read_nonblock(size)
77
+ def io.recv(size)
78
+ sysread(size)
79
+ end
80
+ else
81
+ def io.send(data, flag)
82
+ begin
83
+ result = write_nonblock(data)
84
+ rescue IO::WaitWritable, Errno::EINTR
85
+ IO.select(nil, [self])
86
+ retry
87
+ end
88
+ result
89
+ end
90
+
91
+ def io.recv(size)
92
+ begin
93
+ result = read_nonblock(size)
94
+ rescue IO::WaitReadable, Errno::EINTR
95
+ timeout_in_seconds = 20
96
+ if IO.select([self], nil, [self], timeout_in_seconds) == nil
97
+ raise "Unexpected spurious read wakeup"
98
+ end
99
+ retry
100
+ end
101
+ result
76
102
  end
77
103
  end
78
104
  io
@@ -8,7 +8,7 @@ module Net; module SSH; module Proxy
8
8
  #
9
9
  # require 'net/ssh/proxy/http'
10
10
  #
11
- # proxy = Net::SSH::Proxy::HTTP.new('proxy.host', proxy_port)
11
+ # proxy = Net::SSH::Proxy::HTTP.new('proxy_host', proxy_port)
12
12
  # Net::SSH.start('host', 'user', :proxy => proxy) do |ssh|
13
13
  # ...
14
14
  # end
@@ -16,7 +16,7 @@ module Net; module SSH; module Proxy
16
16
  # If the proxy requires authentication, you can pass :user and :password
17
17
  # to the proxy's constructor:
18
18
  #
19
- # proxy = Net::SSH::Proxy::HTTP.new('proxy.host', proxy_port,
19
+ # proxy = Net::SSH::Proxy::HTTP.new('proxy_host', proxy_port,
20
20
  # :user => "user", :password => "password")
21
21
  #
22
22
  # Note that HTTP digest authentication is not supported; Basic only at
@@ -48,8 +48,8 @@ module Net; module SSH; module Proxy
48
48
 
49
49
  # Return a new socket connected to the given host and port via the
50
50
  # proxy that was requested when the socket factory was instantiated.
51
- def open(host, port, connection_options = nil)
52
- socket = TCPSocket.new(proxy_host, proxy_port)
51
+ def open(host, port, connection_options)
52
+ socket = establish_connection(connection_options[:timeout])
53
53
  socket.write "CONNECT #{host}:#{port} HTTP/1.0\r\n"
54
54
 
55
55
  if options[:user]
@@ -67,7 +67,12 @@ module Net; module SSH; module Proxy
67
67
  raise ConnectError, resp.inspect
68
68
  end
69
69
 
70
- private
70
+ protected
71
+
72
+ def establish_connection(connect_timeout)
73
+ Socket.tcp(proxy_host, proxy_port, nil, nil,
74
+ connect_timeout: connect_timeout)
75
+ end
71
76
 
72
77
  def parse_response(socket)
73
78
  version, code, reason = socket.gets.chomp.split(/ /, 3)
@@ -82,13 +87,12 @@ module Net; module SSH; module Proxy
82
87
  body = socket.read(headers["Content-Length"].to_i)
83
88
  end
84
89
 
85
- return { :version => version,
86
- :code => code.to_i,
87
- :reason => reason,
88
- :headers => headers,
89
- :body => body }
90
+ return { version: version,
91
+ code: code.to_i,
92
+ reason: reason,
93
+ headers: headers,
94
+ body: body }
90
95
  end
91
-
92
96
  end
93
97
 
94
98
  end; end; end
@@ -0,0 +1,49 @@
1
+ require 'socket'
2
+ require 'openssl'
3
+ require 'net/ssh/proxy/errors'
4
+ require 'net/ssh/proxy/http'
5
+
6
+ module Net; module SSH; module Proxy
7
+
8
+ # A specialization of the HTTP proxy which encrypts the whole connection
9
+ # using OpenSSL. This has the advantage that proxy authentication
10
+ # information is not sent in plaintext.
11
+ class HTTPS < HTTP
12
+
13
+ # Create a new socket factory that tunnels via the given host and
14
+ # port. The +options+ parameter is a hash of additional settings that
15
+ # can be used to tweak this proxy connection. In addition to the options
16
+ # taken by Net::SSH::Proxy::HTTP it supports:
17
+ #
18
+ # * :ssl_context => the SSL configuration to use for the connection
19
+ def initialize(proxy_host, proxy_port=80, options={})
20
+ @ssl_context = options.delete(:ssl_context) ||
21
+ OpenSSL::SSL::SSLContext.new
22
+ super(proxy_host, proxy_port, options)
23
+ end
24
+
25
+ protected
26
+
27
+ # Shim to make OpenSSL::SSL::SSLSocket behave like a regular TCPSocket
28
+ # for all intents and purposes of Net::SSH::BufferedIo
29
+ module SSLSocketCompatibility
30
+ def self.extended(object) #:nodoc:
31
+ object.define_singleton_method(:recv, object.method(:sysread))
32
+ object.sync_close = true
33
+ end
34
+
35
+ def send(data, _opts)
36
+ syswrite(data)
37
+ end
38
+ end
39
+
40
+ def establish_connection(connect_timeout)
41
+ plain_socket = super(connect_timeout)
42
+ OpenSSL::SSL::SSLSocket.new(plain_socket, @ssl_context).tap do |socket|
43
+ socket.extend(SSLSocketCompatibility)
44
+ socket.connect
45
+ end
46
+ end
47
+ end
48
+
49
+ end; end; end
@@ -48,7 +48,8 @@ module Net
48
48
  # Return a new socket connected to the given host and port via the
49
49
  # proxy that was requested when the socket factory was instantiated.
50
50
  def open(host, port, connection_options)
51
- socket = TCPSocket.new(proxy_host, proxy_port)
51
+ socket = Socket.tcp(proxy_host, proxy_port, nil, nil,
52
+ connect_timeout: connection_options[:timeout])
52
53
  ip_addr = IPAddr.new(Resolv.getaddress(host))
53
54
 
54
55
  packet = [VERSION, CONNECT, port.to_i, ip_addr.to_i, options[:user]].pack("CCnNZ*")
@@ -62,8 +62,9 @@ module Net
62
62
 
63
63
  # Return a new socket connected to the given host and port via the
64
64
  # proxy that was requested when the socket factory was instantiated.
65
- def open(host, port, connection_options = nil)
66
- socket = TCPSocket.new(proxy_host, proxy_port)
65
+ def open(host, port, connection_options)
66
+ socket = Socket.tcp(proxy_host, proxy_port, nil, nil,
67
+ connect_timeout: connection_options[:timeout])
67
68
 
68
69
  methods = [METHOD_NO_AUTH]
69
70
  methods << METHOD_PASSWD if options[:user]
@@ -9,11 +9,6 @@ class String
9
9
  self[index] = c
10
10
  end
11
11
  end
12
- if RUBY_VERSION < "1.8.7"
13
- def bytesize
14
- self.size
15
- end
16
- end
17
12
  end
18
13
 
19
14
  module Net; module SSH
@@ -21,31 +16,9 @@ module Net; module SSH
21
16
  # This class contains miscellaneous patches and workarounds
22
17
  # for different ruby implementations.
23
18
  class Compat
24
-
25
- # A workaround for an IO#select threading bug in certain versions of MRI 1.8.
26
- # See: http://net-ssh.lighthouseapp.com/projects/36253/tickets/1-ioselect-threading-bug-in-ruby-18
27
- # The root issue is documented here: http://redmine.ruby-lang.org/issues/show/1993
28
- if RUBY_VERSION >= '1.9' || RUBY_PLATFORM == 'java'
29
- def self.io_select(*params)
30
- IO.select(*params)
31
- end
32
- else
33
- SELECT_MUTEX = Mutex.new
34
- def self.io_select(*params)
35
- # It should be safe to wrap calls in a mutex when the timeout is 0
36
- # (that is, the call is not supposed to block).
37
- # We leave blocking calls unprotected to avoid causing deadlocks.
38
- # This should still catch the main case for Capistrano users.
39
- if params[3] == 0
40
- SELECT_MUTEX.synchronize do
41
- IO.select(*params)
42
- end
43
- else
44
- IO.select(*params)
45
- end
46
- end
19
+ def self.io_select(*params)
20
+ IO.select(*params)
47
21
  end
48
-
49
22
  end
50
23
 
51
24
  end; end
@@ -91,6 +91,7 @@ module Net; module SSH; module Service
91
91
 
92
92
  channel.on_open_failed do |ch, code, description|
93
93
  channel.error { "could not establish direct channel: #{description} (#{code})" }
94
+ session.stop_listening_to(channel[:socket])
94
95
  channel[:socket].close
95
96
  end
96
97
  end
@@ -273,7 +274,6 @@ module Net; module SSH; module Service
273
274
  ch[:socket].enqueue(data)
274
275
  end
275
276
 
276
- # Handles server close on the sending side by Miklós Fazekas
277
277
  channel.on_eof do |ch|
278
278
  debug { "eof #{type} on #{type} forwarded channel" }
279
279
  begin
@@ -357,7 +357,7 @@ module Net; module SSH; module Service
357
357
  channel[:invisible] = true
358
358
 
359
359
  begin
360
- agent = Authentication::Agent.connect(logger)
360
+ agent = Authentication::Agent.connect(logger, session.options[:agent_socket_factory])
361
361
  if (agent.socket.is_a? ::IO)
362
362
  prepare_client(agent.socket, channel, :agent)
363
363
  else
@@ -98,6 +98,13 @@ module Net; module SSH; module Test
98
98
  script.sends_channel_close(self)
99
99
  end
100
100
 
101
+ # Scripts the sending of a "request pty" request packet across the channel.
102
+ #
103
+ # channel.sends_request_pty
104
+ def sends_request_pty
105
+ script.sends_channel_request_pty(self)
106
+ end
107
+
101
108
  # Scripts the reception of a channel data packet from the remote end.
102
109
  #
103
110
  # channel.gets_data "bar"
@@ -113,6 +113,22 @@ module Net; module SSH; module Test
113
113
  base.extend(ClassMethods)
114
114
  end
115
115
 
116
+ @extension_enabled = false
117
+
118
+ def self.with_test_extension(&block)
119
+ orig_value = @extension_enabled
120
+ @extension_enabled = true
121
+ begin
122
+ yield
123
+ ensure
124
+ @extension_enabled = orig_value
125
+ end
126
+ end
127
+
128
+ def self.extension_enabled?
129
+ @extension_enabled
130
+ end
131
+
116
132
  module ClassMethods
117
133
  def self.extended(obj) #:nodoc:
118
134
  class <<obj
@@ -125,6 +141,7 @@ module Net; module SSH; module Test
125
141
  # writers, and errors arrays are either nil, or contain only objects
126
142
  # that mix in Net::SSH::Test::Extensions::BufferedIo.
127
143
  def select_for_test(readers=nil, writers=nil, errors=nil, wait=nil)
144
+ return select_for_real(readers, writers, errors, wait) unless Net::SSH::Test::Extensions::IO.extension_enabled?
128
145
  ready_readers = Array(readers).select { |r| r.select_for_read? }
129
146
  ready_writers = Array(writers).select { |r| r.select_for_write? }
130
147
  ready_errors = Array(errors).select { |r| r.select_for_error? }