sshkit 1.21.5 → 1.23.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45797b636d02d174676019185c1199342f1ca21f8b72c09acd1cbebf44bb9263
4
- data.tar.gz: fc271769aefc5c107ff1c406a54d3d085d062d8a24dbd0946b9763170f338d19
3
+ metadata.gz: f40ad7a1382ef707a259094c945692f08c44392e0bba7c37fe4099e748301a5e
4
+ data.tar.gz: 72f3fea395eaaa0036dd701c04692e75c379867465cffaa4ac6cdf1c4f647813
5
5
  SHA512:
6
- metadata.gz: 30396905e750c8f3a82466eb90486eb0287bd716db6762fa9cdb67953369f42decbb44c19915983253286d57cc8a7ba061c05c5cc06f623fcdc884473af7fc1d
7
- data.tar.gz: 6d15cb5f1d640fec3767d7a1130fe2ce5c723364b9e1678b94baeb55994bb2978de03e1790aca017f894efbd4b8f29d5b02556967cbbebcea43e31ea360d599f
6
+ metadata.gz: ab28074eba7cb9bdbfcbca9857f5e74fc89ed06acdf872148fcb7329b45754a714c658dacd879fead076de3bbd2541d84bcfd8ef66369007a1444b3edc2b3ac3
7
+ data.tar.gz: a6d0deb01db101ba2a41dcfcd861a10c7604b17ebe5e25981eebe6d74a0b5c9a9ea345d2dcbb45a4ba30952b571b6736f3bdcd7721d7d8fe684db772cc723700
@@ -0,0 +1,6 @@
1
+ FROM ubuntu:22.04
2
+ WORKDIR /provision
3
+ COPY ./ubuntu_setup.sh ./
4
+ RUN ./ubuntu_setup.sh
5
+ EXPOSE 22
6
+ CMD ["/usr/sbin/sshd", "-D"]
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ export DEBIAN_FRONTEND=noninteractive
6
+ apt -y update
7
+
8
+ # Create `deployer` user that can sudo without a password
9
+ apt-get -y install sudo
10
+ adduser --disabled-password deployer < /dev/null
11
+ echo "deployer:topsecret" | chpasswd
12
+ echo "deployer ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
13
+
14
+ # Install and configure sshd
15
+ apt-get -y install openssh-server
16
+ {
17
+ echo "Port 22"
18
+ echo "PasswordAuthentication yes"
19
+ echo "ChallengeResponseAuthentication no"
20
+ } >> /etc/ssh/sshd_config
21
+ mkdir /var/run/sshd
22
+ chmod 0755 /var/run/sshd
@@ -1,5 +1,5 @@
1
- name-template: "$NEXT_PATCH_VERSION"
2
- tag-template: "v$NEXT_PATCH_VERSION"
1
+ name-template: "$RESOLVED_VERSION"
2
+ tag-template: "v$RESOLVED_VERSION"
3
3
  categories:
4
4
  - title: "⚠️ Breaking Changes"
5
5
  label: "⚠️ Breaking"
@@ -11,7 +11,15 @@ categories:
11
11
  label: "📚 Docs"
12
12
  - title: "🏠 Housekeeping"
13
13
  label: "🏠 Housekeeping"
14
+ version-resolver:
15
+ minor:
16
+ labels:
17
+ - "⚠️ Breaking"
18
+ - "✨ Feature"
19
+ default: patch
14
20
  change-template: "- $TITLE (#$NUMBER) @$AUTHOR"
15
21
  no-changes-template: "- No changes"
16
22
  template: |
17
23
  $CHANGES
24
+
25
+ **Full Changelog:** https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
@@ -8,9 +8,21 @@ jobs:
8
8
  runs-on: ubuntu-latest
9
9
  strategy:
10
10
  matrix:
11
- ruby: ["2.3", "2.4", "2.5", "2.6", "2.7", "3.0", "3.1", "3.2", "head"]
11
+ ruby:
12
+ [
13
+ "2.3",
14
+ "2.4",
15
+ "2.5",
16
+ "2.6",
17
+ "2.7",
18
+ "3.0",
19
+ "3.1",
20
+ "3.2",
21
+ "3.3",
22
+ "head",
23
+ ]
12
24
  steps:
