curb 1.3.5 → 1.3.7
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
- data/README.md +90 -13
- data/Rakefile +8 -3
- data/doc.rb +48 -8
- data/ext/curb.c +33 -0
- data/ext/curb.h +3 -3
- data/ext/curb_easy.c +1471 -87
- data/ext/curb_easy.h +26 -0
- data/ext/curb_errors.c +2 -0
- data/ext/curb_errors.h +1 -0
- data/ext/curb_multi.c +257 -130
- data/ext/curb_multi.h +1 -0
- data/ext/extconf.rb +12 -0
- data/lib/curl/download.rb +160 -0
- data/lib/curl/easy.rb +113 -13
- data/lib/curl/multi.rb +172 -39
- data/lib/curl.rb +471 -11
- data/tests/bug_poison.rb +29 -0
- data/tests/helper.rb +178 -1
- data/tests/io_select_less_scheduler_probe.rb +105 -0
- data/tests/tc_curl_download.rb +86 -0
- data/tests/tc_curl_easy.rb +77 -0
- data/tests/tc_curl_easy_resolve.rb +2 -0
- data/tests/tc_curl_maxfilesize.rb +201 -1
- data/tests/tc_curl_multi.rb +258 -0
- data/tests/tc_curl_native_coverage.rb +1 -0
- data/tests/tc_curl_network_policy.rb +1475 -0
- data/tests/tc_curl_protocols.rb +351 -0
- data/tests/tc_fiber_scheduler.rb +373 -4
- data/tests/tc_ftp_options.rb +58 -0
- data/tests/tc_gc_compact.rb +53 -0
- data/tests/tc_test_server_methods.rb +15 -0
- metadata +9 -2
data/ext/curb_multi.h
CHANGED
data/ext/extconf.rb
CHANGED
|
@@ -298,6 +298,8 @@ have_constant "curlopt_sockoptfunction"
|
|
|
298
298
|
have_constant "curlopt_sockoptdata"
|
|
299
299
|
have_constant "curlopt_opensocketfunction"
|
|
300
300
|
have_constant "curlopt_opensocketdata"
|
|
301
|
+
have_constant "curlopt_prereqfunction"
|
|
302
|
+
have_constant "curlopt_prereqdata"
|
|
301
303
|
|
|
302
304
|
# Deprecated constants (still check for them for backward compat)
|
|
303
305
|
have_constant "curlopt_ioctlfunction"
|
|
@@ -391,6 +393,7 @@ have_constant "curlopt_socks5_gssapi_nec"
|
|
|
391
393
|
have_constant "curlopt_interface"
|
|
392
394
|
have_constant "curlopt_localport"
|
|
393
395
|
have_constant "curlopt_dns_cache_timeout"
|
|
396
|
+
have_constant "curlopt_dns_servers"
|
|
394
397
|
have_constant "curlopt_dns_use_global_cache"
|
|
395
398
|
have_constant "curlopt_buffersize"
|
|
396
399
|
have_constant "curlopt_port"
|
|
@@ -459,6 +462,9 @@ have_constant "curlopt_ftp_use_eprt"
|
|
|
459
462
|
have_constant "curlopt_ftp_use_epsv"
|
|
460
463
|
have_constant "curlopt_ftp_use_pret"
|
|
461
464
|
have_constant "curlopt_ftp_create_missing_dirs"
|
|
465
|
+
have_constant "curlftp_create_dir_none"
|
|
466
|
+
have_constant "curlftp_create_dir"
|
|
467
|
+
have_constant "curlftp_create_dir_retry"
|
|
462
468
|
have_constant "curlopt_ftp_response_timeout"
|
|
463
469
|
have_constant "curlopt_ftp_alternative_to_user"
|
|
464
470
|
have_constant "curlopt_ftp_skip_pasv_ip"
|
|
@@ -531,6 +537,7 @@ have_constant "curlusessl_none"
|
|
|
531
537
|
have_constant "curlusessl_try"
|
|
532
538
|
have_constant "curlusessl_control"
|
|
533
539
|
have_constant "curlusessl_all"
|
|
540
|
+
have_constant "curlopt_connect_to"
|
|
534
541
|
have_constant "curlopt_resolve"
|
|
535
542
|
have_constant "curlopt_request_target"
|
|
536
543
|
have_constant "curlopt_sslcert"
|
|
@@ -558,6 +565,10 @@ have_constant :CURL_SSLVERSION_TLSv1_2
|
|
|
558
565
|
have_constant :CURL_SSLVERSION_TLSv1_3
|
|
559
566
|
|
|
560
567
|
have_constant "curlopt_ssl_verifypeer"
|
|
568
|
+
have_constant "curlopt_doh_url"
|
|
569
|
+
have_constant "curlopt_doh_ssl_verifypeer"
|
|
570
|
+
have_constant "curlopt_doh_ssl_verifyhost"
|
|
571
|
+
have_constant "curlopt_doh_ssl_verifystatus"
|
|
561
572
|
have_constant "curlopt_cainfo"
|
|
562
573
|
have_constant "curlopt_issuercert"
|
|
563
574
|
have_constant "curlopt_capath"
|
|
@@ -692,6 +703,7 @@ have_header('ruby/fiber/scheduler.h')
|
|
|
692
703
|
have_func('rb_fiber_scheduler_current', 'ruby/fiber/scheduler.h')
|
|
693
704
|
have_func('rb_fiber_scheduler_io_wait', 'ruby/fiber/scheduler.h')
|
|
694
705
|
have_func('rb_fiber_scheduler_io_select', 'ruby/fiber/scheduler.h')
|
|
706
|
+
have_func('rb_fiber_scheduler_kernel_sleep', 'ruby/fiber/scheduler.h')
|
|
695
707
|
have_func('rb_io_stdio_file')
|
|
696
708
|
have_func('curl_multi_wait')
|
|
697
709
|
have_func('curl_multi_socket_action')
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
module Curl
|
|
7
|
+
class DownloadTargetExistsError < Errno::EEXIST; end
|
|
8
|
+
DOWNLOAD_OPTION_KEYS = [:download_dir, :overwrite].freeze
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def download_options_hash?(value)
|
|
12
|
+
value.is_a?(Hash) && value.keys.all? { |key| DOWNLOAD_OPTION_KEYS.include?(key) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def normalize_download_arguments(filename, options)
|
|
16
|
+
if download_options_hash?(filename) && options.empty?
|
|
17
|
+
options = filename
|
|
18
|
+
filename = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
[filename, options || {}]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse_download_options(options)
|
|
25
|
+
raise ArgumentError, "download options must be a Hash" unless options.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
options = options.dup
|
|
28
|
+
download_dir = options.delete(:download_dir)
|
|
29
|
+
overwrite = options.key?(:overwrite) ? !!options.delete(:overwrite) : false
|
|
30
|
+
raise ArgumentError, "unsupported download option(s): #{options.keys.join(', ')}" unless options.empty?
|
|
31
|
+
|
|
32
|
+
[download_dir, overwrite]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def resolve_download_output(url, filename = nil, options = {})
|
|
36
|
+
filename, options = normalize_download_arguments(filename, options)
|
|
37
|
+
download_dir, overwrite = parse_download_options(options)
|
|
38
|
+
|
|
39
|
+
if filename.is_a?(IO)
|
|
40
|
+
filename.binmode if filename.respond_to?(:binmode)
|
|
41
|
+
return [nil, filename, false, overwrite]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
path = if download_dir
|
|
45
|
+
safe_download_path(url, download_dir, filename: filename)
|
|
46
|
+
elsif filename.nil?
|
|
47
|
+
safe_download_path(url)
|
|
48
|
+
else
|
|
49
|
+
filename.to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
[path, nil, true, overwrite]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def prepare_download_output(url, filename = nil, options = {})
|
|
56
|
+
path, io, safe_output, overwrite = resolve_download_output(url, filename, options)
|
|
57
|
+
return [path, io, safe_output] unless safe_output
|
|
58
|
+
|
|
59
|
+
[path, open_safe_download_output(path, overwrite: overwrite), true]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def download_filename_from_url(url)
|
|
63
|
+
path = begin
|
|
64
|
+
URI.parse(url.to_s).path
|
|
65
|
+
rescue URI::InvalidURIError
|
|
66
|
+
url.to_s.split(/\?/, 2).first
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
basename = File.basename(path.to_s)
|
|
70
|
+
validate_download_filename!(basename)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def safe_download_path(url, destination_dir = Dir.pwd, filename: nil)
|
|
74
|
+
dir = File.expand_path(destination_dir.to_s)
|
|
75
|
+
raise ArgumentError, "download destination directory does not exist: #{destination_dir}" unless File.directory?(dir)
|
|
76
|
+
|
|
77
|
+
File.join(dir, filename.nil? ? download_filename_from_url(url) : validate_download_filename!(filename))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_download_filename!(filename)
|
|
81
|
+
filename = filename.to_s
|
|
82
|
+
invalid = filename.empty? ||
|
|
83
|
+
filename == '.' ||
|
|
84
|
+
filename == '..' ||
|
|
85
|
+
filename == File::SEPARATOR ||
|
|
86
|
+
filename.start_with?('.') ||
|
|
87
|
+
filename.include?("\0") ||
|
|
88
|
+
filename.include?(File::SEPARATOR) ||
|
|
89
|
+
filename.include?('\\') ||
|
|
90
|
+
filename.match?(/\A[A-Za-z]:/) ||
|
|
91
|
+
(File::ALT_SEPARATOR && filename.include?(File::ALT_SEPARATOR))
|
|
92
|
+
|
|
93
|
+
raise ArgumentError, "unsafe download filename derived from URL: #{filename.inspect}" if invalid
|
|
94
|
+
|
|
95
|
+
filename
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def open_safe_download_output(path, overwrite: false)
|
|
99
|
+
SafeDownloadOutput.new(path, overwrite: overwrite)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class SafeDownloadOutput
|
|
104
|
+
def initialize(path, overwrite: false)
|
|
105
|
+
@path = File.expand_path(path.to_s)
|
|
106
|
+
@overwrite = overwrite
|
|
107
|
+
raise DownloadTargetExistsError, @path if !@overwrite && File.exist?(@path)
|
|
108
|
+
|
|
109
|
+
@tmp = Tempfile.new(['.curb-download-', '.tmp'], File.dirname(@path))
|
|
110
|
+
@tmp.binmode
|
|
111
|
+
@closed = false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
attr_reader :path
|
|
115
|
+
|
|
116
|
+
def write(data)
|
|
117
|
+
@tmp.write(data)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def <<(data)
|
|
121
|
+
write(data)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def close(success = false)
|
|
125
|
+
return if @closed
|
|
126
|
+
|
|
127
|
+
@tmp.flush unless @tmp.closed?
|
|
128
|
+
@tmp.close unless @tmp.closed?
|
|
129
|
+
install_tmp_file if success
|
|
130
|
+
ensure
|
|
131
|
+
@tmp.close! if @tmp
|
|
132
|
+
@closed = true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def install_tmp_file
|
|
138
|
+
if @overwrite
|
|
139
|
+
File.rename(@tmp.path, @path)
|
|
140
|
+
else
|
|
141
|
+
File.link(@tmp.path, @path)
|
|
142
|
+
end
|
|
143
|
+
rescue Errno::EEXIST
|
|
144
|
+
raise DownloadTargetExistsError, @path
|
|
145
|
+
rescue NotImplementedError
|
|
146
|
+
install_tmp_file_by_copy
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def install_tmp_file_by_copy
|
|
150
|
+
flags = File::WRONLY | File::CREAT | File::EXCL
|
|
151
|
+
flags |= File::BINARY if File.const_defined?(:BINARY)
|
|
152
|
+
|
|
153
|
+
File.open(@path, flags) do |dest|
|
|
154
|
+
File.open(@tmp.path, 'rb') { |src| IO.copy_stream(src, dest) }
|
|
155
|
+
end
|
|
156
|
+
rescue Errno::EEXIST
|
|
157
|
+
raise DownloadTargetExistsError, @path
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
data/lib/curl/easy.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require 'curl/download'
|
|
2
3
|
module Curl
|
|
3
4
|
class Easy
|
|
4
5
|
class << self
|
|
@@ -83,10 +84,12 @@ module Curl
|
|
|
83
84
|
|
|
84
85
|
alias_method :_curb_native_close, :close
|
|
85
86
|
alias_method :_curb_native_multi_set, :multi=
|
|
87
|
+
alias_method :_curb_native_reset, :reset
|
|
86
88
|
|
|
87
89
|
def close
|
|
88
90
|
previous_multi = self.multi
|
|
89
91
|
result = _curb_native_close
|
|
92
|
+
__curb_clear_safety_override!
|
|
90
93
|
previous_multi.__send__(:__unregister_idle_easy_reference, self) if previous_multi
|
|
91
94
|
result
|
|
92
95
|
end
|
|
@@ -94,12 +97,22 @@ module Curl
|
|
|
94
97
|
def multi=(multi)
|
|
95
98
|
previous_multi = self.multi
|
|
96
99
|
return multi if previous_multi.equal?(multi)
|
|
97
|
-
|
|
100
|
+
|
|
101
|
+
if previous_multi && previous_multi.requests[self.object_id]
|
|
102
|
+
previous_multi.remove(self)
|
|
103
|
+
end
|
|
104
|
+
|
|
98
105
|
result = _curb_native_multi_set(multi)
|
|
99
106
|
previous_multi.__send__(:__unregister_idle_easy_reference, self) if previous_multi
|
|
100
107
|
multi.__send__(:__register_idle_easy_reference, self) if multi
|
|
101
108
|
result
|
|
102
109
|
end
|
|
110
|
+
|
|
111
|
+
def reset
|
|
112
|
+
result = _curb_native_reset
|
|
113
|
+
__curb_clear_safety_override!
|
|
114
|
+
result
|
|
115
|
+
end
|
|
103
116
|
|
|
104
117
|
alias post http_post
|
|
105
118
|
alias put http_put
|
|
@@ -154,6 +167,84 @@ module Curl
|
|
|
154
167
|
Curl.const_get("CURLOPT_#{opt.to_s.upcase}")
|
|
155
168
|
end
|
|
156
169
|
|
|
170
|
+
def allowed_protocols=(protocols)
|
|
171
|
+
set_protocol_allowlist('CURLOPT_PROTOCOLS_STR', 'CURLOPT_PROTOCOLS', protocols)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def allowed_redirect_protocols=(protocols)
|
|
175
|
+
set_protocol_allowlist('CURLOPT_REDIR_PROTOCOLS_STR', 'CURLOPT_REDIR_PROTOCOLS', protocols)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def safe_http!
|
|
179
|
+
__curb_set_safety_override!(
|
|
180
|
+
:protocols => [:http, :https],
|
|
181
|
+
:redirect_protocols => [:http, :https]
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def __curb_set_safety_override!(options)
|
|
188
|
+
@__curb_safety_override = (defined?(@__curb_safety_override) && @__curb_safety_override) ? @__curb_safety_override.dup : {}
|
|
189
|
+
@__curb_safety_override[:protocols] = Array(options[:protocols]).map { |protocol| protocol.to_s.downcase.to_sym } if options.key?(:protocols)
|
|
190
|
+
@__curb_safety_override[:redirect_protocols] = Array(options[:redirect_protocols]).map { |protocol| protocol.to_s.downcase.to_sym } if options.key?(:redirect_protocols)
|
|
191
|
+
@__curb_safety_override[:max_body_bytes] = options[:max_body_bytes] if options.key?(:max_body_bytes) && options[:max_body_bytes]
|
|
192
|
+
@__curb_safety_override_generation = __curb_safety_override_generation + 1
|
|
193
|
+
Curl.__send__(:apply_safety!, self) if Curl.respond_to?(:apply_safety!, true)
|
|
194
|
+
self
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def __curb_clear_safety_override!
|
|
198
|
+
had_override = defined?(@__curb_safety_override) && @__curb_safety_override
|
|
199
|
+
return unless had_override
|
|
200
|
+
return if frozen?
|
|
201
|
+
|
|
202
|
+
@__curb_safety_override = nil
|
|
203
|
+
@__curb_safety_override_generation = __curb_safety_override_generation + 1
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def __curb_safety_override
|
|
207
|
+
defined?(@__curb_safety_override) ? @__curb_safety_override : nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def __curb_safety_override_generation
|
|
211
|
+
defined?(@__curb_safety_override_generation) ? @__curb_safety_override_generation : 0
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def set_protocol_allowlist(string_option, bitmask_option, protocols)
|
|
215
|
+
protocol_names = Array(protocols).map { |protocol| protocol.to_s.downcase }
|
|
216
|
+
raise ArgumentError, "at least one protocol is required" if protocol_names.empty?
|
|
217
|
+
|
|
218
|
+
valid_names = %w[
|
|
219
|
+
dict file ftp ftps gopher gophers http https imap imaps ldap ldaps
|
|
220
|
+
mqtt pop3 pop3s rtmp rtmpe rtmps rtmpt rtmpte rtmpts rtsp scp sftp
|
|
221
|
+
smb smbs smtp smtps telnet tftp ws wss
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
if Curl.const_defined?(string_option)
|
|
225
|
+
protocol_names.each do |name|
|
|
226
|
+
raise ArgumentError, "unsupported protocol: #{name.inspect}" unless valid_names.include?(name)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
setopt(Curl.const_get(string_option), protocol_names.join(','))
|
|
230
|
+
elsif Curl.const_defined?(bitmask_option)
|
|
231
|
+
protocol_pairs = protocol_names.map do |name|
|
|
232
|
+
const_name = "CURLPROTO_#{name.upcase}"
|
|
233
|
+
raise ArgumentError, "unsupported protocol: #{name.inspect}" unless Curl.const_defined?(const_name)
|
|
234
|
+
|
|
235
|
+
[name, Curl.const_get(const_name)]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
setopt(Curl.const_get(bitmask_option), protocol_pairs.inject(0) { |mask, pair| mask | pair.last })
|
|
239
|
+
else
|
|
240
|
+
raise NotImplementedError, "protocol allowlists are not supported by this libcurl"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
protocols
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
public
|
|
247
|
+
|
|
157
248
|
#
|
|
158
249
|
# call-seq:
|
|
159
250
|
# easy.perform => true
|
|
@@ -163,6 +254,7 @@ module Curl
|
|
|
163
254
|
# the configured HTTP Verb.
|
|
164
255
|
#
|
|
165
256
|
def perform
|
|
257
|
+
Curl.__send__(:apply_safety!, self) if Curl.respond_to?(:apply_safety!, true)
|
|
166
258
|
self.class.flush_deferred_multi_closes
|
|
167
259
|
|
|
168
260
|
if Curl.scheduler_active? && self.multi.nil?
|
|
@@ -210,6 +302,10 @@ module Curl
|
|
|
210
302
|
raise callback_error
|
|
211
303
|
end
|
|
212
304
|
|
|
305
|
+
if respond_to?(:unsafe_destination_error) && (unsafe_destination_error = self.unsafe_destination_error)
|
|
306
|
+
raise Curl::Err::UnsafeDestinationError, unsafe_destination_error
|
|
307
|
+
end
|
|
308
|
+
|
|
213
309
|
if self.last_result != 0 && self.on_failure.nil?
|
|
214
310
|
err_class, err_summary = Curl::Easy.error(self.last_result)
|
|
215
311
|
err_detail = self.last_error
|
|
@@ -641,11 +737,15 @@ module Curl
|
|
|
641
737
|
end
|
|
642
738
|
|
|
643
739
|
# call-seq:
|
|
644
|
-
# Curl::Easy.download(url, filename =
|
|
740
|
+
# Curl::Easy.download(url, filename = nil, options = {}) { |curl| ... }
|
|
645
741
|
#
|
|
646
742
|
# Stream the specified url (via perform) and save the data directly to the
|
|
647
|
-
# supplied filename
|
|
648
|
-
#
|
|
743
|
+
# supplied filename. The destination is written through a temporary file and
|
|
744
|
+
# existing files are not overwritten unless <tt>:overwrite => true</tt> is
|
|
745
|
+
# passed. When filename is omitted, the destination is safely derived from the
|
|
746
|
+
# last URL path component in the current directory. Pass
|
|
747
|
+
# <tt>:download_dir</tt> to treat the filename as a basename inside a trusted
|
|
748
|
+
# directory and reject absolute, parent-directory, dotfile, and nested names.
|
|
649
749
|
#
|
|
650
750
|
# If a block is supplied, it will be passed the curl instance prior to the
|
|
651
751
|
# perform call.
|
|
@@ -658,16 +758,11 @@ module Curl
|
|
|
658
758
|
# returns a size that differs from the data chunk size - in this case, the
|
|
659
759
|
# offending chunk will *not* be written to the file, the file will be closed,
|
|
660
760
|
# and a Curl::Err::AbortedByCallbackError will be raised.
|
|
661
|
-
def download(url, filename =
|
|
761
|
+
def download(url, filename = nil, download_options = {}, &blk)
|
|
662
762
|
curl = Curl::Easy.new(url, &blk)
|
|
763
|
+
_download_path, output, safe_output = Curl.prepare_download_output(url, filename, download_options)
|
|
663
764
|
|
|
664
|
-
|
|
665
|
-
filename.binmode if filename.respond_to?(:binmode)
|
|
666
|
-
filename
|
|
667
|
-
else
|
|
668
|
-
File.open(filename, 'wb')
|
|
669
|
-
end
|
|
670
|
-
|
|
765
|
+
performed = false
|
|
671
766
|
begin
|
|
672
767
|
old_on_body = curl.on_body do |data|
|
|
673
768
|
result = old_on_body ? old_on_body.call(data) : data.length
|
|
@@ -675,8 +770,13 @@ module Curl
|
|
|
675
770
|
result
|
|
676
771
|
end
|
|
677
772
|
curl.perform
|
|
773
|
+
performed = true
|
|
678
774
|
ensure
|
|
679
|
-
|
|
775
|
+
if safe_output
|
|
776
|
+
output.close(performed)
|
|
777
|
+
else
|
|
778
|
+
output.close rescue IOError
|
|
779
|
+
end
|
|
680
780
|
end
|
|
681
781
|
|
|
682
782
|
return curl
|