curb 0.8.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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +579 -0
  3. data/Rakefile +85 -27
  4. data/doc.rb +48 -8
  5. data/ext/banned.h +32 -0
  6. data/ext/curb.c +563 -233
  7. data/ext/curb.h +19 -10
  8. data/ext/curb_easy.c +3229 -368
  9. data/ext/curb_easy.h +42 -0
  10. data/ext/curb_errors.c +117 -18
  11. data/ext/curb_errors.h +9 -5
  12. data/ext/curb_macros.h +33 -21
  13. data/ext/curb_multi.c +1904 -272
  14. data/ext/curb_multi.h +11 -3
  15. data/ext/curb_postfield.c +149 -77
  16. data/ext/curb_postfield.h +1 -0
  17. data/ext/curb_upload.c +38 -11
  18. data/ext/curb_upload.h +2 -0
  19. data/ext/extconf.rb +355 -35
  20. data/lib/curb.rb +1 -0
  21. data/lib/curl/download.rb +160 -0
  22. data/lib/curl/easy.rb +422 -88
  23. data/lib/curl/multi.rb +295 -56
  24. data/lib/curl.rb +676 -11
  25. data/tests/bug_crash_on_debug.rb +14 -28
  26. data/tests/bug_crash_on_progress.rb +32 -16
  27. data/tests/bug_curb_easy_blocks_ruby_threads.rb +10 -15
  28. data/tests/bug_curb_easy_post_with_string_no_content_length_header.rb +6 -30
  29. data/tests/bug_follow_redirect_288.rb +83 -0
  30. data/tests/bug_instance_post_differs_from_class_post.rb +3 -5
  31. data/tests/bug_issue102.rb +17 -0
  32. data/tests/bug_issue_noproxy.rb +56 -0
  33. data/tests/bug_issue_post_redirect.rb +93 -0
  34. data/tests/bug_issue_spnego.rb +41 -0
  35. data/tests/bug_multi_segfault.rb +1 -0
  36. data/tests/bug_poison.rb +29 -0
  37. data/tests/bug_raise_on_callback.rb +30 -0
  38. data/tests/helper.rb +400 -44
  39. data/tests/leak_trace.rb +237 -0
  40. data/tests/mem_check.rb +3 -0
  41. data/tests/tc_curl.rb +31 -1
  42. data/tests/tc_curl_download.rb +98 -7
  43. data/tests/tc_curl_easy.rb +985 -66
  44. data/tests/tc_curl_easy_cookielist.rb +277 -0
  45. data/tests/tc_curl_easy_request_target.rb +41 -0
  46. data/tests/tc_curl_easy_resolve.rb +48 -0
  47. data/tests/tc_curl_maxfilesize.rb +212 -0
  48. data/tests/tc_curl_multi.rb +1111 -51
  49. data/tests/tc_curl_native_coverage.rb +145 -0
  50. data/tests/tc_curl_network_policy.rb +1475 -0
  51. data/tests/tc_curl_postfield.rb +207 -30
  52. data/tests/tc_curl_protocols.rb +388 -0
  53. data/tests/tc_fiber_scheduler.rb +584 -0
  54. data/tests/tc_ftp_options.rb +39 -0
  55. data/tests/tc_gc_compact.rb +223 -0
  56. data/tests/tc_test_server_methods.rb +110 -0
  57. data/tests/timeout.rb +30 -6
  58. metadata +66 -31
  59. data/README +0 -194
data/lib/curl/multi.rb CHANGED
@@ -1,12 +1,25 @@
1
+ # frozen_string_literal: true
2
+ require 'curl/download'
1
3
  module Curl
2
-
3
4
  class Multi
5
+ class DownloadError < RuntimeError
6
+ attr_accessor :errors
7
+ end
8
+
9
+ IDLE_EASY_REFERENCES_USE_WEAK_MAP = begin
10
+ probe = ObjectSpace::WeakMap.new
11
+ probe[Object.new.freeze] = true
12
+ true
13
+ rescue ArgumentError, FrozenError, NameError
14
+ false
15
+ end
16
+
4
17
  class << self
5
18
  # call-seq:
6
- # Curl::Multi.get('url1','url2','url3','url4','url5', :follow_location => true) do|easy|
19
+ # Curl::Multi.get(['url1','url2','url3','url4','url5'], :follow_location => true) do|easy|
7
20
  # easy
8
21
  # end
9
- #
22
+ #
10
23
  # Blocking call to fetch multiple url's in parallel.
