curb 1.3.7 → 1.4.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.
data/ext/curb_multi.h CHANGED
@@ -21,6 +21,10 @@ typedef struct {
21
21
  char allow_close_during_perform;
22
22
  CURLM *handle;
23
23
  struct st_table *attached;
24
+ /* Bumped on every add and stamped onto the easy (multi_attachment_generation),
25
+ * so "attached after point X" is a comparison instead of a table snapshot.
26
+ * Monotonic for the object's lifetime; never reset. */
27
+ unsigned long attachment_sequence;
24
28
  } ruby_curl_multi;
25
29
 
26
30
  extern VALUE cCurlMulti;
data/ext/curb_postfield.c CHANGED
@@ -468,7 +468,10 @@ static VALUE ruby_curl_postfield_to_str(VALUE self) {
468
468
  name = rbcpf->name;
469
469
  if (TYPE(name) != T_STRING) {
470
470
  if (rb_respond_to(name, rb_intern("to_s")))
471
- name = rb_funcall(name, rb_intern("to_s"), 0);
471
+ /* rb_obj_as_string always yields a String, even if #to_s misbehaves
472
+ * and returns a non-String; a raw rb_funcall result could later be
473
+ * handed to RSTRING_PTR/RSTRING_LEN and crash. */
474
+ name = rb_obj_as_string(name);
472
475
  else
473
476
  name = Qnil;
474
477
  }
@@ -481,6 +484,33 @@ static VALUE ruby_curl_postfield_to_str(VALUE self) {
481
484
  /* Force field name to UTF-8 before escaping */
482
485
  VALUE name_utf8 = rb_str_export_to_enc(name, rb_utf8_encoding());
483
486
 
487
+ /* Resolve and coerce the content before creating the curl handle: the
488
+ * content proc, #to_s and the UTF-8 export can all raise, which would
489
+ * otherwise leak the handle. */
490
+ VALUE tmpcontent = Qnil;
491
+ if (rbcpf->content_proc != Qnil) {
492
+ tmpcontent = rb_funcall(rbcpf->content_proc, idCall, 1, self);
493
+ } else if (rbcpf->content != Qnil) {
494
+ tmpcontent = rbcpf->content;
495
+ } else if (rbcpf->local_file != Qnil) {
496
+ tmpcontent = rbcpf->local_file;
497
+ } else if (rbcpf->remote_file != Qnil) {
498
+ tmpcontent = rbcpf->remote_file;
499
+ } else {
500
+ tmpcontent = rb_str_new2("");
501
+ }
502
+ if (TYPE(tmpcontent) != T_STRING) {
503
+ if (rb_respond_to(tmpcontent, rb_intern("to_s")))
504
+ /* rb_obj_as_string always yields a String, even if #to_s misbehaves */
505
+ tmpcontent = rb_obj_as_string(tmpcontent);
506
+ else
507
+ rb_raise(rb_eRuntimeError,
508
+ "postfield(%"PRIsVALUE") is not a string and does not respond_to to_s",
509
+ name);
510
+ }
511
+ /* Force content to UTF-8 before escaping */
512
+ VALUE content_utf8 = rb_str_export_to_enc(tmpcontent, rb_utf8_encoding());
513
+
484
514
  #ifdef HAVE_CURL_EASY_ESCAPE
485
515
  curl_handle = curl_easy_init();
486
516
  if (!curl_handle) {
@@ -499,38 +529,8 @@ static VALUE ruby_curl_postfield_to_str(VALUE self) {
499
529
  #endif
500
530
 
501
531
  VALUE escd_name = rb_str_new2(tmpchrs);
502
- #ifdef HAVE_CURL_EASY_ESCAPE
503
- curl_free(tmpchrs);
504
- #else
505
532
  curl_free(tmpchrs);
506
- #endif
507
533
 
508
- VALUE tmpcontent = Qnil;
509
- if (rbcpf->content_proc != Qnil) {
510
- tmpcontent = rb_funcall(rbcpf->content_proc, idCall, 1, self);
511
- } else if (rbcpf->content != Qnil) {
512
- tmpcontent = rbcpf->content;
513
- } else if (rbcpf->local_file != Qnil) {
514
- tmpcontent = rbcpf->local_file;
515
- } else if (rbcpf->remote_file != Qnil) {
516
- tmpcontent = rbcpf->remote_file;
517
- } else {
518
- tmpcontent = rb_str_new2("");
519
- }
520
- if (TYPE(tmpcontent) != T_STRING) {
521
- if (rb_respond_to(tmpcontent, rb_intern("to_s")))
522
- tmpcontent = rb_funcall(tmpcontent, rb_intern("to_s"), 0);
523
- else {
524
- #ifdef HAVE_CURL_EASY_ESCAPE
525
- curl_easy_cleanup(curl_handle);
526
- #endif
527
- rb_raise(rb_eRuntimeError,
528
- "postfield(%s) is not a string and does not respond_to to_s",
529
- RSTRING_PTR(escd_name));
530
- }
531
- }
532
- /* Force content to UTF-8 before escaping */
533
- VALUE content_utf8 = rb_str_export_to_enc(tmpcontent, rb_utf8_encoding());
534
534
  #ifdef HAVE_CURL_EASY_ESCAPE
535
535
  tmpchrs = curl_easy_escape(curl_handle, StringValuePtr(content_utf8), (int)RSTRING_LEN(content_utf8));
536
536
  if (!tmpchrs) {
data/ext/extconf.rb CHANGED
@@ -260,6 +260,7 @@ have_constant "curl_version_idn"
260
260
  have_constant "curl_version_sspi"
261
261
  have_constant "curl_version_conv"
262
262
  have_constant "curl_version_http2"
263
+ have_constant "curl_version_threadsafe"
263
264
  have_constant "curlproxy_http"
264
265
  have_constant "curlproxy_socks4"
265
266
  have_constant "curlproxy_socks4a"
@@ -704,6 +705,7 @@ have_func('rb_fiber_scheduler_current', 'ruby/fiber/scheduler.h')
704
705
  have_func('rb_fiber_scheduler_io_wait', 'ruby/fiber/scheduler.h')
705
706
  have_func('rb_fiber_scheduler_io_select', 'ruby/fiber/scheduler.h')
706
707
  have_func('rb_fiber_scheduler_kernel_sleep', 'ruby/fiber/scheduler.h')
708
+ have_func('rb_ext_ractor_safe')
707
709
  have_func('rb_io_stdio_file')
708
710
  have_func('curl_multi_wait')
709
711
  have_func('curl_multi_socket_action')
@@ -714,6 +716,10 @@ have_func('curl_multi_socket')
714
716
  have_func('curl_multi_timer_callback')
715
717
  have_constant 'curlmopt_socketfunction'
716
718
  have_constant 'curlmopt_timerfunction'
719
+ # Kernel event queues let the socket-action loop wait on one descriptor for
720
+ # every libcurl socket under a fiber scheduler.
721
+ have_header('sys/epoll.h') && have_func('epoll_create1', 'sys/epoll.h')
722
+ have_header('sys/event.h') && have_func('kqueue', ['sys/types.h', 'sys/event.h'])
717
723
  have_func('curl_easy_duphandle')
718
724
 
719
725
  # Optional: enable verbose socket-action debug logging.
data/lib/curl/easy.rb CHANGED
@@ -3,13 +3,20 @@ require 'curl/download'
3
3
  module Curl
4
4
  class Easy
5
5
  class << self
6
+ def deferred_multi_close_state
7
+ Curl.__send__(:ractor_local_state)
8
+ end
9
+ private :deferred_multi_close_state
10
+
6
11
  def deferred_multi_close_mutex
7
- @deferred_multi_close_mutex ||= Mutex.new
12
+ state = deferred_multi_close_state
13
+ state[:deferred_multi_close_mutex] ||= Mutex.new
8
14
  end
9
15
 
10
16
  def deferred_multi_closes
17
+ state = deferred_multi_close_state
11
18
  deferred_multi_close_mutex.synchronize do
12
- (@deferred_multi_closes ||= []).dup
19
+ (state[:deferred_multi_closes] ||= []).dup
13
20
  end
14
21
  end
15
22
 
@@ -34,27 +41,29 @@ module Curl
34
41
  end
35
42
 
36
43
  def defer_multi_close(multi, easy, owner: Thread.current)
44
+ state = deferred_multi_close_state
37
45
  deferred_multi_close_mutex.synchronize do
38
- @deferred_multi_closes ||= []
39
- return if @deferred_multi_closes.any? { |entry| entry[:multi].equal?(multi) }
46
+ state[:deferred_multi_closes] ||= []
47
+ return if state[:deferred_multi_closes].any? { |entry| entry[:multi].equal?(multi) }
40
48
 
41
49
  multi.instance_variable_set(:@deferred_close, true)
42
- @deferred_multi_closes << { multi: multi, easy: easy, owner: owner }
50
+ state[:deferred_multi_closes] << { multi: multi, easy: easy, owner: owner }
43
51
  end
44
52
  end
45
53
 
46
54
  def flush_deferred_multi_closes(all_threads: false)
55
+ state = deferred_multi_close_state
47
56
  pending = deferred_multi_close_mutex.synchronize do
48
- @deferred_multi_closes ||= []
57
+ deferred_closes = (state[:deferred_multi_closes] ||= [])
49
58
 
50
59
  if all_threads
51
- @deferred_multi_closes.shift(@deferred_multi_closes.length)
60
+ deferred_closes.shift(deferred_closes.length)
52
61
  else
53
62
  owner = Thread.current
54
63
  remaining = []
55
64
  current = []
56
65
 
57
- @deferred_multi_closes.each do |entry|
66
+ deferred_closes.each do |entry|
58
67
  if entry[:owner].equal?(owner)
59
68
  current << entry
60
69
  else
@@ -62,7 +71,7 @@ module Curl
62
71
  end
63
72
  end
64
73
 
65
- @deferred_multi_closes = remaining
74
+ state[:deferred_multi_closes] = remaining
66
75
  current
67
76
  end
68
77
  end
data/lib/curl.rb CHANGED
@@ -8,6 +8,19 @@ require 'uri'
8
8
 
9
9
  # expose shortcut methods
10
10
  module Curl
11
+ # Module/class instance variables cannot be read or written by non-main
12
+ # Ractors, even when the value itself would otherwise be safe. Keep curb's
13
+ # mutable Ruby-side configuration in storage owned by the current Ractor.
14
+ # Older Rubies retain the historical process-wide state.
15
+ def self.ractor_local_state
16
+ if defined?(Ractor) && Ractor.respond_to?(:current) &&
17
+ Ractor.current.respond_to?(:[]) && Ractor.current.respond_to?(:[]=)
18
+ Ractor.current[:__curb_ractor_local_state] ||= {}
19
+ else
20
+ @legacy_local_state ||= {}
21
+ end
22
+ end
23
+
11
24
  class SafetyConfig
12
25
  DEFAULT_PROTOCOLS = [:http, :https].freeze
13
26
 
@@ -176,12 +189,12 @@ module Curl
176
189
  def self.safe!
177
190
  config = SafetyConfig.new
178
191
  yield config if block_given?
179
- @safety_config = config
192
+ ractor_local_state[:safety_config] = config
180
193
  bump_safety_generation!
181
194
  end
182
195
 
183
196
  def self.apply_safety!(easy)
184
- config = @safety_config
197
+ config = ractor_local_state[:safety_config]
185
198
  override = safety_override_for(easy)
186
199
  return easy unless config || override
187
200
 
@@ -215,12 +228,12 @@ module Curl
215
228
  end
216
229
 
217
230
  def self.clear_safe!
218
- @safety_config = nil
231
+ ractor_local_state[:safety_config] = nil
219
232
  bump_safety_generation!
220
233
  end
221
234
 
222
235
  def self.safety_active_for?(easy)
223
- !!(@safety_config || safety_override_for(easy))
236
+ !!(ractor_local_state[:safety_config] || safety_override_for(easy))
224
237
  end
225
238
 
226
239
  def self.safety_signature_for(easy)
@@ -240,11 +253,11 @@ module Curl
240
253
  end
241
254
 
242
255
  def self.safety_generation
243
- @safety_generation ||= 0
256
+ ractor_local_state[:safety_generation] ||= 0
244
257
  end
245
258
 
246
259
  def self.bump_safety_generation!
247
- @safety_generation = safety_generation + 1
260
+ ractor_local_state[:safety_generation] = safety_generation + 1
248
261
  end
249
262
 
250
263
  def self.safety_protocol_intersection(base_protocols, override_protocols)
@@ -395,7 +408,8 @@ module Curl
395
408
  easy.set(Curl::CURLOPT_NOPROXY, "*") if Curl.const_defined?(:CURLOPT_NOPROXY)
396
409
  end
397
410
 
398
- private_class_method :apply_safety!, :clear_safe!, :safety_active_for?,
411
+ private_class_method :ractor_local_state,
412
+ :apply_safety!, :clear_safe!, :safety_active_for?,
399
413
  :safety_signature_for, :safety_override_for,
400
414
  :safety_generation, :bump_safety_generation!,
401
415
  :safety_protocol_intersection,
@@ -480,23 +494,44 @@ module Curl
480
494
  end
481
495
  end
482
496
 
497
+ # Wakes the waiters whose transfers finished since the last call. Completions
498
+ # are queued by the on_complete wrapper in perform_with_scheduler, so each
499
+ # drive-loop pass costs O(new completions) rather than O(all waiters).
483
500
  def self.release_scheduler_waiters(state)
484
- source_id = deferred_exception_source_id(state)
501
+ completed = state[:completed]
502
+ return if completed.empty?
485
503
 
486
- state[:waiters].each do |easy_id, waiter|
487
- next if source_id == easy_id
504
+ source_id = deferred_exception_source_id(state)
505
+ retained = nil
506
+
507
+ until completed.empty?
508
+ easy_id, waiter = completed.shift
509
+ # A waiter whose perform already returned (it raised) is no longer
510
+ # blocked on this completion; waking its fiber would be spurious.
511
+ next unless state[:waiters][easy_id].equal?(waiter)
512
+
513
+ if source_id == easy_id
514
+ # The transfer that raised the deferred exception gets that error via
515
+ # release_scheduler_error. Keep it queued in case the source changes.
516
+ (retained ||= []) << [easy_id, waiter]
517
+ next
518
+ end
488
519
 
489
- complete_scheduler_waiter(waiter) if waiter[:completed]
520
+ complete_scheduler_waiter(waiter)
490
521
  end
522
+
523
+ completed.concat(retained) if retained
491
524
  end
492
525
 
493
526
  def self.perform_with_scheduler(easy)
494
527
  state = scheduler_state
528
+ easy_id = easy.object_id
495
529
  waiter = {completed: false, done: false, error: nil, fiber: nil, scheduler: Fiber.scheduler}
496
- state[:waiters][easy.object_id] = waiter
530
+ state[:waiters][easy_id] = waiter
497
531
  previous_complete = easy.on_complete do |completed_easy|
498
532
  previous_complete.call(completed_easy) if previous_complete
499
533
  waiter[:completed] = true
534
+ state[:completed] << [easy_id, waiter]
500
535
  end
501
536
 
502
537
  state[:pending] << easy
@@ -533,6 +568,8 @@ module Curl
533
568
  driver_running: false,
534
569
  error: nil,
535
570
  waiters: {},
571
+ # [easy object_id, waiter] pairs completed but not yet woken
572
+ completed: [],
536
573
  }
537
574
  Thread.current.thread_variable_set(:curb_scheduler_state, state)
538
575
  state
data/tests/helper.rb CHANGED
@@ -228,12 +228,17 @@ module BugTestServerSetupTeardown
228
228
  @thread = Thread.new(@server) do|srv|
229
229
  srv.start
230
230
  end
231
+
232
+ # WEBrick#shutdown is a no-op until start has created its shutdown pipe
233
+ # and set :Running, so wait for that before the test (and its teardown)
234
+ # runs. Otherwise a teardown racing server startup could miss the brief
235
+ # :Shutdown status and spin forever once the server settled back on :Stop.
236
+ sleep 0.001 until @server.status == :Running || !@thread.alive?
237
+ @thread.value unless @thread.alive?
231
238
  end
232
239
 
233
240
  def teardown
234
- while @server.status != :Shutdown
235
- @server.shutdown
236
- end
241
+ @server.shutdown
237
242
  @thread.join
238
243
  end
239
244
  end
@@ -730,3 +735,117 @@ end
730
735
  unless URI.methods.include?(:encode_www_form)
731
736
  URI.extend(Backports::Ruby18::URIFormEncoding)
732
737
  end
738
+
739
+ # Valgrind slows every request enough that tight wall-clock bounds stop
740
+ # meaning anything; tests use this to relax or skip only those bounds.
741
+ def curb_under_valgrind?
742
+ ENV['LD_PRELOAD'].to_s.include?('vgpreload')
743
+ end
744
+
745
+ # Queue#pop(timeout:) needs Ruby 3.2. Older Rubies take the keyword hash as
746
+ # the positional non_block flag and raise ThreadError immediately.
747
+ def curb_pop_within(queue, seconds)
748
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
749
+ loop do
750
+ begin
751
+ return queue.pop(true)
752
+ rescue ThreadError
753
+ return nil if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
754
+ sleep 0.01
755
+ end
756
+ end
757
+ end
758
+
759
+ # Minimal HTTP/1.1 server so a test controls exactly when and how a response
760
+ # is written. Every request gets its own connection (Connection: close).
761
+ #
762
+ # /fast "fast" immediately
763
+ # /delay/<seconds> "delayed" after sleeping
764
+ # /hang "released" once release_hung is called
765
+ # /stream/<chunks>/<n> <chunks> writes of <n> bytes (see stream_body)
766
+ # /truncate promises 100000 bytes, sends 10, then closes
767
+ # /reset sends part of a body, then aborts with a TCP RST
768
+ class CurbTinyHTTPServer
769
+ attr_reader :port, :requests
770
+
771
+ # Chunk i is filled with one letter so a test can verify reassembly.
772
+ def self.stream_body(chunks, bytes)
773
+ (0...chunks).map { |i| (97 + (i % 26)).chr * bytes }.join
774
+ end
775
+
776
+ def initialize
777
+ @listener = TCPServer.new('127.0.0.1', 0)
778
+ @port = @listener.addr[1]
779
+ @requests = Queue.new
780
+ @release = Queue.new
781
+ @clients = []
782
+ @thread = Thread.new { accept_loop }
783
+ end
784
+
785
+ def url(path)
786
+ "http://127.0.0.1:#{@port}#{path}"
787
+ end
788
+
789
+ # Let pending /hang requests finish.
790
+ def release_hung(count = 64)
791
+ count.times { @release << true }
792
+ end
793
+
794
+ def close
795
+ release_hung
796
+ @listener.close rescue nil
797
+ @thread.kill
798
+ @thread.join(2)
799
+ @clients.each { |t| t.kill; t.join(1) }
800
+ end
801
+
802
+ private
803
+
804
+ def accept_loop
805
+ loop do
806
+ sock = @listener.accept
807
+ @clients << Thread.new(sock) { |s| handle(s) }
808
+ end
809
+ rescue IOError, Errno::EBADF
810
+ nil
811
+ end
812
+
813
+ def handle(sock)
814
+ request_line = sock.gets.to_s
815
+ while (line = sock.gets) && line != "\r\n"; end
816
+ path = request_line.split(' ')[1].to_s
817
+ @requests << path
818
+
819
+ case path
820
+ when %r{\A/delay/([\d.]+)}
821
+ sleep Float($1)
822
+ respond(sock, "delayed")
823
+ when '/hang'
824
+ curb_pop_within(@release, 10)
825
+ respond(sock, "released")
826
+ when %r{\A/stream/(\d+)/(\d+)}
827
+ chunks = Integer($1)
828
+ bytes = Integer($2)
829
+ sock.write("HTTP/1.1 200 OK\r\nContent-Length: #{chunks * bytes}\r\nConnection: close\r\n\r\n")
830
+ chunks.times do |i|
831
+ sock.write((97 + (i % 26)).chr * bytes)
832
+ Thread.pass
833
+ end
834
+ when '/truncate'
835
+ sock.write("HTTP/1.1 200 OK\r\nContent-Length: 100000\r\nConnection: close\r\n\r\n0123456789")
836
+ when '/reset'
837
+ sock.write("HTTP/1.1 200 OK\r\nContent-Length: 100000\r\nConnection: close\r\n\r\n0123456789")
838
+ sock.setsockopt(Socket::Option.linger(true, 0))
839
+ else
840
+ respond(sock, "fast")
841
+ end
842
+ rescue IOError, SystemCallError
843
+ nil
844
+ ensure
845
+ sock.close rescue nil
846
+ end
847
+
848
+ def respond(sock, body)
849
+ sock.write("HTTP/1.1 200 OK\r\nContent-Length: #{body.bytesize}\r\nConnection: close\r\n\r\n#{body}")
850
+ end
851
+ end
@@ -1,8 +1,14 @@
1
1
  require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
- class FooNoToS
2
+ class FooNoToS
3
3
  undef to_s
4
4
  end
5
5
 
6
+ class FooBadToS
7
+ def to_s
8
+ 42
9
+ end
10
+ end
11
+
6
12
  class TestCurbCurlEasy < Test::Unit::TestCase
7
13
  def test_global_reset
8
14
  Curl.get($TEST_URL)
@@ -560,7 +566,24 @@ class TestCurbCurlEasy < Test::Unit::TestCase
560
566
  assert_equal "one\000two three", c.unescape("one%00two%20three")
561
567
  end
562
568
  end
563
-
569
+
570
+ def test_escape_with_non_string_to_s_result_does_not_crash
571
+ c = Curl::Easy.new
572
+ result = nil
573
+ # #to_s returning a non-String (42, a Fixnum) makes rb_obj_as_string
574
+ # fall back to the default Object#to_s representation rather than "42" -
575
+ # the point of this test is that it doesn't crash, not the exact value.
576
+ assert_nothing_raised { result = c.escape(FooBadToS.new) }
577
+ assert_kind_of String, result
578
+ end
579
+
580
+ def test_unescape_with_non_string_to_s_result_raises_type_error
581
+ c = Curl::Easy.new
582
+ # unescape expects an actual String (or something #to_str-coercible);
583
+ # it must raise cleanly rather than crash on a bad #to_s.
584
+ assert_raise(TypeError) { c.unescape(FooBadToS.new) }
585
+ end
586
+
564
587
  def test_headers
565
588
  c = Curl::Easy.new($TEST_URL)
566
589
 
@@ -745,7 +768,23 @@ class TestCurbCurlEasy < Test::Unit::TestCase
745
768
  c.http_auth_types = nil
746
769
  assert_nil c.http_auth_types
747
770
  end
748
-
771
+
772
+ def test_http_auth_types_with_non_string_to_s_result_does_not_crash
773
+ c = Curl::Easy.new
774
+ assert_nothing_raised { c.http_auth_types = FooBadToS.new }
775
+ assert_nothing_raised { c.http_auth_types = [FooBadToS.new, :basic] }
776
+ assert_equal Curl::CURLAUTH_BASIC, c.http_auth_types
777
+ end
778
+
779
+ def test_http_auth_types_array_of_symbols
780
+ c = Curl::Easy.new
781
+ c.http_auth_types = [:basic, :digest]
782
+ assert_equal Curl::CURLAUTH_BASIC | Curl::CURLAUTH_DIGEST, c.http_auth_types
783
+
784
+ c.send(:http_auth_types=, :basic, :ntlm)
785
+ assert_equal Curl::CURLAUTH_BASIC | Curl::CURLAUTH_NTLM, c.http_auth_types
786
+ end
787
+
749
788
  def test_proxy_auth_types_01
750
789
  c = Curl::Easy.new($TEST_URL)
751
790
 
@@ -757,7 +796,23 @@ class TestCurbCurlEasy < Test::Unit::TestCase
757
796
  c.proxy_auth_types = nil
758
797
  assert_nil c.proxy_auth_types
759
798
  end
760
-
799
+
800
+ def test_proxy_auth_types_symbols
801
+ c = Curl::Easy.new
802
+
803
+ c.proxy_auth_types = :basic
804
+ assert_equal Curl::CURLAUTH_BASIC, c.proxy_auth_types
805
+
806
+ c.proxy_auth_types = [:basic, :ntlm]
807
+ assert_equal Curl::CURLAUTH_BASIC | Curl::CURLAUTH_NTLM, c.proxy_auth_types
808
+
809
+ c.send(:proxy_auth_types=, :basic, :digest)
810
+ assert_equal Curl::CURLAUTH_BASIC | Curl::CURLAUTH_DIGEST, c.proxy_auth_types
811
+
812
+ assert_nothing_raised { c.proxy_auth_types = [FooBadToS.new, :basic] }
813
+ assert_equal Curl::CURLAUTH_BASIC, c.proxy_auth_types
814
+ end
815
+
761
816
  def test_max_redirects_01
762
817
  c = Curl::Easy.new($TEST_URL)
763
818
 
@@ -46,14 +46,17 @@ class TestCurbCurlEasyCookielist < Test::Unit::TestCase
46
46
  expires = (Date.today + 2).to_datetime
47
47
  easy.url = "http://localhost:#{TestServlet.port}#{TestServlet.path}/set_cookies"
48
48
  easy.setopt(Curl::CURLOPT_COOKIELIST, "Set-Cookie: c1=v1; domain=localhost; expires=#{expires.httpdate};")
49
- easy.post_body = JSON.generate([{ name: 'c2', value: 'v2', domain: 'localhost', expires: expires.httpdate, path: '/' }])
49
+ # c2 has no Domain attribute so it is host-only on every libcurl version.
50
+ # With domain=localhost, curl >= 8.22 built with libpsl makes it host-only
51
+ # (localhost is a public suffix) while older curl tail-matches (#483).
52
+ easy.post_body = JSON.generate([{ name: 'c2', value: 'v2', expires: expires.httpdate, path: '/' }])
50
53
  easy.perform
51
54
  easy.url = "http://localhost:#{TestServlet.port}#{TestServlet.path}/get_cookies"
52
55
  easy.post_body = nil
53
56
  easy.perform
54
57
 
55
58
  assert !easy.enable_cookies?
56
- assert_equal [".localhost\tTRUE\t/\tFALSE\t#{expires.to_time.to_i}\tc1\tv1", ".localhost\tTRUE\t/\tFALSE\t#{expires.to_time.to_i}\tc2\tv2"], easy.cookielist
59
+ assert_equal [".localhost\tTRUE\t/\tFALSE\t#{expires.to_time.to_i}\tc1\tv1", "localhost\tFALSE\t/\tFALSE\t#{expires.to_time.to_i}\tc2\tv2"], easy.cookielist
57
60
  assert_equal 'c2=v2; c1=v1', easy.body_str
58
61
  end
59
62
 
@@ -245,14 +248,15 @@ class TestCurbCurlEasyCookielist < Test::Unit::TestCase
245
248
  expires = (Date.today + 2).to_datetime
246
249
  easy.url = "http://localhost:#{TestServlet.port}#{TestServlet.path}/set_cookies"
247
250
  easy.setopt(Curl::CURLOPT_COOKIELIST, command)
248
- easy.post_body = JSON.generate([{ name: 'c2', value: 'v2', domain: 'localhost', expires: expires.httpdate, path: '/' }])
251
+ # host-only c2, see test_setopt_cookielist_enables_cookie_engine (#483)
252
+ easy.post_body = JSON.generate([{ name: 'c2', value: 'v2', expires: expires.httpdate, path: '/' }])
249
253
  easy.perform
250
254
  easy.url = "http://localhost:#{TestServlet.port}#{TestServlet.path}/get_cookies"
251
255
  easy.post_body = nil
252
256
  easy.perform
253
257
 
254
258
  assert !easy.enable_cookies?
255
- assert_equal [".localhost\tTRUE\t/\tFALSE\t#{expires.to_time.to_i}\tc2\tv2"], easy.cookielist
259
+ assert_equal ["localhost\tFALSE\t/\tFALSE\t#{expires.to_time.to_i}\tc2\tv2"], easy.cookielist
256
260
  assert_equal 'c2=v2', easy.body_str
257
261
  end
258
262
  end
@@ -586,6 +586,35 @@ class TestCurbCurlMulti < Test::Unit::TestCase
586
586
  end
587
587
  end
588
588
 
589
+ # Only easies attached by the raising callback itself are withdrawn. Work
590
+ # queued by an earlier, successful callback in the same perform was already
591
+ # attached when the failing callback started, so it keeps running and the
592
+ # deferred exception is raised once it drains.
593
+ def test_multi_perform_keeps_work_added_by_earlier_callback_when_later_callback_raises
594
+ server = CurbTinyHTTPServer.new
595
+ multi = Curl::Multi.new
596
+ first = Curl::Easy.new(server.url('/fast'))
597
+ failing = Curl::Easy.new(server.url('/delay/0.15'))
598
+ kept = Curl::Easy.new(server.url('/delay/0.4'))
599
+
600
+ first.on_complete { multi.add(kept) }
601
+ failing.on_complete { raise "complete blew up" }
602
+
603
+ error = assert_raise(Curl::Err::AbortedByCallbackError) do
604
+ multi.add(first)
605
+ multi.add(failing)
606
+ multi.perform
607
+ end
608
+
609
+ assert_equal "complete blew up", error.message
610
+ assert_equal 0, kept.last_result
611
+ assert_equal "delayed", kept.body_str,
612
+ "work added by an earlier successful callback should finish, not be withdrawn with the failing callback's additions"
613
+ ensure
614
+ multi.close if multi
615
+ server.close if server
616
+ end
617
+
589
618
  def test_multi_perform_does_not_start_work_added_within_on_complete_after_on_body_exception
590
619
  with_queue_refill_test_server do |port, hits|
591
620
  multi = Curl::Multi.new