net-scp 4.1.2 → 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: b1413e9916cf7c4d82160a87e9da29044528f7c32dc746d12a873ea4d902a940
4
- data.tar.gz: b0e33d71c6a78b8d263fd95408e23188c3c7e0bb274d52c1df6745bbc6793cc6
3
+ metadata.gz: b666f903f4a48fb442f7ddea7efbdf5a222a44b7e2c9f7ae0cba5b6b8a126e10
4
+ data.tar.gz: 39032bdfeb545761cd2229af060ca0a79855e051ecab1123e2c68100deb03e19
5
5
  SHA512:
6
- metadata.gz: 46648015028eb41a2d7f269f4fe6baa430289da17f82c2945c5ef49ff4fa43085a966734d7e361a98b08cd91f7abf99f219eeebd954607bc74969f398e861ceb
7
- data.tar.gz: e307dc9bb8e181c76acb226ffc8b35f5c2002adbef0f027f8954b9e695cc8436b780a7f4316dba7b685a48048184676c20dacceb2db81d235dcf51a83045ddf5
6
+ metadata.gz: 8608726350662b31607e5beaa21d0276fa93942b045ac29a5762236b99b5a57925f9fde59a9fb59ebdd6ada2d6ccb1f52aa17f90e7ee196d2c7a714d66afce2d
7
+ data.tar.gz: 3c4112b0662c5401f6bc4e50aec2797a583faa5d67947ee3e46c2dd7f1d48ed074f49df9893910d08ea09f0625b14071d1d66d8f7390bff66dee60e1e7d19095
checksums.yaml.gz.sig CHANGED
Binary file
@@ -23,3 +23,18 @@ jobs:
23
23
  run: bundle exec rake test
24
24
  env:
25
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,9 @@
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
+
1
7
  === 4.1.2
2
8
 
3
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]
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:
@@ -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"
@@ -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
@@ -124,6 +125,29 @@ module Net; class SCP
124
125
  end
125
126
  end
126
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(', ')}"
149
+ end
150
+
127
151
  # If the :preserve option is set, send a 'T' directive and wait for the
128
152
  # server to respond before proceeding to either #upload_file_state or
129
153
  # #upload_directory_state, depending on what is being uploaded.
@@ -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 = 2
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
@@ -259,13 +259,24 @@ module Net
259
259
  #
260
260
  # * :recursive - the +local+ parameter refers to a local directory, which
261
261
  # should be uploaded to a new directory named +remote+ on the remote
262
- # 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.
263
264
  # * :preserve - the atime and mtime of the file should be preserved.
264
265
  # * :verbose - the process should result in verbose output on the server
265
266
  # end (useful for debugging).
266
267
  # * :chunk_size - the size of each "chunk" that should be sent. Defaults
267
268
  # to 2048. Changing this value may improve throughput at the expense
268
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.
269
280
  #
270
281
  # This method will return immediately, returning the Net::SSH::Connection::Channel
271
282
  # object that will support the upload. To wait for the upload to finish,
@@ -298,6 +309,7 @@ module Net
298
309
  # * :preserve - the atime and mtime of the file should be preserved.
299
310
  # * :verbose - the process should result in verbose output on the server
300
311
  # end (useful for debugging).
312
+ # * :quote - how +remote+ is quoted for the remote shell (see #upload).
301
313
  #
302
314
  # This method will return immediately, returning the Net::SSH::Connection::Channel
303
315
  # object that will support the download. To wait for the download to finish,
@@ -348,15 +360,14 @@ module Net
348
360
  # (See Net::SCP::Upload and Net::SCP::Download).
349
361
  def start_command(mode, local, remote, options={}, &callback)
350
362
  local = local.to_path if local.is_a?(Pathname)
351
- session.open_channel do |channel|
352
-
353
- if options[:shell]
354
- escaped_file = shellescape(remote).gsub(/'/) { |m| "'\\''" }
355
- command = "#{options[:shell]} -c '#{scp_command(mode, options)} #{escaped_file}'"
356
- else
357
- command = "#{scp_command(mode, options)} #{shellescape remote}"
358
- end
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
359
369
 
370
+ session.open_channel do |channel|
360
371
  channel.exec(command) do |ch, success|
361
372
  if success
362
373
  channel[:local ] = local
@@ -369,22 +380,24 @@ module Net
369
380
  channel[:error_string] = +''
370
381
 
371
382
  channel.on_close do
372
- # The close can be dispatched before on_process has consumed data that arrived with it.
373
- send("#{channel[:state]}_state", channel)
374
- # If we got an exit-status and it is not 0, something went wrong
375
- if !channel[:exit].nil? && channel[:exit] != 0
376
- raise Net::SCP::Error, 'SCP did not finish successfully ' \
377
- "(#{channel[:exit]}): #{channel[:error_string]}"
378
- end
379
- # A zero or missing exit-status does not prove the transfer completed.
380
- if channel[:state] != :finish
381
- message = +'SCP did not finish successfully (channel closed before end of transmission)'
382
- message << ": #{channel[:error_string]}" unless channel[:error_string].empty?
383
- raise Net::SCP::Error, message
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?
384
400
  end
385
- ensure
386
- io = channel[:io]
387
- io.close if io && io != channel[:local] && !io.closed?
388
401
  end
389
402
  channel.on_data { |ch2, data| channel[:buffer].append(data) }
390
403
  channel.on_extended_data { |ch2, type, data| debug { data.chomp } }
@@ -428,6 +441,7 @@ module Net
428
441
  # of the pipe, and allows the pipe to drain until the server closes it.
429
442
  def finish_state(channel)
430
443
  channel.eof!
444
+ raise_if_entries_skipped(channel)
431
445
  end
432
446
 
433
447
  # Invoked to report progress back to the client. If a callback was not
@@ -436,6 +450,29 @@ module Net
436
450
  channel[:callback].call(channel, name, sent, total) if channel[:callback]
437
451
  end
438
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
+
439
476
  # Imported from ruby 1.9.2 shellwords.rb
440
477
  def shellescape(path)
441
478
  # Convert path to a string if it isn't already one.
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
  }
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.2
4
+ version: 4.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jamis Buck
@@ -32,7 +32,7 @@ cert_chain:
32
32
  vNiMFH5yLu/tlav4xh+HlYscWVdFQIvd7f/Uxa+Vip1Pi16wmhnrE15z/vDX921v
33
33
  hukBRIoCEAuUgNVBh+L/C8aETzbf/77WU3Si/w==
34
34
  -----END CERTIFICATE-----
35
- date: 2026-10-25 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
@@ -104,6 +104,7 @@ files:
104
104
  - Rakefile
105
105
  - SECURITY.md
106
106
  - changelog.d/README.md
107
+ - gemfiles/legacy.gemfile
107
108
  - lib/net/scp.rb
108
109
  - lib/net/scp/download.rb
109
110
  - lib/net/scp/errors.rb
@@ -127,7 +128,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
127
128
  requirements:
128
129
  - - ">="
129
130
  - !ruby/object:Gem::Version
130
- version: '0'
131
+ version: '2.3'
131
132
  required_rubygems_version: !ruby/object:Gem::Requirement
132
133
  requirements:
133
134
  - - ">="
metadata.gz.sig CHANGED
Binary file