curb 1.3.5 → 1.3.6

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.
data/ext/curb_easy.h CHANGED
@@ -11,6 +11,18 @@
11
11
 
12
12
  #include <curl/easy.h>
13
13
 
14
+ #define CURB_NETWORK_POLICY_NONE 0
15
+ #define CURB_NETWORK_POLICY_PUBLIC 1
16
+
17
+ #define CURB_CIDR_FAMILY_IPV4 4
18
+ #define CURB_CIDR_FAMILY_IPV6 6
19
+
20
+ typedef struct {
21
+ unsigned char family;
22
+ unsigned char prefix_bits;
23
+ unsigned char address[16];
24
+ } curb_cidr_rule;
25
+
14
26
  #ifdef CURL_VERSION_SSL
15
27
  #if LIBCURL_VERSION_NUM >= 0x070b00
16
28
  # if LIBCURL_VERSION_NUM <= 0x071004
@@ -38,6 +50,7 @@ typedef struct {
38
50
 
39
51
  /* Buffer for error details from CURLOPT_ERRORBUFFER */
40
52
  char err_buf[CURL_ERROR_SIZE];
53
+ char unsafe_destination_error[CURL_ERROR_SIZE];
41
54
 
42
55
  VALUE self; /* owning Ruby object */
43
56
  VALUE opts; /* rather then allocate everything we might need to store, allocate a Hash and only store objects we actually use... */
@@ -67,6 +80,7 @@ typedef struct {
67
80
  long ftp_filemethod;
68
81
  long http_version;
69
82
  unsigned short resolve_mode;
83
+ unsigned short network_policy;
70
84
 
71
85
  /* bool flags */
72
86
  char proxy_tunnel;
@@ -83,13 +97,25 @@ typedef struct {
83
97
  char cookielist_engine_enabled; /* track if CURLOPT_COOKIELIST was used with a non-command to enable engine */
84
98
  char ignore_content_length;
85
99
  char callback_active;
100
+ char unsafe_destination_blocked;
101
+ char allow_proxy;
102
+ char allow_unix_socket;
103
+ char forbid_reuse_set;
104
+ unsigned int native_active;
105
+ long forbid_reuse;
86
106
 
87
107
  struct curl_slist *curl_headers;
88
108
  struct curl_slist *curl_proxy_headers;
89
109
  struct curl_slist *curl_ftp_commands;
90
110
  struct curl_slist *curl_resolve;
111
+ struct curl_slist *curl_connect_to;
112
+ curb_cidr_rule *network_allowed_cidr_rules;
113
+ char **network_allowed_hosts;
91
114
 
92
115
  unsigned long multi_attachment_generation;
116
+ curl_off_t downloaded_body_bytes;
117
+ size_t network_allowed_cidr_rule_count;
118
+ size_t network_allowed_host_count;
93
119
  int last_result; /* last result code from multi loop */
94
120
 
95
121
  } ruby_curl_easy;
data/ext/curb_errors.c CHANGED
@@ -110,6 +110,7 @@ VALUE eCurlErrSSLShutdownFailed;
110
110
  VALUE eCurlErrAgain;
111
111
  VALUE eCurlErrSSLCRLBadfile;
112
112
  VALUE eCurlErrSSLIssuerError;
113
+ VALUE eCurlErrUnsafeDestination;
113
114
 
114
115
  /* multi errors */
115
116
  VALUE mCurlErrFailedInit;
@@ -699,6 +700,7 @@ void init_curb_errors() {
699
700
  eCurlErrSSLCacertBadfile = rb_define_class_under(mCurlErr, "SSLCaertBadFile", eCurlErrError);
700
701
  eCurlErrSSLCRLBadfile = rb_define_class_under(mCurlErr, "SSLCRLBadfile", eCurlErrError);
701
702
  eCurlErrSSLIssuerError = rb_define_class_under(mCurlErr, "SSLIssuerError", eCurlErrError);
703
+ eCurlErrUnsafeDestination = rb_define_class_under(mCurlErr, "UnsafeDestinationError", eCurlErrError);
702
704
  eCurlErrSSLShutdownFailed = rb_define_class_under(mCurlErr, "SSLShutdownFailed", eCurlErrError);
703
705
  eCurlErrSSH = rb_define_class_under(mCurlErr, "SSH", eCurlErrError);
704
706
 
data/ext/curb_errors.h CHANGED
@@ -106,6 +106,7 @@ extern VALUE eCurlErrSSLShutdownFailed;
106
106
  extern VALUE eCurlErrAgain;
107
107
  extern VALUE eCurlErrSSLCRLBadfile;
108
108
  extern VALUE eCurlErrSSLIssuerError;
109
+ extern VALUE eCurlErrUnsafeDestination;
109
110
 
110
111
  /* multi errors */
111
112
  extern VALUE mCurlErrFailedInit;
data/ext/curb_multi.c CHANGED
@@ -74,6 +74,7 @@ extern VALUE mCurl;
74
74
  static VALUE idCall;
75
75
  static ID id_deferred_exception_ivar;
76
76
  static ID id_deferred_exception_source_id_ivar;
77
+ static ID id_native_safety_signatures_ivar;
77
78
  static ID id_socket_io_cache_ivar;
78
79
 
79
80
  #ifdef RDOC_NEVER_DEFINED
@@ -286,6 +287,33 @@ void rb_curl_multi_forget_easy(ruby_curl_multi *rbcm, void *rbce_ptr) {
286
287
  st_delete(rbcm->attached, &key, NULL);
287
288
  }
288
289
 
290
+ CURLMcode rb_curl_multi_detach_easy(ruby_curl_multi *rbcm, void *rbce_ptr) {
291
+ ruby_curl_easy *rbce = (ruby_curl_easy *)rbce_ptr;
292
+ st_data_t key;
293
+
294
+ if (!rbcm || !rbce || !rbcm->attached) {
295
+ return CURLM_OK;
296
+ }
297
+
298
+ key = (st_data_t)rbce;
299
+ if (!st_delete(rbcm->attached, &key, NULL)) {
300
+ return CURLM_OK;
301
+ }
302
+
303
+ if (rbcm->handle && rbce->curl) {
304
+ CURLMcode result = curl_multi_remove_handle(rbcm->handle, rbce->curl);
305
+ if (result != CURLM_OK) {
306
+ return result;
307
+ }
308
+ }
309
+
310
+ if (rbcm->active > 0) {
311
+ rbcm->active--;
312
+ }
313
+
314
+ return CURLM_OK;
315
+ }
316
+
289
317
  static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm) {
290
318
  if (!rbcm || !rbcm->attached) {
291
319
  return;
@@ -314,6 +342,8 @@ static int rb_curl_multi_has_easy(ruby_curl_multi *rbcm, ruby_curl_easy *rbce) {
314
342
 
315
343
  static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy) {
316
344
  VALUE requests;
345
+ VALUE object_id;
346
+ VALUE safety_signatures;
317
347
 
318
348
  if (NIL_P(self) || NIL_P(easy)) {
319
349
  return;
@@ -324,7 +354,17 @@ static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy) {
324
354
  return;
325
355
  }
326
356
 
327
- rb_hash_delete(requests, rb_obj_id(easy));
357
+ object_id = rb_obj_id(easy);
358
+ rb_hash_delete(requests, object_id);
359
+
360
+ if (!rb_ivar_defined(self, id_native_safety_signatures_ivar)) {
361
+ return;
362
+ }
363
+
364
+ safety_signatures = rb_ivar_get(self, id_native_safety_signatures_ivar);
365
+ if (RB_TYPE_P(safety_signatures, T_HASH)) {
366
+ rb_hash_delete(safety_signatures, object_id);
367
+ }
328
368
  }
329
369
 
330
370
  /* TypedData-compatible free function */
@@ -1427,6 +1467,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1427
1467
  long wait_ms = cCurlMutiDefaulttimeout;
1428
1468
 
1429
1469
  if (multi_socket_timer_due(ctx)) {
1470
+ ctx->timeout_deadline_ms = -1;
1430
1471
  mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
1431
1472
  curb_debugf("[curb.socket] socket_action timeout(due) -> mrc=%d running=%d", mrc, rbcm->running);
1432
1473
  if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
@@ -1688,10 +1729,14 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1688
1729
  #else
1689
1730
  rb_thread_wait_for(tv);
1690
1731
  #endif
1691
- did_timeout = multi_socket_timer_due(ctx);
1732
+ /* libcurl can report active work without a socket callback or deadline;
1733
+ * drive the timeout socket after the scheduler-aware sleep so the state
1734
+ * machine does not stall indefinitely. */
1735
+ did_timeout = 1;
1692
1736
  }
1693
1737
 
1694
1738
  if (did_timeout) {
1739
+ ctx->timeout_deadline_ms = -1;
1695
1740
  mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
1696
1741
  curb_debugf("[curb.socket] socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
1697
1742
  if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
@@ -2190,6 +2235,7 @@ void init_curb_multi() {
2190
2235
  idCall = rb_intern("call");
2191
2236
  id_deferred_exception_ivar = rb_intern("@__curb_deferred_exception");
2192
2237
  id_deferred_exception_source_id_ivar = rb_intern("@__curb_deferred_exception_source_id");
2238
+ id_native_safety_signatures_ivar = rb_intern("@__curb_native_safety_signatures");
2193
2239
  id_socket_io_cache_ivar = rb_intern("@__curb_socket_io_cache");
2194
2240
  cCurlMulti = rb_define_class_under(mCurl, "Multi", rb_cObject);
2195
2241
 
data/ext/curb_multi.h CHANGED
@@ -28,6 +28,7 @@ extern const rb_data_type_t ruby_curl_multi_data_type;
28
28
 
29
29
  void init_curb_multi();
30
30
  void rb_curl_multi_forget_easy(ruby_curl_multi *rbcm, void *rbce_ptr);
31
+ CURLMcode rb_curl_multi_detach_easy(ruby_curl_multi *rbcm, void *rbce_ptr);
31
32
 
32
33
 
33
34
  #endif
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"
@@ -531,6 +534,7 @@ have_constant "curlusessl_none"
531
534
  have_constant "curlusessl_try"
532
535
  have_constant "curlusessl_control"
533
536
  have_constant "curlusessl_all"
537
+ have_constant "curlopt_connect_to"
534
538
  have_constant "curlopt_resolve"
535
539
  have_constant "curlopt_request_target"
536
540
  have_constant "curlopt_sslcert"
@@ -558,6 +562,10 @@ have_constant :CURL_SSLVERSION_TLSv1_2
558
562
  have_constant :CURL_SSLVERSION_TLSv1_3
559
563
 
560
564
  have_constant "curlopt_ssl_verifypeer"
565
+ have_constant "curlopt_doh_url"
566
+ have_constant "curlopt_doh_ssl_verifypeer"
567
+ have_constant "curlopt_doh_ssl_verifyhost"
568
+ have_constant "curlopt_doh_ssl_verifystatus"
561
569
  have_constant "curlopt_cainfo"
562
570
  have_constant "curlopt_issuercert"
563
571
  have_constant "curlopt_capath"
@@ -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 = url.split(/\?/).first.split(/\//).last) { |curl| ... }
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 (defaults to the last component of the URL path, which will
648
- # usually be the filename most simple urls).
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 = url.split(/\?/).first.split(/\//).last, &blk)
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
- output = if filename.is_a? IO
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
- output.close rescue IOError
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