13
- - uses: actions/checkout@v3
25
+ - uses: actions/checkout@v4
14
26
  - name: Set up Ruby
15
27
  uses: ruby/setup-ruby@v1
16
28
  with:
@@ -25,7 +37,7 @@ jobs:
25
37
  matrix:
26
38
  ruby: ["2.0", "2.1", "2.2"]
27
39
  steps:
28
- - uses: actions/checkout@v3
40
+ - uses: actions/checkout@v4
29
41
  - name: Set up Ruby
30
42
  uses: ruby/setup-ruby@v1
31
43
  with:
@@ -49,7 +61,7 @@ jobs:
49
61
  rubocop:
50
62
  runs-on: ubuntu-latest
51
63
  steps:
52
- - uses: actions/checkout@v3
64
+ - uses: actions/checkout@v4
53
65
  - name: Set up Ruby
54
66
  uses: ruby/setup-ruby@v1
55
67
  with:
@@ -59,44 +71,17 @@ jobs:
59
71
  run: bundle exec rake lint
60
72
 
61
73
  functional:
62
- runs-on: macos-12
74
+ runs-on: ubuntu-latest
63
75
  strategy:
64
76
  matrix:
65
- ruby:
66
- [
67
- "2.0",
68
- "2.1",
69
- "2.2",
70
- "2.3",
71
- "2.4",
72
- "2.5",
73
- "2.6",
74
- "2.7",
75
- "3.0",
76
- "3.1",
77
- "3.2",
78
- "head",
79
- ]
77
+ ruby: ["2.0", "ruby"]
80
78
  steps:
81
- - uses: actions/checkout@v3
82
-
83
- - name: Cache Vagrant boxes
84
- uses: actions/cache@v3
85
- with:
86
- path: ~/.vagrant.d/boxes
87
- key: ${{ runner.os }}-vagrant-v2-${{ hashFiles('Vagrantfile') }}
88
- restore-keys: |
89
- ${{ runner.os }}-vagrant-v2-
90
-
91
- - name: Run vagrant up
92
- run: vagrant up
93
-
79
+ - uses: actions/checkout@v4
94
80
  - name: Set up Ruby
95
81
  uses: ruby/setup-ruby@v1
96
82
  with:
97
83
  ruby-version: ${{ matrix.ruby }}
98
84
  bundler-cache: true
99
-
100
85
  - name: Run functional tests
101
86
  run: bundle exec rake test:functional
102
87
 
@@ -7,6 +7,6 @@ jobs:
7
7
  steps:
8
8
  - uses: actions/checkout@master
9
9
  - name: Draft Release
10
- uses: toolmantim/release-drafter@v5.24.0
10
+ uses: toolmantim/release-drafter@v6.0.0
11
11
  env:
12
12
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
data/.gitignore CHANGED
@@ -3,6 +3,5 @@
3
3
  bin/rake
4
4
  .bundle
5
5
  .yardoc
6
- .vagrant*
7
6
  test/tmp
8
7
  Gemfile.lock
data/.rubocop_todo.yml CHANGED
@@ -102,7 +102,6 @@ Layout/IndentHash:
102
102
  Exclude:
103
103
  - 'test/functional/backends/test_local.rb'
104
104
  - 'test/functional/backends/test_netssh.rb'
105
- - 'test/support/vagrant_wrapper.rb'
106
105
  - 'test/unit/formatters/test_custom.rb'
107
106
  - 'test/unit/formatters/test_pretty.rb'
108
107
  - 'test/unit/test_mapping_interaction_handler.rb'
@@ -445,12 +444,6 @@ Style/MethodName:
445
444
  Exclude:
446
445
  - 'test/unit/test_color.rb'
447
446
 
448
- # Offense count: 1
449
- # Cop supports --auto-correct.
450
- Style/MutableConstant:
451
- Exclude:
452
- - 'Vagrantfile'
453
-
454
447
  # Offense count: 1
455
448
  # Cop supports --auto-correct.
456
449
  # Configuration parameters: Strict.
data/CONTRIBUTING.md CHANGED
@@ -24,8 +24,8 @@ using unsupported features.
24
24
 
25
25
  ## Tests
26
26
 
