net-ssh 7.3.6 → 8.0.2

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 (68) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/pull_request_template.md +1 -0
  4. data/.github/workflows/changelog.yml +43 -0
  5. data/.github/workflows/ci-with-docker.yml +0 -3
  6. data/.github/workflows/ci.yml +2 -17
  7. data/.github/workflows/rubocop.yml +3 -1
  8. data/.rubocop.yml +5 -1
  9. data/.rubocop_todo.yml +1 -7
  10. data/CHANGES.txt +82 -7
  11. data/CONTRIBUTING.md +35 -0
  12. data/Dockerfile +3 -3
  13. data/Dockerfile.openssl3 +1 -1
  14. data/Dockerfile.zlib_ng +1 -1
  15. data/README.md +71 -57
  16. data/Rakefile +66 -12
  17. data/appveyor.yml +4 -16
  18. data/changelog.d/README.md +1 -0
  19. data/docker-compose.yml +1 -7
  20. data/lib/net/ssh/authentication/agent.rb +35 -11
  21. data/lib/net/ssh/authentication/ed25519.rb +137 -44
  22. data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
  23. data/lib/net/ssh/authentication/key_manager.rb +22 -11
  24. data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
  25. data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
  26. data/lib/net/ssh/authentication/security_key.rb +71 -0
  27. data/lib/net/ssh/authentication/session.rb +29 -17
  28. data/lib/net/ssh/buffer.rb +6 -1
  29. data/lib/net/ssh/buffered_io.rb +40 -3
  30. data/lib/net/ssh/config.rb +28 -12
  31. data/lib/net/ssh/connection/channel.rb +49 -2
  32. data/lib/net/ssh/connection/session.rb +58 -8
  33. data/lib/net/ssh/errors.rb +24 -6
  34. data/lib/net/ssh/key_factory.rb +7 -1
  35. data/lib/net/ssh/known_hosts.rb +18 -8
  36. data/lib/net/ssh/multiplex/client.rb +162 -0
  37. data/lib/net/ssh/multiplex/control_path.rb +120 -0
  38. data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
  39. data/lib/net/ssh/multiplex.rb +126 -0
  40. data/lib/net/ssh/prompt.rb +3 -3
  41. data/lib/net/ssh/proxy/jump.rb +40 -2
  42. data/lib/net/ssh/service/forward.rb +35 -24
  43. data/lib/net/ssh/test.rb +2 -1
  44. data/lib/net/ssh/transport/algorithms.rb +88 -33
  45. data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
  46. data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
  47. data/lib/net/ssh/transport/cipher_factory.rb +23 -1
  48. data/lib/net/ssh/transport/constants.rb +1 -0
  49. data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
  50. data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
  51. data/lib/net/ssh/transport/hmac.rb +2 -0
  52. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
  53. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
  54. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
  55. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
  56. data/lib/net/ssh/transport/openssl.rb +2 -1
  57. data/lib/net/ssh/transport/packet_stream.rb +12 -0
  58. data/lib/net/ssh/transport/session.rb +43 -45
  59. data/lib/net/ssh/verifiers/always.rb +0 -3
  60. data/lib/net/ssh/version.rb +3 -3
  61. data/lib/net/ssh.rb +141 -58
  62. data/net-ssh.gemspec +5 -8
  63. data/rakelib/changelog.rb +229 -0
  64. data.tar.gz.sig +0 -0
  65. metadata +37 -36
  66. metadata.gz.sig +0 -0
  67. data/Gemfile.noed25519 +0 -12
  68. data/Gemfile.norbnacl +0 -12
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'open3'
5
+
6
+ # Builds CHANGES.txt sections from the fragment files in changelog.d/ (see CONTRIBUTING.md).
7
+ class Changelog
8
+ class Error < StandardError; end
9
+
10
+ # Output order of the groups in a release section; `none` fragments are never emitted.
11
+ TYPES = {
12
+ 'breaking-security' => 'BREAKING: Security: ',
13
+ 'breaking' => 'BREAKING: ',
14
+ 'security' => 'Security: ',
15
+ 'change' => '',
16
+ 'none' => nil
17
+ }.freeze
18
+
19
+ FILENAME = /\A(?<number>[1-9]\d*)\.(?<type>[a-z-]+)(?:\.(?<seq>[1-9]\d*))?\.txt\z/.freeze
20
+ REF = /#\d+|GHSA(?:-[a-z0-9]{4}){3}|CVE-\d{4}-\d+/i.freeze
21
+ REF_LIST = /\[(?:#{REF})(?:,\s*(?:#{REF}))*\]\z/.freeze
22
+ UNRELEASED = '=== Unreleased'
23
+ PLACEHOLDER = 'TODO: describe the change as it should read in CHANGES.txt'
24
+
25
+ Fragment = Struct.new(:path, :number, :type, :seq, :text) do
26
+ def entry
27
+ "#{TYPES.fetch(type)}#{text}"
28
+ end
29
+
30
+ def sort_key
31
+ [-number, seq]
32
+ end
33
+ end
34
+
35
+ attr_reader :dir, :changes_path
36
+
37
+ def initialize(root)
38
+ @root = root
39
+ @dir = File.join(root, 'changelog.d')
40
+ @changes_path = File.join(root, 'CHANGES.txt')
41
+ end
42
+
43
+ # Raises Error listing every invalid fragment; returns all fragments, including `none` ones.
44
+ def fragments
45
+ errors = []
46
+ fragments = []
47
+ entries_in_dir.each do |name|
48
+ result = parse_fragment(name)
49
+ result.is_a?(Fragment) ? fragments << result : errors << "changelog.d/#{name}: #{result}"
50
+ end
51
+ raise Error, "Invalid changelog fragments:\n #{errors.join("\n ")}" unless errors.empty?
52
+
53
+ fragments.sort_by(&:sort_key)
54
+ end
55
+
56
+ # Validates the fragments and the `=== Unreleased` section of CHANGES.txt; returns the fragments.
57
+ def check
58
+ all = fragments
59
+ split_changes(read_changes)
60
+ all
61
+ end
62
+
63
+ # The text of the section for +heading+, or nil for a pre-release with nothing to release.
64
+ def section(heading)
65
+ assemble(heading, fragments, split_changes(read_changes))
66
+ end
67
+
68
+ # Writes the section for +heading+ to the top of CHANGES.txt, folding in any `=== Unreleased`
69
+ # entries, and removes all fragments; returns the section, or nil when CHANGES.txt was left unchanged.
70
+ def release(heading)
71
+ all = fragments
72
+ content = read_changes
73
+ raise Error, "CHANGES.txt already has a '=== #{heading}' section" if content.lines.any? { |l| l.chomp == "=== #{heading}" }
74
+
75
+ parts = split_changes(content)
76
+ section = assemble(heading, all, parts)
77
+ if section
78
+ rest = parts[:rest].empty? ? '' : "\n#{parts[:rest]}"
79
+ File.write(changes_path, "#{parts[:preamble]}#{section}#{rest}")
80
+ end
81
+ remove(all.map(&:path))
82
+ section
83
+ end
84
+
85
+ # Creates a fragment for PR +number+ and returns its path; picks the next free `.N` suffix if needed.
86
+ def new_fragment(number, type, text = nil)
87
+ path = (1..).lazy.map { |seq| File.join(dir, fragment_name(number, type, seq)) }.find { |p| !File.exist?(p) }
88
+ error = validate_name(File.basename(path))
89
+ raise Error, error if error
90
+
91
+ FileUtils.mkdir_p(dir)
92
+ text = PLACEHOLDER if (text.nil? || text.strip.empty?) && type != 'none'
93
+ File.write(path, "#{text}\n")
94
+ path
95
+ end
96
+
97
+ def self.heading(major, minor, tiny, pre)
98
+ ["#{major}.#{minor}.#{tiny}", pre].compact.join(' ')
99
+ end
100
+
101
+ private
102
+
103
+ def entries_in_dir
104
+ return [] unless Dir.exist?(dir)
105
+
106
+ Dir.children(dir).sort.reject { |name| name == 'README.md' || name.start_with?('.') }
107
+ end
108
+
109
+ def fragment_name(number, type, seq)
110
+ seq == 1 ? "#{number}.#{type}.txt" : "#{number}.#{type}.#{seq}.txt"
111
+ end
112
+
113
+ def validate_name(name)
114
+ match = FILENAME.match(name)
115
+ return "file name must be <PR number>.<type>[.<n>].txt, types: #{TYPES.keys.join(', ')}" unless match
116
+ return "unknown type '#{match[:type]}', expected one of: #{TYPES.keys.join(', ')}" unless TYPES.key?(match[:type])
117
+
118
+ nil
119
+ end
120
+
121
+ # Returns a Fragment, or an error message.
122
+ def parse_fragment(name)
123
+ path = File.join(dir, name)
124
+ return 'is not a file' unless File.file?(path)
125
+
126
+ error = validate_name(name)
127
+ return error if error
128
+
129
+ match = FILENAME.match(name)
130
+ fragment = Fragment.new(path, match[:number].to_i, match[:type], (match[:seq] || 1).to_i, nil)
131
+ return fragment if fragment.type == 'none'
132
+
133
+ text = parse_text(read_utf8(path))
134
+ return text[:error] if text[:error]
135
+
136
+ fragment.text = text[:text]
137
+ fragment.text = "#{fragment.text} [##{fragment.number}]" unless fragment.text.match?(REF_LIST)
138
+ fragment
139
+ end
140
+
141
+ def parse_text(raw)
142
+ return { error: 'is not valid UTF-8' } unless raw.valid_encoding?
143
+
144
+ lines = raw.strip.lines.map(&:strip)
145
+ return { error: 'is empty' } if lines.empty?
146
+ return { error: 'must be a single paragraph (no blank lines)' } if lines.include?('')
147
+
148
+ text = lines.join(' ')
149
+ return { error: 'still contains the placeholder text' } if text == PLACEHOLDER
150
+ return { error: "should not start with a bullet, it's added when assembling" } if text.match?(/\A[*-] /)
151
+ return { error: 'should not start with BREAKING:/Security:, the type adds the prefix' } if text.match?(/\A(BREAKING|Security):/i)
152
+
153
+ { text: text }
154
+ end
155
+
156
+ def read_changes
157
+ File.exist?(changes_path) ? read_utf8(changes_path).gsub("\r\n", "\n") : ''
158
+ end
159
+
160
+ # Explicit so a non-UTF-8 locale (e.g. LANG=C) doesn't break on non-ASCII text.
161
+ def read_utf8(path)
162
+ File.read(path, mode: 'r:bom|utf-8')
163
+ end
164
+
165
+ # A final release (a heading without a pre-release suffix) always gets a section, so every published version has a heading.
166
+ def assemble(heading, fragments, parts)
167
+ entries = grouped_entries(fragments, parts[:unreleased])
168
+ if entries.empty?
169
+ return nil unless heading.match?(/\A\d+\.\d+\.\d+\z/)
170
+
171
+ previous = parts[:rest][/\A=== (.+)$/, 1]
172
+ entries = [" * #{previous ? "No changes since #{previous}." : 'No changes.'}\n"]
173
+ end
174
+ "=== #{heading}\n\n#{entries.join}"
175
+ end
176
+
177
+ # Splits CHANGES.txt into the text before the first heading, the entries of a leading
178
+ # `=== Unreleased` section (as lists of raw lines) and everything from the first released heading on.
179
+ def split_changes(content)
180
+ lines = content.lines
181
+ first = lines.index { |l| l.start_with?('=== ') } || lines.size
182
+ preamble = lines[0...first].join
183
+ return { preamble: preamble, unreleased: [], rest: lines[first..].join } unless lines[first]&.chomp == UNRELEASED
184
+
185
+ body_end = lines.index.with_index { |l, i| i > first && l.start_with?('=== ') } || lines.size
186
+ { preamble: preamble, unreleased: unreleased_entries(lines[(first + 1)...body_end]), rest: lines[body_end..].join }
187
+ end
188
+
189
+ def unreleased_entries(lines)
190
+ lines.each_with_object([]) do |line, entries|
191
+ next if line.strip.empty?
192
+
193
+ if line.match?(/\A\s*\* /)
194
+ entries << [line]
195
+ elsif entries.empty?
196
+ raise Error, "Cannot parse the Unreleased section of CHANGES.txt near: #{line.strip}"
197
+ else
198
+ entries.last << line
199
+ end
200
+ end
201
+ end
202
+
203
+ # Fragments come before the hand-written Unreleased entries of the same group.
204
+ def grouped_entries(fragments, unreleased)
205
+ TYPES.keys.flat_map do |type|
206
+ from_fragments = fragments.select { |f| f.type == type && type != 'none' }.map { |f| " * #{f.entry}\n" }
207
+ from_unreleased = unreleased.select { |lines| unreleased_type(lines.first) == type }.map do |lines|
208
+ lines.join.end_with?("\n") ? lines.join : "#{lines.join}\n"
209
+ end
210
+ from_fragments + from_unreleased
211
+ end
212
+ end
213
+
214
+ def unreleased_type(line)
215
+ text = line.sub(/\A\s*\* /, '')
216
+ TYPES.find { |_type, prefix| prefix && !prefix.empty? && text.start_with?(prefix) }&.first || 'change'
217
+ end
218
+
219
+ # `git rm` so the deletions are staged with the version bump; plain delete outside a git checkout.
220
+ def remove(paths)
221
+ return if paths.empty?
222
+
223
+ relative = paths.map { |p| p.delete_prefix("#{@root}/") }
224
+ _out, status = Open3.capture2e('git', '-C', @root, 'rm', '-q', '--', *relative)
225
+ FileUtils.rm_f(paths) unless status.success?
226
+ rescue SystemCallError
227
+ FileUtils.rm_f(paths)
228
+ end
229
+ end
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-ssh
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.3.6
4
+ version: 8.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamis Buck
@@ -32,64 +32,50 @@ cert_chain:
32
32
  ufHf4AX2UIkJbh7zCPkiNCqIr7MSWLNFG/9lOlHYsEJM8XujT1ofPobYx6YSFx/C
33
33
  7HBrI7UX7awt6UvBZebhcHzyMHxg/B5PVQllPA==
34
34
  -----END CERTIFICATE-----
35
- date: 2026-10-05 00:00:00.000000000 Z
35
+ date: 2026-10-22 00:00:00.000000000 Z
36
36
  dependencies:
37
37
  - !ruby/object:Gem::Dependency
38
- name: bcrypt_pbkdf
38
+ name: logger
39
39
  requirement: !ruby/object:Gem::Requirement
40
40
  requirements:
41
- - - "~>"
42
- - !ruby/object:Gem::Version
43
- version: '1.0'
44
- type: :development
45
- prerelease: false
46
- version_requirements: !ruby/object:Gem::Requirement
47
- requirements:
48
- - - "~>"
49
- - !ruby/object:Gem::Version
50
- version: '1.0'
51
- - !ruby/object:Gem::Dependency
52
- name: ed25519
53
- requirement: !ruby/object:Gem::Requirement
54
- requirements:
55
- - - "~>"
41
+ - - ">="
56
42
  - !ruby/object:Gem::Version
57
- version: '1.2'
58
- type: :development
43
+ version: '0'
44
+ type: :runtime
59
45
  prerelease: false
60
46
  version_requirements: !ruby/object:Gem::Requirement
61
47
  requirements:
62
- - - "~>"
48
+ - - ">="
63
49
  - !ruby/object:Gem::Version
64
- version: '1.2'
50
+ version: '0'
65
51
  - !ruby/object:Gem::Dependency
66
- name: x25519
52
+ name: openssl
67
53
  requirement: !ruby/object:Gem::Requirement
68
54
  requirements:
69
55
  - - ">="
70
56
  - !ruby/object:Gem::Version
71
- version: '0'
72
- type: :development
57
+ version: 3.2.0
58
+ type: :runtime
73
59
  prerelease: false
74
60
  version_requirements: !ruby/object:Gem::Requirement
75
61
  requirements:
76
62
  - - ">="
77
63
  - !ruby/object:Gem::Version
78
- version: '0'
64
+ version: 3.2.0
79
65
  - !ruby/object:Gem::Dependency
80
- name: rbnacl
66
+ name: bcrypt_pbkdf
81
67
  requirement: !ruby/object:Gem::Requirement
82
68
  requirements:
83
69
  - - "~>"
84
70
  - !ruby/object:Gem::Version
85
- version: '7.1'
71
+ version: '1.0'
86
72
  type: :development
87
73
  prerelease: false
88
74
  version_requirements: !ruby/object:Gem::Requirement
89
75
  requirements:
90
76
  - - "~>"
91
77
  - !ruby/object:Gem::Version
92
- version: '7.1'
78
+ version: '1.0'
93
79
  - !ruby/object:Gem::Dependency
94
80
  name: base64
95
81
  requirement: !ruby/object:Gem::Requirement
@@ -136,16 +122,22 @@ dependencies:
136
122
  name: mocha
137
123
  requirement: !ruby/object:Gem::Requirement
138
124
  requirements:
139
- - - "~>"
125
+ - - ">="
140
126
  - !ruby/object:Gem::Version
141
- version: 2.1.0
127
+ version: '2.1'
128
+ - - "<"
129
+ - !ruby/object:Gem::Version
130
+ version: '4'
142
131
  type: :development
143
132
  prerelease: false
144
133
  version_requirements: !ruby/object:Gem::Requirement
145
134
  requirements:
146
- - - "~>"
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '2.1'
138
+ - - "<"
147
139
  - !ruby/object:Gem::Version
148
- version: 2.1.0
140
+ version: '4'
149
141
  - !ruby/object:Gem::Dependency
150
142
  name: ostruct
151
143
  requirement: !ruby/object:Gem::Requirement
@@ -202,6 +194,8 @@ files:
202
194
  - ".dockerignore"
203
195
  - ".github/FUNDING.yml"
204
196
  - ".github/config/rubocop_linter_action.yml"
197
+ - ".github/pull_request_template.md"
198
+ - ".github/workflows/changelog.yml"
205
199
  - ".github/workflows/ci-with-docker.yml"
206
200
  - ".github/workflows/ci.yml"
207
201
  - ".github/workflows/rubocop.yml"
@@ -209,13 +203,12 @@ files:
209
203
  - ".rubocop.yml"
210
204
  - ".rubocop_todo.yml"
211
205
  - CHANGES.txt
206
+ - CONTRIBUTING.md
212
207
  - DEVELOPMENT.md
213
208
  - Dockerfile
214
209
  - Dockerfile.openssl3
215
210
  - Dockerfile.zlib_ng
216
211
  - Gemfile
217
- - Gemfile.noed25519
218
- - Gemfile.norbnacl
219
212
  - ISSUE_TEMPLATE.md
220
213
  - LICENSE.txt
221
214
  - Manifest
@@ -224,6 +217,7 @@ files:
224
217
  - SECURITY.md
225
218
  - THANKS.txt
226
219
  - appveyor.yml
220
+ - changelog.d/README.md
227
221
  - docker-compose.yml
228
222
  - lib/net/ssh.rb
229
223
  - lib/net/ssh/authentication/agent.rb
@@ -240,6 +234,7 @@ files:
240
234
  - lib/net/ssh/authentication/methods/publickey.rb
241
235
  - lib/net/ssh/authentication/pageant.rb
242
236
  - lib/net/ssh/authentication/pub_key_fingerprint.rb
237
+ - lib/net/ssh/authentication/security_key.rb
243
238
  - lib/net/ssh/authentication/session.rb
244
239
  - lib/net/ssh/buffer.rb
245
240
  - lib/net/ssh/buffered_io.rb
@@ -254,6 +249,10 @@ files:
254
249
  - lib/net/ssh/key_factory.rb
255
250
  - lib/net/ssh/known_hosts.rb
256
251
  - lib/net/ssh/loggable.rb
252
+ - lib/net/ssh/multiplex.rb
253
+ - lib/net/ssh/multiplex/client.rb
254
+ - lib/net/ssh/multiplex/control_path.rb
255
+ - lib/net/ssh/multiplex/proxy_transport.rb
257
256
  - lib/net/ssh/packet.rb
258
257
  - lib/net/ssh/prompt.rb
259
258
  - lib/net/ssh/proxy/command.rb
@@ -290,6 +289,7 @@ files:
290
289
  - lib/net/ssh/transport/hmac/ripemd160.rb
291
290
  - lib/net/ssh/transport/hmac/sha1.rb
292
291
  - lib/net/ssh/transport/hmac/sha1_96.rb
292
+ - lib/net/ssh/transport/hmac/sha1_etm.rb
293
293
  - lib/net/ssh/transport/hmac/sha2_256.rb
294
294
  - lib/net/ssh/transport/hmac/sha2_256_96.rb
295
295
  - lib/net/ssh/transport/hmac/sha2_256_etm.rb
@@ -324,6 +324,7 @@ files:
324
324
  - lib/net/ssh/version.rb
325
325
  - net-ssh-public_cert.pem
326
326
  - net-ssh.gemspec
327
+ - rakelib/changelog.rb
327
328
  - support/ssh_tunnel_bug.rb
328
329
  homepage: https://github.com/net-ssh/net-ssh
329
330
  licenses:
@@ -338,7 +339,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
338
339
  requirements:
339
340
  - - ">="
340
341
  - !ruby/object:Gem::Version
341
- version: '2.6'
342
+ version: '2.7'
342
343
  required_rubygems_version: !ruby/object:Gem::Requirement
343
344
  requirements:
344
345
  - - ">="
metadata.gz.sig CHANGED
Binary file
data/Gemfile.noed25519 DELETED
@@ -1,12 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- ENV['NET_SSH_NO_ED25519'] = 'true'
4
- # Specify your gem's dependencies in mygem.gemspec
5
- gemspec
6
-
7
- if ENV["CI"] && !Gem.win_platform?
8
- gem 'simplecov', require: false, group: :test
9
- gem 'codecov', require: false, group: :test
10
- end
11
-
12
- gem 'webrick', group: %i[development test] if RUBY_VERSION.split(".")[0].to_i >= 3
data/Gemfile.norbnacl DELETED
@@ -1,12 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- ENV['NET_SSH_NO_RBNACL'] = 'true'
4
- # Specify your gem's dependencies in mygem.gemspec
5
- gemspec
6
-
7
- if ENV["CI"] && !Gem.win_platform?
8
- gem 'simplecov', require: false, group: :test
9
- gem 'codecov', require: false, group: :test
10
- end
11
-
12
- gem 'webrick', group: %i[development test] if RUBY_VERSION.split(".")[0].to_i >= 3