11
24
  def get(urls, easy_options={}, multi_options={}, &blk)
12
25
  url_confs = []
@@ -22,13 +35,13 @@ module Curl
22
35
  # {:url => 'url2', :post_fields => {'field1' => 'value1', 'field2' => 'value2'}},
23
36
  # {:url => 'url3', :post_fields => {'field1' => 'value1', 'field2' => 'value2'}}],
24
37
  # { :follow_location => true, :multipart_form_post => true },
25
- # {:pipeline => true }) do|easy|
38
+ # {:pipeline => Curl::CURLPIPE_HTTP1}) do|easy|
26
39
  # easy_handle_on_request_complete
27
40
  # end
28
- #
41
+ #
29
42
  # Blocking call to POST multiple form's in parallel.
30
- #
31
- # urls_with_config: is a hash of url's pointing to the postfields to send
43
+ #
44
+ # urls_with_config: is a hash of url's pointing to the postfields to send
32
45
  # easy_options: are a set of common options to set on all easy handles
33
46
  # multi_options: options to set on the Curl::Multi handle
34
47
  #
@@ -46,13 +59,13 @@ module Curl
46
59
  # {:url => 'url2', :put_data => IO.read('filepath')},
47
60
  # {:url => 'url3', :put_data => "maybe another string or socket?"],
48
61
  # {:follow_location => true},
49
- # {:pipeline => true }) do|easy|
62
+ # {:pipeline => Curl::CURLPIPE_HTTP1}) do|easy|
50
63
  # easy_handle_on_request_complete
51
64
  # end
52
- #
65
+ #
53
66
  # Blocking call to POST multiple form's in parallel.
54
- #
55
- # urls_with_config: is a hash of url's pointing to the postfields to send
67
+ #
68
+ # urls_with_config: is a hash of url's pointing to the postfields to send
56
69
  # easy_options: are a set of common options to set on all easy handles
57
70
  # multi_options: options to set on the Curl::Multi handle
58
71
  #
@@ -74,12 +87,12 @@ module Curl
74
87
  # :follow_location => true, :max_redirects => 3 },
75
88
  # { :url => 'url3', :method => :put, :put_data => File.open('file.txt','rb') },
76
89
  # { :url => 'url4', :method => :head }
77
- # ], {:pipeline => true})
90
+ # ], {:pipeline => Curl::CURLPIPE_HTTP1})
78
91
  #
79
92
  # Blocking call to issue multiple HTTP requests with varying verb's.
80
93
  #
81
94
  # urls_with_config: is a hash of url's pointing to the easy handle options as well as the special option :method, that can by one of [:get, :post, :put, :delete, :head], when no verb is provided e.g. :method => nil -> GET is used
82
- # multi_options: options for the multi handle
95
+ # multi_options: options for the multi handle
83
96
  # blk: a callback, that yeilds when a handle is completed
84
97
  #
85
98
  def http(urls_with_config, multi_options={}, &blk)
@@ -92,13 +105,14 @@ module Curl
92
105
 
93
106
  # configure the multi handle
94
107
  multi_options.each { |k,v| m.send("#{k}=", v) }
95
- callbacks = [:on_progress,:on_debug,:on_failure,:on_success,:on_body,:on_header]
108
+ callbacks = [:on_progress,:on_debug,:on_failure,:on_success,:on_redirect,:on_missing,:on_body,:on_header]
96
109
 
97
110
  add_free_handle = proc do|conf, easy|
98
111
  c = conf.dup # avoid being destructive to input
99
112
  url = c.delete(:url)
100
113
  method = c.delete(:method)
101
114
  headers = c.delete(:headers)
115
+ internal_info = c.delete(:__curb_internal_info)
102
116
 
103
117
  easy = Curl::Easy.new if easy.nil?
104
118
 
@@ -128,23 +142,29 @@ module Curl
128
142
 
129
143
  # headers is a special key
130
144
  headers.each {|k,v| easy.headers[k] = v } if headers
131
-
145
+
132
146
  #
133
147
  # use the remaining options as specific configuration to the easy handle
134
148
  # bad options should raise an undefined method error
135
149
  #
136
150
  c.each { |k,v| easy.send("#{k}=",v) }
137
151
 