27
- SSHKit has a unit test suite and a functional test suite. Some functional tests run against
28
- [Vagrant](https://www.vagrantup.com/) VMs. If possible, you should make sure that the
27
+ SSHKit has a unit test suite and a functional test suite. Some functional tests run using
28
+ [Docker](https://docs.docker.com/get-docker/). If possible, you should make sure that the
29
29
  tests pass for each commit by running `rake` in the sshkit directory. This is in case we
30
30
  need to cherry pick commits or rebase. You should ensure the tests pass, (preferably on
31
31
  the minimum and maximum ruby version), before creating a PR.
data/EXAMPLES.md CHANGED
@@ -121,9 +121,6 @@ on hosts do |host|
121
121
  end
122
122
  ```
123
123
 
124
- **Note:** The `upload!()` method doesn't honor the values of `as()` etc, this
125
- will be improved as the library matures, but we're not there yet.
126
-
127
124
  ## Upload a file from a stream
128
125
 
129
126
  ```ruby
@@ -148,9 +145,6 @@ end
148
145
  This spares one from having to figure out the correct escaping sequences for
149
146
  something like "echo(:cat, '...?...', '> /etc/sudoers.d/yolo')".
150
147
 
151
- **Note:** The `upload!()` method doesn't honor the values of `within()`, `as()`
152
- etc, this will be improved as the library matures, but we're not there yet.
153
-
154
148
  ## Upload a directory of files
155
149
 
156
150
  ```ruby
@@ -160,7 +154,25 @@ end
160
154
  ```
161
155
 
162
156
  In this case the `recursive: true` option mirrors the same options which are
163
- available to [`Net::{SCP,SFTP}`](http://net-ssh.github.io/net-scp/).
157
+ available to [`Net::SCP`](https://github.com/net-ssh/net-scp) and
158
+ [`Net::SFTP`](https://github.com/net-ssh/net-sftp).
159
+
160
+ ## Set the upload/download method (SCP or SFTP).
161
+
162
+ SSHKit can use SCP or SFTP for file transfers. The default is SCP, but this can be changed to SFTP per host:
163
+
164
+ ```ruby
165
+ host = SSHKit::Host.new('user@example.com')
166
+ host.transfer_method = :sftp
167
+ ```
168
+
169
+ or globally:
170
+
171
+ ```ruby
172
+ SSHKit::Backend::Netssh.configure do |ssh|
173
+ ssh.transfer_method = :sftp
174
+ end
175
+ ```
164
176
 
165
177
  ## Setting global SSH options
166
178
 
@@ -235,16 +247,16 @@ end
235
247
 
236
248
  ```ruby
237
249
  # The default format is pretty, which outputs colored text
238
- SSHKit.config.format = :pretty
250
+ SSHKit.config.use_format :pretty
239
251
 
240
252
  # Text with no coloring
241
- SSHKit.config.format = :simpletext
253
+ SSHKit.config.use_format :simpletext
242
254
 
243
255
  # Red / Green dots for each completed step
244
- SSHKit.config.format = :dot
256
+ SSHKit.config.use_format :dot
245
257
 
246
258
  # No output
247
- SSHKit.config.format = :blackhole
259
+ SSHKit.config.use_format :blackhole
248
260
  ```
249
261
 
250
262
  ## Implement a dirt-simple formatter class
@@ -254,7 +266,7 @@ module SSHKit
254
266
  module Formatter
255
267
  class MyFormatter < SSHKit::Formatter::Abstract
256
268
  def write(obj)
257
- case obj.is_a? SSHKit::Command
269
+ if obj.is_a? SSHKit::Command
258
270
  # Do something here, see the SSHKit::Command documentation
259
271
  end
260
272
  end
@@ -263,7 +275,7 @@ module SSHKit
263
275
  end
264
276
 
265
277
  # If your formatter is defined in the SSHKit::Formatter module configure with the format option:
266
- SSHKit.config.format = :myformatter
278
+ SSHKit.config.use_format :myformatter
267
279
 
268
280
  # Or configure the output directly
269
281
  SSHKit.config.output = MyFormatter.new($stdout)
data/README.md CHANGED
@@ -68,7 +68,8 @@ you can pass the `strip: false` option: `capture(:ls, '-l', strip: false)`
68
68
  ### Transferring files
69
69
 
70
70
  All backends also support the `upload!` and `download!` methods for transferring files.
71
- For the remote backend, the file is transferred with scp.
71
+ For the remote backend, the file is transferred with scp by default, but sftp is also
72
+ supported. See [EXAMPLES.md](EXAMPLES.md) for details.
72
73
 
73
74
  ```ruby
74
75
  on '1.example.com' do
data/RELEASING.md CHANGED
@@ -9,7 +9,7 @@
9
9
  ## How to release
10
10
 
11
11
  1. Run `bundle install` to make sure that you have all the gems necessary for testing and releasing.
12
- 2. **Ensure the tests are passing by running `rake test`.** If functional tests fail, ensure you have [Vagrant](https://www.vagrantup.com) installed and have started it with `vagrant up`.
12
+ 2. **Ensure the tests are passing by running `rake test`.** If functional tests fail, ensure you have [Docker installed](https://docs.docker.com/get-docker/) and running.
13
13
  3. Determine which would be the correct next version number according to [semver](http://semver.org/).
14
14
  4. Update the version in `./lib/sshkit/version.rb`.
15
15
  5. Commit the `version.rb` change with a message like "Preparing vX.Y.Z"
data/Rakefile CHANGED
@@ -21,10 +21,6 @@ namespace :test do
21
21
 
22
22
  end
23
23
 
24
- Rake::Task["test:functional"].enhance do
25
- warn "Remember there are still some VMs running, kill them with `vagrant halt` if you are finished using them."
26
- end
27
-
28
24
  desc 'Run RuboCop lint checks'
29
25
  RuboCop::RakeTask.new(:lint) do |task|
30
26
  task.options = ['--lint']
@@ -0,0 +1,8 @@
1
+ name: sshkit
2
+
3
+ services:
4
+ ssh_server:
5
+ build:
6
+ context: .docker
7
+ ports:
8
+ - "2122:22"
@@ -36,8 +36,8 @@ class SSHKit::Backend::ConnectionPool::Cache
36
36
  def evict
37
37
  # Peek at the first connection to see if it is still fresh. If so, we can
38
38
  # return right away without needing to use `synchronize`.
39
- first_expires_at, first_conn = connections.first
40
- return if (first_expires_at.nil? || fresh?(first_expires_at)) && !closed?(first_conn)
39
+ first_expires_at, _first_conn = connections.first
40
+ return if (first_expires_at.nil? || fresh?(first_expires_at))
41
41
 
42
42
  connections.synchronize do
43
43
  fresh, stale = connections.partition do |expires_at, conn|
@@ -1,3 +1,5 @@
1
+ require "base64"
2
+
1
3
  module SSHKit
2
4
 
3
5
  module Backend
@@ -5,12 +7,11 @@ module SSHKit
5
7
  class Netssh < Abstract
6
8
 
7
9
  class KnownHostsKeys
8
- include Mutex_m
9
-
10
10
  def initialize(path)
11
11
  super()
12
12
  @path = File.expand_path(path)
13
13
  @hosts_keys = nil
14
+ @mutex = Mutex.new
14
15
  end
15
16
 
16
17
  def keys_for(hostlist)
@@ -44,7 +45,7 @@ module SSHKit
44
45
  end
45
46
 
46
47
  def parse_file
47
- synchronize do
48
+ @mutex.synchronize do
48
49
  return if hosts_keys && hosts_hashes
49
50
 
50
51
  unless File.readable?(path)
@@ -110,11 +111,10 @@ module SSHKit
110
111
  end
111
112
 
112
113
  class KnownHosts
113
- include Mutex_m
114
-
115
114
  def initialize
116
115
  super()
117
116
  @files = {}
117
+ @mutex = Mutex.new
118
118
  end
119
119
 
120
120
  def search_for(host, options = {})
@@ -126,13 +126,13 @@ module SSHKit
126
126
 
127
127
  def add(*args)
128
128
  ::Net::SSH::KnownHosts.add(*args)
129
- synchronize { @files = {} }
129
+ @mutex.synchronize { @files = {} }
130
130
  end
131
131
 
132
132
  private
133
133
 
134
134
  def known_hosts_file(path)
135
- @files[path] || synchronize { @files[path] ||= KnownHostsKeys.new(path) }
135
+ @files[path] || @mutex.synchronize { @files[path] ||= KnownHostsKeys.new(path) }
136
136
  end
137
137
  end
138
138
 
@@ -140,4 +140,4 @@ module SSHKit
140
140
 
141
141
  end
142
142
 
143
- end
143
+ end
@@ -0,0 +1,26 @@
1
+ require "net/scp"
2
+
3
+ module SSHKit
4
+ module Backend
5
+ class Netssh < Abstract
6
+ class ScpTransfer
7
+ def initialize(ssh, summarizer)
8
+ @ssh = ssh
9
+ @summarizer = summarizer
10
+ end
11
+
12
+ def upload!(local, remote, options)
13
+ ssh.scp.upload!(local, remote, options, &summarizer)
14
+ end
15
+
16
+ def download!(remote, local, options)
17
+ ssh.scp.download!(remote, local, options, &summarizer)
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :ssh, :summarizer
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ require "net/sftp"
2
+
3
+ module SSHKit
4
+ module Backend
5
+ class Netssh < Abstract
6
+ class SftpTransfer
7
+ def initialize(ssh, summarizer)
8
+ @ssh = ssh
9
+ @summarizer = summarizer
10
+ end
11
+
12
+ def upload!(local, remote, options)
13
+ options = { progress: self }.merge(options || {})
14
+ ssh.sftp.connect!
15
+ ssh.sftp.upload!(local, remote, options)
16
+ ensure
17
+ ssh.sftp.close_channel
18
+ end
19
+
20
+ def download!(remote, local, options)
21
+ options = { progress: self }.merge(options || {})
22
+ destination = local ? local : StringIO.new.tap { |io| io.set_encoding('BINARY') }
23
+
24
+ ssh.sftp.connect!
25
+ ssh.sftp.download!(remote, destination, options)
26
+ local ? true : destination.string
27
+ ensure
28
+ ssh.sftp.close_channel
29
+ end
30
+
31
+ def on_get(download, entry, offset, data)
32
+ entry.size ||= download.sftp.file.open(entry.remote) { |file| file.stat.size }
33
+ summarizer.call(nil, entry.remote, offset + data.bytesize, entry.size)
34
+ end
35
+
36
+ def on_put(_upload, file, offset, data)
37
+ summarizer.call(nil, file.local, offset + data.bytesize, file.size)
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :ssh, :summarizer
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,8 +1,6 @@
1
1
  require 'English'
2
2
  require 'strscan'
3
- require 'mutex_m'
4
3
  require 'net/ssh'
5
- require 'net/scp'
6
4
 
7
5
  module Net
8
6
  module SSH
@@ -23,10 +21,27 @@ module SSHKit
23
21
  module Backend
24
22
 
25
23
  class Netssh < Abstract
24
+ def self.assert_valid_transfer_method!(method)
25
+ return if [:scp, :sftp].include?(method)
26
+
27
+ raise ArgumentError, "#{method.inspect} is not a valid transfer method. Supported methods are :scp, :sftp."
28
+ end
29
+
26
30
  class Configuration
27
31
  attr_accessor :connection_timeout, :pty
32
+ attr_reader :transfer_method
28
33
  attr_writer :ssh_options
29
34
 
35
+ def initialize
36
+ self.transfer_method = :scp
37
+ end
38
+
39
+ def transfer_method=(method)
40
+ Netssh.assert_valid_transfer_method!(method)
41
+
42
+ @transfer_method = method
43
+ end
44
+
30
45
  def ssh_options
31
46
  default_options.merge(@ssh_options ||= {})
32
47
  end
@@ -64,16 +79,16 @@ module SSHKit
64
79
  def upload!(local, remote, options = {})
65
80
  summarizer = transfer_summarizer('Uploading', options)
66
81
  remote = File.join(pwd_path, remote) unless remote.to_s.start_with?("/") || pwd_path.nil?
67
- with_ssh do |ssh|
68
- ssh.scp.upload!(local, remote, options, &summarizer)
82
+ with_transfer(summarizer) do |transfer|
83
+ transfer.upload!(local, remote, options)
69
84
  end
70
85
  end
71
86
 
72
87
  def download!(remote, local=nil, options = {})
73
88
  summarizer = transfer_summarizer('Downloading', options)
74
89
  remote = File.join(pwd_path, remote) unless remote.to_s.start_with?("/") || pwd_path.nil?
75
- with_ssh do |ssh|
76
- ssh.scp.download!(remote, local, options, &summarizer)
90
+ with_transfer(summarizer) do |transfer|
91
+ transfer.download!(remote, local, options)
77
92
  end
78
93
  end
79
94
 
@@ -105,7 +120,7 @@ module SSHKit
105
120
  last_percentage = nil
106
121
  proc do |_ch, name, transferred, total|
107
122
  percentage = (transferred.to_f * 100 / total.to_f)
108
- unless percentage.nan?
123
+ unless percentage.nan? || percentage.infinite?
109
124
  message = "#{action} #{name} #{percentage.round(2)}%"
110
125
  percentage_r = (percentage / log_percent).truncate * log_percent
111
126
  if percentage_r > 0 && (last_name != name || last_percentage != percentage_r)
@@ -183,6 +198,20 @@ module SSHKit
183
198
  )
184
199
  end
185
200
 
201
+ def with_transfer(summarizer)
202
+ transfer_method = host.transfer_method || self.class.config.transfer_method
203
+ transfer_class = if transfer_method == :sftp
204
+ require_relative "netssh/sftp_transfer"
205
+ SftpTransfer
206
+ else
207
+ require_relative "netssh/scp_transfer"
208
+ ScpTransfer
209
+ end
210
+
211
+ with_ssh do |ssh|
212
+ yield(transfer_class.new(ssh, summarizer))
213
+ end
214
+ end
186
215
  end
187
216
  end
188
217
 
data/lib/sshkit/host.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'ostruct'
2
+ require 'resolv'
2
3
 
3
4
  module SSHKit
4
5
 
@@ -7,6 +8,7 @@ module SSHKit
7
8
  class Host
8
9
 
9
10
  attr_accessor :password, :hostname, :port, :user, :ssh_options
11
+ attr_reader :transfer_method
10
12
 
11
13
  def key=(new_key)
12
14
  @keys = [new_key]
@@ -41,6 +43,12 @@ module SSHKit
41
43
  end
42
44
  end
43
45
 
46
+ def transfer_method=(method)
47
+ Backend::Netssh.assert_valid_transfer_method!(method) unless method.nil?
48
+
49
+ @transfer_method = method
50
+ end
51
+
44
52
  def local?
45
53
  @local
46
54
  end
@@ -115,6 +123,22 @@ module SSHKit
115
123
 
116
124
  end
117
125
 
126
+ # @private
127
+ # :nodoc:
128
+ class IPv6HostParser < SimpleHostParser
129
+ def self.suitable?(host_string)
130
+ host_string.match(Resolv::IPv6::Regex)
131
+ end
132
+
133
+ def port
134
+
135
+ end
136
+
137
+ def hostname
138
+ @host_string.match(Resolv::IPv6::Regex)[0]
139
+ end
140
+ end
141
+
118
142
  class HostWithPortParser < SimpleHostParser
119
143
 
120
144
  def self.suitable?(host_string)
@@ -185,6 +209,7 @@ module SSHKit
185
209
 
186
210
  PARSERS = [
187
211
  SimpleHostParser,
212
+ IPv6HostParser,
188
213
  HostWithPortParser,
189
214
  HostWithUsernameAndPortParser,
190
215
  IPv6HostWithPortParser,
@@ -1,3 +1,3 @@
1
1
  module SSHKit
2
- VERSION = "1.21.5".freeze
2
+ VERSION = "1.23.0".freeze
3
3
  end
data/sshkit.gemspec CHANGED
@@ -20,8 +20,10 @@ Gem::Specification.new do |gem|
20
20
  gem.require_paths = ["lib"]
21
21
  gem.version = SSHKit::VERSION
22
22
 
23
+ gem.add_runtime_dependency('base64') if RUBY_VERSION >= "2.4"
23
24
  gem.add_runtime_dependency('net-ssh', '>= 2.8.0')
24
25
  gem.add_runtime_dependency('net-scp', '>= 1.1.2')
26
+ gem.add_runtime_dependency('net-sftp', '>= 2.1.2')
25
27
 
26
28
  gem.add_development_dependency('danger')
27
29
  gem.add_development_dependency('minitest', '>= 5.0.0')