curb 1.4.1 → 1.5.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/ChangeLog.md +16 -1
- data/README.md +17 -6
- data/ext/curb.h +4 -4
- data/ext/curb_easy.c +302 -34
- data/ext/curb_easy.h +5 -0
- data/ext/curb_multi.c +32 -15
- data/ext/extconf.rb +1 -0
- data/lib/curl/easy.rb +15 -2
- data/lib/curl/multi.rb +30 -8
- data/lib/curl.rb +69 -8
- data/tests/bug_curb_easy_blocks_ruby_threads.rb +1 -1
- data/tests/bug_follow_redirect_288.rb +1 -1
- data/tests/bug_poison.rb +1 -1
- data/tests/bug_raise_on_callback.rb +1 -1
- data/tests/helper.rb +9 -8
- data/tests/tc_curl_easy.rb +188 -0
- data/tests/tc_curl_multi.rb +32 -0
- data/tests/tc_curl_network_policy.rb +25 -32
- data/tests/tc_curl_security_hardening.rb +386 -0
- data/tests/tc_fiber_scheduler.rb +46 -6
- data/tests/tc_ractor.rb +5 -1
- metadata +4 -2
data/ext/curb_multi.c
CHANGED
|
@@ -908,20 +908,29 @@ static VALUE call_status_handler2(VALUE ary) {
|
|
|
908
908
|
return rb_funcall(rb_ary_entry(ary, 0), idCall, 2, rb_ary_entry(ary, 1), rb_ary_entry(ary, 2));
|
|
909
909
|
}
|
|
910
910
|
|
|
911
|
+
/* The default body/header buffers grow geometrically, leaving up to half
|
|
912
|
+
* their capacity unused. Once the transfer is complete, give that back so a
|
|
913
|
+
* retained body_str/header_str is exact-fit. rb_str_resize only reallocates
|
|
914
|
+
* when the excess is significant. */
|
|
915
|
+
static void trim_default_buffers(ruby_curl_easy *rbce) {
|
|
916
|
+
VALUE body = rb_easy_get("body_data");
|
|
917
|
+
VALUE header = rb_easy_get("header_data");
|
|
918
|
+
if (RB_TYPE_P(body, T_STRING) && !OBJ_FROZEN(body)) {
|
|
919
|
+
rb_str_resize(body, RSTRING_LEN(body));
|
|
920
|
+
}
|
|
921
|
+
if (RB_TYPE_P(header, T_STRING) && !OBJ_FROZEN(header)) {
|
|
922
|
+
rb_str_resize(header, RSTRING_LEN(header));
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
911
926
|
static void flush_stderr_if_any(ruby_curl_easy *rbce) {
|
|
912
927
|
VALUE stderr_io = rb_easy_get("stderr_io");
|
|
913
|
-
|
|
914
|
-
|
|
928
|
+
/* The caller may have closed their IO; curb's own stream stays valid. */
|
|
929
|
+
if (stderr_io != Qnil && !RTEST(rb_funcall(stderr_io, rb_intern("closed?"), 0))) {
|
|
915
930
|
rb_funcall(stderr_io, rb_intern("flush"), 0);
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
if (RB_TYPE_P(stderr_io, T_FILE)) {
|
|
920
|
-
GetOpenFile(stderr_io, open_f_ptr);
|
|
921
|
-
FILE *fp = rb_io_stdio_file(open_f_ptr);
|
|
922
|
-
if (fp) fflush(fp);
|
|
923
|
-
}
|
|
924
|
-
#endif
|
|
931
|
+
}
|
|
932
|
+
if (rbce->stderr_file) {
|
|
933
|
+
fflush(rbce->stderr_file);
|
|
925
934
|
}
|
|
926
935
|
}
|
|
927
936
|
|
|
@@ -1203,6 +1212,7 @@ static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int res
|
|
|
1203
1212
|
|
|
1204
1213
|
/* Flush again after removal to cover any last buffered data. */
|
|
1205
1214
|
flush_stderr_if_any(rbce);
|
|
1215
|
+
trim_default_buffers(rbce);
|
|
1206
1216
|
|
|
1207
1217
|
struct multi_complete_callback_args args = {
|
|
1208
1218
|
self,
|
|
@@ -1788,6 +1798,9 @@ static int collect_io_select_ready_i(st_data_t key, st_data_t val, st_data_t arg
|
|
|
1788
1798
|
|
|
1789
1799
|
static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_socket_ctx *ctx, VALUE block) {
|
|
1790
1800
|
CURLMcode mrc;
|
|
1801
|
+
/* Looking up the Ractor-local default costs two method calls; read it once
|
|
1802
|
+
* per drive rather than on every wait. */
|
|
1803
|
+
const long default_timeout_ms = curb_multi_default_timeout();
|
|
1791
1804
|
|
|
1792
1805
|
do {
|
|
1793
1806
|
/* Prime the state: let libcurl act on timeouts to set up sockets. */
|
|
@@ -1799,7 +1812,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1799
1812
|
|
|
1800
1813
|
while (rbcm->running) {
|
|
1801
1814
|
struct timeval tv = {0, 0};
|
|
1802
|
-
long wait_ms =
|
|
1815
|
+
long wait_ms = default_timeout_ms;
|
|
1803
1816
|
|
|
1804
1817
|
if (multi_socket_timer_due(ctx)) {
|
|
1805
1818
|
ctx->timeout_deadline_ms = -1;
|
|
@@ -2289,6 +2302,7 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
|
|
|
2289
2302
|
CURLMcode mcode;
|
|
2290
2303
|
ruby_curl_multi *rbcm;
|
|
2291
2304
|
long timeout_milliseconds;
|
|
2305
|
+
long default_timeout_ms;
|
|
2292
2306
|
VALUE block = Qnil;
|
|
2293
2307
|
#ifndef CURB_USE_MULTI_POLL
|
|
2294
2308
|
int maxfd, rc = -1;
|
|
@@ -2311,7 +2325,10 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
|
|
|
2311
2325
|
clear_multi_deferred_exception_source_id_if_any(self);
|
|
2312
2326
|
}
|
|
2313
2327
|
|
|
2314
|
-
|
|
2328
|
+
/* Looking up the Ractor-local default costs two method calls; read it once
|
|
2329
|
+
* per perform rather than up to twice per wait-loop iteration. */
|
|
2330
|
+
default_timeout_ms = curb_multi_default_timeout();
|
|
2331
|
+
timeout_milliseconds = default_timeout_ms;
|
|
2315
2332
|
|
|
2316
2333
|
// Run curl_multi_perform for the first time to get the ball rolling
|
|
2317
2334
|
rb_curl_multi_run( self, rbcm->handle, &(rbcm->running) );
|
|
@@ -2351,8 +2368,8 @@ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
|
|
|
2351
2368
|
continue;
|
|
2352
2369
|
}
|
|
2353
2370
|
|
|
2354
|
-
if (timeout_milliseconds < 0 || timeout_milliseconds >
|
|
2355
|
-
timeout_milliseconds =
|
|
2371
|
+
if (timeout_milliseconds < 0 || timeout_milliseconds > default_timeout_ms) {
|
|
2372
|
+
timeout_milliseconds = default_timeout_ms; /* libcurl doesn't know how long to wait, use a default timeout */
|
|
2356
2373
|
/* or buggy versions libcurl sometimes reports huge timeouts... let's cap it */
|
|
2357
2374
|
}
|
|
2358
2375
|
|
data/ext/extconf.rb
CHANGED
|
@@ -699,6 +699,7 @@ have_header('ruby/thread.h') && have_func('rb_thread_call_without_gvl', 'ruby/th
|
|
|
699
699
|
have_header('ruby/io.h')
|
|
700
700
|
# Ruby 4.x exports rb_thread_fd_select without declaring it in ruby/io.h.
|
|
701
701
|
have_func('rb_thread_fd_select')
|
|
702
|
+
have_func('rb_io_descriptor', 'ruby/io.h')
|
|
702
703
|
have_func('rb_wait_for_single_fd', 'ruby/io.h')
|
|
703
704
|
have_header('ruby/fiber/scheduler.h')
|
|
704
705
|
have_func('rb_fiber_scheduler_current', 'ruby/fiber/scheduler.h')
|
data/lib/curl/easy.rb
CHANGED
|
@@ -53,6 +53,11 @@ module Curl
|
|
|
53
53
|
|
|
54
54
|
def flush_deferred_multi_closes(all_threads: false)
|
|
55
55
|
state = deferred_multi_close_state
|
|
56
|
+
# Fast path: nothing queued, so skip the mutex and list partitioning.
|
|
57
|
+
# A close queued concurrently by another thread is picked up next time.
|
|
58
|
+
queued = state[:deferred_multi_closes]
|
|
59
|
+
return if queued.nil? || queued.empty?
|
|
60
|
+
|
|
56
61
|
pending = deferred_multi_close_mutex.synchronize do
|
|
57
62
|
deferred_closes = (state[:deferred_multi_closes] ||= [])
|
|
58
63
|
|
|
@@ -189,11 +194,15 @@ module Curl
|
|
|
189
194
|
config = Curl.__send__(:ractor_local_state)[:safety_config] || Curl::SafetyConfig.new
|
|
190
195
|
limit = Curl::SafetyConfig.new
|
|
191
196
|
limit.max_body_bytes = options.fetch(:max_body_bytes, config.max_body_bytes)
|
|
197
|
+
limit.timeout = options.fetch(:timeout, config.timeout)
|
|
198
|
+
limit.max_redirects = options.fetch(:max_redirects, config.max_redirects)
|
|
192
199
|
__curb_set_safety_override!(
|
|
193
200
|
:protocols => [:http, :https],
|
|
194
201
|
:redirect_protocols => [:http, :https],
|
|
195
202
|
:network_policy => :public,
|
|
196
|
-
:max_body_bytes => limit.max_body_bytes
|
|
203
|
+
:max_body_bytes => limit.max_body_bytes,
|
|
204
|
+
:timeout => limit.timeout,
|
|
205
|
+
:max_redirects => limit.max_redirects
|
|
197
206
|
)
|
|
198
207
|
end
|
|
199
208
|
|
|
@@ -205,6 +214,8 @@ module Curl
|
|
|
205
214
|
@__curb_safety_override[:redirect_protocols] = Array(options[:redirect_protocols]).map { |protocol| protocol.to_s.downcase.to_sym } if options.key?(:redirect_protocols)
|
|
206
215
|
@__curb_safety_override[:max_body_bytes] = options[:max_body_bytes] if options.key?(:max_body_bytes)
|
|
207
216
|
@__curb_safety_override[:network_policy] = options[:network_policy] if options.key?(:network_policy)
|
|
217
|
+
@__curb_safety_override[:timeout] = options[:timeout] if options.key?(:timeout)
|
|
218
|
+
@__curb_safety_override[:max_redirects] = options[:max_redirects] if options.key?(:max_redirects)
|
|
208
219
|
@__curb_safety_override_generation = __curb_safety_override_generation + 1
|
|
209
220
|
Curl.__send__(:apply_safety!, self) if Curl.respond_to?(:apply_safety!, true)
|
|
210
221
|
self
|
|
@@ -270,7 +281,9 @@ module Curl
|
|
|
270
281
|
# the configured HTTP Verb.
|
|
271
282
|
#
|
|
272
283
|
def perform
|
|
273
|
-
|
|
284
|
+
# Validate eagerly so an unsafe request raises in the caller (and not in
|
|
285
|
+
# a shared scheduler driver fiber). Multi#add re-applies it as well.
|
|
286
|
+
Curl.__send__(:apply_safety!, self) if Curl.__send__(:safety_active_for?, self)
|
|
274
287
|
self.class.flush_deferred_multi_closes
|
|
275
288
|
|
|
276
289
|
if Curl.scheduler_active? && self.multi.nil?
|
data/lib/curl/multi.rb
CHANGED
|
@@ -353,6 +353,15 @@ module Curl
|
|
|
353
353
|
]
|
|
354
354
|
end
|
|
355
355
|
|
|
356
|
+
# True when no per-easy safety state (override or native policy) is set.
|
|
357
|
+
# Callers must separately check that no global Curl.safe! config exists.
|
|
358
|
+
def __curb_safety_unconstrained?(easy)
|
|
359
|
+
easy.instance_variable_get(:@__curb_safety_override).nil? &&
|
|
360
|
+
easy.network_policy == :none &&
|
|
361
|
+
easy.allowed_hosts.nil? &&
|
|
362
|
+
easy.allowed_cidrs.nil?
|
|
363
|
+
end
|
|
364
|
+
|
|
356
365
|
def __record_native_safety_signature(easy)
|
|
357
366
|
__curb_native_safety_signatures[easy.object_id] = __curb_safety_signature_for(easy)
|
|
358
367
|
end
|
|
@@ -419,24 +428,33 @@ module Curl
|
|
|
419
428
|
|
|
420
429
|
private :__idle_easy_references, :__curb_native_safety_signatures,
|
|
421
430
|
:__curb_safety_signature_for, :__record_native_safety_signature,
|
|
431
|
+
:__curb_safety_unconstrained?,
|
|
422
432
|
:__register_idle_easy_reference,
|
|
423
433
|
:__unregister_idle_easy_reference, :__clear_idle_easy_references,
|
|
424
434
|
:__new_idle_easy_references
|
|
425
435
|
|
|
426
436
|
alias_method :_curb_native_perform, :perform
|
|
427
437
|
|
|
428
|
-
def perform(
|
|
438
|
+
def perform(&block)
|
|
439
|
+
safety_config_active = !Curl.__send__(:ractor_local_state)[:safety_config].nil?
|
|
440
|
+
signatures = __curb_native_safety_signatures
|
|
441
|
+
|
|
429
442
|
requests.each_value do |easy|
|
|
430
|
-
|
|
443
|
+
# An easy added with no safety state recorded no signature. If it is
|
|
444
|
+
# still unconstrained its signature would be unchanged, so there is
|
|
445
|
+
# nothing to apply and no need to re-run native setup.
|
|
446
|
+
next if !safety_config_active && !signatures.key?(easy.object_id) &&
|
|
447
|
+
__curb_safety_unconstrained?(easy)
|
|
448
|
+
|
|
449
|
+
Curl.__send__(:apply_safety!, easy)
|
|
431
450
|
signature = __curb_safety_signature_for(easy)
|
|
432
|
-
if
|
|
433
|
-
easy.respond_to?(:__curb_native_setup!, true)
|
|
451
|
+
if signatures[easy.object_id] != signature
|
|
434
452
|
easy.__send__(:__curb_native_setup!)
|
|
435
|
-
|
|
453
|
+
signatures[easy.object_id] = signature
|
|
436
454
|
end
|
|
437
455
|
end
|
|
438
456
|
|
|
439
|
-
_curb_native_perform(
|
|
457
|
+
_curb_native_perform(&block)
|
|
440
458
|
end
|
|
441
459
|
|
|
442
460
|
def add(easy)
|
|
@@ -444,11 +462,15 @@ module Curl
|
|
|
444
462
|
# Once a deferred callback exception is pending, Multi#perform is
|
|
445
463
|
# draining existing transfers only and must not start replacement work.
|
|
446
464
|
return self if instance_variable_defined?(:@__curb_deferred_exception)
|
|
447
|
-
Curl.__send__(:
|
|
465
|
+
unconstrained = Curl.__send__(:ractor_local_state)[:safety_config].nil? &&
|
|
466
|
+
__curb_safety_unconstrained?(easy)
|
|
467
|
+
Curl.__send__(:apply_safety!, easy) unless unconstrained
|
|
448
468
|
_add(easy)
|
|
449
469
|
__unregister_idle_easy_reference(easy)
|
|
450
470
|
requests[easy.object_id] = easy
|
|
451
|
-
|
|
471
|
+
# Unconstrained easies record no signature; #perform treats a missing
|
|
472
|
+
# signature as "added with no safety state" (see #perform).
|
|
473
|
+
__record_native_safety_signature(easy) unless unconstrained
|
|
452
474
|
self
|
|
453
475
|
end
|
|
454
476
|
|
data/lib/curl.rb
CHANGED
|
@@ -12,9 +12,14 @@ module Curl
|
|
|
12
12
|
# Ractors, even when the value itself would otherwise be safe. Keep curb's
|
|
13
13
|
# mutable Ruby-side configuration in storage owned by the current Ractor.
|
|
14
14
|
# Older Rubies retain the historical process-wide state.
|
|
15
|
+
# Ractor capabilities can't change at runtime, so probe them once instead of
|
|
16
|
+
# on every call: this lookup sits on the per-request path.
|
|
17
|
+
RACTOR_LOCAL_STORAGE = !!(defined?(Ractor) && Ractor.respond_to?(:current) &&
|
|
18
|
+
Ractor.current.respond_to?(:[]) && Ractor.current.respond_to?(:[]=))
|
|
19
|
+
private_constant :RACTOR_LOCAL_STORAGE
|
|
20
|
+
|
|
15
21
|
def self.ractor_local_state
|
|
16
|
-
if
|
|
17
|
-
Ractor.current.respond_to?(:[]) && Ractor.current.respond_to?(:[]=)
|
|
22
|
+
if RACTOR_LOCAL_STORAGE
|
|
18
23
|
Ractor.current[:__curb_ractor_local_state] ||= {}
|
|
19
24
|
else
|
|
20
25
|
@legacy_local_state ||= {}
|
|
@@ -24,14 +29,18 @@ module Curl
|
|
|
24
29
|
class SafetyConfig
|
|
25
30
|
DEFAULT_PROTOCOLS = [:http, :https].freeze
|
|
26
31
|
DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024
|
|
32
|
+
DEFAULT_TIMEOUT = 60
|
|
33
|
+
DEFAULT_MAX_REDIRECTS = 20
|
|
27
34
|
|
|
28
|
-
attr_reader :max_body_bytes, :network_policy
|
|
35
|
+
attr_reader :max_body_bytes, :network_policy, :timeout, :max_redirects
|
|
29
36
|
attr_accessor :allow_proxies, :allow_resolve, :allow_connect_to, :allow_doh, :allow_unix_socket
|
|
30
37
|
|
|
31
38
|
def initialize
|
|
32
39
|
@protocols = DEFAULT_PROTOCOLS
|
|
33
40
|
@redirect_protocols = nil
|
|
34
41
|
@max_body_bytes = DEFAULT_MAX_BODY_BYTES
|
|
42
|
+
@timeout = DEFAULT_TIMEOUT
|
|
43
|
+
@max_redirects = DEFAULT_MAX_REDIRECTS
|
|
35
44
|
@network_policy = :public
|
|
36
45
|
@allowed_hosts = nil
|
|
37
46
|
@allowed_proxy_hosts = nil
|
|
@@ -83,6 +92,32 @@ module Curl
|
|
|
83
92
|
@max_body_bytes = bytes.zero? ? nil : bytes
|
|
84
93
|
end
|
|
85
94
|
|
|
95
|
+
# Seconds; applied to handles with no timeout of their own. nil disables.
|
|
96
|
+
def timeout=(seconds)
|
|
97
|
+
if seconds.nil?
|
|
98
|
+
@timeout = nil
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
seconds = Float(seconds)
|
|
103
|
+
raise ArgumentError, "timeout must be greater than zero" unless seconds > 0
|
|
104
|
+
|
|
105
|
+
@timeout = seconds
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Applied to handles whose max_redirects is unset or unlimited. nil disables.
|
|
109
|
+
def max_redirects=(count)
|
|
110
|
+
if count.nil?
|
|
111
|
+
@max_redirects = nil
|
|
112
|
+
return
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
count = Integer(count)
|
|
116
|
+
raise ArgumentError, "max_redirects must be greater than or equal to zero" if count < 0
|
|
117
|
+
|
|
118
|
+
@max_redirects = count
|
|
119
|
+
end
|
|
120
|
+
|
|
86
121
|
def network_policy=(policy)
|
|
87
122
|
@network_policy = normalize_network_policy(policy)
|
|
88
123
|
end
|
|
@@ -227,9 +262,26 @@ module Curl
|
|
|
227
262
|
|
|
228
263
|
apply_max_body_bytes!(easy, config.max_body_bytes) if config&.max_body_bytes
|
|
229
264
|
apply_max_body_bytes!(easy, override[:max_body_bytes]) if override && override.key?(:max_body_bytes)
|
|
265
|
+
apply_default_timeout!(easy, override && override.key?(:timeout) ? override[:timeout] : config&.timeout)
|
|
266
|
+
apply_default_max_redirects!(easy, override && override.key?(:max_redirects) ? override[:max_redirects] : config&.max_redirects)
|
|
230
267
|
easy
|
|
231
268
|
end
|
|
232
269
|
|
|
270
|
+
# Only fills in a limit the handle has not chosen itself.
|
|
271
|
+
def self.apply_default_timeout!(easy, seconds)
|
|
272
|
+
return unless seconds
|
|
273
|
+
return unless easy.timeout_ms.to_i.zero?
|
|
274
|
+
|
|
275
|
+
easy.timeout = seconds
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def self.apply_default_max_redirects!(easy, count)
|
|
279
|
+
return unless count
|
|
280
|
+
|
|
281
|
+
current = easy.max_redirects
|
|
282
|
+
easy.max_redirects = count if current.nil? || current < 0
|
|
283
|
+
end
|
|
284
|
+
|
|
233
285
|
def self.clear_safe!
|
|
234
286
|
ractor_local_state[:safety_config] = nil
|
|
235
287
|
bump_safety_generation!
|
|
@@ -250,9 +302,7 @@ module Curl
|
|
|
250
302
|
end
|
|
251
303
|
|
|
252
304
|
def self.safety_override_for(easy)
|
|
253
|
-
|
|
254
|
-
easy.__send__(:__curb_safety_override)
|
|
255
|
-
end
|
|
305
|
+
easy.instance_variable_get(:@__curb_safety_override)
|
|
256
306
|
end
|
|
257
307
|
|
|
258
308
|
def self.safety_generation
|
|
@@ -417,6 +467,7 @@ module Curl
|
|
|
417
467
|
:safety_generation, :bump_safety_generation!,
|
|
418
468
|
:safety_protocol_intersection,
|
|
419
469
|
:apply_max_body_bytes!, :apply_public_network_policy_controls!,
|
|
470
|
+
:apply_default_timeout!, :apply_default_max_redirects!,
|
|
420
471
|
:apply_allowed_hosts!, :apply_allowed_cidrs!,
|
|
421
472
|
:apply_allowed_proxy!, :normalize_proxy_host,
|
|
422
473
|
:reject_resolve_override!, :reject_connect_to_override!,
|
|
@@ -424,6 +475,12 @@ module Curl
|
|
|
424
475
|
:reject_unix_socket_override!,
|
|
425
476
|
:disable_proxy!
|
|
426
477
|
|
|
478
|
+
# Multi#perform under a fiber scheduler uses the socket-action loop, which
|
|
479
|
+
# waits through the scheduler, exactly when this native method exists
|
|
480
|
+
# (same build condition). Otherwise it falls back to a thread-blocking wait.
|
|
481
|
+
SCHEDULER_COOPERATIVE_PERFORM = Curl::Multi.private_method_defined?(:_socket_perform)
|
|
482
|
+
private_constant :SCHEDULER_COOPERATIVE_PERFORM
|
|
483
|
+
|
|
427
484
|
def self.scheduler_active?
|
|
428
485
|
Fiber.respond_to?(:scheduler) && !Fiber.scheduler.nil?
|
|
429
486
|
end
|
|
@@ -603,7 +660,11 @@ module Curl
|
|
|
603
660
|
state[:multi].perform do
|
|
604
661
|
drain_scheduler_pending(state)
|
|
605
662
|
release_scheduler_waiters(state)
|
|
606
|
-
|
|
663
|
+
# The socket-action loop already waits through the scheduler on
|
|
664
|
+
# every pass, so sibling fibers run without an extra yield (which
|
|
665
|
+
# cost ~4 yields per request). The fallback loop blocks the thread
|
|
666
|
+
# while waiting, so it still needs one.
|
|
667
|
+
scheduler_yield unless SCHEDULER_COOPERATIVE_PERFORM
|
|
607
668
|
end
|
|
608
669
|
ensure
|
|
609
670
|
# Release any siblings that completed just before a deferred
|
|
@@ -757,7 +818,7 @@ module Curl
|
|
|
757
818
|
end
|
|
758
819
|
|
|
759
820
|
def self.safe_http_option_keys
|
|
760
|
-
[:max_body_bytes]
|
|
821
|
+
[:max_body_bytes, :timeout, :max_redirects]
|
|
761
822
|
end
|
|
762
823
|
|
|
763
824
|
def self.reset
|
|
@@ -4,7 +4,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
|
|
|
4
4
|
include BugTestServerSetupTeardown
|
|
5
5
|
|
|
6
6
|
def setup
|
|
7
|
-
@port =
|
|
7
|
+
@port = :auto
|
|
8
8
|
super
|
|
9
9
|
@server.mount_proc("/redirect_to_test") do|req,res|
|
|
10
10
|
res.set_redirect(WEBrick::HTTPStatus::TemporaryRedirect, "/test")
|
data/tests/bug_poison.rb
CHANGED
data/tests/helper.rb
CHANGED
|
@@ -206,16 +206,17 @@ class TestServlet < WEBrick::HTTPServlet::AbstractServlet
|
|
|
206
206
|
end
|
|
207
207
|
|
|
208
208
|
module BugTestServerSetupTeardown
|
|
209
|
-
def unused_local_port
|
|
210
|
-
socket = TCPServer.new('127.0.0.1', 0)
|
|
211
|
-
socket.addr[1]
|
|
212
|
-
ensure
|
|
213
|
-
socket.close if socket
|
|
214
|
-
end
|
|
215
|
-
|
|
216
209
|
def setup
|
|
217
210
|
@port ||= 9992
|
|
218
|
-
|
|
211
|
+
# Bind 127.0.0.1 explicitly: with no bind address, WEBrick silently keeps
|
|
212
|
+
# just the IPv6 listener if the IPv4 bind fails, leaving 127.0.0.1:@port
|
|
213
|
+
# refusing connections. :auto lets the kernel pick a free port at bind
|
|
214
|
+
# time instead of reserving one and re-binding it later, which could race
|
|
215
|
+
# with client connections reusing that ephemeral port (seen on macOS).
|
|
216
|
+
@server = WEBrick::HTTPServer.new(:Port => (@port == :auto ? 0 : @port),
|
|
217
|
+
:BindAddress => '127.0.0.1',
|
|
218
|
+
:Logger => WEBRICK_TEST_LOG, :AccessLog => [])
|
|
219
|
+
@port = @server.config[:Port]
|
|
219
220
|
@server.mount_proc("/test") do|req,res|
|
|
220
221
|
if @response_proc
|
|
221
222
|
@response_proc.call(res)
|
data/tests/tc_curl_easy.rb
CHANGED
|
@@ -179,6 +179,32 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
|
179
179
|
assert_match(/GET/, http.body)
|
|
180
180
|
end
|
|
181
181
|
|
|
182
|
+
# The default body/header buffers used to reserve 32 KB/16 KB up front, and
|
|
183
|
+
# that capacity stayed attached to every returned string.
|
|
184
|
+
def test_small_response_strings_do_not_retain_large_buffers
|
|
185
|
+
require 'objspace'
|
|
186
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
187
|
+
easy.perform
|
|
188
|
+
|
|
189
|
+
body = easy.body_str
|
|
190
|
+
header = easy.header_str
|
|
191
|
+
assert_operator body.bytesize, :>, 0
|
|
192
|
+
assert_operator ObjectSpace.memsize_of(body), :<, 4096, 'body_str keeps excess capacity'
|
|
193
|
+
assert_operator ObjectSpace.memsize_of(header), :<, 4096, 'header_str keeps excess capacity'
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def test_large_response_body_is_complete
|
|
197
|
+
require 'objspace'
|
|
198
|
+
server = CurbTinyHTTPServer.new
|
|
199
|
+
expected = CurbTinyHTTPServer.stream_body(64, 4096)
|
|
200
|
+
easy = Curl::Easy.new(server.url('/stream/64/4096'))
|
|
201
|
+
easy.perform
|
|
202
|
+
assert_equal expected, easy.body_str
|
|
203
|
+
assert_operator ObjectSpace.memsize_of(easy.body_str), :<, expected.bytesize + 4096
|
|
204
|
+
ensure
|
|
205
|
+
server.close if server
|
|
206
|
+
end
|
|
207
|
+
|
|
182
208
|
def test_curlopt_stderr_with_file
|
|
183
209
|
# does not work with Tempfile directly
|
|
184
210
|
path = Tempfile.new('curb_test_curlopt_stderr').path
|
|
@@ -239,6 +265,92 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
|
239
265
|
tmp.close! if defined?(tmp) && tmp
|
|
240
266
|
end
|
|
241
267
|
|
|
268
|
+
# libcurl used to write through the IO's own FILE*, which Ruby frees on
|
|
269
|
+
# close. curb now owns a stream on a duplicate descriptor instead.
|
|
270
|
+
def test_curlopt_stderr_survives_block_form_file_close
|
|
271
|
+
tmp = Tempfile.new('curb_test_curlopt_stderr_closed')
|
|
272
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
273
|
+
easy.verbose = true
|
|
274
|
+
File.open(tmp.path, 'w') { |file| easy.setopt(Curl::CURLOPT_STDERR, file) }
|
|
275
|
+
|
|
276
|
+
2.times { easy.perform }
|
|
277
|
+
|
|
278
|
+
output = File.read(tmp.path)
|
|
279
|
+
assert_equal 2, output.scan(/HTTP\/1\.1 200 OK/).length
|
|
280
|
+
ensure
|
|
281
|
+
tmp.close! if tmp
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def test_curlopt_stderr_survives_close_after_multi_add
|
|
285
|
+
tmp = Tempfile.new('curb_test_curlopt_stderr_multi')
|
|
286
|
+
file = File.open(tmp.path, 'w')
|
|
287
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
288
|
+
easy.verbose = true
|
|
289
|
+
easy.setopt(Curl::CURLOPT_STDERR, file)
|
|
290
|
+
multi = Curl::Multi.new
|
|
291
|
+
multi.add(easy)
|
|
292
|
+
file.close
|
|
293
|
+
GC.start
|
|
294
|
+
|
|
295
|
+
multi.perform
|
|
296
|
+
|
|
297
|
+
assert_equal 200, easy.response_code
|
|
298
|
+
assert_match(/HTTP\/1\.1 200 OK/, File.read(tmp.path))
|
|
299
|
+
ensure
|
|
300
|
+
multi.close if multi
|
|
301
|
+
tmp.close! if tmp
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def test_curlopt_stderr_clone_owns_its_stream
|
|
305
|
+
tmp = Tempfile.new('curb_test_curlopt_stderr_clone')
|
|
306
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
307
|
+
easy.verbose = true
|
|
308
|
+
File.open(tmp.path, 'w') { |file| easy.setopt(Curl::CURLOPT_STDERR, file) }
|
|
309
|
+
copy = easy.clone
|
|
310
|
+
|
|
311
|
+
# Releasing the original's stream must not affect the clone's.
|
|
312
|
+
easy.close
|
|
313
|
+
GC.start
|
|
314
|
+
copy.perform
|
|
315
|
+
|
|
316
|
+
assert_equal 200, copy.response_code
|
|
317
|
+
assert_match(/HTTP\/1\.1 200 OK/, File.read(tmp.path))
|
|
318
|
+
ensure
|
|
319
|
+
tmp.close! if tmp
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def test_curlopt_stderr_streams_are_released
|
|
323
|
+
fd_dir = '/proc/self/fd'
|
|
324
|
+
omit('needs /proc/self/fd to count open descriptors') unless File.directory?(fd_dir)
|
|
325
|
+
tmp = Tempfile.new('curb_test_curlopt_stderr_release')
|
|
326
|
+
file = File.open(tmp.path, 'w')
|
|
327
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
328
|
+
before = Dir.children(fd_dir).length
|
|
329
|
+
|
|
330
|
+
# Replacement, reset, and close each release curb's descriptor.
|
|
331
|
+
20.times { easy.setopt(Curl::CURLOPT_STDERR, file) }
|
|
332
|
+
easy.reset
|
|
333
|
+
easy.setopt(Curl::CURLOPT_STDERR, file)
|
|
334
|
+
easy.close
|
|
335
|
+
easy.setopt(Curl::CURLOPT_STDERR, file)
|
|
336
|
+
easy.clone.close
|
|
337
|
+
easy.close
|
|
338
|
+
|
|
339
|
+
assert_equal before, Dir.children(fd_dir).length
|
|
340
|
+
ensure
|
|
341
|
+
file.close if file && !file.closed?
|
|
342
|
+
tmp.close! if tmp
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def test_curlopt_stderr_rejects_unwritable_io
|
|
346
|
+
tmp = Tempfile.new('curb_test_curlopt_stderr_readonly')
|
|
347
|
+
File.open(tmp.path, 'r') do |file|
|
|
348
|
+
assert_raise(IOError) { Curl::Easy.new.setopt(Curl::CURLOPT_STDERR, file) }
|
|
349
|
+
end
|
|
350
|
+
ensure
|
|
351
|
+
tmp.close! if tmp
|
|
352
|
+
end
|
|
353
|
+
|
|
242
354
|
def test_head_request_no_body_and_no_timeout
|
|
243
355
|
# Ensure a HEAD request completes and does not attempt to read a body
|
|
244
356
|
# even when the server advertises a Content-Length.
|
|
@@ -2119,4 +2231,80 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
|
2119
2231
|
server_setup
|
|
2120
2232
|
end
|
|
2121
2233
|
|
|
2234
|
+
# Replacing request state while libcurl is still sending the old state
|
|
2235
|
+
# freed buffers libcurl was reading (use-after-free). Every entry point that
|
|
2236
|
+
# hands libcurl new request state must refuse while the handle is active.
|
|
2237
|
+
ACTIVE_HANDLE_MUTATORS = {
|
|
2238
|
+
'post_body=' => ->(easy) { easy.post_body = 'replacement' },
|
|
2239
|
+
'put_data=' => ->(easy) { easy.put_data = 'replacement' },
|
|
2240
|
+
'autoreferer=' => ->(easy) { easy.autoreferer = true },
|
|
2241
|
+
'http_post' => ->(easy) { easy.http_post('replacement') },
|
|
2242
|
+
'http_put' => ->(easy) { easy.http_put('replacement') },
|
|
2243
|
+
'http_patch' => ->(easy) { easy.http_patch('replacement') },
|
|
2244
|
+
'http_delete' => ->(easy) { easy.http_delete },
|
|
2245
|
+
'http(:OPTIONS)' => ->(easy) { easy.http(:OPTIONS) },
|
|
2246
|
+
'__curb_native_setup!' => ->(easy) { easy.__send__(:__curb_native_setup!) },
|
|
2247
|
+
}.freeze
|
|
2248
|
+
|
|
2249
|
+
def test_request_state_cannot_be_replaced_during_own_transfer
|
|
2250
|
+
ACTIVE_HANDLE_MUTATORS.each do |name, mutate|
|
|
2251
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
2252
|
+
easy.post_body = 'original'
|
|
2253
|
+
errors = []
|
|
2254
|
+
easy.on_body do |data|
|
|
2255
|
+
begin
|
|
2256
|
+
mutate.call(easy)
|
|
2257
|
+
rescue RuntimeError => e
|
|
2258
|
+
errors << e
|
|
2259
|
+
end
|
|
2260
|
+
data.bytesize
|
|
2261
|
+
end
|
|
2262
|
+
|
|
2263
|
+
easy.perform
|
|
2264
|
+
|
|
2265
|
+
assert_equal 1, errors.length, name
|
|
2266
|
+
assert_match(/Cannot modify an active curl handle/, errors.first.message, name)
|
|
2267
|
+
assert_equal 200, easy.response_code, name
|
|
2268
|
+
assert_equal 'original', easy.post_body, name
|
|
2269
|
+
|
|
2270
|
+
# Once idle, the same mutation is allowed again.
|
|
2271
|
+
easy.on_body
|
|
2272
|
+
assert_nothing_raised(name) { mutate.call(easy) }
|
|
2273
|
+
end
|
|
2274
|
+
end
|
|
2275
|
+
|
|
2276
|
+
def test_request_state_cannot_be_replaced_on_active_sibling_transfer
|
|
2277
|
+
ACTIVE_HANDLE_MUTATORS.each do |name, mutate|
|
|
2278
|
+
# The sibling's response is delayed so it is still in flight when the
|
|
2279
|
+
# first transfer's body callback runs.
|
|
2280
|
+
server = CurbTinyHTTPServer.new
|
|
2281
|
+
multi = Curl::Multi.new
|
|
2282
|
+
first = Curl::Easy.new(TestServlet.url)
|
|
2283
|
+
sibling = Curl::Easy.new(server.url('/delay/0.3'))
|
|
2284
|
+
sibling.post_body = 'original'
|
|
2285
|
+
errors = []
|
|
2286
|
+
first.on_body do |data|
|
|
2287
|
+
begin
|
|
2288
|
+
mutate.call(sibling)
|
|
2289
|
+
rescue RuntimeError => e
|
|
2290
|
+
errors << e
|
|
2291
|
+
end
|
|
2292
|
+
data.bytesize
|
|
2293
|
+
end
|
|
2294
|
+
|
|
2295
|
+
multi.add(first)
|
|
2296
|
+
multi.add(sibling)
|
|
2297
|
+
multi.perform
|
|
2298
|
+
|
|
2299
|
+
assert_operator errors.length, :>=, 1, name
|
|
2300
|
+
errors.each { |e| assert_match(/Cannot modify an active curl handle/, e.message, name) }
|
|
2301
|
+
assert_equal 200, sibling.response_code, name
|
|
2302
|
+
assert_equal 'delayed', sibling.body_str, name
|
|
2303
|
+
assert_equal 'original', sibling.post_body, name
|
|
2304
|
+
ensure
|
|
2305
|
+
multi.close if multi
|
|
2306
|
+
server.close if server
|
|
2307
|
+
end
|
|
2308
|
+
end
|
|
2309
|
+
|
|
2122
2310
|
end
|