net-scp 4.1.0 → 4.1.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: 8fd30ff1a8f383d70371edecd81dd7aeb7f98eea7735cb156eb27b027fd3e8a6
4
- data.tar.gz: f4e232b5edbd50eeb342a665c0f73cb7d74f58b396b8e09f0adf7ad152804b33
3
+ metadata.gz: b666f903f4a48fb442f7ddea7efbdf5a222a44b7e2c9f7ae0cba5b6b8a126e10
4
+ data.tar.gz: 39032bdfeb545761cd2229af060ca0a79855e051ecab1123e2c68100deb03e19
5
5
  SHA512:
6
- metadata.gz: 47a7b29bfa8a6658f529eeaa5606af0ecdb4c7f1fd9bc17ab71744f2d5156724c8e7e4ddddfa08c728352b4a116f84631892e073503b5f43eadbb7987521fce7
7
- data.tar.gz: ecebbde9ce32e37d38e243b72be7297b953ecce7b9229a68fc82c54dd6f633ae53b4e9b9ff7a9068ed829a3ed573e7b52a89dcba7c86dc5025a953ed0d7c2ae2
6
+ metadata.gz: 8608726350662b31607e5beaa21d0276fa93942b045ac29a5762236b99b5a57925f9fde59a9fb59ebdd6ada2d6ccb1f52aa17f90e7ee196d2c7a714d66afce2d
7
+ data.tar.gz: 3c4112b0662c5401f6bc4e50aec2797a583faa5d67947ee3e46c2dd7f1d48ed074f49df9893910d08ea09f0625b14071d1d66d8f7390bff66dee60e1e7d19095
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1 @@
1
+ <!-- After opening the PR, add a changelog.d/<PR number>.<type>.txt fragment (none, change, security, breaking or breaking-security) instead of editing CHANGES.txt; see CONTRIBUTING.md#changelog. -->
@@ -0,0 +1,18 @@
1
+ name: Changelog
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened, labeled, unlabeled]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ changelog:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ with:
16
+ # HEAD is the PR merged into the base branch; HEAD^1 is the base it was merged into.
17
+ fetch-depth: 2
18
+ - uses: net-ssh/changelogd@v1
@@ -10,7 +10,7 @@ jobs:
10
10
  strategy:
11
11
  fail-fast: false
12
12
  matrix:
13
- ruby-version: ['2.3', '2.4', '2.5', '2.6', '2.7', '3.0', '3.1', '3.2', '3.3', 'ruby-head']
13
+ ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0', 'ruby-head']
14
14
  continue-on-error: ${{ matrix.ruby-version == 'ruby-head' }}
15
15
  steps:
16
16
  - uses: actions/checkout@v3
@@ -21,3 +21,20 @@ jobs:
21
21
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
22
22
  - name: Run Tests
23
23
  run: bundle exec rake test
24
+ env:
25
+ RUBYOPT: "--enable=frozen_string_literal"
26
+ test-legacy:
27
+ runs-on: ubuntu-latest
28
+ strategy:
29
+ fail-fast: false
30
+ matrix:
31
+ ruby-version: ['2.3', '2.4', '2.5']
32
+ steps:
33
+ - uses: actions/checkout@v3
34
+ # Plain docker run rather than container: the ruby:2.3/2.4 images' glibc is too old for the actions' Node.
35
+ # Not rake test: the Rakefile loads changelogd, which these Rubies can't install.
36
+ # No frozen_string_literal: net-ssh 6 mutates string literals.
37
+ - name: Run Tests
38
+ run: >-
39
+ docker run --rm -v "$PWD:/w" -w /w -e BUNDLE_GEMFILE=gemfiles/legacy.gemfile ruby:${{ matrix.ruby-version }}
40
+ sh -c 'bundle install --jobs 4 && bundle exec ruby -Ilib -Itest -e "Dir.glob(\"./test/test_*.rb\").each { |f| require f }"'
data/.gitignore CHANGED
@@ -11,3 +11,4 @@ doc
11
11
  Gemfile.lock
12
12
 
13
13
  lib/net/ssh/version.rb.old
