net-sftp 4.0.1 → 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 +15 -1
- data/CHANGES.txt +36 -0
- data/CONTRIBUTING.md +33 -0
- data/Gemfile +3 -0
- data/README.rdoc +3 -0
- data/Rakefile +40 -4
- 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 +2 -2
- data/lib/net/sftp.rb +16 -10
- data/net-sftp.gemspec +1 -0
- data.tar.gz.sig +1 -3
- metadata +8 -3
- metadata.gz.sig +0 -0
|
@@ -12,6 +12,11 @@ module Net; module SFTP; module Operations
|
|
|
12
12
|
# puts file.gets
|
|
13
13
|
# file.close
|
|
14
14
|
class File
|
|
15
|
+
# The largest amount of data sent in a single write request. Servers drop
|
|
16
|
+
# the connection on oversized packets (OpenSSH's limit is 256 KiB including
|
|
17
|
+
# headers), and the SFTP drafts only guarantee packets of about 34000 bytes.
|
|
18
|
+
MAX_WRITE_SIZE = 32 * 1024
|
|
19
|
+
|
|
15
20
|
# A reference to the Net::SFTP::Session instance that drives this wrapper
|
|
16
21
|
attr_reader :sftp
|
|
17
22
|
|
|
@@ -30,14 +35,14 @@ module Net; module SFTP; module Operations
|
|
|
30
35
|
@pos = 0
|
|
31
36
|
@real_pos = 0
|
|
32
37
|
@real_eof = false
|
|
33
|
-
@buffer = ""
|
|
38
|
+
@buffer = +""
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
# Repositions the file pointer to the given offset (relative to the
|
|
37
42
|
# start of the file). This will also reset the EOF flag.
|
|
38
43
|
def pos=(offset)
|
|
39
44
|
@real_pos = @pos = offset
|
|
40
|
-
@buffer = ""
|
|
45
|
+
@buffer = +""
|
|
41
46
|
@real_eof = false
|
|
42
47
|
end
|
|
43
48
|
|
|
@@ -63,15 +68,17 @@ module Net; module SFTP; module Operations
|
|
|
63
68
|
#
|
|
64
69
|
# This will advance the file pointer (#pos).
|
|
65
70
|
def read(n=nil)
|
|
71
|
+
raise ArgumentError, "negative length #{n} given" if n && n < 0
|
|
72
|
+
|
|
66
73
|
loop do
|
|
67
74
|
break if n && @buffer.length >= n
|
|
68
75
|
break unless fill
|
|
69
76
|
end
|
|
70
77
|
|
|
71
78
|
if n
|
|
72
|
-
result, @buffer = @buffer[0,n], (@buffer[n..-1] || "")
|
|
79
|
+
result, @buffer = @buffer[0,n], (@buffer[n..-1] || +"")
|
|
73
80
|
else
|
|
74
|
-
result, @buffer = @buffer, ""
|
|
81
|
+
result, @buffer = @buffer, +""
|
|
75
82
|
end
|
|
76
83
|
|
|
77
84
|
@pos += result.length
|
|
@@ -92,6 +99,7 @@ module Net; module SFTP; module Operations
|
|
|
92
99
|
sep_string = sep_or_limit
|
|
93
100
|
lim = limit
|
|
94
101
|
end
|
|
102
|
+
lim = Float::INFINITY if lim.nil? || lim < 0
|
|
95
103
|
|
|
96
104
|
delim = if sep_string && sep_string.length == 0
|
|
97
105
|
"#{$/}#{$/}"
|
|
@@ -99,22 +107,26 @@ module Net; module SFTP; module Operations
|
|
|
99
107
|
sep_string
|
|
100
108
|
end
|
|
101
109
|
|
|
110
|
+
search_from = 0
|
|
102
111
|
loop do
|
|
103
|
-
at = @buffer.index(delim) if delim
|
|
112
|
+
at = @buffer.index(delim, search_from) if delim
|
|
104
113
|
if at
|
|
105
114
|
offset = [at + delim.length, lim].min
|
|
106
115
|
@pos += offset
|
|
107
116
|
line, @buffer = @buffer[0,offset], @buffer[offset..-1]
|
|
108
117
|
return line
|
|
109
|
-
elsif lim
|
|
118
|
+
elsif lim <= @buffer.length
|
|
110
119
|
@pos += lim
|
|
111
120
|
line, @buffer = @buffer[0,lim], @buffer[lim..-1]
|
|
112
121
|
return line
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
else
|
|
123
|
+
search_from = [@buffer.length - delim.length + 1, 0].max if delim
|
|
124
|
+
unless fill
|
|
125
|
+
return nil if @buffer.empty?
|
|
126
|
+
@pos += @buffer.length
|
|
127
|
+
line, @buffer = @buffer, +""
|
|
128
|
+
return line
|
|
129
|
+
end
|
|
118
130
|
end
|
|
119
131
|
end
|
|
120
132
|
end
|
|
@@ -128,13 +140,21 @@ module Net; module SFTP; module Operations
|
|
|
128
140
|
end
|
|
129
141
|
|
|
130
142
|
# Writes the given data to the stream, incrementing the file position and
|
|
131
|
-
# returning the number of bytes written.
|
|
143
|
+
# returning the number of bytes written. Data larger than MAX_WRITE_SIZE
|
|
144
|
+
# is sent as several sequential write requests; if one fails, the
|
|
145
|
+
# position reflects the bytes written before it.
|
|
132
146
|
def write(data)
|
|
133
147
|
data = data.to_s
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
size = data.bytesize
|
|
149
|
+
written = 0
|
|
150
|
+
loop do
|
|
151
|
+
chunk = data.byteslice(written, MAX_WRITE_SIZE)
|
|
152
|
+
sftp.write!(handle, @pos, chunk)
|
|
153
|
+
written += chunk.bytesize
|
|
154
|
+
self.pos = @pos + chunk.bytesize unless chunk.empty?
|
|
155
|
+
break if written >= size
|
|
156
|
+
end
|
|
157
|
+
size
|
|
138
158
|
end
|
|
139
159
|
|
|
140
160
|
# Writes each argument to the stream. If +$\+ is set, it will be written
|
|
@@ -155,16 +175,12 @@ module Net; module SFTP; module Operations
|
|
|
155
175
|
end
|
|
156
176
|
|
|
157
177
|
# Writes each argument to the stream, appending a newline to any item
|
|
158
|
-
# that does not already end in a newline. Array arguments
|
|
178
|
+
# that does not already end in a newline, like IO#puts. Array arguments
|
|
179
|
+
# are flattened, and with no arguments a single newline is written.
|
|
159
180
|
def puts(*items)
|
|
160
|
-
items
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
else
|
|
164
|
-
write(item)
|
|
165
|
-
write("\n") unless item[-1] == ?\n
|
|
166
|
-
end
|
|
167
|
-
end
|
|
181
|
+
items = [""] if items.empty?
|
|
182
|
+
recursion = {}.compare_by_identity
|
|
183
|
+
items.each { |item| write_puts_item(item, recursion) }
|
|
168
184
|
nil
|
|
169
185
|
end
|
|
170
186
|
|
|
@@ -178,12 +194,30 @@ module Net; module SFTP; module Operations
|
|
|
178
194
|
|
|
179
195
|
private
|
|
180
196
|
|
|
197
|
+
def write_puts_item(item, recursion)
|
|
198
|
+
array = Array.try_convert(item)
|
|
199
|
+
if array
|
|
200
|
+
if recursion.key?(item)
|
|
201
|
+
item = "[...]"
|
|
202
|
+
else
|
|
203
|
+
recursion[item] = true
|
|
204
|
+
array.each { |child| write_puts_item(child, recursion) }
|
|
205
|
+
recursion.delete(item)
|
|
206
|
+
return
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
item = item.to_s
|
|
211
|
+
write(item) unless item.empty?
|
|
212
|
+
write("\n") unless item[-1] == ?\n
|
|
213
|
+
end
|
|
214
|
+
|
|
181
215
|
# Fills the buffer. Returns +true+ if it succeeded, and +false+ if
|
|
182
216
|
# EOF was encountered before any data was read.
|
|
183
217
|
def fill
|
|
184
218
|
data = sftp.read!(handle, @real_pos, 8192)
|
|
185
219
|
|
|
186
|
-
if data.nil?
|
|
220
|
+
if data.nil? || data.empty?
|
|
187
221
|
@real_eof = true
|
|
188
222
|
return false
|
|
189
223
|
else
|
|
@@ -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
|