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.
- checksums.yaml +4 -4
- data/README.md +38 -1
- data/Rakefile +12 -1
- data/ext/curb.c +17 -4
- data/ext/curb.h +4 -4
- data/ext/curb_easy.c +71 -31
- data/ext/curb_easy.h +1 -0
- data/ext/curb_multi.c +428 -95
- data/ext/curb_multi.h +4 -0
- data/ext/curb_postfield.c +31 -31
- data/ext/extconf.rb +6 -0
- data/lib/curl/easy.rb +18 -9
- data/lib/curl.rb +49 -12
- data/tests/helper.rb +122 -3
- data/tests/tc_curl_easy.rb +59 -4
- data/tests/tc_curl_easy_cookielist.rb +8 -4
- data/tests/tc_curl_multi.rb +29 -0
- data/tests/tc_curl_multi_poll.rb +223 -0
- data/tests/tc_curl_multi_socket_poller.rb +374 -0
- data/tests/tc_curl_postfield.rb +39 -0
- data/tests/tc_fiber_scheduler.rb +101 -2
- data/tests/tc_ractor.rb +94 -0
- metadata +8 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
39
|
-
return if
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
deferred_closes = (state[:deferred_multi_closes] ||= [])
|
|
49
58
|
|
|
50
59
|
if all_threads
|
|
51
|
-
|
|
60
|
+
deferred_closes.shift(deferred_closes.length)
|
|
52
61
|
else
|
|
53
62
|
owner = Thread.current
|
|
54
63
|
remaining = []
|
|
55
64
|
current = []
|
|
56
65
|
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
!!(
|
|
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
|
-
|
|
256
|
+
ractor_local_state[:safety_generation] ||= 0
|
|
244
257
|
end
|
|
245
258
|
|
|
246
259
|
def self.bump_safety_generation!
|
|
247
|
-
|
|
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 :
|
|
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
|
-
|
|
501
|
+
completed = state[:completed]
|
|
502
|
+
return if completed.empty?
|
|
485
503
|
|
|
486
|
-
|
|
487
|
-
|
|
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)
|
|
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][
|
|
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
|
-
|
|
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
|
data/tests/tc_curl_easy.rb
CHANGED
|
@@ -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
|
-
|
|
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", "
|
|
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
|
-
|
|
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 ["
|
|
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
|
data/tests/tc_curl_multi.rb
CHANGED
|
@@ -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
|