net-ssh 8.0.1 → 8.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b548f463639f7a9a85eca15cc065ab99d7b3c803330733198f311eb46cbc3c13
4
- data.tar.gz: 56dd051e3ff6346e4f5684c571d1136978314762b2eb2a248af00666c4ab136f
3
+ metadata.gz: db558d490e7fb5d8ca959a00ddd5a07d6c24bdc3957a8dcbb067848466928072
4
+ data.tar.gz: 91a1983ea97a6c94cd542c8dd55cbc0c46f99f2926d7fd168f027a90a404b693
5
5
  SHA512:
6
- metadata.gz: f2d469ea5e169da065852011e64ecc4c30168baac60f4b241014ec1b7c1d76512e95163f726858f94cbaf722578fd0acddacfd446f31d4dfb65faafd62c97f1d
7
- data.tar.gz: 3582df1af6a3686ff510d86268730c6eecb5104ddcc239cfe31c324940b3a57d6522fe74c1b8cde2e42385167ee57958c669284032d39eda947ddfa1b8fc6c43
6
+ metadata.gz: e154b5ec3ae2c1ac272e638ba3e7e51715f738ccb926a7dc2cf73ffea3819a6a39a1f08cd5b92ff6b403bd43383c400ff2b6817000f566240baedb2384419c70
7
+ data.tar.gz: 064a69e61d1bc28b22116fd6c26efd3339404527aa6e3a796ddca85b8eaee4aeb8d9b76c2a80f3bad0c1289436f35aaad403cb55dee2d74cd3d104764a0af0bb
checksums.yaml.gz.sig CHANGED
Binary file
@@ -9,35 +9,10 @@ permissions:
9
9
 
10
10
  jobs:
11
11
  changelog:
12
- if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' }}
13
12
  runs-on: ubuntu-latest
14
13
  steps:
15
14
  - uses: actions/checkout@v4
16
15
  with:
17
16
  # HEAD is the PR merged into the base branch; HEAD^1 is the base it was merged into.
18
17
  fetch-depth: 2
19
- - name: Set up Ruby 3.1
20
- uses: ruby/setup-ruby@v1
21
- with:
22
- ruby-version: 3.1
23
- - name: Validate fragments
24
- run: rake changelog:check
25
- - name: Require a fragment
26
- if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-changelog') }}
27
- env:
28
- PR: ${{ github.event.number }}
29
- run: |
30
- changed=$(git diff --name-only HEAD^1 HEAD)
31
- if echo "$changed" | grep -x 'CHANGES\.txt' > /dev/null; then
32
- echo "::error::Don't edit CHANGES.txt, add a changelog.d/$PR.<type>.txt fragment instead, see CONTRIBUTING.md#changelog"
33
- exit 1
34
- fi
35
- own=$(git diff --name-only --diff-filter=AMR HEAD^1 HEAD -- changelog.d/ | grep -E "^changelog\.d/$PR\.[a-z-]+(\.[0-9]+)?\.txt$" || true)
36
- if [ -n "$own" ]; then
37
- echo "Changelog fragments:"; echo "$own"
38
- elif ! echo "$changed" | grep -v '^changelog\.d/' > /dev/null; then
39
- echo "Only changelog.d/ changed, no fragment needed"
40
- else
41
- echo "::error::Add a changelog fragment named changelog.d/$PR.<type>.txt (use $PR.none.txt if nothing user-visible changed), see CONTRIBUTING.md#changelog"
42
- exit 1
43
- fi
18
+ - uses: net-ssh/changelogd@v1
data/.rubocop.yml CHANGED
@@ -20,3 +20,7 @@ Layout/LineLength:
20
20
 
21
21
  Style/EmptyLiteral:
22
22
  Enabled: false
23
+
24
+ # CI runs the suite with RUBYOPT=--enable=frozen-string-literal instead of a magic comment in every file.
25
+ Style/FrozenStringLiteralComment:
26
+ Enabled: false
data/.rubocop_todo.yml CHANGED
@@ -587,13 +587,6 @@ Style/FormatString:
587
587
  - 'lib/net/ssh/authentication/pageant.rb'
588
588
  - 'lib/net/ssh/loggable.rb'
589
589
 
590
- # Offense count: 174
591
- # This cop supports safe auto-correction (--auto-correct).
592
- # Configuration parameters: EnforcedStyle.
593
- # SupportedStyles: always, always_true, never
594
- Style/FrozenStringLiteralComment:
595
- Enabled: false
596
-
597
590
  # Offense count: 1
