net-ssh 8.0.0.rc4 → 8.0.0.rc5
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/pull_request_template.md +1 -0
- data/.github/workflows/changelog.yml +43 -0
- data/CHANGES.txt +26 -0
- data/CONTRIBUTING.md +35 -0
- data/README.md +58 -50
- data/Rakefile +48 -0
- data/changelog.d/README.md +1 -0
- data/lib/net/ssh/authentication/agent.rb +4 -1
- data/lib/net/ssh/authentication/key_manager.rb +22 -12
- data/lib/net/ssh/authentication/security_key.rb +2 -1
- data/lib/net/ssh/buffer.rb +2 -1
- data/lib/net/ssh/config.rb +6 -0
- data/lib/net/ssh/connection/channel.rb +4 -1
- data/lib/net/ssh/connection/session.rb +7 -4
- data/lib/net/ssh/errors.rb +10 -0
- data/lib/net/ssh/known_hosts.rb +18 -8
- data/lib/net/ssh/multiplex/client.rb +162 -0
- data/lib/net/ssh/multiplex/control_path.rb +120 -0
- data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
- data/lib/net/ssh/multiplex.rb +130 -0
- data/lib/net/ssh/proxy/jump.rb +40 -2
- data/lib/net/ssh/test.rb +2 -1
- data/lib/net/ssh/transport/algorithms.rb +33 -7
- data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
- data/lib/net/ssh/transport/hmac.rb +2 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +8 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
- data/lib/net/ssh/transport/openssl.rb +2 -1
- data/lib/net/ssh/transport/session.rb +17 -44
- data/lib/net/ssh/verifiers/always.rb +0 -3
- data/lib/net/ssh/version.rb +1 -1
- data/lib/net/ssh.rb +125 -51
- data/net-ssh.gemspec +1 -1
- data/rakelib/changelog.rb +229 -0
- data.tar.gz.sig +0 -0
- metadata +22 -6
- metadata.gz.sig +0 -0
data/lib/net/ssh.rb
CHANGED
|
@@ -8,6 +8,7 @@ require 'net/ssh/loggable'
|
|
|
8
8
|
require 'net/ssh/transport/session'
|
|
9
9
|
require 'net/ssh/authentication/session'
|
|
10
10
|
require 'net/ssh/connection/session'
|
|
11
|
+
require 'net/ssh/multiplex'
|
|
11
12
|
require 'net/ssh/prompt'
|
|
12
13
|
|
|
13
14
|
module Net
|
|
@@ -62,7 +63,7 @@ module Net
|
|
|
62
63
|
auth_methods bind_address compression compression_level config
|
|
63
64
|
encryption forward_agent hmac host_key identity_agent remote_user
|
|
64
65
|
keepalive keepalive_interval keepalive_maxcount kex keys key_data
|
|
65
|
-
keycerts keycert_data languages logger
|
|
66
|
+
keycerts keycert_data languages logger password port proxy
|
|
66
67
|
rekey_blocks_limit rekey_limit rekey_packet_limit timeout verbose
|
|
67
68
|
known_hosts global_known_hosts_file user_known_hosts_file host_key_alias
|
|
68
69
|
host_name user properties passphrase keys_only max_pkt_size
|
|
@@ -70,6 +71,7 @@ module Net
|
|
|
70
71
|
append_all_supported_algorithms non_interactive password_prompt
|
|
71
72
|
agent_socket_factory minimum_dh_bits verify_host_key
|
|
72
73
|
fingerprint_hash check_host_ip pubkey_algorithms no_delay
|
|
74
|
+
control_master control_path
|
|
73
75
|
]
|
|
74
76
|
|
|
75
77
|
# The standard means of starting a new SSH connection. When used with a
|
|
@@ -98,7 +100,10 @@ module Net
|
|
|
98
100
|
# ssh.loop
|
|
99
101
|
# end
|
|
100
102
|
#
|
|
101
|
-
# This method accepts the following options (all are optional)
|
|
103
|
+
# This method accepts the following options (all are optional). Omit an
|
|
104
|
+
# option to use its default or ssh_config value; passing +nil+ (or an array
|
|
105
|
+
# containing +nil+) raises ArgumentError, except for :password,
|
|
106
|
+
# :passphrase, :logger and :password_prompt, where +nil+ means the default.
|
|
102
107
|
#
|
|
103
108
|
# * :auth_methods => an array of authentication methods to try
|
|
104
109
|
# * :bind_address => the IP address on the connecting machine to use in
|
|
@@ -109,6 +114,40 @@ module Net
|
|
|
109
114
|
# * :compression => the compression algorithm to use, or +true+ to use
|
|
110
115
|
# whatever is supported.
|
|
111
116
|
# * :compression_level => the compression level to use when sending data
|
|
117
|
+
# * :control_master => set to +:auto+ to run the session through an
|
|
118
|
+
# already running OpenSSH ControlMaster for the same host, port and user
|
|
119
|
+
# (started e.g. with <tt>ssh -M</tt> or <tt>ControlMaster auto</tt>),
|
|
120
|
+
# using its proxy mode (OpenSSH 7.4 or later). This skips the TCP
|
|
121
|
+
# connection, key exchange and authentication. If there is no usable
|
|
122
|
+
# master (no ControlPath, no socket, refused, timed out, owned by another
|
|
123
|
+
# user) Net::SSH logs why and connects as usual; it never starts a master
|
|
124
|
+
# itself. The master's own settings then apply instead of the options
|
|
125
|
+
# for setting up a connection (:keys, :key_data, :password, :passphrase,
|
|
126
|
+
# :auth_methods, :verify_host_key, :known_hosts, :host_key, :kex,
|
|
127
|
+
# :encryption, :hmac, :compression, :rekey_*, :proxy, :keepalive and the
|
|
128
|
+
# like); explicitly passed ones are named in a logger warning. Over the
|
|
129
|
+
# master, remote port forwarding (Service::Forward#remote and
|
|
130
|
+
# #cancel_remote) raises Net::SSH::NotSupportedOverControlMaster, as the
|
|
131
|
+
# master consumes the global request reply, so success, failure and the
|
|
132
|
+
# port allocated for port 0 can't be observed; other global requests
|
|
133
|
+
# raise it too. Agent and X11 forwarding requests raise it
|
|
134
|
+
# too, and :forward_agent can't be combined with it, because the master
|
|
135
|
+
# serves them from its own agent or display only if it has ForwardAgent
|
|
136
|
+
# or ForwardX11 enabled itself, and rejects them otherwise. Keepalive and
|
|
137
|
+
# rekeying are left to the master. ssh_config's ControlMaster is
|
|
138
|
+
# ignored, only this option enables it. An invalid ControlPath raises
|
|
139
|
+
# ArgumentError. A missing or stale socket is logged at info level, a
|
|
140
|
+
# socket that can't be used (another owner, refused, wrong protocol,
|
|
141
|
+
# timed out) at warn level. Has no effect on Windows. Defaults to
|
|
142
|
+
# +false+.
|
|
143
|
+
# * :control_path => the ControlMaster socket to use with
|
|
144
|
+
# :control_master, with the same ~, ${ENV} and % tokens as ssh_config's
|
|
145
|
+
# ControlPath (%C, %d, %h, %i, %j, %k, %L, %l, %n, %p, %r, %u, %%).
|
|
146
|
+
# The master at that path decides which host and user the session is
|
|
147
|
+
# connected to, so a fixed path without %h, %p, %r or %C sends every
|
|
148
|
+
# host through the same master and the explicitly passed host key and
|
|
149
|
+
# authentication options are ignored. Defaults to ControlPath from
|
|
150
|
+
# ssh_config; +"none"+ disables the master.
|
|
112
151
|
# * :config => set to +true+ to load the default OpenSSH config files
|
|
113
152
|
# (~/.ssh/config, /etc/ssh_config), or to +false+ to not load them, or to
|
|
114
153
|
# a file-name (or array of file-names) to load those specific configuration
|
|
@@ -137,7 +176,11 @@ module Net
|
|
|
137
176
|
# Defaults to +300+ seconds.
|
|
138
177
|
# * :keepalive_maxcount => the maximun number of keepalive packet miss allowed.
|
|
139
178
|
# Defaults to 3
|
|
140
|
-
# * :kex => the key exchange algorithm (or algorithms) to use
|
|
179
|
+
# * :kex => the key exchange algorithm (or algorithms) to use. Like
|
|
180
|
+
# KexAlgorithms, entries starting with +, - or ^ modify the default list.
|
|
181
|
+
# diffie-hellman-group14-sha1 is supported but not offered unless listed
|
|
182
|
+
# here (e.g. '+diffie-hellman-group14-sha1') or
|
|
183
|
+
# :append_all_supported_algorithms is set.
|
|
141
184
|
# * :keys => an array of file names of private keys to use for publickey
|
|
142
185
|
# and hostbased authentication
|
|
143
186
|
# * :keycerts => an array of file names of key certificates to use
|
|
@@ -155,7 +198,14 @@ module Net
|
|
|
155
198
|
# packet. Default is 0x8000 (32768 bytes). Increase to 0x10000 (65536 bytes)
|
|
156
199
|
# for better performance if your SSH server supports it (most do).
|
|
157
200
|
# * :max_win_size => maximum size we tell the other side that is supported for
|
|
158
|
-
# the window.
|
|
201
|
+
# the window, i.e. how much the server may send on a channel before net-ssh
|
|
202
|
+
# reads it. Default is 0x200000 (2 MB), as in OpenSSH. Smaller values limit
|
|
203
|
+
# how much data is in flight; some servers (e.g. Mikrotik) truncate output
|
|
204
|
+
# when the window is small.
|
|
205
|
+
# * :minimum_dh_bits => the smallest group, in bits, accepted from the server
|
|
206
|
+
# in diffie-hellman-group-exchange key exchange. Defaults to 2048, as in
|
|
207
|
+
# OpenSSH; lower it (e.g. to 1024) only to reach legacy servers that offer
|
|
208
|
+
# nothing larger.
|
|
159
209
|
# * :no_delay => turns Nagle's algorithm off on the connection's socket
|
|
160
210
|
# (TCP_NODELAY), so a packet written while the previous one is still
|
|
161
211
|
# unacknowledged is sent right away instead of waiting for the server's
|
|
@@ -166,7 +216,6 @@ module Net
|
|
|
166
216
|
# authentication failure vs password prompt. Non-interactive applications
|
|
167
217
|
# should set it to true to prefer failing a password/etc auth methods vs.
|
|
168
218
|
# asking for password.
|
|
169
|
-
# * :paranoid => deprecated alias for :verify_host_key
|
|
170
219
|
# * :passphrase => the passphrase to use when loading a private key (default
|
|
171
220
|
# is +nil+, for no passphrase)
|
|
172
221
|
# * :password => the password to use to login
|
|
@@ -192,7 +241,8 @@ module Net
|
|
|
192
241
|
# * :user => the user name to log in as; this overrides the +user+
|
|
193
242
|
# parameter, and is primarily only useful when provided via an SSH
|
|
194
243
|
# configuration file.
|
|
195
|
-
# * :remote_user => used for substitution into the '%r' part of a ProxyCommand
|
|
244
|
+
# * :remote_user => used for substitution into the '%r' part of a ProxyCommand;
|
|
245
|
+
# defaults to the user name the connection logs in as.
|
|
196
246
|
# * :user_known_hosts_file => the location of the user known hosts file.
|
|
197
247
|
# Set to an array to specify multiple user known hosts files.
|
|
198
248
|
# Defaults to %w(~/.ssh/known_hosts ~/.ssh/known_hosts2).
|
|
@@ -204,7 +254,9 @@ module Net
|
|
|
204
254
|
# default. The symbols :debug, :info, :warn, :error, and :fatal are also
|
|
205
255
|
# supported and are translated to the corresponding Logger constant.
|
|
206
256
|
# * :append_all_supported_algorithms => set to +true+ to append all supported
|
|
207
|
-
# algorithms by net-ssh
|
|
257
|
+
# algorithms by net-ssh, except the +none+ cipher and MAC, which must be
|
|
258
|
+
# named explicitly (e.g. encryption: '+none'). Was the default behaviour
|
|
259
|
+
# until 2.10
|
|
208
260
|
# * :number_of_password_prompts => Number of prompts for the password
|
|
209
261
|
# authentication method defaults to 3 set to 0 to disable prompt for
|
|
210
262
|
# password auth method
|
|
@@ -219,21 +271,32 @@ module Net
|
|
|
219
271
|
# * :accept_new_or_local_tunnel (insecure) ::Net::SSH::Verifiers::AcceptNewOrLocalTunnel
|
|
220
272
|
# * :accept_new (insecure) ::Net::SSH::Verifiers::AcceptNew
|
|
221
273
|
# * :always (secure) ::Net::SSH::Verifiers::Always
|
|
222
|
-
# You can also provide an own Object which responds to +verify
|
|
223
|
-
# given to +verify+ is a hash consisting
|
|
224
|
-
# the +:fingerprint+ and the +:session+.
|
|
225
|
-
# returning false declines it and
|
|
274
|
+
# You can also provide an own Object which responds to +verify+ and
|
|
275
|
+
# +verify_signature+. The argument given to +verify+ is a hash consisting
|
|
276
|
+
# of the +:key+, the +:key_blob+, the +:fingerprint+ and the +:session+.
|
|
277
|
+
# Returning true accepts the host key, returning false declines it and
|
|
278
|
+
# closes the connection. +verify_signature+ is given a block that checks
|
|
279
|
+
# the server's host key signature and must return the block's result.
|
|
226
280
|
# * :fingerprint_hash => 'MD5' or 'SHA256', defaults to 'SHA256'
|
|
227
281
|
# If +user+ parameter is nil it defaults to USER from ssh_config, or
|
|
228
282
|
# local username
|
|
229
283
|
def self.start(host, user = nil, options = {}, &block)
|
|
284
|
+
if options.key?(:paranoid)
|
|
285
|
+
raise ArgumentError, ":paranoid has been removed, use :verify_host_key (same values)"
|
|
286
|
+
end
|
|
287
|
+
|
|
230
288
|
invalid_options = options.keys - VALID_OPTIONS
|
|
231
289
|
if invalid_options.any?
|
|
232
290
|
raise ArgumentError, "invalid option(s): #{invalid_options.join(', ')}"
|
|
233
291
|
end
|
|
234
292
|
|
|
293
|
+
reject_nil_options(options)
|
|
294
|
+
Multiplex.validate_options!(options)
|
|
295
|
+
explicit_options = options.keys
|
|
296
|
+
host_arg = host
|
|
297
|
+
|
|
298
|
+
options = options.dup
|
|
235
299
|
assign_defaults(options)
|
|
236
|
-
_sanitize_options(options)
|
|
237
300
|
|
|
238
301
|
options[:user] = user if user
|
|
239
302
|
options = configuration_for(host, options.fetch(:config, true)).merge(options)
|
|
@@ -245,8 +308,6 @@ module Net
|
|
|
245
308
|
options[:number_of_password_prompts] = 0
|
|
246
309
|
end
|
|
247
310
|
|
|
248
|
-
_support_deprecated_option_paranoid(options)
|
|
249
|
-
|
|
250
311
|
if options[:verbose]
|
|
251
312
|
options[:logger].level = case options[:verbose]
|
|
252
313
|
when Integer then options[:verbose]
|
|
@@ -259,26 +320,35 @@ module Net
|
|
|
259
320
|
end
|
|
260
321
|
end
|
|
261
322
|
|
|
262
|
-
transport = Transport::Session.new(host, options)
|
|
263
|
-
auth = Authentication::Session.new(transport, options)
|
|
264
|
-
|
|
265
323
|
user = options.fetch(:user, user) || Etc.getpwuid.name
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
324
|
+
options[:remote_user] ||= user
|
|
325
|
+
|
|
326
|
+
connection = Multiplex.connect(host_arg, host, user, options, explicit: explicit_options) if options[:control_master]
|
|
327
|
+
connection ||= connect_directly(host, user, options)
|
|
328
|
+
|
|
329
|
+
if block_given?
|
|
330
|
+
begin
|
|
331
|
+
yield connection
|
|
332
|
+
ensure
|
|
333
|
+
connection.close unless connection.closed?
|
|
276
334
|
end
|
|
277
335
|
else
|
|
336
|
+
connection
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def self.connect_directly(host, user, options)
|
|
341
|
+
transport = Transport::Session.new(host, options)
|
|
342
|
+
auth = Authentication::Session.new(transport, options)
|
|
343
|
+
|
|
344
|
+
unless auth.authenticate("ssh-connection", user, options[:password])
|
|
278
345
|
transport.close
|
|
279
346
|
raise AuthenticationFailed, "Authentication failed for user #{user}@#{host}"
|
|
280
347
|
end
|
|
348
|
+
|
|
349
|
+
Connection::Session.new(transport, options)
|
|
281
350
|
end
|
|
351
|
+
private_class_method :connect_directly
|
|
282
352
|
|
|
283
353
|
# Returns a hash of the configuration options for the given host, as read
|
|
284
354
|
# from the SSH configuration file(s). If +use_ssh_config+ is true (the
|
|
@@ -312,31 +382,35 @@ module Net
|
|
|
312
382
|
end
|
|
313
383
|
end
|
|
314
384
|
|
|
315
|
-
|
|
316
|
-
invalid_option_values = [nil, [nil]]
|
|
317
|
-
unless (options.values & invalid_option_values).empty?
|
|
318
|
-
nil_options = options.select { |_k, v| invalid_option_values.include?(v) }.map(&:first)
|
|
319
|
-
Kernel.warn "#{caller_locations(2, 1)[0]}: Passing nil, or [nil] to Net::SSH.start is deprecated for keys: #{nil_options.join(', ')}"
|
|
320
|
-
end
|
|
321
|
-
end
|
|
322
|
-
private_class_method :_sanitize_options
|
|
385
|
+
NIL_ALLOWED_OPTIONS = %i[password passphrase logger password_prompt].freeze
|
|
323
386
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
387
|
+
NIL_OPTION_HINTS = {
|
|
388
|
+
config: "config: false to skip ssh_config files",
|
|
389
|
+
proxy: "proxy: false to connect without a proxy",
|
|
390
|
+
keys: "keys: [] to use no key files",
|
|
391
|
+
use_agent: "use_agent: false to not use ssh-agent",
|
|
392
|
+
forward_agent: "forward_agent: false to not forward ssh-agent",
|
|
393
|
+
keepalive: "keepalive: false to send no keepalives",
|
|
394
|
+
compression: "compression: false to disable compression",
|
|
395
|
+
control_master: "control_master: false to not use a ControlMaster",
|
|
396
|
+
control_path: "control_path: 'none' to not use a ControlMaster socket",
|
|
397
|
+
send_env: "send_env: [] to send no environment variables",
|
|
398
|
+
set_env: "set_env: {} to set no environment variables"
|
|
399
|
+
}.freeze
|
|
400
|
+
private_constant :NIL_ALLOWED_OPTIONS, :NIL_OPTION_HINTS
|
|
401
|
+
|
|
402
|
+
def self.reject_nil_options(options)
|
|
403
|
+
nil_keys = options.select do |key, value|
|
|
404
|
+
!NIL_ALLOWED_OPTIONS.include?(key) && (value.nil? || (value.is_a?(Array) && value.include?(nil)))
|
|
405
|
+
end.keys
|
|
406
|
+
return if nil_keys.empty?
|
|
407
|
+
|
|
408
|
+
message = "nil is not allowed for #{nil_keys.map(&:inspect).join(', ')}; " \
|
|
409
|
+
"omit #{nil_keys.one? ? 'it' : 'them'} to use the default or ssh_config value"
|
|
410
|
+
hints = NIL_OPTION_HINTS.values_at(*nil_keys).compact
|
|
411
|
+
message += ", or pass #{hints.join(', or ')}" if hints.any?
|
|
412
|
+
raise ArgumentError, message
|
|
339
413
|
end
|
|
340
|
-
private_class_method :
|
|
414
|
+
private_class_method :reject_nil_options
|
|
341
415
|
end
|
|
342
416
|
end
|
data/net-ssh.gemspec
CHANGED
|
@@ -38,7 +38,7 @@ Gem::Specification.new do |spec|
|
|
|
38
38
|
spec.add_development_dependency "base64"
|
|
39
39
|
spec.add_development_dependency "bundler", ">= 1.17"
|
|
40
40
|
spec.add_development_dependency "minitest", "~> 5.19"
|
|
41
|
-
spec.add_development_dependency "mocha", "
|
|
41
|
+
spec.add_development_dependency "mocha", ">= 2.1", "< 4"
|
|
42
42
|
spec.add_development_dependency "ostruct"
|
|
43
43
|
spec.add_development_dependency "rake", "~> 12.0"
|
|
44
44
|
spec.add_development_dependency "rubocop", "~> 1.28.0"
|
|
@@ -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: 8.0.0.
|
|
4
|
+
version: 8.0.0.rc5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jamis Buck
|
|
@@ -32,7 +32,7 @@ cert_chain:
|
|
|
32
32
|
ufHf4AX2UIkJbh7zCPkiNCqIr7MSWLNFG/9lOlHYsEJM8XujT1ofPobYx6YSFx/C
|
|
33
33
|
7HBrI7UX7awt6UvBZebhcHzyMHxg/B5PVQllPA==
|
|
34
34
|
-----END CERTIFICATE-----
|
|
35
|
-
date: 2026-10-
|
|
35
|
+
date: 2026-10-20 00:00:00.000000000 Z
|
|
36
36
|
dependencies:
|
|
37
37
|
- !ruby/object:Gem::Dependency
|
|
38
38
|
name: logger
|
|
@@ -122,16 +122,22 @@ dependencies:
|
|
|
122
122
|
name: mocha
|
|
123
123
|
requirement: !ruby/object:Gem::Requirement
|
|
124
124
|
requirements:
|
|
125
|
-
- - "
|
|
125
|
+
- - ">="
|
|
126
|
+
- !ruby/object:Gem::Version
|
|
127
|
+
version: '2.1'
|
|
128
|
+
- - "<"
|
|
126
129
|
- !ruby/object:Gem::Version
|
|
127
|
-
version:
|
|
130
|
+
version: '4'
|
|
128
131
|
type: :development
|
|
129
132
|
prerelease: false
|
|
130
133
|
version_requirements: !ruby/object:Gem::Requirement
|
|
131
134
|
requirements:
|
|
132
|
-
- - "
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '2.1'
|
|
138
|
+
- - "<"
|
|
133
139
|
- !ruby/object:Gem::Version
|
|
134
|
-
version:
|
|
140
|
+
version: '4'
|
|
135
141
|
- !ruby/object:Gem::Dependency
|
|
136
142
|
name: ostruct
|
|
137
143
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -188,6 +194,8 @@ files:
|
|
|
188
194
|
- ".dockerignore"
|
|
189
195
|
- ".github/FUNDING.yml"
|
|
190
196
|
- ".github/config/rubocop_linter_action.yml"
|
|
197
|
+
- ".github/pull_request_template.md"
|
|
198
|
+
- ".github/workflows/changelog.yml"
|
|
191
199
|
- ".github/workflows/ci-with-docker.yml"
|
|
192
200
|
- ".github/workflows/ci.yml"
|
|
193
201
|
- ".github/workflows/rubocop.yml"
|
|
@@ -195,6 +203,7 @@ files:
|
|
|
195
203
|
- ".rubocop.yml"
|
|
196
204
|
- ".rubocop_todo.yml"
|
|
197
205
|
- CHANGES.txt
|
|
206
|
+
- CONTRIBUTING.md
|
|
198
207
|
- DEVELOPMENT.md
|
|
199
208
|
- Dockerfile
|
|
200
209
|
- Dockerfile.openssl3
|
|
@@ -208,6 +217,7 @@ files:
|
|
|
208
217
|
- SECURITY.md
|
|
209
218
|
- THANKS.txt
|
|
210
219
|
- appveyor.yml
|
|
220
|
+
- changelog.d/README.md
|
|
211
221
|
- docker-compose.yml
|
|
212
222
|
- lib/net/ssh.rb
|
|
213
223
|
- lib/net/ssh/authentication/agent.rb
|
|
@@ -239,6 +249,10 @@ files:
|
|
|
239
249
|
- lib/net/ssh/key_factory.rb
|
|
240
250
|
- lib/net/ssh/known_hosts.rb
|
|
241
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
|
|
242
256
|
- lib/net/ssh/packet.rb
|
|
243
257
|
- lib/net/ssh/prompt.rb
|
|
244
258
|
- lib/net/ssh/proxy/command.rb
|
|
@@ -275,6 +289,7 @@ files:
|
|
|
275
289
|
- lib/net/ssh/transport/hmac/ripemd160.rb
|
|
276
290
|
- lib/net/ssh/transport/hmac/sha1.rb
|
|
277
291
|
- lib/net/ssh/transport/hmac/sha1_96.rb
|
|
292
|
+
- lib/net/ssh/transport/hmac/sha1_etm.rb
|
|
278
293
|
- lib/net/ssh/transport/hmac/sha2_256.rb
|
|
279
294
|
- lib/net/ssh/transport/hmac/sha2_256_96.rb
|
|
280
295
|
- lib/net/ssh/transport/hmac/sha2_256_etm.rb
|
|
@@ -309,6 +324,7 @@ files:
|
|
|
309
324
|
- lib/net/ssh/version.rb
|
|
310
325
|
- net-ssh-public_cert.pem
|
|
311
326
|
- net-ssh.gemspec
|
|
327
|
+
- rakelib/changelog.rb
|
|
312
328
|
- support/ssh_tunnel_bug.rb
|
|
313
329
|
homepage: https://github.com/net-ssh/net-ssh
|
|
314
330
|
licenses:
|
metadata.gz.sig
CHANGED
|
Binary file
|