net-ssh 8.0.2 → 8.0.4
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/workflows/changelog.yml +1 -26
- data/CHANGES.txt +8 -0
- data/CONTRIBUTING.md +3 -4
- data/Gemfile +2 -0
- data/Rakefile +12 -48
- data/lib/net/ssh/connection/session.rb +93 -5
- data/lib/net/ssh/proxy/jump.rb +21 -6
- data/lib/net/ssh/version.rb +1 -1
- data/lib/net/ssh.rb +7 -1
- data.tar.gz.sig +0 -0
- metadata +2 -3
- metadata.gz.sig +0 -0
- data/rakelib/changelog.rb +0 -229
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 110d479784d8cc56b52a5a3e9b14e5f23dbf556873b64a8e1563df31b1205911
|
|
4
|
+
data.tar.gz: e0f0d31f6476b896ddc6354906cc1b861f99687365a0783527b738e69f4d4e15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 985c71acd2df6c9e5a287c8e64c6e960cd80bd47c5d18837e1381a7c38f09ee2bbad51e3946f8d26d5f467c4999fca322e3ce4b48aa16115c2d434f675cf52cd
|
|
7
|
+
data.tar.gz: 81a1286e7e73a6c6b17a4a64e5c5004844a734bfa0118d39041ff291eb0e364c9fa4d3777cd7c745fb0ba038bd1aed2cef191b9c001cfb47937b7154a0506813
|
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
|
-
-
|
|
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/CHANGES.txt
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
=== 8.0.4
|
|
2
|
+
|
|
3
|
+
* Security: validate every ProxyJump hop, not only the first, as [user@]host[:port] (refusing a path, query, fragment or password) and shell-escape the hops passed to ssh -J; a hop such as `evil/x&$(cmd)` ran `cmd` through the shell [GHSA-g2r2-x3p2-5677]
|
|
4
|
+
|
|
5
|
+
=== 8.0.3
|
|
6
|
+
|
|
7
|
+
* Add `preopen_channels` option: keep a spare session channel open so each command saves a round trip [#1071]
|
|
8
|
+
|
|
1
9
|
=== 8.0.2
|
|
2
10
|
|
|
3
11
|
* 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]
|
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]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
data/lib/net/ssh/proxy/jump.rb
CHANGED
|
@@ -54,20 +54,21 @@ module Net
|
|
|
54
54
|
# We cannot build the ProxyCommand template until we know if the :config
|
|
55
55
|
# option was specified during `Net::SSH.start`.
|
|
56
56
|
def build_proxy_command_equivalent(connection_options = nil)
|
|
57
|
-
|
|
57
|
+
raise ArgumentError, "no jump host given" if jump_proxies.empty?
|
|
58
|
+
|
|
58
59
|
config = connection_options && connection_options[:config]
|
|
59
|
-
uri, = jump_proxies.split(",").map { |jump| validate_jump!(jump) }
|
|
60
|
+
uri, *extra_jumps = jump_proxies.split(",", -1).map { |jump| validate_jump!(jump) }
|
|
60
61
|
|
|
61
62
|
template = "ssh".dup
|
|
62
|
-
template << " -l #{uri.user}"
|
|
63
|
-
template << " -p #{uri.port}"
|
|
64
|
-
template << " -J #{extra_jumps}"
|
|
63
|
+
template << " -l #{escape(uri.user)}" if uri.user
|
|
64
|
+
template << " -p #{uri.port}" if uri.port
|
|
65
|
+
template << " -J #{escape(extra_jumps.map { |jump| jump_spec(jump) }.join(','))}" unless extra_jumps.empty?
|
|
65
66
|
template << " -F #{escape(config)}" if config != true && config
|
|
66
67
|
@options.each do |key, values|
|
|
67
68
|
Array(values).each { |value| template << " -o #{escape(key)}=#{escape(value)}" }
|
|
68
69
|
end
|
|
69
70
|
template << " -W %h:%p "
|
|
70
|
-
template << uri.host
|
|
71
|
+
template << escape(uri.host)
|
|
71
72
|
|
|
72
73
|
@command_line_template = template
|
|
73
74
|
end
|
|
@@ -90,11 +91,25 @@ module Net
|
|
|
90
91
|
Shellwords.escape(value).gsub("%", "%%")
|
|
91
92
|
end
|
|
92
93
|
|
|
94
|
+
# Only [user@]host[:port] is checked, so anything else URI accepts (a path,
|
|
95
|
+
# query, fragment or password) must be refused before it reaches the shell.
|
|
93
96
|
def validate_jump!(jump)
|
|
94
97
|
uri = URI.parse("ssh://#{jump}")
|
|
98
|
+
raise URI::InvalidURIError if uri.host.to_s.empty? || jump_spec(uri) != jump
|
|
99
|
+
|
|
95
100
|
validate_user_name!(uri.user) if uri.user
|
|
96
101
|
validate_host_name!(uri.host)
|
|
102
|
+
validate_port!(uri.port) if uri.port
|
|
97
103
|
uri
|
|
104
|
+
rescue URI::InvalidURIError
|
|
105
|
+
raise ArgumentError, "jump host is not [user@]host[:port]: #{jump.inspect}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def jump_spec(uri)
|
|
109
|
+
spec = uri.host.dup
|
|
110
|
+
spec.prepend("#{uri.user}@") if uri.user
|
|
111
|
+
spec << ":#{uri.port}" if uri.port
|
|
112
|
+
spec
|
|
98
113
|
end
|
|
99
114
|
end
|
|
100
115
|
end
|
data/lib/net/ssh/version.rb
CHANGED
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.
|
|
4
|
+
version: 8.0.4
|
|
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-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
|