net-scp 3.0.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/pull_request_template.md +1 -0
- data/.github/workflows/changelog.yml +18 -0
- data/.github/workflows/ci.yml +40 -0
- data/.gitignore +8 -0
- data/CHANGES.txt +32 -0
- data/CONTRIBUTING.md +33 -0
- data/Gemfile +9 -2
- data/Manifest +1 -1
- data/README.md +161 -0
- data/Rakefile +116 -12
- data/SECURITY.md +4 -0
- data/changelog.d/README.md +1 -0
- data/gemfiles/legacy.gemfile +7 -0
- data/lib/net/scp/download.rb +49 -14
- data/lib/net/scp/upload.rb +29 -1
- data/lib/net/scp/version.rb +4 -4
- data/lib/net/scp.rb +73 -13
- data/lib/uri/open-scp.rb +14 -2
- data/lib/uri/scp.rb +18 -7
- data/net-scp-public_cert.pem +19 -18
- data/net-scp.gemspec +5 -4
- data/setup.rb +19 -19
- data.tar.gz.sig +0 -0
- metadata +37 -30
- metadata.gz.sig +0 -0
- data/.travis.yml +0 -16
- data/README.rdoc +0 -124
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b666f903f4a48fb442f7ddea7efbdf5a222a44b7e2c9f7ae0cba5b6b8a126e10
|
|
4
|
+
data.tar.gz: 39032bdfeb545761cd2229af060ca0a79855e051ecab1123e2c68100deb03e19
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
push: { branches: master }
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0', 'ruby-head']
|
|
14
|
+
continue-on-error: ${{ matrix.ruby-version == 'ruby-head' }}
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v3
|
|
17
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
18
|
+
uses: ruby/setup-ruby@v1
|
|
19
|
+
with:
|
|
20
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
21
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
|
22
|
+
- name: Run Tests
|
|
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
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
|
-
gem "bundler", "
|
|
8
|
-
gem "rake", "
|
|
10
|
+
gem "bundler", ">= 1.11"
|
|
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/Manifest
CHANGED
data/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Net::SCP
|
|
2
|
+
|
|
3
|
+
***Please note: this project is in maintenance mode. It is not under active
|
|
4
|
+
development but pull requests are very much welcome. Just be sure to include
|
|
5
|
+
tests! -- delano***
|
|
6
|
+
|
|
7
|
+
* Docs: http://net-ssh.github.io/net-scp
|
|
8
|
+
* Issues: https://github.com/net-ssh/net-scp/issues
|
|
9
|
+
* Codes: https://github.com/net-ssh/net-scp
|
|
10
|
+
* Email: net-ssh@solutious.com
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
*As of v1.0.5, all gem releases are signed. See INSTALL.*
|
|
14
|
+
|
|
15
|
+
## DESCRIPTION:
|
|
16
|
+
|
|
17
|
+
Net::SCP is a pure-Ruby implementation of the SCP protocol. This operates over
|
|
18
|
+
SSH (and requires the Net::SSH library), and allows files and directory trees
|
|
19
|
+
to be copied to and from a remote server.
|
|
20
|
+
|
|
21
|
+
## FEATURES/PROBLEMS:
|
|
22
|
+
|
|
23
|
+
* Transfer files or entire directory trees to or from a remote host via SCP
|
|
24
|
+
* Can preserve file attributes across transfers
|
|
25
|
+
* Can download files in-memory, or direct-to-disk
|
|
26
|
+
* Support for SCP URI's, and OpenURI
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## SYNOPSIS:
|
|
30
|
+
|
|
31
|
+
In a nutshell:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require 'net/scp'
|
|
35
|
+
|
|
36
|
+
# upload a file to a remote server
|
|
37
|
+
Net::SCP.upload!("remote.host.com", "username",
|
|
38
|
+
"/local/path", "/remote/path",
|
|
39
|
+
:ssh => { :password => "password" })
|
|
40
|
+
|
|
41
|
+
# upload recursively
|
|
42
|
+
Net::SCP.upload!("remote.host", "username", "/path/to/local", "/path/to/remote",
|
|
43
|
+
:ssh => { :password => "foo" }, :recursive => true)
|
|
44
|
+
|
|
45
|
+
# download a file from a remote server
|
|
46
|
+
Net::SCP.download!("remote.host.com", "username",
|
|
47
|
+
"/remote/path", "/local/path",
|
|
48
|
+
:ssh => { :password => "password" })
|
|
49
|
+
|
|
50
|
+
# download a file to an in-memory buffer
|
|
51
|
+
data = Net::SCP::download!("remote.host.com", "username", "/remote/path")
|
|
52
|
+
|
|
53
|
+
# use a persistent connection to transfer files
|
|
54
|
+
Net::SCP.start("remote.host.com", "username", :password => "password") do |scp|
|
|
55
|
+
# upload a file to a remote server
|
|
56
|
+
scp.upload! "/local/path", "/remote/path"
|
|
57
|
+
|
|
58
|
+
# upload from an in-memory buffer
|
|
59
|
+
scp.upload! StringIO.new("some data to upload"), "/remote/path"
|
|
60
|
+
|
|
61
|
+
# run multiple downloads in parallel
|
|
62
|
+
d1 = scp.download("/remote/path", "/local/path")
|
|
63
|
+
d2 = scp.download("/remote/path2", "/local/path2")
|
|
64
|
+
[d1, d2].each { |d| d.wait }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# You can also use open-uri to grab data via scp:
|
|
68
|
+
require 'uri/open-scp'
|
|
69
|
+
data = open("scp://user@host/path/to/file.txt").read
|
|
70
|
+
```
|
|
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
|
+
|
|
90
|
+
For more information, see Net::SCP.
|
|
91
|
+
|
|
92
|
+
## REQUIREMENTS:
|
|
93
|
+
|
|
94
|
+
* Net::SSH 2
|
|
95
|
+
|
|
96
|
+
If you wish to run the tests, you'll also need:
|
|
97
|
+
|
|
98
|
+
* Echoe (for Rakefile use)
|
|
99
|
+
* Mocha (for tests)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## INSTALL:
|
|
103
|
+
|
|
104
|
+
* ```gem install net-scp (might need sudo privileges)```
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
However, in order to be sure the code you're installing hasn't been tampered
|
|
108
|
+
with, it's recommended that you verify the
|
|
109
|
+
[signature](http://docs.seattlerb.org/rubygems/Gem/Security.html). To do this,
|
|
110
|
+
you need to add my public key as a trusted certificate (you only need to do
|
|
111
|
+
this once):
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
# Add the public key as a trusted certificate
|
|
115
|
+
# (You only need to do this once)
|
|
116
|
+
$ curl -O https://raw.githubusercontent.com/net-ssh/net-ssh/master/net-ssh-public_cert.pem
|
|
117
|
+
$ gem cert --add net-ssh-public_cert.pem
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Then- when installing the gem - do so with high security:
|
|
121
|
+
|
|
122
|
+
$ gem install net-scp -P HighSecurity
|
|
123
|
+
|
|
124
|
+
If you don't add the public key, you'll see an error like "Couldn't verify
|
|
125
|
+
data signature". If you're still having trouble let me know and I'll give you
|
|
126
|
+
a hand.
|
|
127
|
+
|
|
128
|
+
Or, you can do it the hard way (without Rubygems):
|
|
129
|
+
|
|
130
|
+
* tar xzf net-scp-*.tgz
|
|
131
|
+
* cd net-scp-*
|
|
132
|
+
* ruby setup.rb config
|
|
133
|
+
* ruby setup.rb install (might need sudo privileges)
|
|
134
|
+
|
|
135
|
+
## Security contact information
|
|
136
|
+
|
|
137
|
+
See [SECURITY.md](SECURITY.md)
|
|
138
|
+
|
|
139
|
+
## LICENSE:
|
|
140
|
+
|
|
141
|
+
(The MIT License)
|
|
142
|
+
|
|
143
|
+
Copyright (c) 2008 Jamis Buck <jamis@37signals.com>
|
|
144
|
+
|
|
145
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
146
|
+
of this software and associated documentation files (the 'Software'), to deal
|
|
147
|
+
in the Software without restriction, including without limitation the rights
|
|
148
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
149
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
150
|
+
furnished to do so, subject to the following conditions:
|
|
151
|
+
|
|
152
|
+
The above copyright notice and this permission notice shall be included in all
|
|
153
|
+
copies or substantial portions of the Software.
|
|
154
|
+
|
|
155
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
156
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
157
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
158
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
159
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
160
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
161
|
+
SOFTWARE.
|
data/Rakefile
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
require "rubygems"
|
|
2
2
|
require "rake"
|
|
3
3
|
require "rake/clean"
|
|
4
|
-
|
|
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,15 +80,106 @@ Rake::TestTask.new do |t|
|
|
|
67
80
|
end
|
|
68
81
|
|
|
69
82
|
extra_files = %w[LICENSE.txt THANKS.txt CHANGES.txt ]
|
|
70
|
-
RDoc::Task
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def change_version(&block)
|
|
99
|
+
Rake::Task["changelog:check"].invoke
|
|
100
|
+
version_file = 'lib/net/scp/version.rb'
|
|
101
|
+
require_relative version_file
|
|
102
|
+
pre = Net::SCP::Version::PRE
|
|
103
|
+
tiny = Net::SCP::Version::TINY
|
|
104
|
+
result = block[pre: pre, tiny: Net::SCP::Version::TINY]
|
|
105
|
+
raise ArgumentError, "Version change logic should always return a pre" unless result.key?(:pre)
|
|
106
|
+
|
|
107
|
+
new_pre = result[:pre]
|
|
108
|
+
new_tiny = result[:tiny] || tiny
|
|
109
|
+
found = { pre: false, tiny: false }
|
|
110
|
+
File.open("#{version_file}.new", "w") do |f|
|
|
111
|
+
File.readlines(version_file).each do |line|
|
|
112
|
+
match =
|
|
113
|
+
if pre.nil?
|
|
114
|
+
/^(\s+PRE\s+=\s+)nil(\s*)$/.match(line)
|
|
115
|
+
else
|
|
116
|
+
/^(\s+PRE\s+=\s+")#{pre}("\s*)$/.match(line)
|
|
117
|
+
end
|
|
118
|
+
if match
|
|
119
|
+
prefix = match[1]
|
|
120
|
+
postfix = match[2]
|
|
121
|
+
prefix.delete_suffix!('"')
|
|
122
|
+
postfix.delete_prefix!('"')
|
|
123
|
+
new_line = "#{prefix}#{new_pre.inspect}#{postfix}"
|
|
124
|
+
puts "Changing:\n - #{line} + #{new_line}"
|
|
125
|
+
line = new_line
|
|
126
|
+
found[:pre] = true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if new_tiny != tiny
|
|
130
|
+
match = /^(\s+TINY\s+=\s+)#{tiny}(\s*)$/.match(line)
|
|
131
|
+
if match
|
|
132
|
+
prefix = match[1]
|
|
133
|
+
postfix = match[2]
|
|
134
|
+
new_line = "#{prefix}#{new_tiny}#{postfix}"
|
|
135
|
+
puts "Changing:\n - #{line} + #{new_line}"
|
|
136
|
+
line = new_line
|
|
137
|
+
found[:tiny] = true
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
f.write(line)
|
|
142
|
+
end
|
|
143
|
+
raise ArgumentError, "Cound not find line: PRE = \"#{pre}\" in #{version_file}" unless found[:pre]
|
|
144
|
+
raise ArgumentError, "Cound not find line: TINY = \"#{tiny}\" in #{version_file}" unless found[:tiny] || new_tiny == tiny
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
FileUtils.mv version_file, "#{version_file}.old"
|
|
148
|
+
FileUtils.mv "#{version_file}.new", version_file
|
|
149
|
+
Rake::Task["changelog:release"].invoke
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
namespace :vbump do
|
|
153
|
+
desc "Final release"
|
|
154
|
+
task :final do
|
|
155
|
+
change_version do |pre:, tiny:|
|
|
156
|
+
_ = tiny
|
|
157
|
+
if pre.nil?
|
|
158
|
+
{ tiny: tiny + 1, pre: nil }
|
|
159
|
+
else
|
|
160
|
+
raise ArgumentError, "Unexpected pre: #{pre}" if pre.nil?
|
|
161
|
+
|
|
162
|
+
{ pre: nil }
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
desc "Increment prerelease"
|
|
168
|
+
task :pre, [:type] do |_t, args|
|
|
169
|
+
change_version do |pre:, tiny:|
|
|
170
|
+
puts " PRE => #{pre.inspect}"
|
|
171
|
+
match = /^([a-z]+)(\d+)/.match(pre)
|
|
172
|
+
raise ArgumentError, "Unexpected pre: #{pre}" if match.nil? && args[:type].nil?
|
|
173
|
+
|
|
174
|
+
if match.nil? || (!args[:type].nil? && args[:type] != match[1])
|
|
175
|
+
if pre.nil?
|
|
176
|
+
{ pre: "#{args[:type]}1", tiny: tiny + 1 }
|
|
177
|
+
else
|
|
178
|
+
{ pre: "#{args[:type]}1" }
|
|
179
|
+
end
|
|
180
|
+
else
|
|
181
|
+
{ pre: "#{match[1]}#{match[2].to_i + 1}" }
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
81
185
|
end
|
data/SECURITY.md
ADDED
|
@@ -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).
|
data/lib/net/scp/download.rb
CHANGED
|
@@ -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
|
|
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] =
|
|
132
|
+
channel[:local] = local_path_for(channel, directive[:name])
|
|
130
133
|
|
|
131
|
-
if File.
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|