net-sftp 4.0.0 → 4.1.0
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 +25 -23
- data/.gitignore +1 -0
- data/CHANGES.txt +45 -0
- data/CONTRIBUTING.md +33 -0
- data/Gemfile +7 -2
- data/README.rdoc +3 -0
- data/Rakefile +124 -1
- data/changelog.d/README.md +1 -0
- data/lib/net/sftp/operations/dir.rb +5 -4
- data/lib/net/sftp/operations/download.rb +236 -29
- data/lib/net/sftp/operations/file.rb +60 -26
- data/lib/net/sftp/operations/upload.rb +102 -11
- data/lib/net/sftp/protocol/01/attributes.rb +6 -2
- data/lib/net/sftp/protocol/01/base.rb +2 -1
- data/lib/net/sftp/protocol/01/name.rb +1 -1
- data/lib/net/sftp/protocol/03/base.rb +11 -1
- data/lib/net/sftp/protocol/04/attributes.rb +12 -2
- data/lib/net/sftp/protocol/04/base.rb +2 -1
- data/lib/net/sftp/protocol/04/name.rb +23 -14
- data/lib/net/sftp/protocol/05/base.rb +9 -6
- data/lib/net/sftp/protocol/base.rb +10 -2
- data/lib/net/sftp/protocol/bounded_read.rb +25 -0
- data/lib/net/sftp/session.rb +21 -3
- data/lib/net/sftp/version.rb +1 -1
- data/lib/net/sftp.rb +16 -10
- data/net-sftp-public_cert.pem +19 -18
- data/net-sftp.gemspec +4 -3
- data.tar.gz.sig +1 -1
- metadata +33 -27
- metadata.gz.sig +4 -4
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'pathname'
|
|
1
2
|
require 'net/ssh/loggable'
|
|
2
3
|
|
|
3
4
|
module Net; module SFTP; module Operations
|
|
@@ -50,9 +51,20 @@ module Net; module SFTP; module Operations
|
|
|
50
51
|
# higher might improve throughput. Reducing it will reduce throughput.
|
|
51
52
|
# * <tt>:read_size</tt> - the maximum number of bytes to read at a time
|
|
52
53
|
# from the source. Increasing this value might improve throughput. It
|
|
53
|
-
# defaults to 32,000 bytes.
|
|
54
|
+
# defaults to 32,000 bytes. Each chunk is sent as one write request, so
|
|
55
|
+
# it must stay within the server's packet limit (OpenSSH drops the
|
|
56
|
+
# connection on packets over 256 KiB).
|
|
54
57
|
# * <tt>:name</tt> - the filename to report to the progress monitor when
|
|
55
58
|
# an IO object is given as +local+. This defaults to "<memory>".
|
|
59
|
+
# * <tt>:preserve</tt> - if true, the remote files and directories get the
|
|
60
|
+
# access and modification times of the local ones, and their permissions
|
|
61
|
+
# are set even on files that already exist remotely.
|
|
62
|
+
#
|
|
63
|
+
# Like OpenSSH's sftp client, new remote files and directories are created
|
|
64
|
+
# with the permissions of the local ones, without setuid, setgid and sticky
|
|
65
|
+
# bits and subject to the server's umask. Nothing is set when +local+ is an
|
|
66
|
+
# IO object. If the server refuses to set the attributes after the data has
|
|
67
|
+
# been written, a warning is logged and the upload still succeeds.
|
|
56
68
|
#
|
|
57
69
|
# == Progress Monitoring
|
|
58
70
|
#
|
|
@@ -141,6 +153,9 @@ module Net; module SFTP; module Operations
|
|
|
141
153
|
# This will return immediately, and requires that the SSH event loop be
|
|
142
154
|
# run in order to effect the upload. (See #wait.)
|
|
143
155
|
def initialize(sftp, local, remote, options={}, &progress) #:nodoc:
|
|
156
|
+
local = local.to_path if local.is_a?(Pathname)
|
|
157
|
+
remote = remote.to_path if remote.is_a?(Pathname)
|
|
158
|
+
|
|
144
159
|
@sftp = sftp
|
|
145
160
|
@local = local
|
|
146
161
|
@remote = remote
|
|
@@ -148,11 +163,14 @@ module Net; module SFTP; module Operations
|
|
|
148
163
|
@options = options
|
|
149
164
|
@properties = options[:properties] || {}
|
|
150
165
|
@active = 0
|
|
166
|
+
@directories = []
|
|
151
167
|
|
|
152
168
|
self.logger = sftp.logger
|
|
153
169
|
|
|
154
170
|
@uploads = []
|
|
155
171
|
@recursive = local.respond_to?(:read) ? false : ::File.directory?(local)
|
|
172
|
+
requests
|
|
173
|
+
read_size
|
|
156
174
|
|
|
157
175
|
if recursive?
|
|
158
176
|
@stack = [entries_for(local)]
|
|
@@ -161,10 +179,10 @@ module Net; module SFTP; module Operations
|
|
|
161
179
|
|
|
162
180
|
@active += 1
|
|
163
181
|
if @options[:mkdir]
|
|
164
|
-
sftp.mkdir(remote) do |response|
|
|
182
|
+
sftp.mkdir(remote, directory_attributes(local, remote)) do |response|
|
|
165
183
|
@active -= 1
|
|
166
184
|
raise StatusException.new(response, "mkdir `#{remote}'") unless response.ok?
|
|
167
|
-
|
|
185
|
+
requests.times do
|
|
168
186
|
break unless process_next_entry
|
|
169
187
|
end
|
|
170
188
|
end
|
|
@@ -225,9 +243,28 @@ module Net; module SFTP; module Operations
|
|
|
225
243
|
# The progress handler for this instance. Possibly nil.
|
|
226
244
|
def progress; @progress; end
|
|
227
245
|
|
|
246
|
+
def requests
|
|
247
|
+
@requests ||= positive_option(:requests, recursive? ? RECURSIVE_READERS : SINGLE_FILE_READERS)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def read_size
|
|
251
|
+
@read_size ||= positive_option(:read_size, DEFAULT_READ_SIZE)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def positive_option(name, default)
|
|
255
|
+
value = (options[name] || default).to_i
|
|
256
|
+
raise ArgumentError, ":#{name} must be positive" unless value > 0
|
|
257
|
+
|
|
258
|
+
value
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def preserve?
|
|
262
|
+
options[:preserve]
|
|
263
|
+
end
|
|
264
|
+
|
|
228
265
|
# A simple struct for recording metadata about the file currently being
|
|
229
266
|
# uploaded.
|
|
230
|
-
LiveFile = Struct.new(:local, :remote, :io, :size, :handle)
|
|
267
|
+
LiveFile = Struct.new(:local, :remote, :io, :size, :handle, :attributes)
|
|
231
268
|
|
|
232
269
|
# The default # of bytes to read from disk at a time.
|
|
233
270
|
DEFAULT_READ_SIZE = 32_000
|
|
@@ -245,7 +282,11 @@ module Net; module SFTP; module Operations
|
|
|
245
282
|
if @uploads.any?
|
|
246
283
|
write_next_chunk(@uploads.first)
|
|
247
284
|
elsif !active?
|
|
248
|
-
|
|
285
|
+
if @directories.any?
|
|
286
|
+
set_directory_attributes
|
|
287
|
+
else
|
|
288
|
+
update_progress(:finish)
|
|
289
|
+
end
|
|
249
290
|
end
|
|
250
291
|
return false
|
|
251
292
|
elsif @stack.last.empty?
|
|
@@ -265,7 +306,7 @@ module Net; module SFTP; module Operations
|
|
|
265
306
|
|
|
266
307
|
@active += 1
|
|
267
308
|
update_progress(:mkdir, rpath)
|
|
268
|
-
request = sftp.mkdir(rpath, &method(:on_mkdir))
|
|
309
|
+
request = sftp.mkdir(rpath, directory_attributes(lpath, rpath), &method(:on_mkdir))
|
|
269
310
|
request[:dir] = rpath
|
|
270
311
|
else
|
|
271
312
|
open_file(lpath, rpath)
|
|
@@ -294,10 +335,11 @@ module Net; module SFTP; module Operations
|
|
|
294
335
|
size = file.size
|
|
295
336
|
end
|
|
296
337
|
|
|
297
|
-
|
|
338
|
+
attributes = local.respond_to?(:read) ? {} : attributes_for(file.stat)
|
|
339
|
+
metafile = LiveFile.new(name, remote, file, size, nil, attributes)
|
|
298
340
|
update_progress(:open, metafile)
|
|
299
341
|
|
|
300
|
-
request = sftp.open(remote, "w", &method(:on_open))
|
|
342
|
+
request = sftp.open(remote, "w", attributes, &method(:on_open))
|
|
301
343
|
request[:file] = metafile
|
|
302
344
|
end
|
|
303
345
|
|
|
@@ -326,7 +368,7 @@ module Net; module SFTP; module Operations
|
|
|
326
368
|
write_next_chunk(file)
|
|
327
369
|
|
|
328
370
|
if !recursive?
|
|
329
|
-
(
|
|
371
|
+
(requests - 1).times { write_next_chunk(file) }
|
|
330
372
|
end
|
|
331
373
|
end
|
|
332
374
|
|
|
@@ -350,6 +392,49 @@ module Net; module SFTP; module Operations
|
|
|
350
392
|
process_next_entry
|
|
351
393
|
end
|
|
352
394
|
|
|
395
|
+
# setuid, setgid and sticky bits are never sent, matching OpenSSH.
|
|
396
|
+
def attributes_for(stat)
|
|
397
|
+
attributes = { :permissions => stat.mode & 0777 }
|
|
398
|
+
attributes.update(:atime => stat.atime.to_i, :mtime => stat.mtime.to_i) if preserve?
|
|
399
|
+
attributes
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# The directory is created writable and searchable by its owner so its
|
|
403
|
+
# contents can be uploaded; its real attributes are set once everything
|
|
404
|
+
# else is done (see #set_directory_attributes).
|
|
405
|
+
def directory_attributes(local, remote)
|
|
406
|
+
stat = ::File.stat(local)
|
|
407
|
+
@directories << [remote, attributes_for(stat)]
|
|
408
|
+
{ :permissions => (stat.mode & 0777) | 0300 }
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Deepest directories first; setting attributes on a directory doesn't
|
|
412
|
+
# change its parent's modification time.
|
|
413
|
+
def set_directory_attributes
|
|
414
|
+
@directories.reverse_each do |path, attributes|
|
|
415
|
+
@active += 1
|
|
416
|
+
sftp.setstat(path, attributes) do |response|
|
|
417
|
+
@active -= 1
|
|
418
|
+
warn_unless_ok(response, "setstat #{path}")
|
|
419
|
+
process_next_entry
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
@directories.clear
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Called when the +fsetstat+ for :preserve finishes.
|
|
426
|
+
def on_fsetstat(response)
|
|
427
|
+
@active -= 1
|
|
428
|
+
file = response.request[:file]
|
|
429
|
+
warn_unless_ok(response, "fsetstat #{file.remote}")
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Like OpenSSH's sftp client, failing to set attributes only warns; the
|
|
433
|
+
# data has already been transferred.
|
|
434
|
+
def warn_unless_ok(response, operation)
|
|
435
|
+
lwarn { "#{operation} failed: #{response}" } unless response.ok?
|
|
436
|
+
end
|
|
437
|
+
|
|
353
438
|
# Attempts to send the next chunk from the given file (where +file+ is
|
|
354
439
|
# a LiveFile instance).
|
|
355
440
|
def write_next_chunk(file)
|
|
@@ -358,12 +443,18 @@ module Net; module SFTP; module Operations
|
|
|
358
443
|
else
|
|
359
444
|
@active += 1
|
|
360
445
|
offset = file.io.pos
|
|
361
|
-
data = file.io.read(
|
|
446
|
+
data = file.io.read(read_size)
|
|
362
447
|
if data.nil?
|
|
363
448
|
update_progress(:close, file)
|
|
449
|
+
if preserve? && file.attributes.any?
|
|
450
|
+
@active += 1
|
|
451
|
+
request = sftp.fsetstat(file.handle, file.attributes, &method(:on_fsetstat))
|
|
452
|
+
request[:file] = file
|
|
453
|
+
end
|
|
364
454
|
request = sftp.close(file.handle, &method(:on_close))
|
|
365
455
|
request[:file] = file
|
|
366
|
-
|
|
456
|
+
# An IO passed in by the caller is theirs to close.
|
|
457
|
+
file.io.close unless file.io.equal?(local)
|
|
367
458
|
file.io = nil
|
|
368
459
|
@uploads.delete(file)
|
|
369
460
|
else
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'net/ssh/buffer'
|
|
2
|
+
require 'net/sftp/protocol/bounded_read'
|
|
2
3
|
|
|
3
4
|
module Net; module SFTP; module Protocol; module V01
|
|
4
5
|
|
|
@@ -39,6 +40,8 @@ module Net; module SFTP; module Protocol; module V01
|
|
|
39
40
|
T_BLOCK_DEVICE = 8
|
|
40
41
|
T_FIFO = 9
|
|
41
42
|
|
|
43
|
+
extend Net::SFTP::Protocol::BoundedRead
|
|
44
|
+
|
|
42
45
|
class <<self
|
|
43
46
|
# Returns the array of attribute meta-data that defines the structure of
|
|
44
47
|
# the attributes packet as described by this version of the protocol.
|
|
@@ -106,7 +109,8 @@ module Net; module SFTP; module Protocol; module V01
|
|
|
106
109
|
# Parse the hash of extended data from the buffer.
|
|
107
110
|
def parse_extended(buffer)
|
|
108
111
|
extended = Hash.new
|
|
109
|
-
|
|
112
|
+
# Each pair is two strings, at least 8 bytes.
|
|
113
|
+
read_bounded_count!(buffer, 8, "extended attribute").times do
|
|
110
114
|
extended[buffer.read_string] = buffer.read_string
|
|
111
115
|
end
|
|
112
116
|
extended
|
|
@@ -150,7 +154,7 @@ module Net; module SFTP; module Protocol; module V01
|
|
|
150
154
|
# * :mtime:: the modification time of the file (integer, seconds since epoch)
|
|
151
155
|
# * :extended:: a hash of name/value pairs identifying extended info
|
|
152
156
|
def initialize(attributes={})
|
|
153
|
-
@attributes = attributes
|
|
157
|
+
@attributes = attributes.dup
|
|
154
158
|
end
|
|
155
159
|
|
|
156
160
|
# Returns the user-id of the user that owns the file, or +nil+ if that
|
|
@@ -54,7 +54,8 @@ module Net; module SFTP; module Protocol; module V01
|
|
|
54
54
|
def parse_name_packet(packet)
|
|
55
55
|
names = []
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
# Each entry is two strings and the attribute flags, at least 12 bytes.
|
|
58
|
+
read_bounded_count!(packet, 12, "name").times do
|
|
58
59
|
filename = packet.read_string
|
|
59
60
|
longname = packet.read_string
|
|
60
61
|
attrs = attribute_factory.from_buffer(packet)
|
|
@@ -5,7 +5,7 @@ module Net; module SFTP; module Protocol; module V01
|
|
|
5
5
|
# for use when displaying directory data, and has no specified format.
|
|
6
6
|
class Name
|
|
7
7
|
# The name of the item on the remote server.
|
|
8
|
-
|
|
8
|
+
attr_accessor :name
|
|
9
9
|
|
|
10
10
|
# The display-ready name of the item, possibly with other attributes.
|
|
11
11
|
attr_reader :longname
|
|
@@ -18,6 +18,16 @@ module Net; module SFTP; module Protocol; module V03
|
|
|
18
18
|
3
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Version 3 adds a message and language tag to status responses.
|
|
22
|
+
def parse_status_packet(packet)
|
|
23
|
+
data = super
|
|
24
|
+
unless packet.eof?
|
|
25
|
+
data[:message] = packet.read_string
|
|
26
|
+
data[:language] = packet.read_string
|
|
27
|
+
end
|
|
28
|
+
data
|
|
29
|
+
end
|
|
30
|
+
|
|
21
31
|
# Sends a FXP_READLINK packet to the server to request that the target of
|
|
22
32
|
# the given symlink on the remote host (+path+) be returned.
|
|
23
33
|
def readlink(path)
|
|
@@ -32,4 +42,4 @@ module Net; module SFTP; module Protocol; module V03
|
|
|
32
42
|
|
|
33
43
|
end
|
|
34
44
|
|
|
35
|
-
end; end; end; end
|
|
45
|
+
end; end; end; end
|
|
@@ -69,7 +69,8 @@ module Net; module SFTP; module Protocol; module V04
|
|
|
69
69
|
def parse_acl(buffer)
|
|
70
70
|
acl_buf = Net::SSH::Buffer.new(buffer.read_string)
|
|
71
71
|
acl = []
|
|
72
|
-
|
|
72
|
+
# Each entry is three longs and a string, at least 16 bytes.
|
|
73
|
+
read_bounded_count!(acl_buf, 16, "ACL").times do
|
|
73
74
|
acl << ACL.new(acl_buf.read_long, acl_buf.read_long, acl_buf.read_long, acl_buf.read_string)
|
|
74
75
|
end
|
|
75
76
|
acl
|
|
@@ -123,7 +124,7 @@ module Net; module SFTP; module Protocol; module V04
|
|
|
123
124
|
# to T_REGULAR.
|
|
124
125
|
def initialize(attributes={})
|
|
125
126
|
super
|
|
126
|
-
|
|
127
|
+
self.type ||= T_REGULAR
|
|
127
128
|
end
|
|
128
129
|
|
|
129
130
|
private
|
|
@@ -134,6 +135,15 @@ module Net; module SFTP; module Protocol; module V04
|
|
|
134
135
|
# were given on instantiation
|
|
135
136
|
owner
|
|
136
137
|
group
|
|
138
|
+
|
|
139
|
+
# The subsecond flag applies to every timestamp present in the packet.
|
|
140
|
+
subseconds = self.class.elements.select { |_name, _type, flags| flags & F_SUBSECOND_TIMES != 0 }
|
|
141
|
+
if subseconds.any? { |name, _type, _flags| attributes[name] }
|
|
142
|
+
subseconds.each do |name, _type, _flags|
|
|
143
|
+
seconds = name.to_s.sub(/_nseconds\z/, "").to_sym
|
|
144
|
+
attributes[name] ||= 0 if attributes[seconds]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
137
147
|
end
|
|
138
148
|
|
|
139
149
|
# Performs protocol-version-specific encoding of the access control
|
|
@@ -28,7 +28,8 @@ module Net; module SFTP; module Protocol; module V04
|
|
|
28
28
|
def parse_name_packet(packet)
|
|
29
29
|
names = []
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
# Each entry is a string, the attribute flags and the type, at least 9 bytes.
|
|
32
|
+
read_bounded_count!(packet, 9, "name").times do
|
|
32
33
|
filename = packet.read_string
|
|
33
34
|
attrs = attribute_factory.from_buffer(packet)
|
|
34
35
|
names << name_factory.new(filename, attrs)
|
|
@@ -9,7 +9,7 @@ module Net; module SFTP; module Protocol; module V04
|
|
|
9
9
|
# a directory listing.
|
|
10
10
|
class Name
|
|
11
11
|
# The name of the item on the remote server.
|
|
12
|
-
|
|
12
|
+
attr_accessor :name
|
|
13
13
|
|
|
14
14
|
# Attributes instance describing this item.
|
|
15
15
|
attr_reader :attributes
|
|
@@ -44,24 +44,33 @@ module Net; module SFTP; module Protocol; module V04
|
|
|
44
44
|
"l"
|
|
45
45
|
else
|
|
46
46
|
"-"
|
|
47
|
-
end
|
|
47
|
+
end.dup
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
# Fields the server omitted are shown as "?", like GNU ls -l does for entries it cannot stat.
|
|
50
|
+
if (permissions = attributes.permissions)
|
|
51
|
+
longname << (permissions & 0400 != 0 ? "r" : "-")
|
|
52
|
+
longname << (permissions & 0200 != 0 ? "w" : "-")
|
|
53
|
+
longname << (permissions & 0100 != 0 ? "x" : "-")
|
|
54
|
+
longname << (permissions & 0040 != 0 ? "r" : "-")
|
|
55
|
+
longname << (permissions & 0020 != 0 ? "w" : "-")
|
|
56
|
+
longname << (permissions & 0010 != 0 ? "x" : "-")
|
|
57
|
+
longname << (permissions & 0004 != 0 ? "r" : "-")
|
|
58
|
+
longname << (permissions & 0002 != 0 ? "w" : "-")
|
|
59
|
+
longname << (permissions & 0001 != 0 ? "x" : "-")
|
|
60
|
+
else
|
|
61
|
+
longname << "?" * 9
|
|
62
|
+
end
|
|
58
63
|
|
|
59
|
-
longname << (" %-8s %-8s %
|
|
64
|
+
longname << (" %-8s %-8s %8s " % [attributes.owner || "?", attributes.group || "?", attributes.size || "?"])
|
|
60
65
|
|
|
61
|
-
longname <<
|
|
66
|
+
longname << if attributes.mtime
|
|
67
|
+
Time.at(attributes.mtime).strftime("%b %e %H:%M ")
|
|
68
|
+
else
|
|
69
|
+
"%12s " % "?"
|
|
70
|
+
end
|
|
62
71
|
longname << name
|
|
63
72
|
end
|
|
64
73
|
end
|
|
65
74
|
end
|
|
66
75
|
|
|
67
|
-
end; end; end; end
|
|
76
|
+
end; end; end; end
|
|
@@ -35,25 +35,28 @@ module Net; module SFTP; module Protocol; module V05
|
|
|
35
35
|
def open(path, flags, options)
|
|
36
36
|
flags = normalize_open_flags(flags)
|
|
37
37
|
|
|
38
|
-
sftp_flags
|
|
39
|
-
open = if flags & (IO::CREAT | IO::EXCL) == (IO::CREAT | IO::EXCL)
|
|
38
|
+
sftp_flags = if flags & (IO::CREAT | IO::EXCL) == (IO::CREAT | IO::EXCL)
|
|
40
39
|
FV5::CREATE_NEW
|
|
41
40
|
elsif flags & (IO::CREAT | IO::TRUNC) == (IO::CREAT | IO::TRUNC)
|
|
42
41
|
FV5::CREATE_TRUNCATE
|
|
43
42
|
elsif flags & IO::CREAT == IO::CREAT
|
|
44
43
|
FV5::OPEN_OR_CREATE
|
|
44
|
+
elsif flags & IO::TRUNC == IO::TRUNC
|
|
45
|
+
FV5::TRUNCATE_EXISTING
|
|
45
46
|
else
|
|
46
47
|
FV5::OPEN_EXISTING
|
|
47
48
|
end
|
|
49
|
+
|
|
50
|
+
desired_access = if flags & (IO::WRONLY | IO::RDWR) != 0
|
|
48
51
|
access = ACE::Mask::WRITE_DATA | ACE::Mask::WRITE_ATTRIBUTES
|
|
49
52
|
access |= ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES if (flags & IO::RDWR) == IO::RDWR
|
|
50
53
|
if flags & IO::APPEND == IO::APPEND
|
|
51
|
-
|
|
54
|
+
sftp_flags |= FV5::APPEND_DATA
|
|
52
55
|
access |= ACE::Mask::APPEND_DATA
|
|
53
56
|
end
|
|
54
|
-
|
|
57
|
+
access
|
|
55
58
|
else
|
|
56
|
-
|
|
59
|
+
ACE::Mask::READ_DATA | ACE::Mask::READ_ATTRIBUTES
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
attributes = attribute_factory.new(options)
|
|
@@ -63,4 +66,4 @@ module Net; module SFTP; module Protocol; module V05
|
|
|
63
66
|
|
|
64
67
|
end
|
|
65
68
|
|
|
66
|
-
end; end; end; end
|
|
69
|
+
end; end; end; end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'net/ssh/loggable'
|
|
2
2
|
require 'net/sftp/constants'
|
|
3
|
+
require 'net/sftp/protocol/bounded_read'
|
|
3
4
|
|
|
4
5
|
module Net; module SFTP; module Protocol
|
|
5
6
|
|
|
@@ -10,6 +11,7 @@ module Net; module SFTP; module Protocol
|
|
|
10
11
|
include Net::SSH::Loggable
|
|
11
12
|
include Net::SFTP::Constants
|
|
12
13
|
include Net::SFTP::Constants::PacketTypes
|
|
14
|
+
include Net::SFTP::Protocol::BoundedRead
|
|
13
15
|
|
|
14
16
|
# The SFTP session object that acts as client to this protocol instance
|
|
15
17
|
attr_reader :session
|
|
@@ -19,6 +21,7 @@ module Net; module SFTP; module Protocol
|
|
|
19
21
|
@session = session
|
|
20
22
|
self.logger = session.logger
|
|
21
23
|
@request_id_counter = -1
|
|
24
|
+
@request_ids_wrapped = false
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
# Attept to parse the given packet. If the packet is of an unsupported
|
|
@@ -41,10 +44,15 @@ module Net; module SFTP; module Protocol
|
|
|
41
44
|
# A new request identifier will be allocated to this request, and will
|
|
42
45
|
# be returned.
|
|
43
46
|
def send_request(type, *args)
|
|
44
|
-
|
|
47
|
+
begin
|
|
48
|
+
previous_request_id = @request_id_counter
|
|
49
|
+
@request_id_counter = (@request_id_counter + 1) & 0xffffffff
|
|
50
|
+
@request_ids_wrapped = true if previous_request_id == 0xffffffff
|
|
51
|
+
end while @request_ids_wrapped && session.pending_requests.key?(@request_id_counter)
|
|
52
|
+
|
|
45
53
|
session.send_packet(type, :long, @request_id_counter, *args)
|
|
46
54
|
return @request_id_counter
|
|
47
55
|
end
|
|
48
56
|
end
|
|
49
57
|
|
|
50
|
-
end; end; end
|
|
58
|
+
end; end; end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'net/sftp/errors'
|
|
2
|
+
|
|
3
|
+
module Net; module SFTP; module Protocol
|
|
4
|
+
|
|
5
|
+
# Several SFTP structures are a 32-bit item count followed by that many
|
|
6
|
+
# items. Net::SSH::Buffer returns nil instead of raising when a read runs
|
|
7
|
+
# past the end of the data, so a loop over an unchecked count from the
|
|
8
|
+
# server keeps allocating long after the packet is exhausted.
|
|
9
|
+
module BoundedRead
|
|
10
|
+
# Reads an item count from +buffer+ and raises Net::SFTP::Exception if it
|
|
11
|
+
# is missing, or larger than the remaining bytes could hold given that
|
|
12
|
+
# each item takes at least +min_item_size+ bytes.
|
|
13
|
+
def read_bounded_count!(buffer, min_item_size, what)
|
|
14
|
+
count = buffer.read_long
|
|
15
|
+
raise Net::SFTP::Exception, "truncated packet: missing #{what} count" if count.nil?
|
|
16
|
+
|
|
17
|
+
if count > buffer.available / min_item_size
|
|
18
|
+
raise Net::SFTP::Exception, "server sent #{count} #{what} entries in only #{buffer.available} bytes"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
count
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end; end; end
|
data/lib/net/sftp/session.rb
CHANGED
|
@@ -68,6 +68,10 @@ module Net; module SFTP
|
|
|
68
68
|
# the server has not yet responded to will be represented here.
|
|
69
69
|
attr_reader :pending_requests
|
|
70
70
|
|
|
71
|
+
# The extensions the server advertised in its FXP_VERSION packet, as a
|
|
72
|
+
# hash of name => data (e.g. "posix-rename@openssh.com" => "1").
|
|
73
|
+
attr_reader :extensions
|
|
74
|
+
|
|
71
75
|
# Creates a new Net::SFTP instance atop the given Net::SSH connection.
|
|
72
76
|
# This will return immediately, before the SFTP connection has been properly
|
|
73
77
|
# initialized. Once the connection is ready, the given block will be called.
|
|
@@ -81,6 +85,7 @@ module Net; module SFTP
|
|
|
81
85
|
@input = Net::SSH::Buffer.new
|
|
82
86
|
self.logger = session.logger
|
|
83
87
|
@state = :closed
|
|
88
|
+
@extensions = {}
|
|
84
89
|
@pending_requests = {}
|
|
85
90
|
|
|
86
91
|
connect(&block)
|
|
@@ -269,6 +274,10 @@ module Net; module SFTP
|
|
|
269
274
|
# Request object. If a block is given, it will be invoked when the
|
|
270
275
|
# server responds.
|
|
271
276
|
#
|
|
277
|
+
# +data+ is sent as a single request, so it must stay within the
|
|
278
|
+
# server's packet limit (OpenSSH drops the connection on packets over
|
|
279
|
+
# 256 KiB). Net::SFTP::Operations::File#write splits large data for you.
|
|
280
|
+
#
|
|
272
281
|
# request = sftp.write(handle, 0, "hello, world!\n")
|
|
273
282
|
# request.wait
|
|
274
283
|
def write(handle, offset, data, &callback)
|
|
@@ -772,6 +781,7 @@ module Net; module SFTP
|
|
|
772
781
|
when :closed
|
|
773
782
|
@state = :opening
|
|
774
783
|
@channel = session.open_channel(&method(:when_channel_confirmed))
|
|
784
|
+
@input = Net::SSH::Buffer.new
|
|
775
785
|
@packet_length = nil
|
|
776
786
|
@protocol = nil
|
|
777
787
|
@on_ready = Array(block)
|
|
@@ -919,19 +929,27 @@ module Net; module SFTP
|
|
|
919
929
|
# version negotiation, instantiating the appropriate Protocol instance
|
|
920
930
|
# and invoking the callback given to #connect, if any.
|
|
921
931
|
def do_version(packet)
|
|
932
|
+
# Negotiation happens once. A later FXP_VERSION would swap the protocol
|
|
933
|
+
# driver and drop every pending request.
|
|
934
|
+
unless state == :init
|
|
935
|
+
raise Net::SFTP::Exception, "unexpected FXP_VERSION packet while session is #{state}"
|
|
936
|
+
end
|
|
937
|
+
|
|
922
938
|
debug { "negotiating sftp protocol version, mine is #{HIGHEST_PROTOCOL_VERSION_SUPPORTED}" }
|
|
923
939
|
|
|
924
940
|
server_version = packet.read_long
|
|
925
941
|
debug { "server reports sftp version #{server_version}" }
|
|
926
942
|
|
|
927
|
-
|
|
943
|
+
# Servers reply with their own version whatever we offered (OpenSSH
|
|
944
|
+
# always says 3), so cap at the version we offered as well.
|
|
945
|
+
negotiated_version = [server_version, @version, HIGHEST_PROTOCOL_VERSION_SUPPORTED].compact.min
|
|
928
946
|
info { "negotiated version is #{negotiated_version}" }
|
|
929
947
|
|
|
930
|
-
extensions = {}
|
|
948
|
+
@extensions = {}
|
|
931
949
|
until packet.eof?
|
|
932
950
|
name = packet.read_string
|
|
933
951
|
data = packet.read_string
|
|
934
|
-
extensions[name] = data
|
|
952
|
+
@extensions[name] = data
|
|
935
953
|
end
|
|
936
954
|
|
|
937
955
|
@protocol = Protocol.load(self, negotiated_version)
|
data/lib/net/sftp/version.rb
CHANGED
data/lib/net/sftp.rb
CHANGED
|
@@ -34,18 +34,24 @@ module Net
|
|
|
34
34
|
# set the SFTP protocol version to be used)
|
|
35
35
|
def self.start(host, user, ssh_options={}, sftp_options={}, &block)
|
|
36
36
|
session = Net::SSH.start(host, user, ssh_options)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
begin
|
|
38
|
+
# We only use a single option here, but this leaves room for more later
|
|
39
|
+
# without breaking the external API.
|
|
40
|
+
version = sftp_options.fetch(:version, nil)
|
|
41
|
+
sftp = Net::SFTP::Session.new(session, version, &block).connect!
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
end
|
|
43
|
+
if block_given?
|
|
44
|
+
sftp.loop
|
|
45
|
+
return nil
|
|
46
|
+
end
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
sftp
|
|
49
|
+
rescue Object
|
|
50
|
+
failed = true
|
|
51
|
+
raise
|
|
52
|
+
ensure
|
|
53
|
+
session.close if block_given? && !failed
|
|
54
|
+
end
|
|
49
55
|
rescue Object => anything
|
|
50
56
|
begin
|
|
51
57
|
session.shutdown!
|
data/net-sftp-public_cert.pem
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
-----BEGIN CERTIFICATE-----
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
2
|
+
MIIDeDCCAmCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMQ8wDQYDVQQDDAZuZXRz
|
|
3
|
+
c2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZFgNj
|
|
4
|
+
b20wHhcNMjYxMDIyMDk1NDI1WhcNMjcxMDIyMDk1NDI1WjBBMQ8wDQYDVQQDDAZu
|
|
5
|
+
ZXRzc2gxGTAXBgoJkiaJk/IsZAEZFglzb2x1dGlvdXMxEzARBgoJkiaJk/IsZAEZ
|
|
6
|
+
FgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGJ4TbZ9H+qZ08
|
|
7
|
+
pQfJhPJTHaDCyQvCsKTFrL5O9z3tllQ7B/zksMMM+qFBpNYu9HCcg4yBATacE/PB
|
|
8
|
+
qVVyUrpr6lbH/XwoN5ljXm+bdCfmnjZvTCL2FTE6o+bcnaF0IsJyC0Q2B1fbWdXN
|
|
9
|
+
6Off1ZWoUk6We2BIM1bn6QJLxBpGyYhvOPXsYoqSuzDf2SJDDsWFZ8kV5ON13Ohm
|
|
10
|
+
JbBzn0oD8HF8FuYOewwsC0C1q4w7E5GtvHcQ5juweS7+RKsyDcVcVrLuNzoGRttS
|
|
11
|
+
KP4yMn+TzaXijyjRg7gECfJr3TGASaA4bQsILFGG5dAWcwO4OMrZedR7SHj/o0Kf
|
|
12
|
+
3gL7P0axAgMBAAGjezB5MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQW
|
|
13
|
+
BBQF8qLA7Z4zg0SJGtUbv3eoQ8tjIzAfBgNVHREEGDAWgRRuZXRzc2hAc29sdXRp
|
|
14
|
+
b3VzLmNvbTAfBgNVHRIEGDAWgRRuZXRzc2hAc29sdXRpb3VzLmNvbTANBgkqhkiG
|
|
15
|
+
9w0BAQsFAAOCAQEAbVrRpiCFmHDMLNU+SCjpg43snWYksQOtejPPTEMdOc9cCmmv
|
|
16
|
+
ctJbaPIySLUgzwFbQDZStDPbLJmIujh0fm5dHp/sDJpvPZ3Mm3FfzWQMJqzBx4uk
|
|
17
|
+
OMx00yJK8VeFrwUViE3zO5raWvw85dUybp25pgaKGW4AA5mJ/2UHqYLc4lefKFKK
|
|
18
|
+
9xFsyYSciMEKyAsUNSNTJzPKUo2vn7TNF/pS0ZL+h8Qkr0A0F0ncfj7uM1kOWjO0
|
|
19
|
+
mMdUtmMoVbjbik3hqkY/18G03c3PoFNpL38VLKuyyROYz1DaCqOaRju3sbOoirD+
|
|
20
|
+
piQTZDCWxeKuX0Iuxq02uxkYHfoGB6In0y3OOQ==
|
|
20
21
|
-----END CERTIFICATE-----
|