net-ssh 2.9.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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? }