598
591
  # This cop supports safe auto-correction (--auto-correct).
599
592
  Style/GlobalStdStream:
data/CHANGES.txt CHANGED
@@ -1,6 +1,14 @@
1
+ === 8.0.3
2
+
3
+ * Add `preopen_channels` option: keep a spare session channel open so each command saves a round trip [#1071]
4
+
5
+ === 8.0.2
6
+
7
+ * Fixed `Session#close` raising `EOFError: cannot send data if channel has declared eof` when a forwarded connection still had unread data from its local client; data net-ssh had already read is still sent before the EOF and, as in OpenSSH, the rest is dropped; a regression in 8.0.0 [#1066, #1067]
8
+
1
9
  === 8.0.1
2
10
 
3
- * Fixed `unexpected packet type 94 during initial key exchange` (or 93) when channel data arrives during a client-initiated re-key, which broke bulk transfers at the first re-key (e.g. with `rekey_limit`, or after about 1 GB); a regression in 8.0.0 [#1063]
11
+ * Fixed `unexpected packet type 94 during initial key exchange` (or 93) when channel data arrives during a client-initiated re-key, which broke bulk transfers at the first re-key (e.g. with `rekey_limit`, or after about 1 GB); a regression in 7.3.5 and 8.0.0 [#1063]
4
12
  * `control_master: :auto` now ignores `forward_agent` (logged at info level) instead of raising ArgumentError, and logs the ignored connection options at info instead of warn level, so it works with sshkit (Kamal, Capistrano), which always passes `forward_agent: true`, `keys`, `known_hosts` and `keepalive` [#1062]
5
13
 
6
14
  === 8.0.0
data/CONTRIBUTING.md CHANGED
@@ -6,7 +6,7 @@ Run the unit tests with `bundle exec rake test` and the linter with `bundle exec
6
6
 
7
7
  Don't edit `CHANGES.txt`. Instead, add a fragment file to `changelog.d/` in your PR; the release's version bump (`rake vbump:*`) moves all fragments into a new `=== <version>` section at the top of `CHANGES.txt` and deletes them.
8
8
 
9
- A fragment is named `<number>.<type>.txt`, where `<number>` is the PR number (so open the PR first, then add the fragment), and contains a single UTF-8 paragraph: the entry as it should read in `CHANGES.txt`, without the bullet or prefix. Lines are joined, so it can be wrapped. If it doesn't end with a reference list such as `[#985, #1056]`, `[GHSA-xxxx-xxxx-xxxx]` or `[CVE-2024-12345, #1060]`, ` [#<number>]` is appended; write the list yourself to reference issues or advisories too.
9
+ A fragment is named `<number>.<type>.txt`, where `<number>` is the PR number (so open the PR first, then add the fragment), and contains a single UTF-8 paragraph: the entry as it should read in `CHANGES.txt`, without the bullet or prefix. Lines are joined, so it can be wrapped. If it doesn't end with a reference list such as `[#985, #1056]` or `[GHSA-xxxx-xxxx-xxxx]`, ` [#<number>]` is appended.
10
10
 
11
11
  | Type | Written as | Use for |
12
12
  | ------------------- | ------------------------------ | ------------------------------------------------------------ |
@@ -24,12 +24,11 @@ rake "changelog:new[1058,change,Session#exec! now returns the exit status]"
24
24
 
25
25
  echo 'Removed the `:paranoid` option; use `:verify_host_key` [#1050]' > changelog.d/1050.breaking.txt
26
26
  echo 'Reject packet lengths above 256 KB, as OpenSSH does [GHSA-pfqh-p9vh-wx4p]' > changelog.d/1060.security.txt
27
- echo 'Custom verifiers must implement `verify_signature` [#1050]' > changelog.d/1050.breaking-security.txt
28
27
  touch changelog.d/1061.none.txt
29
28
  ```
30
29
 
31
30
  For several entries from one PR add a suffix: `1050.breaking.txt`, `1050.breaking.2.txt` (`changelog:new` picks the next free one).
32
31
 
33
- In a release section, entries are grouped as `BREAKING: Security:`, `BREAKING:`, `Security:`, then the rest; within a group fragments are ordered by number, newest first, followed by any hand-written entries from an `=== Unreleased` section in their written order. A final release with nothing to report still gets a `=== <version>` section saying `No changes since <previous version>.`
32
+ `rake changelog:check` validates the fragments and `rake changelog:preview` prints the section the next release would add. CI fails a PR that edits `CHANGES.txt` or doesn't add or modify a `changelog.d/<PR number>.<type>.txt` fragment, unless it only touches `changelog.d/` or a maintainer adds the `skip-changelog` label; Dependabot PRs are skipped.
34
33
 
35
- `rake changelog:check` validates the fragments and the `=== Unreleased` section, and `rake changelog:preview` prints the section the next release would add. CI fails a PR that edits `CHANGES.txt` or doesn't add or modify a `changelog.d/<PR number>.<type>.txt` fragment, unless it only touches `changelog.d/` or a maintainer adds the `skip-changelog` label; Dependabot PRs are skipped.
34
+ The full rules, including how a release orders the entries, are in the [changelogd README](https://github.com/net-ssh/changelogd#readme).
data/Gemfile CHANGED
@@ -3,6 +3,8 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in mygem.gemspec
4
4
  gemspec
5
5
 
6
+ gem "changelogd", git: "https://github.com/net-ssh/changelogd.git", tag: "v1", group: :development
7
+
6
8
  gem 'byebug', group: %i[development test] if !Gem.win_platform? && RUBY_ENGINE == "ruby"
7
9
 
8
10
  if ENV["CI"]
data/Rakefile CHANGED
@@ -28,9 +28,18 @@ CLEAN.include ['pkg', 'rdoc']
28
28
  name = "net-ssh"
29
29
 
30
30
  require_relative "lib/net/ssh/version"
31
- require_relative "rakelib/changelog"
32
31
  version = Net::SSH::Version::CURRENT
33
32
 
33
+ begin
34
+ require "changelogd/rake"
35
+ rescue LoadError
36
+ # A git gem isn't on the load path under plain `rake`, as the release workflow runs it; bundler/setup could clash with the running rake.
37
+ require "bundler"
38
+ $LOAD_PATH.unshift(*Bundler.definition.specs["changelogd"].flat_map(&:full_require_paths))
39
+ require "changelogd/rake"
40
+ end
41
+ Changelogd::RakeTasks.new(version_file: "lib/net/ssh/version.rb")
42
+
34
43
  extra_files = %w[LICENSE.txt THANKS.txt CHANGES.txt]
35
44
  if defined?(RDoc::Task)
36
45
  RDoc::Task.new do |rdoc|
@@ -63,6 +72,7 @@ namespace :cert do
63
72
  end
64
73
 
65
74
  def change_version(&block)
75
+ Rake::Task["changelog:check"].invoke
66
76
  version_file = 'lib/net/ssh/version.rb'
67
77
  require_relative version_file
68
78
  pre = Net::SSH::Version::PRE
@@ -110,17 +120,9 @@ def change_version(&block)
110
120
  raise ArgumentError, "Cound not find line: TINY = \"#{tiny}\" in #{version_file}" unless found[:tiny] || new_tiny == tiny
111
121
  end
112
122
 
113
- begin
114
- heading = Changelog.heading(Net::SSH::Version::MAJOR, Net::SSH::Version::MINOR, new_tiny, new_pre)
115
- section = Changelog.new(__dir__).release(heading)
116
- rescue StandardError
117
- FileUtils.rm_f "#{version_file}.new"
118
- raise
119
- end
120
- puts section ? "Added to CHANGES.txt:\n#{section}" : "Nothing to release for a pre-release, CHANGES.txt unchanged"
121
-
122
123
  FileUtils.mv version_file, "#{version_file}.old"
123
124
  FileUtils.mv "#{version_file}.new", version_file
125
+ Rake::Task["changelog:release"].invoke
124
126
  end
125
127
 
126
128
  namespace :vbump do
@@ -158,44 +160,6 @@ namespace :vbump do
158
160
  end
159
161
  end
160
162
 
161
- namespace :changelog do
162
- changelog = Changelog.new(__dir__)
163
-
164
- desc "Validate the fragments in changelog.d/"
165
- task :check do
166
- count = changelog.check.size
167
- puts "#{count} valid changelog fragment#{'s' unless count == 1}"
168
- rescue Changelog::Error => e
169
- abort e.message
170
- end
171
-
172
- desc "Print the CHANGES.txt section the next version bump would write"
173
- task :preview, [:heading] do |_t, args|
174
- puts changelog.section(args[:heading] || "Unreleased") || "Nothing to release"
175
- rescue Changelog::Error => e
176
- abort e.message
177
- end
178
-
179
- desc "Create a fragment: rake \"changelog:new[1234,change,Text of the entry]\""
180
- task :new, [:number, :type] do |_t, args|
181
- abort "Usage: rake \"changelog:new[<PR number>,<#{Changelog::TYPES.keys.join('|')}>,<text>]\"" unless args[:number] && args[:type]
182
- # Rake splits arguments on commas and strips the spaces around them, so take the text from the raw task string.
183
- raw = Rake.application.top_level_tasks.find { |t| t.start_with?("changelog:new[") }
184
- text = raw&.[](/\A[^\[]*\[[^,]*,[^,]*,\s*(.*)\]\z/m, 1) || args.extras.join(", ")
185
- puts "Created #{changelog.new_fragment(args[:number], args[:type], text)}"
186
- rescue Changelog::Error => e
187
- abort e.message
188
- end
189
-
190
- desc "Write the fragments to CHANGES.txt under a heading, e.g. rake \"changelog:release[7.3.4]\" (vbump does this)"
191
- task :release, [:heading] do |_t, args|
192
- abort "Usage: rake \"changelog:release[<version heading>]\"" unless args[:heading]
193
- puts changelog.release(args[:heading]) || "Nothing to release"
194
- rescue Changelog::Error => e
195
- abort e.message
196
- end
197
- end
198
-
199
163
  namespace :rdoc do
200
164
  desc "Update gh-pages branch"
201
165
  task :publish do
@@ -53,6 +53,10 @@ module Net
53
53
  # The list of callbacks for pending requests. See #send_global_request.
54
54
  attr_reader :pending_requests # :nodoc:
55
55
 
56
+ # A "session" channel opened ahead of time by :preopen_channels, and
57
+ # the server's confirmation for it once that has arrived.
58
+ SpareChannel = Struct.new(:local_id, :remote_id, :window_size, :packet_size) # :nodoc:
59
+
56
60
  class NilChannel
57
61
  def initialize(session)
58
62
  @session = session
@@ -84,6 +88,11 @@ module Net
84
88
  @max_pkt_size = (options.key?(:max_pkt_size) ? options[:max_pkt_size] : 0x8000)
85
89
  @max_win_size = (options.key?(:max_win_size) ? options[:max_win_size] : Channel::DEFAULT_MAX_WIN_SIZE)
86
90
 
91
+ @preopen_channels = options.fetch(:preopen_channels, false)
92
+ @spare_channel = nil
93
+ @spare_wanted = false
94
+ @preopened = []
95
+
87
96
  @keepalive = Keepalive.new(self)
88
97
 
89
98
  @event_loop = options[:event_loop] || SingleSessionEventLoop.new
@@ -121,6 +130,7 @@ module Net
121
130
  # successfully closed, and then closes the underlying transport layer
122
131
  # connection.
123
132
  def close
133
+ stop_preopening
124
134
  info { "closing remaining channels (#{channels.length} open)" }
125
135
 
126
136
  channels.each do |id, channel|
@@ -143,6 +153,7 @@ module Net
143
153
  # when the connection needs to close but you don't know the status of the
144
154
  # underlying protocol's state).
145
155
  def shutdown!
156
+ stop_preopening
146
157
  transport.shutdown!
147
158
  end
148
159
 
@@ -258,8 +269,10 @@ module Net
258
269
  # Called by event loop to process available data before going to
259
270
  # event multiplexing
260
271
  def ev_preprocess(&block)
272
+ confirm_preopened_channels
261
273
  dispatch_incoming_packets(raise_disconnect_errors: false)
262
274
  each_channel { |id, channel| channel.process unless channel.local_closed? }
275
+ open_spare_channel
263
276
  end
264
277
 
265
278
  # Returns the file descriptors the event loop should wait for read/write events,
@@ -348,14 +361,20 @@ module Net
348
361
  # end
349
362
  #
350
363
  # channel.wait
364
+ #
365
+ # With the :preopen_channels option a plain "session" channel reuses
366
+ # the spare opened ahead of time, if there is one. Its callback still
367
+ # runs from the event loop, as if the server had just confirmed it.
351
368
  def open_channel(type = "session", *extra, &on_confirm)
352
- local_id = get_next_channel_id
369
+ spare = take_spare_channel if type == "session" && extra.empty?
370
+ local_id = spare ? spare.local_id : get_next_channel_id
353
371
 
354
372
  channel = Channel.new(self, type, local_id, @max_pkt_size, @max_win_size, &on_confirm)
355
- msg = Buffer.from(:byte, CHANNEL_OPEN, :string, type, :long, local_id,
356
- :long, channel.local_maximum_window_size,
357
- :long, channel.local_maximum_packet_size, *extra)
358
- send_message(msg)
373
+ if spare
374
+ @preopened << [channel, spare] if spare.remote_id
375
+ else
376
+ send_channel_open(type, channel.local_id, channel.local_maximum_window_size, channel.local_maximum_packet_size, *extra)
377
+ end
359
378
 
360
379
  channels[local_id] = channel
361
380
  end
@@ -603,6 +622,8 @@ module Net
603
622
  end
604
623
 
605
624
  def force_channel_cleanup_on_close
625
+ stop_preopening
626
+ @preopened.clear
606
627
  channels.each do |id, channel|
607
628
  channel_closed(channel)
608
629
  end
@@ -694,18 +715,37 @@ module Net
694
715
 
695
716
  def channel_open_confirmation(packet)
696
717
  info { "channel_open_confirmation: #{packet[:local_id]} #{packet[:remote_id]} #{packet[:window_size]} #{packet[:packet_size]}" }
718
+ if (spare = spare_channel(packet))
719
+ spare.remote_id = packet[:remote_id]
720
+ spare.window_size = packet[:window_size]
721
+ spare.packet_size = packet[:packet_size]
722
+ return
723
+ end
724
+
697
725
  channel = channels[packet[:local_id]]
698
726
  channel.do_open_confirmation(packet[:remote_id], packet[:window_size], packet[:packet_size])
699
727
  end
700
728
 
701
729
  def channel_open_failure(packet)
702
730
  error { "channel_open_failed: #{packet[:local_id]} #{packet[:reason_code]} #{packet[:description]}" }
731
+ if spare_channel(packet)
732
+ info { "preopen_channels: server refused the spare channel, no longer pre-opening" }
733
+ stop_preopening
734
+ @spare_channel = nil
735
+ return
736
+ end
737
+
703
738
  channel = channels.delete(packet[:local_id])
704
739
  channel.do_open_failed(packet[:reason_code], packet[:description])
705
740
  end
706
741
 
707
742
  def channel_window_adjust(packet)
708
743
  info { "channel_window_adjust: #{packet[:local_id]} +#{packet[:extra_bytes]}" }
744
+ if (spare = spare_channel(packet))
745
+ spare.window_size += packet[:extra_bytes]
746
+ return
747
+ end
748
+
709
749
  channels[packet[:local_id]].do_window_adjust(packet[:extra_bytes])
710
750
  end
711
751
 
@@ -726,11 +766,20 @@ module Net
726
766
 
727
767
  def channel_eof(packet)
728
768
  info { "channel_eof: #{packet[:local_id]}" }
769
+ return if spare_channel(packet)
770
+
729
771
  channels[packet[:local_id]].do_eof
730
772
  end
731
773
 
732
774
  def channel_close(packet)
733
775
  info { "channel_close: #{packet[:local_id]}" }
776
+ if (spare = spare_channel(packet))
777
+ send_message(Buffer.from(:byte, CHANNEL_CLOSE, :long, spare.remote_id))
778
+ @spare_channel = nil
779
+ # Wait for the next plain open_channel before opening another, so a server closing spares can't make us loop.
780
+ @spare_wanted = false
781
+ return
782
+ end
734
783
 
735
784
  channel = channels[packet[:local_id]]
736
785
  channel_closed(channel)
@@ -746,6 +795,45 @@ module Net
746
795
  channels[packet[:local_id]].do_failure
747
796
  end
748
797
 
798
+ def send_channel_open(type, local_id, window_size, packet_size, *extra)
799
+ send_message(Buffer.from(:byte, CHANNEL_OPEN, :string, type, :long, local_id,
800
+ :long, window_size, :long, packet_size, *extra))
801
+ end
802
+
803
+ def take_spare_channel
804
+ return unless @preopen_channels
805
+
806
+ @spare_wanted = true
807
+ spare = @spare_channel
808
+ @spare_channel = nil
809
+ spare
810
+ end
811
+
812
+ def open_spare_channel
813
+ return unless @spare_wanted && @preopen_channels && @spare_channel.nil? && !closed?
814
+
815
+ @spare_channel = SpareChannel.new(get_next_channel_id)
816
+ info { "preopen_channels: opening spare channel #{@spare_channel.local_id}" }
817
+ send_channel_open("session", @spare_channel.local_id, @max_win_size, @max_pkt_size)
818
+ end
819
+
820
+ # An unused spare is never closed, as that would cost round trips just before the transport goes away;
821
+ # it stays in @spare_channel to absorb the server's late replies for it.
822
+ def stop_preopening
823
+ @preopen_channels = false
824
+ end
825
+
826
+ def confirm_preopened_channels
827
+ until @preopened.empty?
828
+ channel, spare = @preopened.shift
829
+ channel.do_open_confirmation(spare.remote_id, spare.window_size, spare.packet_size)
830
+ end
831
+ end
832
+
833
+ def spare_channel(packet)
834
+ @spare_channel if @spare_channel && @spare_channel.local_id == packet[:local_id]
835
+ end
836
+
749
837
  def io_select_wait(wait)
750
838
  [wait, max_select_wait_time].compact.min
751
839
  end
@@ -346,6 +346,10 @@ module Net
346
346
  ch.info { "#{type} forwarded connection closed" }
347
347
  session.stop_listening_to(socket)
348
348
  ch.close
349
+ elsif ch.eof?
350
+ # Like OpenSSH's chan_shutdown_read: data already read still goes out before CHANNEL_EOF, the rest is dropped.
351
+ ch.output.append(socket.read_available) if socket.available > 0
352
+ session.pause_reading(socket)
349
353
  else
350
354
  if socket.available > 0
351
355
  data = socket.read_available(8192)
@@ -376,6 +380,8 @@ module Net
376
380
  end
377
381
 
378
382
  channel.on_process do |ch|
383
+ next if ch.eof?
384
+
379
385
  data = ch[:socket].read(8192)
380
386
  if data
381
387
  ch.debug { "read #{data.length} bytes from client, sending over #{type} forwarded connection" }
@@ -52,7 +52,7 @@ module Net
52
52
  MINOR = 0
53
53
 
54
54
  # The tiny component of this version of the Net::SSH library
55
- TINY = 1
55
+ TINY = 3
56
56
 
57
57
  # The prerelease component of this version of the Net::SSH library
58
58
  # nil allowed
data/lib/net/ssh.rb CHANGED
@@ -71,7 +71,7 @@ module Net
71
71
  append_all_supported_algorithms non_interactive password_prompt
72
72
  agent_socket_factory minimum_dh_bits verify_host_key
73
73
  fingerprint_hash check_host_ip pubkey_algorithms no_delay
74
- control_master control_path
74
+ control_master control_path preopen_channels
75
75
  ]
76
76
 
77
77
  # The standard means of starting a new SSH connection. When used with a
@@ -221,6 +221,12 @@ module Net
221
221
  # is +nil+, for no passphrase)
222
222
  # * :password => the password to use to login
223
223
  # * :port => the port to use when connecting to the remote host
224
+ # * :preopen_channels => set to +true+ to keep one spare "session" channel
225
+ # open, so that Net::SSH::Connection::Session#open_channel can hand it
226
+ # out at once and a command that waits for the previous one to finish
227
+ # costs one round trip instead of two. The spare counts against the
228
+ # server's MaxSessions; if the server refuses it, pre-opening stops for
229
+ # the session. Defaults to +false+.
224
230
  # * :properties => a hash of key/value pairs to add to the new connection's
225
231
  # properties (see Net::SSH::Connection::Session#properties)
226
232
  # * :proxy => a proxy instance (see Proxy) to use when connecting
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.1
4
+ version: 8.0.3
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-21 00:00:00.000000000 Z
35
+ date: 2026-10-25 00:00:00.000000000 Z
36
36
  dependencies:
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: logger
@@ -324,7 +324,6 @@ files:
324
324
  - lib/net/ssh/version.rb
325
325
  - net-ssh-public_cert.pem
326
326
  - net-ssh.gemspec
327
- - rakelib/changelog.rb
328
327
  - support/ssh_tunnel_bug.rb
329
328
  homepage: https://github.com/net-ssh/net-ssh
330
329
  licenses:
metadata.gz.sig CHANGED
Binary file
data/rakelib/changelog.rb DELETED
@@ -1,229 +0,0 @@
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