Skip to content

Commit f0f2189

Browse files
committed
Land rapid7#8512, Enable adaptive download with variable block sizes
2 parents 385dadd + cc0ff8f commit f0f2189

File tree

2 files changed

+40
-8
lines changed
  • lib/rex/post/meterpreter
    • extensions/stdapi/fs
    • ui/console/command_dispatcher/stdapi

2 files changed

+40
-8
lines changed

lib/rex/post/meterpreter/extensions/stdapi/fs/file.rb

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
require 'rex/post/meterpreter/extensions/stdapi/fs/io'
88
require 'rex/post/meterpreter/extensions/stdapi/fs/file_stat'
99
require 'fileutils'
10+
require 'filesize'
1011

1112
module Rex
1213
module Post
@@ -25,6 +26,8 @@ class File < Rex::Post::Meterpreter::Extensions::Stdapi::Fs::IO
2526

2627
include Rex::Post::File
2728

29+
MIN_BLOCK_SIZE = 1024
30+
2831
class << self
2932
attr_accessor :client
3033
end
@@ -312,7 +315,7 @@ def File.download(dest, src_files, opts = nil, &stat)
312315
dest += timestamp
313316
end
314317

315-
stat.call('downloading', src, dest) if (stat)
318+
stat.call('Downloading', src, dest) if (stat)
316319
result = download_file(dest, src, opts, &stat)
317320
stat.call(result, src, dest) if (stat)
318321
}
@@ -325,8 +328,11 @@ def File.download_file(dest_file, src_file, opts = nil, &stat)
325328
continue=false
326329
tries=false
327330
tries_no=0
331+
stat ||= lambda { }
332+
328333
if opts
329334
continue = true if opts["continue"]
335+
adaptive = true if opts['adaptive']
330336
tries = true if opts["tries"]
331337
tries_no = opts["tries_no"]
332338
end
@@ -346,17 +352,19 @@ def File.download_file(dest_file, src_file, opts = nil, &stat)
346352
dir = ::File.dirname(dest_file)
347353
::FileUtils.mkdir_p(dir) if dir and not ::File.directory?(dir)
348354

355+
src_size = Filesize.new(src_stat.size).pretty
356+
349357
if continue
350358
# continue downloading the file - skip downloaded part in the source
351359
dst_fd = ::File.new(dest_file, "ab")
352360
begin
353361
dst_fd.seek(0, ::IO::SEEK_END)
354362
in_pos = dst_fd.pos
355363
src_fd.seek(in_pos)
356-
stat.call('continuing from ', in_pos, src_file) if (stat)
364+
stat.call("Continuing from #{Filesize.new(in_pos).pretty} of #{src_size}", src_file, dest_file)
357365
rescue
358366
# if we can't seek, download again
359-
stat.call('error continuing - downloading from scratch', src_file, dest_file) if (stat)
367+
stat.call('Error continuing - downloading from scratch', src_file, dest_file)
360368
dst_fd.close
361369
dst_fd = ::File.new(dest_file, "wb")
362370
end
@@ -365,41 +373,59 @@ def File.download_file(dest_file, src_file, opts = nil, &stat)
365373
end
366374

367375
# Keep transferring until EOF is reached...
376+
block_size = opts['block_size'] || 1024 * 1024
368377
begin
369378
if tries
370379
# resume when timeouts encountered
371380
seek_back = false
381+
adjust_block = false
372382
tries_cnt = 0
373383
begin # while
374384
begin # exception
375385
if seek_back
376386
in_pos = dst_fd.pos
377387
src_fd.seek(in_pos)
378388
seek_back = false
379-
stat.call('resuming at ', in_pos, src_file) if (stat)
389+
stat.call("Resuming at #{Filesize.new(in_pos).pretty} of #{src_size}", src_file, dest_file)
380390
else
381391
# succesfully read and wrote - reset the counter
382392
tries_cnt = 0
383393
end
384-
data = src_fd.read
394+
adjust_block = true
395+
data = src_fd.read(block_size)
396+
adjust_block = false
385397
rescue Rex::TimeoutError
386398
# timeout encountered - either seek back and retry or quit
387399
if (tries && (tries_no == 0 || tries_cnt < tries_no))
388400
tries_cnt += 1
389401
seek_back = true
390-
stat.call('error downloading - retry #', tries_cnt, src_file) if (stat)
402+
# try a smaller block size for the next round
403+
if adaptive && adjust_block
404+
block_size = [block_size >> 1, MIN_BLOCK_SIZE].max
405+
adjust_block = false
406+
msg = "Error downloading, block size set to #{block_size} - retry # #{tries_cnt}"
407+
stat.call(msg, src_file, dest_file)
408+
else
409+
stat.call("Error downloading - retry # #{tries_cnt}", src_file, dest_file)
410+
end
391411
retry
392412
else
393-
stat.call('error downloading - giving up', src_file, dest_file) if (stat)
413+
stat.call('Error downloading - giving up', src_file, dest_file)
394414
raise
395415
end
396416
end
397417
dst_fd.write(data) if (data != nil)
418+
percent = dst_fd.pos.to_f / src_stat.size.to_f * 100.0
419+
msg = "Downloaded #{Filesize.new(dst_fd.pos).pretty} of #{src_size} (#{percent.round(2)}%)"
420+
stat.call(msg, src_file, dest_file)
398421
end while (data != nil)
399422
else
400423
# do the simple copying quiting on the first error
401-
while ((data = src_fd.read) != nil)
424+
while ((data = src_fd.read(block_size)) != nil)
402425
dst_fd.write(data)
426+
percent = dst_fd.pos.to_f / src_stat.size.to_f * 100.0
427+
msg = "Downloaded #{Filesize.new(dst_fd.pos).pretty} of #{src_size} (#{percent.round(2)}%)"
428+
stat.call(msg, src_file, dest_file)
403429
end
404430
end
405431
rescue EOFError

lib/rex/post/meterpreter/ui/console/command_dispatcher/stdapi/fs.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class Console::CommandDispatcher::Stdapi::Fs
2828
@@download_opts = Rex::Parser::Arguments.new(
2929
"-h" => [ false, "Help banner." ],
3030
"-c" => [ false, "Resume getting a partially-downloaded file." ],
31+
"-a" => [ false, "Enable adaptive download buffer size." ],
32+
"-b" => [ true, "Set the initial block size for the download." ],
3133
"-l" => [ true, "Set the limit of retries (0 unlimits)." ],
3234
"-r" => [ false, "Download recursively." ],
3335
"-t" => [ false, "Timestamp downloaded files." ])
@@ -382,6 +384,10 @@ def cmd_download(*args)
382384

383385
@@download_opts.parse(args) { |opt, idx, val|
384386
case opt
387+
when "-a"
388+
opts['adaptive'] = true
389+
when "-b"
390+
opts['block_size'] = val.to_i
385391
when "-r"
386392
recursive = true
387393
opts['recursive'] = true

0 commit comments

Comments
 (0)