14
+ /gemfiles/*.lock
data/CHANGES.txt CHANGED
@@ -1,3 +1,35 @@
1
+ === 4.1.3
2
+
3
+ * Load again on Ruby 2.3-2.5 (4.1.2 needed 2.5), and declare `required_ruby_version >= 2.3` [#112, #110]
4
+ * Add a `quote:` option to `upload`/`download` for the remote path: `:posix` (default), `:windows` for Win32-OpenSSH under cmd.exe or PowerShell (double-quoted, `\` sent as `/`, paths containing `"`, `$`, `` ` ``, `%`, `!`, curly quotes, CR, LF or NUL rejected), `:none` to send the path unchanged to servers without a shell, or a callable. By default a leading `~` or `~/` is now left for the remote shell to expand to the home directory; such paths previously referred to a literal `~` entry in the login directory and usually failed. Thanks @ArtsiomMusin for the Windows patch in #34 [#109, #34, #9]
5
+ * Recursive uploads no longer abort on a broken symlink: it is skipped with a warning, the rest is uploaded, and a Net::SCP::Error listing the skipped paths is raised at the end, like OpenSSH scp. Based on #23 by @maage. [#108, #23]
6
+
7
+ === 4.1.2
8
+
9
+ * Security: Downloads no longer apply setuid, setgid or sticky bits sent by the server, check that a server-chosen local name matches the requested remote basename (as OpenSSH does since CVE-2019-6111), and refuse to follow an existing local symlink at a server-named path instead of writing through it. The local path passed by the caller may still be a symlink. O_NOFOLLOW suggested in net-sftp#162 by srosenhamer [#104]
10
+ * Security: Recursive downloads, and downloads into a directory, reject file names from the server that are not a single path component (such as `../x` or `a/b`) instead of writing outside the target directory. Reported in net-sftp#162 by srosenhamer [#103]
11
+ * Raise `Net::SCP::Error` when the server ends a transfer before the SCP protocol finishes, instead of reporting a truncated transfer as successful [#105, #96]
12
+ * Opening an `scp://` URI through `uri/open-scp` now percent-decodes the user, password and path (once, keeping a literal `+`) and passes an IPv6 address to SSH without its brackets, so `scp://user@[::1]/a%20b` downloads `/a b` from `::1`. Thanks @OskarEichler [#94]
13
+ * Opening an `scp://` URI through `uri/open-scp` now writes the download into the OpenURI buffer as it arrives, so large files spill to a temporary file instead of being held in memory twice. Thanks @OskarEichler [#93]
14
+ * Parsing an `scp://` URI with a valueless query entry (`?flag`) or an empty one (`?a=1&&b=2`) no longer raises `NoMethodError`, and `URI::SCP#options` reflects the current query after `query=`. Thanks @OskarEichler [#92]
15
+ * Opening an `scp://` URI through `uri/open-scp` now calls `:progress_proc` with the number of bytes received, as OpenURI documents, instead of with the SCP progress arguments (which raised `ArgumentError` for a lambda), and calls `:content_length_proc` once with the file size. Thanks @OskarEichler [#91]
16
+ * Opening an `scp://` URI through `uri/open-scp` now uses the port and password given in the URI; they were passed outside the `:ssh` options and ignored. Options given in `:ssh` still take precedence. Thanks @OskarEichler [#90]
17
+ * Close the local file net-scp opened for an upload or download when the transfer fails, so it is not left open (and locked on Windows) [#89, #20]
18
+ * With `preserve: true`, recursive downloads now keep directories' modification times, which were overwritten when their contents were created. Thanks @OskarEichler [#88]
19
+ * `upload`/`upload!` with a zero, negative or non-Integer `:chunk_size` now raises `ArgumentError` before opening a channel; previously `0` hung forever sending empty chunks, a negative value or a String failed only after the channel was open, and a Float was silently truncated. Thanks @OskarEichler [#87]
20
+ * Uploading from an IO that isn't at its start now announces only the remaining bytes, instead of the full size, which made the transfer hang or fail. Thanks @OskarEichler [#86]
21
+ * Accept a `Pathname` as the local path for uploads and downloads; previously uploads looped forever and downloads kept only the last chunk. Thanks @OskarEichler [#85, #29]
22
+
23
+ === 4.1.1
24
+
25
+ * Allow net-ssh 8
26
+ * Fix frozen string literal compatibility [npezza93, mfazekas]
27
+ - Use mutable string for scp command construction
28
+ - Use mutable string for error_string initialization
29
+ * Add Ruby 3.4 and ruby-head to CI matrix
30
+ * Add Ruby 4.0+ compatibility (logger, rdoc gems)
31
+ * Drop EOL Ruby 2.3-2.5 from CI
32
+
1
33
  === 3.0.0
2
34
 
3
35
  * NetSHH 6.* support
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,33 @@
1
+ # Contributing to net-scp
2
+
3
+ Run the tests with `bundle exec rake test`.
4
+
5
+ ## Changelog
6
+
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
+
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 `[#95, #101]` or `[GHSA-xxxx-xxxx-xxxx]`, ` [#<number>]` is appended.
10
+
11
+ | Type | Written as | Use for |
12
+ | ------------------- | ------------------------------ | ------------------------------------------------------------ |
13
+ | `breaking-security` | `* BREAKING: Security: <text>` | an incompatible change made for security |
14
+ | `breaking` | `* BREAKING: <text>` | an incompatible change |
15
+ | `security` | `* Security: <text>` | a security fix or hardening |
16
+ | `change` | `* <text>` | any other user-visible fix or feature |
17
+ | `none` | nothing | PRs with no user-visible change (tests, CI, docs, refactors) |
18
+
19
+ Examples:
20
+
21
+ ```sh
22
+ rake "changelog:new[102,change,Fixed uploading files with spaces in their names]"
23
+ # changelog.d/102.change.txt -> * Fixed uploading files with spaces in their names [#102]
24
+
25
+ echo 'Refuse downloads that write outside the target directory [GHSA-xxxx-xxxx-xxxx]' > changelog.d/103.security.txt
26
+ touch changelog.d/104.none.txt
27
+ ```
28
+
29
+ For several entries from one PR add a suffix: `102.change.txt`, `102.change.2.txt` (`changelog:new` picks the next free one).
30
+
31
+ `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.
32
+
33
+ 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,9 +3,16 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in mygem.gemspec
4
4
  gemspec
5
5
 
6
+ gem "net-ssh", ">= 7.3.1"
7
+ gem "logger" # no longer a default gem in Ruby 4.0+
8
+
6
9
  # TODO: add to gemspec
7
10
  gem "bundler", ">= 1.11"
8
11
  gem "rake", ">= 12.0"
12
+ gem "rdoc"
13
+
14
+ # git: rather than github:, which Bundler 1.x (the publish workflow uses 1.17.3) expands to an unsupported git:// URL.
15
+ gem "changelogd", git: "https://github.com/net-ssh/changelogd.git", tag: "v1", group: :development
9
16
 
10
17
  gem 'byebug', group: %i[development test] if !Gem.win_platform? && RUBY_ENGINE == "ruby"
11
18
 
data/README.md CHANGED
@@ -69,6 +69,24 @@ require 'uri/open-scp'
69
69
  data = open("scp://user@host/path/to/file.txt").read
70
70
  ```
71
71
 
72
+ The remote path is escaped for a POSIX shell, except that a leading `~` or
73
+ `~/` is left for the remote shell to expand to the home directory (`~user/` is
74
+ not expanded). Use the `quote:` option of `upload`/`download` for other
75
+ servers:
76
+
77
+ ```ruby
78
+ # Win32-OpenSSH (cmd.exe or PowerShell): the path is double-quoted and "\" becomes "/".
79
+ # Paths containing ", $, `, %, !, curly quotes, CR, LF or NUL raise ArgumentError.
80
+ scp.upload! "/local/path", "C:\\Program Files\\App\\file.txt", quote: :windows
81
+
82
+ # Servers that don't run a shell (MikroTik and other appliances): the path is
83
+ # sent unchanged. Never use this with untrusted paths.
84
+ scp.download! "flash/backup.rsc", "/local/backup.rsc", quote: :none
85
+
86
+ # Or quote it yourself
87
+ scp.upload! "/local/path", remote, quote: ->(path) { "'#{path}'" }
88
+ ```
89
+
72
90
  For more information, see Net::SCP.
73
91
 
74
92
  ## REQUIREMENTS:
data/Rakefile CHANGED
@@ -1,7 +1,10 @@
1
1
  require "rubygems"
2
2
  require "rake"
3
3
  require "rake/clean"
4
- require "rdoc/task"
4
+ begin
5
+ require "rdoc/task"
6
+ rescue LoadError
7
+ end
5
8
  require "bundler/gem_tasks"
6
9
 
7
10
  desc "When releasing make sure NET_SSH_BUILDGEM_SIGNED is set"
@@ -19,6 +22,16 @@ name = "net-scp"
19
22
  require_relative "lib/net/scp/version"
20
23
  version = Net::SCP::Version::CURRENT
21
24
 
25
+ begin
26
+ require "changelogd/rake"
27
+ rescue LoadError
28
+ # 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.
29
+ require "bundler"
30
+ $LOAD_PATH.unshift(*Bundler.definition.specs["changelogd"].flat_map(&:full_require_paths))
31
+ require "changelogd/rake"
32
+ end
33
+ Changelogd::RakeTasks.new(version_file: "lib/net/scp/version.rb")
34
+
22
35
  namespace :cert do
23
36
  desc "Update public cert from private - only run if public is expired"
24
37
  task :update_public_when_expired do
@@ -67,20 +80,23 @@ Rake::TestTask.new do |t|
67
80
  end
68
81
 
69
82
  extra_files = %w[LICENSE.txt THANKS.txt CHANGES.txt ]
70
- RDoc::Task.new do |rdoc|
71
- rdoc.rdoc_dir = "rdoc"
72
- rdoc.title = "#{name} #{version}"
73
- rdoc.generator = 'hanna' # gem install hanna-nouveau
74
- rdoc.main = 'README.md'
75
- rdoc.rdoc_files.include("README*")
76
- rdoc.rdoc_files.include("bin/*.rb")
77
- rdoc.rdoc_files.include("lib/**/*.rb")
78
- extra_files.each { |file|
79
- rdoc.rdoc_files.include(file) if File.exist?(file)
80
- }
83
+ if defined?(RDoc::Task)
84
+ RDoc::Task.new do |rdoc|
85
+ rdoc.rdoc_dir = "rdoc"
86
+ rdoc.title = "#{name} #{version}"
87
+ rdoc.generator = 'hanna' # gem install hanna-nouveau
88
+ rdoc.main = 'README.md'
89
+ rdoc.rdoc_files.include("README*")
90
+ rdoc.rdoc_files.include("bin/*.rb")
91
+ rdoc.rdoc_files.include("lib/**/*.rb")
92
+ extra_files.each { |file|
93
+ rdoc.rdoc_files.include(file) if File.exist?(file)
94
+ }
95
+ end
81
96
  end