138
- easy.on_complete {|curl,code|
152
+ easy.on_complete {|curl|
139
153
  free_handles << curl
140
- blk.call(curl,code,method) if blk
154
+ if blk
155
+ if internal_info
156
+ blk.call(curl,curl.response_code,method,internal_info)
157
+ else
158
+ blk.call(curl,curl.response_code,method)
159
+ end
160
+ end
141
161
  }
142
162
  m.add(easy)
143
163
  end
144
164
 
145
165
  max_connects.times do
146
166
  conf = urls_with_config.pop
147
- add_free_handle.call conf, nil
167
+ add_free_handle.call(conf, nil) if conf
148
168
  break if urls_with_config.empty?
149
169
  end
150
170
 
@@ -153,96 +173,315 @@ module Curl
153
173
  if urls_with_config.size > 0 && free_handles.size > 0
154
174
  easy = free_handles.pop
155
175
  conf = urls_with_config.pop
156
- add_free_handle.call conf, easy
176
+ add_free_handle.call(conf, easy) if conf
157
177
  end
158
178
  end
159
179
 
160
- if urls_with_config.empty?
161
- m.perform
162
- else
163
- until urls_with_config.empty?
164
- m.perform do
180
+ begin
181
+ if urls_with_config.empty?
182
+ m.perform
183
+ else
184
+ until urls_with_config.empty?
185
+ m.perform do
186
+ consume_free_handles.call
187
+ end
165
188
  consume_free_handles.call
166
189
  end
167
- consume_free_handles.call
190
+ free_handles = nil
168
191
  end
169
- free_handles = nil
192
+ ensure
193
+ m.close
170
194
  end
195
+
171
196
  end
172
197
 
173
198
  # call-seq:
174
199
  #
175
200
  # Curl::Multi.download(['http://example.com/p/a/t/h/file1.txt','http://example.com/p/a/t/h/file2.txt']){|c|}
176
201
  #
177
- # will create 2 new files file1.txt and file2.txt
178
- #
202
+ # will create 2 new files file1.txt and file2.txt, unless either file
203
+ # already exists. Auto-derived filenames are safely derived from the last
204
+ # URL path component. Pass <tt>:download_dir</tt> as the fifth argument to
205
+ # treat download_paths as basenames inside a trusted directory and reject
206
+ # absolute, parent-directory, dotfile, and nested names.
207
+ #
179
208
  # 2 files will be opened, and remain open until the call completes
180
209
  #
181
210
  # when using the :post or :put method, urls should be a hash, including the individual post fields per post
182
211
  #
183
- def download(urls,easy_options={},multi_options={},download_paths=nil,&blk)
212
+ def download(urls,easy_options={},multi_options={},download_paths=nil,download_options={},&blk)
184
213
  errors = []
185
214
  procs = []
186
215
  files = []
187
216
  urls_with_config = []
188
- url_to_download_paths = {}
217
+ seen_download_paths = {}
218
+ download_infos = []
219
+
220
+ if Curl.download_options_hash?(download_paths) && download_options.empty?
221
+ download_options = download_paths
222
+ download_paths = nil
223
+ end
189
224
 
190
225
  urls.each_with_index do|urlcfg,i|
191
226
  if urlcfg.is_a?(Hash)
192
- url = url[:url]
227
+ url = urlcfg[:url]
193
228
  else
194
229
  url = urlcfg
195
230
  end
196
231
 
197
- if download_paths and download_paths[i]
198
- download_path = download_paths[i]
199
- else
200
- download_path = File.basename(url)
232
+ download_path_arg = download_paths && download_paths[i]
233
+ download_path, file, safe_output, overwrite = Curl.resolve_download_output(url, download_path_arg, download_options)
234
+
235
+ if safe_output
236
+ expanded_path = File.expand_path(download_path)
237
+ raise ArgumentError, "duplicate download destination: #{download_path}" if seen_download_paths[expanded_path]
238
+
239
+ seen_download_paths[expanded_path] = true
201
240
  end
202
241
 
203
- file = lambda do|dp|
204
- file = File.open(dp,"wb")
205
- procs << (lambda {|data| file.write data; data.size })
206
- files << file
207
- file
208
- end.call(download_path)
242
+ download_infos << {
243
+ :url => url,
244
+ :urlcfg => urlcfg,
245
+ :path => download_path,
246
+ :file => file,
247
+ :safe_output => safe_output,
248
+ :overwrite => overwrite
249
+ }
250
+ end
209
251
 
210
- if urlcfg.is_a?(Hash)
211
- urls_with_config << urlcfg.merge({:on_body => procs.last}.merge(easy_options))
252
+ download_infos.each do |info|
253
+ info[:file] ||= Curl.open_safe_download_output(info[:path], :overwrite => info[:overwrite])
254
+ file = info[:file]
255
+ procs << (lambda {|data| file.write data; data.size })
256
+ files << file
257
+
258
+ if info[:urlcfg].is_a?(Hash)
259
+ urls_with_config << info[:urlcfg].merge({:on_body => procs.last, :__curb_internal_info => info}.merge(easy_options))
212
260
  else
213
- urls_with_config << {:url => url, :on_body => procs.last, :method => :get}.merge(easy_options)
261
+ urls_with_config << {:url => info[:url], :on_body => procs.last, :method => :get, :__curb_internal_info => info}.merge(easy_options)
214
262
  end
215
- url_to_download_paths[url] = {:path => download_path, :file => file} # store for later
216
263
  end
217
264
 
218
- if blk
219
- # when injecting the block, ensure file is closed before yielding
220
- Curl::Multi.http(urls_with_config, multi_options) do |c,code,method|
221
- info = url_to_download_paths[c.url]
265
+ finalize_download = lambda do |curl, info|
266
+ file = info[:file]
267
+ files.reject!{|f| f == file }
268
+
269
+ if curl.last_result != 0
222
270
  begin
223
- file = info[:file]
224
- files.reject!{|f| f == file }
225
- file.close
271
+ if info[:safe_output]
272
+ file.close(false)
273
+ else
274
+ file.close
275
+ end
226
276
  rescue => e
227
277
  errors << e
228
278
  end
279
+ err_class, err_summary = Curl::Easy.error(curl.last_result)
280
+ err_detail = curl.last_error
281
+ errors << err_class.new([err_summary, err_detail].compact.join(": "))
282
+ false
283
+ else
284
+ begin
285
+ if info[:safe_output]
286
+ file.close(true)
287
+ else
288
+ file.close
289
+ end
290
+ true
291
+ rescue => e
292
+ errors << e
293
+ false
294
+ end
295
+ end
296
+ end
297
+
298
+ Curl::Multi.http(urls_with_config, multi_options) do |c,code,method,info|
299
+ if finalize_download.call(c, info) && blk
229
300
  blk.call(c,info[:path])
230
301
  end
231
- else
232
- Curl::Multi.http(urls_with_config, multi_options)
233
302
  end
234
303
 
235
304
  ensure
305
+ pending_exception = $!
236
306
  files.each {|f|
237
307
  begin
238
- f.close
308
+ if f.is_a?(Curl::SafeDownloadOutput)
309
+ f.close(false)
310
+ else
311
+ f.close
312
+ end
239
313
  rescue => e
240
314
  errors << e
241
315
  end
242
316
  }
243
- raise errors unless errors.empty?
317
+ if errors.any? && !pending_exception
318
+ de = Curl::Multi::DownloadError.new
319
+ de.errors = errors
320
+ raise de
321
+ end
322
+ end
323
+ end
324
+
325
+ def cancel!
326
+ requests.each do |_,easy|
327
+ remove(easy)
328
+ end
329
+ end
330
+
331
+ def idle?
332
+ requests.empty?
333
+ end
334
+
335
+ def requests
336
+ @requests ||= {}
337
+ end
338
+
339
+ def __curb_native_safety_signatures
340
+ @__curb_native_safety_signatures ||= {}
341
+ end
342
+
343
+ def __curb_safety_signature_for(easy)
344
+ safety_signature = if Curl.respond_to?(:safety_signature_for, true)
345
+ Curl.__send__(:safety_signature_for, easy)
346
+ end
347
+
348
+ [
349
+ safety_signature,
350
+ easy.respond_to?(:network_policy) ? easy.network_policy : nil,
351
+ easy.respond_to?(:allowed_hosts) ? easy.allowed_hosts : nil,
352
+ easy.respond_to?(:allowed_cidrs) ? easy.allowed_cidrs : nil
353
+ ]
354
+ end
355
+
356
+ def __record_native_safety_signature(easy)
357
+ __curb_native_safety_signatures[easy.object_id] = __curb_safety_signature_for(easy)
358
+ end
359
+
360
+ def __idle_easy_references
361
+ @__curb_idle_easy_references ||= __new_idle_easy_references
362
+ end
363
+
364
+ def __register_idle_easy_reference(easy)
365
+ if IDLE_EASY_REFERENCES_USE_WEAK_MAP
366
+ __idle_easy_references[easy] = true
367
+ else
368
+ __idle_easy_references[easy.object_id] = true
244
369
  end
370
+ self
371
+ end
372
+
373
+ def __unregister_idle_easy_reference(easy)
374
+ return self unless instance_variable_defined?(:@__curb_idle_easy_references)
375
+
376
+ if !IDLE_EASY_REFERENCES_USE_WEAK_MAP
377
+ @__curb_idle_easy_references.delete(easy.object_id)
378
+ elsif @__curb_idle_easy_references.respond_to?(:delete)
379
+ @__curb_idle_easy_references.delete(easy)
380
+ else
381
+ retained_references = ObjectSpace::WeakMap.new
382
+ @__curb_idle_easy_references.each_key do |tracked_easy|
383
+ next if tracked_easy.equal?(easy)
245
384
 
385
+ retained_references[tracked_easy] = true
386
+ end
387
+ @__curb_idle_easy_references = retained_references
388
+ end
389
+
390
+ self
391
+ end
392
+
393
+ def __clear_idle_easy_references
394
+ return unless instance_variable_defined?(:@__curb_idle_easy_references)
395
+
396
+ if IDLE_EASY_REFERENCES_USE_WEAK_MAP
397
+ @__curb_idle_easy_references.keys.each do |easy|
398
+ easy.multi = nil if easy.multi.equal?(self)
399
+ end
400
+ else
401
+ @__curb_idle_easy_references.keys.each do |easy_object_id|
402
+ begin
403
+ easy = ObjectSpace._id2ref(easy_object_id)
404
+ rescue RangeError
405
+ next
406
+ end
407
+
408
+ next unless easy.is_a?(Curl::Easy)
409
+
410
+ easy.multi = nil if easy.multi.equal?(self)
411
+ end
412
+ end
413
+ @__curb_idle_easy_references = __new_idle_easy_references
246
414
  end
415
+
416
+ def __new_idle_easy_references
417
+ IDLE_EASY_REFERENCES_USE_WEAK_MAP ? ObjectSpace::WeakMap.new : {}
418
+ end
419
+
420
+ private :__idle_easy_references, :__curb_native_safety_signatures,
421
+ :__curb_safety_signature_for, :__record_native_safety_signature,
422
+ :__register_idle_easy_reference,
423
+ :__unregister_idle_easy_reference, :__clear_idle_easy_references,
424
+ :__new_idle_easy_references
425
+
426
+ alias_method :_curb_native_perform, :perform
427
+
428
+ def perform(*args, &block)
429
+ requests.each_value do |easy|
430
+ Curl.__send__(:apply_safety!, easy) if Curl.respond_to?(:apply_safety!, true)
431
+ signature = __curb_safety_signature_for(easy)
432
+ if __curb_native_safety_signatures[easy.object_id] != signature &&
433
+ easy.respond_to?(:__curb_native_setup!, true)
434
+ easy.__send__(:__curb_native_setup!)
435
+ __curb_native_safety_signatures[easy.object_id] = signature
436
+ end
437
+ end
438
+
439
+ _curb_native_perform(*args, &block)
440
+ end
441
+
442
+ def add(easy)
443
+ return self if requests[easy.object_id]
444
+ # Once a deferred callback exception is pending, Multi#perform is
445
+ # draining existing transfers only and must not start replacement work.
446
+ return self if instance_variable_defined?(:@__curb_deferred_exception)
447
+ Curl.__send__(:apply_safety!, easy) if Curl.respond_to?(:apply_safety!, true)
448
+ _add(easy)
449
+ __unregister_idle_easy_reference(easy)
450
+ requests[easy.object_id] = easy
451
+ __record_native_safety_signature(easy)
452
+ self
453
+ end
454
+
455
+ def remove(easy)
456
+ return self if !requests[easy.object_id]
457
+ requests.delete(easy.object_id)
458
+ __curb_native_safety_signatures.delete(easy.object_id)
459
+ _remove(easy)
460
+ self
461
+ end
462
+
463
+ def close
464
+ __close(true)
465
+ end
466
+
467
+ def _autoclose
468
+ __close(false)
469
+ end
470
+
471
+ private :_autoclose
472
+
473
+ private
474
+
475
+ def __close(permanent)
476
+ requests.values.each {|easy|
477
+ _remove(easy)
478
+ }
479
+ __clear_idle_easy_references if permanent
480
+ @requests = {}
481
+ _close
482
+ _mark_closed if permanent
483
+ self
484
+ end
485
+
247
486
  end
248
487
  end