82
97
 
83
98
  def change_version(&block)
99
+ Rake::Task["changelog:check"].invoke
84
100
  version_file = 'lib/net/scp/version.rb'
85
101
  require_relative version_file
86
102
  pre = Net::SCP::Version::PRE
@@ -130,6 +146,7 @@ def change_version(&block)
130
146
 
131
147
  FileUtils.mv version_file, "#{version_file}.old"
132
148
  FileUtils.mv "#{version_file}.new", version_file
149
+ Rake::Task["changelog:release"].invoke
133
150
  end
134
151
 
135
152
  namespace :vbump do
@@ -0,0 +1 @@
1
+ Changelog fragments: one `<PR number>.<type>.txt` file per user-visible change, assembled into `CHANGES.txt` when the version is bumped. See [CONTRIBUTING.md](../CONTRIBUTING.md#changelog).
@@ -0,0 +1,7 @@
1
+ # Ruby 2.3-2.5: the last net-ssh and mocha releases that still install there.
2
+ source 'https://rubygems.org'
3
+
4
+ gemspec path: ".."
5
+
6
+ gem "net-ssh", "< 7"
7
+ gem "mocha", "< 2"
@@ -50,8 +50,11 @@ module Net; class SCP
50
50
  when :file
51
51
  read_file(channel, directive)
52
52
  when :end
53
+ directory = channel[:stack].pop
54
+ if channel[:options][:preserve] && directory && directory[:times]
55
+ File.utime(directory[:times][:atime], directory[:times][:mtime], channel[:local])
56
+ end
53
57
  channel[:local] = File.dirname(channel[:local])
54
- channel[:stack].pop
55
58
  channel[:state] = :finish if channel[:stack].empty?
56
59
  end
57
60
 
@@ -73,7 +76,7 @@ module Net; class SCP
73
76
  # Finishes off the read, sets the times for the file (if any), and then
74
77
  # jumps to either #finish_state (for single-file downloads) or
75
78
  # #read_directive_state (for recursive downloads). A 0-byte is sent to the
76
- # server to indicate that the file was recieved successfully.
79
+ # server to indicate that the file was received successfully.
77
80
  def finish_read_state(channel)
78
81
  channel[:io].close unless channel[:io] == channel[:local]
79
82
 
@@ -126,19 +129,17 @@ module Net; class SCP
126
129
  raise Net::SCP::Error, ":recursive not specified for directory download"
127
130
  end
128
131
 
129
- channel[:local] = File.join(channel[:local], directive[:name])
132
+ channel[:local] = local_path_for(channel, directive[:name])
130
133
 
131
- if File.exist?(channel[:local]) && !File.directory?(channel[:local])
134
+ if File.symlink?(channel[:local])
135
+ raise Net::SCP::Error, "#{channel[:local]} is a symlink, refusing to follow it"
136
+ elsif File.exist?(channel[:local]) && !File.directory?(channel[:local])
132
137
  raise Net::SCP::Error, "#{channel[:local]} already exists and is not a directory"
133
138
  elsif !File.exist?(channel[:local])
134
- Dir.mkdir(channel[:local], directive[:mode] | 0700)
139
+ Dir.mkdir(channel[:local], (directive[:mode] & 0777) | 0700)
135
140
  end
136
141
 
137
- if channel[:options][:preserve] && channel[:times]
138
- File.utime(channel[:times][:atime], channel[:times][:mtime], channel[:local])
139
- end
140
-
141
- channel[:stack] << directive
142
+ channel[:stack] << directive.merge(:times => channel[:times])
142
143
  channel[:times] = nil
143
144
  end
144
145
 
@@ -146,20 +147,54 @@ module Net; class SCP
146
147
  # actual read.
147
148
  def read_file(channel, directive)
148
149
  if !channel[:local].respond_to?(:write)
149
- directive[:name] = (channel[:options][:recursive] || File.directory?(channel[:local])) ?
150
- File.join(channel[:local], directive[:name]) :
151
- channel[:local]
150
+ server_named = channel[:options][:recursive] || File.directory?(channel[:local])
151
+ directive[:name] = server_named ? local_path_for(channel, directive[:name]) : channel[:local]
152
152
  end
153
153
 
154
154
  channel[:file] = directive.merge(:times => channel[:times])
155
+ # Setuid/setgid/sticky bits are never taken from the server, even with :preserve.
155
156
  channel[:io] = channel[:local].respond_to?(:write) ? channel[:local] :
156
- File.new(directive[:name], "wb", directive[:mode] | 0600)
157
+ open_local_file(directive[:name], (directive[:mode] & 0777) | 0600, server_named)
157
158
  channel[:times] = nil
158
159
  channel[:remaining] = channel[:file][:size]
159
160
  channel[:state] = :read_data
160
161
 
161
162
  progress_callback(channel, channel[:file][:name], 0, channel[:file][:size])
162
163
  end
164
+
165
+ # The name comes from the server, so anything but a single path component
166
+ # would let a malicious server write outside the target (CVE-2019-6111).
167
+ def local_path_for(channel, name)
168
+ if name.empty? || name == "." || name == ".." || name.include?("/") || name.include?("\0") ||
169
+ (File::ALT_SEPARATOR && name.include?(File::ALT_SEPARATOR))
170
+ raise Net::SCP::Error, "invalid file name from server: #{name.inspect}"
171
+ end
172
+
173
+ if channel[:stack].empty? && !requested_name?(channel[:remote], name)
174
+ raise Net::SCP::Error, "file name from server does not match request: #{name.inspect}"
175
+ end
176
+
177
+ File.join(channel[:local], name)
178
+ end
179
+
180
+ # OpenSSH's CVE-2019-6111 check: the flags mirror its brace expansion plus fnmatch(3) with flags 0.
181
+ def requested_name?(remote, name)
182
+ pattern = File.basename(remote.to_s)
183
+ name == pattern || File.fnmatch(pattern, name, File::FNM_DOTMATCH | File::FNM_EXTGLOB)
184
+ end
185
+
186
+ # A symlink planted at a server-named path must not redirect the write;
187
+ # the caller's own target may be a symlink on purpose (net-sftp#162).
188
+ def open_local_file(path, perm, nofollow)
189
+ return File.new(path, "wb", perm) unless nofollow && defined?(File::NOFOLLOW)
190
+
191
+ begin
192
+ File.new(path, "wb", perm, :flags => File::NOFOLLOW)
193
+ rescue SystemCallError
194
+ raise unless File.symlink?(path)
195
+ raise Net::SCP::Error, "#{path} is a symlink, refusing to follow it"
196
+ end
197
+ end
163
198
  end
164
199
 
165
200
  end; end
@@ -49,7 +49,8 @@ module Net; class SCP
49
49
  directive = "D%04o %d %s\n" % [mode, 0, File.basename(channel[:current])]
50
50
  channel.send_data(directive)
51
51
  channel[:cwd] = channel[:current]
52
- channel[:stack] << Dir.entries(channel[:current]).reject { |i| i == "." || i == ".." }
52
+ entries = Dir.entries(channel[:current]).reject { |i| i == "." || i == ".." }
53
+ channel[:stack] << entries.reject { |i| skip_broken_symlink(channel, File.join(channel[:current], i)) }
53
54
  await_response(channel, :next_item)
54
55
  end
55
56
  end
@@ -91,6 +92,7 @@ module Net; class SCP
91
92
  # upload and moves to #upload_current_state.
92
93
  def next_item_state(channel)
93
94
  if channel[:stack].empty?
95
+ channel[:state] = :finish
94
96
  finish_state(channel)
95
97
  else
96
98
  next_item = channel[:stack].last.shift
@@ -118,6 +120,32 @@ module Net; class SCP
118
120
  end
119
121
 
120
122
  channel[:size] = channel[:stat] ? channel[:stat].size : channel[:current].size
123
+ if channel[:current].respond_to?(:read) && channel[:current].respond_to?(:pos)
124
+ channel[:size] = [channel[:size] - channel[:current].pos, 0].max
125
+ end
126
+ end
127
+
128
+ # Returns true if +path+ is a symlink whose target doesn't exist, after
129
+ # logging a warning and recording it for #raise_if_entries_skipped.
130
+ def skip_broken_symlink(channel, path)
131
+ return false unless File.symlink?(path) && !File.exist?(path)
132
+
133
+ lwarn { "#{path}: broken symlink, skipping" }
134
+ (channel[:skipped] ||= []) << path
135
+ true
136
+ end
137
+
138
+ # Once the remote has closed the channel, raises a Net::SCP::Error listing
139
+ # the entries skipped by #skip_broken_symlink, if any.
140
+ def raise_if_entries_skipped(channel)
141
+ return unless channel.closing? && channel[:skipped]
142
+
143
+ # Cleared so it raises once, as on_process keeps calling this until the close is flushed.
144
+ skipped, channel[:skipped] = channel[:skipped], nil
145
+ # A failed remote scp is reported by start_command's exit-status check instead.
146
+ return unless channel[:exit].to_i.zero?
147
+
148
+ raise Net::SCP::Error, "upload incomplete, skipped broken symlinks: #{skipped.map(&:inspect).join(', ')}"
121
149
  end
122
150
 
123
151
  # If the :preserve option is set, send a 'T' directive and wait for the
@@ -52,7 +52,7 @@ module Net
52
52
  MINOR = 1
53
53
 
54
54
  # The tiny component of this version of the Net::SSH library
55
- TINY = 0
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/scp.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'pathname'
1
2
  require 'stringio'
2
3
  require 'shellwords'
3
4
 
@@ -64,7 +65,7 @@ module Net
64
65
  # puts "#{name}: #{sent}/#{total}"
65
66
  # end
66
67
  #
67
- # Whenever a new chunk of data is recieved for or sent to a file, the callback
68
+ # Whenever a new chunk of data is received for or sent to a file, the callback
68
69
  # will be invoked, indicating the name of the file (local for downloads,
69
70
  # remote for uploads), the number of bytes that have been sent or received
70
71
  # so far for the file, and the size of the file.
@@ -258,13 +259,24 @@ module Net
258
259
  #
259
260
  # * :recursive - the +local+ parameter refers to a local directory, which
260
261
  # should be uploaded to a new directory named +remote+ on the remote
261
- # server.
262
+ # server. Broken symlinks in it are skipped with a warning, and once
263
+ # everything else is uploaded a Net::SCP::Error listing them is raised.
262
264
  # * :preserve - the atime and mtime of the file should be preserved.
263
265
  # * :verbose - the process should result in verbose output on the server
264
266
  # end (useful for debugging).
265
267
  # * :chunk_size - the size of each "chunk" that should be sent. Defaults
266
268
  # to 2048. Changing this value may improve throughput at the expense
267
269
  # of decreasing interactivity.
270
+ # * :quote - how +remote+ is quoted in the scp command run by the remote
271
+ # shell. :posix (the default) escapes it for a POSIX shell, leaving a
272
+ # leading "~" or "~/" unescaped so it expands to the remote home
273
+ # directory ("~user/" is not expanded). :windows is for Win32-OpenSSH,
274
+ # whose shell may be cmd.exe or PowerShell: backslashes become "/", the
275
+ # path is wrapped in double quotes, and paths containing a double or curly
276
+ # quote, $, `, %, !, CR, LF or NUL raise ArgumentError. :none sends the
277
+ # path unchanged, for servers that don't run a shell (some routers and
278
+ # appliances); never use it with untrusted input. A callable is given
279
+ # +remote+ and returns the string to send.
268
280
  #
269
281
  # This method will return immediately, returning the Net::SSH::Connection::Channel
270
282
  # object that will support the upload. To wait for the upload to finish,
@@ -274,6 +286,10 @@ module Net
274
286
  # channel = scp.upload("/local/path", "/remote/path")
275
287
  # channel.wait
276
288
  def upload(local, remote, options={}, &progress)
289
+ chunk_size = options[:chunk_size] || Upload::DEFAULT_CHUNK_SIZE
290
+ unless chunk_size.is_a?(Integer) && chunk_size > 0
291
+ raise ArgumentError, ":chunk_size must be a positive Integer"
292
+ end
277
293
  start_command(:upload, local, remote, options, &progress)
278
294
  end
279
295
 
@@ -293,6 +309,7 @@ module Net
293
309
  # * :preserve - the atime and mtime of the file should be preserved.
294
310
  # * :verbose - the process should result in verbose output on the server
295
311
  # end (useful for debugging).
312
+ # * :quote - how +remote+ is quoted for the remote shell (see #upload).
296
313
  #
297
314
  # This method will return immediately, returning the Net::SSH::Connection::Channel
298
315
  # object that will support the download. To wait for the download to finish,
@@ -329,7 +346,7 @@ module Net
329
346
  # (:verbose, :recursive, :preserve). Returns the command-line as a
330
347
  # string, ready to execute.
331
348
  def scp_command(mode, options)
332
- command = "scp "
349
+ command = +"scp "
333
350
  command << (mode == :upload ? "-t" : "-f")
334
351
  command << " -v" if options[:verbose]
335
352
  command << " -r" if options[:recursive]
@@ -342,15 +359,15 @@ module Net
342
359
  # sets up a state machine to use to process the upload or download.
343
360
  # (See Net::SCP::Upload and Net::SCP::Download).
344
361
  def start_command(mode, local, remote, options={}, &callback)
345
- session.open_channel do |channel|
346
-
347
- if options[:shell]
348
- escaped_file = shellescape(remote).gsub(/'/) { |m| "'\\''" }
349
- command = "#{options[:shell]} -c '#{scp_command(mode, options)} #{escaped_file}'"
350
- else
351
- command = "#{scp_command(mode, options)} #{shellescape remote}"
352
- end
362
+ local = local.to_path if local.is_a?(Pathname)
363
+ # Kept in channel[:remote] too, so a download's basename check sees "C:/a/b.txt" as b.txt.
364
+ remote = remote.to_s.tr("\\", "/") if options[:quote] == :windows
365
+ command = "#{scp_command(mode, options)} #{quote_remote(remote, options[:quote] || :posix)}"
366
+ if options[:shell]
367
+ command = "#{options[:shell]} -c '#{command.gsub("'") { "'\\''" }}'"
368
+ end
353
369
 
370
+ session.open_channel do |channel|
354
371
  channel.exec(command) do |ch, success|
355
372
  if success
356
373
  channel[:local ] = local
@@ -360,22 +377,27 @@ module Net
360
377
  channel[:buffer ] = Net::SSH::Buffer.new
361
378
  channel[:state ] = "#{mode}_start"
362
379
  channel[:stack ] = []
363
- channel[:error_string] = ''
380
+ channel[:error_string] = +''
364
381
 
365
382
  channel.on_close do
366
- # If we got an exit-status and it is not 0, something went wrong
367
- if !channel[:exit].nil? && channel[:exit] != 0
368
- raise Net::SCP::Error, 'SCP did not finish successfully ' \
369
- "(#{channel[:exit]}): #{channel[:error_string]}"
383
+ begin
384
+ # The close can be dispatched before on_process has consumed data that arrived with it.
385
+ send("#{channel[:state]}_state", channel)
386
+ # If we got an exit-status and it is not 0, something went wrong
387
+ if !channel[:exit].nil? && channel[:exit] != 0
388
+ raise Net::SCP::Error, 'SCP did not finish successfully ' \
389
+ "(#{channel[:exit]}): #{channel[:error_string]}"
390
+ end
391
+ # A zero or missing exit-status does not prove the transfer completed.
392
+ if channel[:state] != :finish
393
+ message = +'SCP did not finish successfully (channel closed before end of transmission)'
394
+ message << ": #{channel[:error_string]}" unless channel[:error_string].empty?
395
+ raise Net::SCP::Error, message
396
+ end
397
+ ensure
398
+ io = channel[:io]
399
+ io.close if io && io != channel[:local] && !io.closed?
370
400
  end
371
- # We may get no exit-status at all as returning a status is only RECOMENDED
372
- # in RFC4254. But if our state is not :finish, something went wrong
373
- if channel[:exit].nil? && channel[:state] != :finish
374
- raise Net::SCP::Error, 'SCP did not finish successfully ' \
375
- '(channel closed before end of transmission)'
376
- end
377
- # At this point, :state can be :finish or :next_item
378
- send("#{channel[:state]}_state", channel)
379
401
  end
380
402
  channel.on_data { |ch2, data| channel[:buffer].append(data) }
381
403
  channel.on_extended_data { |ch2, type, data| debug { data.chomp } }
@@ -419,6 +441,7 @@ module Net
419
441
  # of the pipe, and allows the pipe to drain until the server closes it.
420
442
  def finish_state(channel)
421
443
  channel.eof!
444
+ raise_if_entries_skipped(channel)
422
445
  end
423
446
 
424
447
  # Invoked to report progress back to the client. If a callback was not
@@ -427,6 +450,29 @@ module Net
427
450
  channel[:callback].call(channel, name, sent, total) if channel[:callback]
428
451
  end
429
452
 
453
+ # Inside double quotes cmd.exe still expands % and !, PowerShell expands $ and `, and PowerShell also ends a string at U+2018..U+201E.
454
+ WINDOWS_UNSAFE = /["$`%!\r\n\0\u2018-\u201E]/
455
+
456
+ def quote_remote(remote, quote)
457
+ case quote
458
+ when :posix
459
+ remote = remote.to_s
460
+ return remote if remote == "~" || remote == "~/"
461
+ return "~/#{shellescape(remote[2..-1])}" if remote.start_with?("~/")
462
+ shellescape(remote)
463
+ when :windows
464
+ if remote.dup.force_encoding(Encoding::UTF_8) =~ WINDOWS_UNSAFE
465
+ raise ArgumentError, "remote path #{remote.inspect} contains a character that can't be quoted safely for a Windows shell"
466
+ end
467
+ "\"#{remote}\""
468
+ when :none
469
+ remote.to_s
470
+ else
471
+ raise ArgumentError, ":quote must be :posix, :windows, :none or a callable, got #{quote.inspect}" unless quote.respond_to?(:call)
472
+ quote.call(remote).to_s
473
+ end
474
+ end
475
+
430
476
  # Imported from ruby 1.9.2 shellwords.rb
431
477
  def shellescape(path)
432
478
  # Convert path to a string if it isn't already one.
data/lib/uri/open-scp.rb CHANGED
@@ -8,9 +8,21 @@ module URI
8
8
 
9
9
  class SCP
10
10
  def buffer_open(buf, proxy, open_options)
11
- options = open_options.merge(:port => port, :password => password)
11
+ options = open_options.dup
12
+ options[:ssh] = { :port => port }
13
+ options[:ssh][:password] = URI::DEFAULT_PARSER.unescape(password) if password
14
+ options[:ssh].merge!(open_options[:ssh] || {})
12
15
  progress = options.delete(:progress_proc)
13
- buf << Net::SCP.download!(host, user, path, nil, options, &progress)
16
+ content_length = options.delete(:content_length_proc)
17
+ output = Object.new
18
+ output.define_singleton_method(:write) { |data| buf << data }
19
+ Net::SCP.download!(hostname, user && URI::DEFAULT_PARSER.unescape(user), URI::DEFAULT_PARSER.unescape(path), output, options) do |_channel, _name, received, total|
20
+ if content_length
21
+ content_length.call(total)
22
+ content_length = nil
23
+ end
24
+ progress.call(received) if progress
25
+ end
14
26
  buf.io.rewind
15
27
  end
16
28
 
data/lib/uri/scp.rb CHANGED
@@ -11,8 +11,6 @@ module URI
11
11
  :query
12
12
  ].freeze
13
13
 
14
- attr_reader :options
15
-
16
14
  def self.new2(user, password, host, port, path, query)
17
15
  new('scp', [user, password], host, port, nil, path, nil, query)
18
16
  end
@@ -20,14 +18,23 @@ module URI
20
18
  def initialize(*args)
21
19
  super(*args)
22
20
 
21
+ options
22
+ end
23
+
24
+ def options
25
+ return @options if defined?(@options_query) && @options_query == query
26
+
27
+ @options_query = query && query.dup
23
28
  @options = Hash.new
24
29
  (query || "").split(/&/).each do |pair|
30
+ next if pair.empty?
25
31
  name, value = pair.split(/=/, 2)
26
32
  opt_name = name.to_sym
27
- values = value.split(/,/).map { |v| v.to_i.to_s == v ? v.to_i : v }
33
+ values = value.to_s.split(/,/).map { |v| v.to_i.to_s == v ? v.to_i : v }
28
34
  values = values.first if values.length == 1
29
- options[opt_name] = values
35
+ @options[opt_name] = values
30
36
  end
37
+ @options
31
38
  end
32
39
  end
33
40
 
@@ -1,7 +1,7 @@
1
1
  -----BEGIN CERTIFICATE-----
2
2
  MIIDeDCCAmCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMQ8wDQYDVQQDDAZuZXRz
3
3
  c2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZFgNj
4
- b20wHhcNMjQxMjI1MTEzNDQ3WhcNMjUxMjI1MTEzNDQ3WjBBMQ8wDQYDVQQDDAZu
4
+ b20wHhcNMjYwNTE2MTYyMDUzWhcNMjcwNTE2MTYyMDUzWjBBMQ8wDQYDVQQDDAZu
5
5
  ZXRzc2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZ
6
6
  FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGJ4TbZ9H+qZ08
7
7
  pQfJhPJTHaDCyQvCsKTFrL5O9z3tllQ7B/zksMMM+qFBpNYu9HCcg4yBATacE/PB
@@ -12,10 +12,10 @@ KP4yMn+TzaXijyjRg7gECfJr3TGASaA4bQsILFGG5dAWcwO4OMrZedR7SHj/o0Kf
12
12
  3gL7P0axAgMBAAGjezB5MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
13
13
  BBQF8qLA7Z4zg0SJGtUbv3eoQ8tjIzAfBgNVHREEGDAWgRRuZXRzc2hAc29sdXRp
14
14
  b3VzLmNvbTAfBgNVHRIEGDAWgRRuZXRzc2hAc29sdXRpb3VzLmNvbTANBgkqhkiG
15
- 9w0BAQsFAAOCAQEAOEVGPubOS9dBQmiJYIZHOXe2Q50iQgxKa7hyEJcyA7q69Q5h
16
- Ha5r4WpZyW0Dkr0+jIkT8GS7hO0XnUZdOiuNFQrx30jfRSVT7680dF6wAHEQZJqC
17
- ZmYFthhR/mtzi7bA+Ubd0PyBNivqt3WhWP+Z19j1bVWIwzczUcFFao+FBjXptI0m
18
- VGRPnRIzATA2qQUuKGkwrNFSHD9tDIHXSvwJ62U9ahoMKfMoDP0WHdPIpFCB8bPg
19
- wxMvGTA/RH93o6dL09sq7rVtsS9NNFmBGJWLZWWPfcspNBUXS0HTWXsWS9XTm2bm
20
- bbXS+I4xE1yFIPs39ej57LGJDMgMhWTyJF2zVg==
15
+ 9w0BAQsFAAOCAQEAVx/yCCX0ZPQ8TFeZe1T+OBEn3Co/AfjAmCdhstf1TF+Jziyx
16
+ dFJ8m8mF4596gru87Mi8mvu+WbPsLlFMOf5LDUkAsgnlvJ+zqNL7jPjGmsh0KOHJ
17
+ 0dzPYnTeGcfyrp1AXNN3Z2jWlEInrnqvo376vOmejjTtmDVVlhPjITHQqLSXzRaQ
18
+ e3M8fbJ+nupobti6jpXCYvH86KtpaxF66mwaynfdjzxQAVycV/ilKtL+sHH8zxlg
19
+ vNiMFH5yLu/tlav4xh+HlYscWVdFQIvd7f/Uxa+Vip1Pi16wmhnrE15z/vDX921v
20
+ hukBRIoCEAuUgNVBh+L/C8aETzbf/77WU3Si/w==
21
21
  -----END CERTIFICATE-----
data/net-scp.gemspec CHANGED
@@ -17,6 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.homepage = "https://github.com/net-ssh/net-scp"
18
18
  spec.license = "MIT"
19
19
  spec.required_rubygems_version = Gem::Requirement.new(">= 0") if spec.respond_to? :required_rubygems_version=
20
+ spec.required_ruby_version = ">= 2.3"
20
21
  spec.metadata = {
21
22
  "changelog_uri" => "https://github.com/net-ssh/net-scp/blob/master/CHANGES.txt"
22
23
  }
@@ -33,16 +34,16 @@ Gem::Specification.new do |spec|
33
34
  spec.specification_version = 3
34
35
 
35
36
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
36
- spec.add_runtime_dependency(%q<net-ssh>, [">= 2.6.5", "< 8.0.0"])
37
+ spec.add_runtime_dependency(%q<net-ssh>, [">= 2.6.5", "< 9.0.0"])
37
38
  spec.add_development_dependency(%q<test-unit>, [">= 0"])
38
39
  spec.add_development_dependency(%q<mocha>, [">= 0"])
39
40
  else
40
- spec.add_dependency(%q<net-ssh>, [">= 2.6.5", "< 8.0.0"])
41
+ spec.add_dependency(%q<net-ssh>, [">= 2.6.5", "< 9.0.0"])
41
42
  spec.add_dependency(%q<test-unit>, [">= 0"])
42
43
  spec.add_dependency(%q<mocha>, [">= 0"])
43
44
  end
44
45
  else
45
- spec.add_dependency(%q<net-ssh>, [">= 2.6.5", "< 8.0.0"])
46
+ spec.add_dependency(%q<net-ssh>, [">= 2.6.5", "< 9.0.0"])
46
47
  spec.add_dependency(%q<test-unit>, [">= 0"])
47
48
  spec.add_dependency(%q<mocha>, [">= 0"])
48
49
  end
data/setup.rb CHANGED
@@ -120,7 +120,7 @@ class ConfigTable
120
120
  ((major == 1) and
121
121
  ((minor >= 5) or
122
122
  ((minor == 4) and (teeny >= 4)))))
123
-
123
+
124
124
  subprefix = lambda {|path|
125
125
  path.sub(/\A#{Regexp.quote(c['prefix'])}/o, '$prefix')
126
126
  }
@@ -172,7 +172,7 @@ class ConfigTable
172
172
  'the directory for ruby scripts' ] ],
173
173
  [ 'so-dir', [ sodir,
174
174
  'path',
175
- 'the directory for ruby extentions' ] ],
175
+ 'the directory for ruby extensions' ] ],
176
176
  [ 'data-dir', [ '$prefix/share',
177
177
  'path',
178
178
  'the directory for shared data' ] ],
@@ -184,10 +184,10 @@ class ConfigTable
184
184
  'the ruby program using for installation' ] ],
185
185
  [ 'make-prog', [ makeprog,
186
186
  'name',
187
- 'the make program to compile ruby extentions' ] ],
187
+ 'the make program to compile ruby extensions' ] ],
188
188
  [ 'without-ext', [ 'no',
189
189
  'yes/no',
190
- 'does not compile/install ruby extentions' ] ]
190
+ 'does not compile/install ruby extensions' ] ]
191
191
  ]
192
192
  multipackage_descripters = [
193
193
  [ 'with', [ '',
@@ -303,7 +303,7 @@ class ConfigTable
303
303
  unless ConfigTable.config_key?(k)
304
304
  @table[k] = v
305
305
  end
306
-
306
+
307
307
  def [](key)
308
308
  return nil unless @table[key]
309
309
  @table[key].gsub(%r<\$([^/]+)>) { self[$1] }
@@ -479,7 +479,7 @@ module FileOperations
479
479
  def ruby(str)
480
480
  command config('ruby-prog') + ' ' + str
481
481
  end
482
-
482
+
483
483
  def make(task = '')
484
484
  command config('make-prog') + ' ' + task
485
485
  end
@@ -572,7 +572,7 @@ module HookScriptAPI
572
572
  def srcdirectory?(path)
573
573
  File.dir?(srcfile(path))
574
574
  end
575
-
575
+
576
576
  def srcfile?(path)
577
577
  File.file? srcfile(path)
578
578
  end
@@ -606,10 +606,10 @@ class ToplevelInstaller
606
606
  TASKS = [
607
607
  [ 'config', 'saves your configurations' ],
608
608
  [ 'show', 'shows current configuration' ],
609
- [ 'setup', 'compiles ruby extentions and others' ],
609
+ [ 'setup', 'compiles ruby extensions and others' ],
610
610
  [ 'install', 'installs files' ],
611
- [ 'clean', "does `make clean' for each extention" ],
612
- [ 'distclean',"does `make distclean' for each extention" ]
611
+ [ 'clean', "does `make clean' for each extension" ],
612
+ [ 'distclean',"does `make distclean' for each extension" ]
613
613
  ]
614
614
 
615
615
  def ToplevelInstaller.invoke
@@ -708,7 +708,7 @@ class ToplevelInstaller
708
708
  when '-v', '--version'
709
709
  puts "#{File.basename($0)} version #{Version}"
710
710
  exit 0
711
-
711
+
712
712
  when '--copyright'
713
713
  puts Copyright
714
714
  exit 0
@@ -911,7 +911,7 @@ class ToplevelInstallerMulti < ToplevelInstaller
911
911
 
912
912
  def print_usage(f)
913
913
  super
914
- f.puts 'Inluded packages:'
914
+ f.puts 'Included packages:'
915
915
  f.puts ' ' + @packages.sort.join(' ')
916
916
  f.puts
917
917
  end
@@ -1146,7 +1146,7 @@ class Installer
1146
1146
 
1147
1147
  def install_dir_ext(rel)
1148
1148
  return unless extdir?(curr_srcdir())
1149
- install_files ruby_extentions('.'),
1149
+ install_files ruby_extensions('.'),
1150
1150
  "#{config('so-dir')}/#{File.dirname(rel)}",
1151
1151
  0555
1152
1152
  end
@@ -1165,9 +1165,9 @@ class Installer
1165
1165
  def ruby_scripts
1166
1166
  collect_filenames_auto().select {|n| /\.rb\z/ =~ n || "module.yml" == n }
1167
1167
  end
1168
-
1168
+
1169
1169
  # picked up many entries from cvs-1.11.1/src/ignore.c
1170
- reject_patterns = %w(
1170
+ reject_patterns = %w(
1171
1171
  core RCSLOG tags TAGS .make.state
1172
1172
  .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb
1173
1173
  *~ *.old *.bak *.BAK *.orig *.rej _$* *$
@@ -1212,14 +1212,14 @@ class Installer
1212
1212
  }
1213
1213
  end
1214
1214
 
1215
- def ruby_extentions(dir)
1216
- _ruby_extentions(dir) or
1217
- raise InstallError, "no ruby extention exists: 'ruby #{$0} setup' first"
1215
+ def ruby_extensions(dir)
1216
+ _ruby_extensions(dir) or
1217
+ raise InstallError, "no ruby extension exists: 'ruby #{$0} setup' first"
1218
1218
  end
1219
1219
 
1220
1220
  DLEXT = /\.#{ ::Config::CONFIG['DLEXT'] }\z/
1221
1221
 
1222
- def _ruby_extentions(dir)
1222
+ def _ruby_extensions(dir)
1223
1223
  Dir.open(dir) {|d|
1224
1224
  return d.select {|fname| DLEXT =~ fname }
1225
1225
  }
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-scp
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0
4
+ version: 4.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamis Buck
@@ -14,7 +14,7 @@ cert_chain:
14
14
  -----BEGIN CERTIFICATE-----
15
15
  MIIDeDCCAmCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMQ8wDQYDVQQDDAZuZXRz
16
16
  c2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZFgNj
17
- b20wHhcNMjQxMjI1MTEzNDQ3WhcNMjUxMjI1MTEzNDQ3WjBBMQ8wDQYDVQQDDAZu
17
+ b20wHhcNMjYwNTE2MTYyMDUzWhcNMjcwNTE2MTYyMDUzWjBBMQ8wDQYDVQQDDAZu
18
18
  ZXRzc2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZ
19
19
  FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGJ4TbZ9H+qZ08
20
20
  pQfJhPJTHaDCyQvCsKTFrL5O9z3tllQ7B/zksMMM+qFBpNYu9HCcg4yBATacE/PB
@@ -25,14 +25,14 @@ cert_chain:
25
25
  3gL7P0axAgMBAAGjezB5MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
26
26
  BBQF8qLA7Z4zg0SJGtUbv3eoQ8tjIzAfBgNVHREEGDAWgRRuZXRzc2hAc29sdXRp
27
27
  b3VzLmNvbTAfBgNVHRIEGDAWgRRuZXRzc2hAc29sdXRpb3VzLmNvbTANBgkqhkiG
28
- 9w0BAQsFAAOCAQEAOEVGPubOS9dBQmiJYIZHOXe2Q50iQgxKa7hyEJcyA7q69Q5h
29
- Ha5r4WpZyW0Dkr0+jIkT8GS7hO0XnUZdOiuNFQrx30jfRSVT7680dF6wAHEQZJqC
30
- ZmYFthhR/mtzi7bA+Ubd0PyBNivqt3WhWP+Z19j1bVWIwzczUcFFao+FBjXptI0m
31
- VGRPnRIzATA2qQUuKGkwrNFSHD9tDIHXSvwJ62U9ahoMKfMoDP0WHdPIpFCB8bPg
32
- wxMvGTA/RH93o6dL09sq7rVtsS9NNFmBGJWLZWWPfcspNBUXS0HTWXsWS9XTm2bm
33
- bbXS+I4xE1yFIPs39ej57LGJDMgMhWTyJF2zVg==
28
+ 9w0BAQsFAAOCAQEAVx/yCCX0ZPQ8TFeZe1T+OBEn3Co/AfjAmCdhstf1TF+Jziyx
29
+ dFJ8m8mF4596gru87Mi8mvu+WbPsLlFMOf5LDUkAsgnlvJ+zqNL7jPjGmsh0KOHJ
30
+ 0dzPYnTeGcfyrp1AXNN3Z2jWlEInrnqvo376vOmejjTtmDVVlhPjITHQqLSXzRaQ
31
+ e3M8fbJ+nupobti6jpXCYvH86KtpaxF66mwaynfdjzxQAVycV/ilKtL+sHH8zxlg
32
+ vNiMFH5yLu/tlav4xh+HlYscWVdFQIvd7f/Uxa+Vip1Pi16wmhnrE15z/vDX921v
33
+ hukBRIoCEAuUgNVBh+L/C8aETzbf/77WU3Si/w==
34
34
  -----END CERTIFICATE-----
35
- date: 2025-01-22 00:00:00.000000000 Z
35
+ date: 2026-10-27 00:00:00.000000000 Z
36
36
  dependencies:
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: net-ssh
@@ -43,7 +43,7 @@ dependencies:
43
43
  version: 2.6.5
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
- version: 8.0.0
46
+ version: 9.0.0
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
@@ -53,7 +53,7 @@ dependencies:
53
53
  version: 2.6.5
54
54
  - - "<"
55
55
  - !ruby/object:Gem::Version
56
- version: 8.0.0
56
+ version: 9.0.0
57
57
  - !ruby/object:Gem::Dependency
58
58
  name: test-unit
59
59
  requirement: !ruby/object:Gem::Requirement
@@ -91,15 +91,20 @@ extra_rdoc_files:
91
91
  - LICENSE.txt
92
92
  - README.md
93
93
  files:
94
+ - ".github/pull_request_template.md"
95
+ - ".github/workflows/changelog.yml"
94
96
  - ".github/workflows/ci.yml"
95
97
  - ".gitignore"
96
98
  - CHANGES.txt
99
+ - CONTRIBUTING.md
97
100
  - Gemfile
98
101
  - LICENSE.txt
99
102
  - Manifest
100
103
  - README.md
101
104
  - Rakefile
102
105
  - SECURITY.md
106
+ - changelog.d/README.md
107
+ - gemfiles/legacy.gemfile
103
108
  - lib/net/scp.rb
104
109
  - lib/net/scp/download.rb
105
110
  - lib/net/scp/errors.rb
@@ -123,7 +128,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
123
128
  requirements:
124
129
  - - ">="
125
130
  - !ruby/object:Gem::Version
126
- version: '0'
131
+ version: '2.3'
127
132
  required_rubygems_version: !ruby/object:Gem::Requirement
128
133
  requirements:
129
134
  - - ">="
metadata.gz.sig CHANGED
Binary file