curb 1.3.7 → 1.4.1
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 +432 -0
- data/README.md +62 -5
- data/Rakefile +12 -1
- data/ext/curb.c +17 -4
- data/ext/curb.h +4 -4
- data/ext/curb_easy.c +344 -191
- data/ext/curb_easy.h +6 -0
- data/ext/curb_macros.h +16 -0
- data/ext/curb_multi.c +479 -103
- data/ext/curb_multi.h +8 -0
- data/ext/curb_postfield.c +134 -145
- data/ext/curb_postfield.h +10 -1
- data/ext/extconf.rb +6 -0
- data/lib/curl/easy.rb +28 -12
- data/lib/curl/multi.rb +1 -1
- data/lib/curl.rb +58 -19
- data/tests/helper.rb +122 -3
- data/tests/tc_curl_easy.rb +175 -4
- data/tests/tc_curl_easy_cookielist.rb +18 -12
- data/tests/tc_curl_maxfilesize.rb +109 -5
- 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_native_coverage.rb +57 -0
- data/tests/tc_curl_network_policy.rb +108 -1
- data/tests/tc_curl_postfield.rb +176 -0
- data/tests/tc_curl_protocols.rb +19 -7
- data/tests/tc_curl_transfer_guard.rb +112 -0
- data/tests/tc_fiber_scheduler.rb +101 -2
- data/tests/tc_ractor.rb +141 -0
- metadata +11 -2
|
@@ -12,6 +12,110 @@ class TestCurbCurlNetworkPolicy < Test::Unit::TestCase
|
|
|
12
12
|
super
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
def test_safe_bang_defaults_to_public_network_policy
|
|
16
|
+
assert_equal :public, Curl::SafetyConfig.new.network_policy
|
|
17
|
+
require_public_network_policy!
|
|
18
|
+
Curl.safe!
|
|
19
|
+
|
|
20
|
+
assert_raise(Curl::Err::UnsafeDestinationError) { Curl.get(TestServlet.url) }
|
|
21
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
22
|
+
easy.network_policy = :none
|
|
23
|
+
assert_raise(Curl::Err::UnsafeDestinationError) { easy.perform }
|
|
24
|
+
assert_equal :public, easy.network_policy
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_safe_bang_default_blocks_private_and_metadata_destinations
|
|
28
|
+
require_public_network_policy!
|
|
29
|
+
Curl.safe!
|
|
30
|
+
['10.0.0.1', '172.16.0.1', '192.168.0.1', '169.254.169.254', '[::1]'].each do |host|
|
|
31
|
+
assert_raise(Curl::Err::UnsafeDestinationError, host) do
|
|
32
|
+
Curl.get("http://#{host}/") do |easy|
|
|
33
|
+
easy.network_policy = :none
|
|
34
|
+
easy.connect_timeout_ms = 100
|
|
35
|
+
easy.timeout_ms = 200
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_safe_bang_default_applies_to_previously_added_multi_handles
|
|
42
|
+
require_public_network_policy!
|
|
43
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
44
|
+
multi = Curl::Multi.new
|
|
45
|
+
multi.add(easy)
|
|
46
|
+
Curl.safe!
|
|
47
|
+
multi.perform
|
|
48
|
+
|
|
49
|
+
assert_equal :public, easy.network_policy
|
|
50
|
+
assert_match(/127\.0\.0\.1/, easy.unsafe_destination_error)
|
|
51
|
+
ensure
|
|
52
|
+
multi.close if multi
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_safe_helpers_block_loopback_without_global_policy
|
|
56
|
+
require_public_network_policy!
|
|
57
|
+
[:get, :post, :put, :delete, :patch, :head, :options].each do |verb|
|
|
58
|
+
assert_raise(Curl::Err::UnsafeDestinationError, verb.to_s) do
|
|
59
|
+
Curl.public_send("safe_#{verb}", TestServlet.url) do |easy|
|
|
60
|
+
easy.network_policy = :none
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_safe_http_reapplies_public_policy_before_perform
|
|
67
|
+
require_public_network_policy!
|
|
68
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
69
|
+
assert_same easy, easy.safe_http!
|
|
70
|
+
assert_equal :public, easy.network_policy
|
|
71
|
+
easy.network_policy = :none
|
|
72
|
+
|
|
73
|
+
assert_raise(Curl::Err::UnsafeDestinationError) { easy.perform }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_safe_http_cannot_be_weakened_by_global_policy
|
|
77
|
+
require_public_network_policy!
|
|
78
|
+
Curl.safe! { |config| config.network_policy = :none }
|
|
79
|
+
|
|
80
|
+
assert_raise(Curl::Err::UnsafeDestinationError) { Curl.safe_get(TestServlet.url) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_safe_helpers_block_private_and_metadata_destinations
|
|
84
|
+
require_public_network_policy!
|
|
85
|
+
['10.0.0.1', '172.16.0.1', '192.168.0.1', '169.254.169.254', '[::1]'].each do |host|
|
|
86
|
+
assert_raise(Curl::Err::UnsafeDestinationError, host) do
|
|
87
|
+
Curl.safe_get("http://#{host}/") do |easy|
|
|
88
|
+
easy.connect_timeout_ms = 100
|
|
89
|
+
easy.timeout_ms = 200
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_safe_http_rejects_resolve_override_without_global_policy
|
|
96
|
+
require_public_network_policy!
|
|
97
|
+
assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
98
|
+
Curl.safe_get('http://example.invalid/') do |easy|
|
|
99
|
+
easy.resolve = ['example.invalid:80:127.0.0.1']
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_safe_http_applies_to_multi
|
|
105
|
+
require_public_network_policy!
|
|
106
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
107
|
+
easy.safe_http!
|
|
108
|
+
easy.network_policy = :none
|
|
109
|
+
multi = Curl::Multi.new
|
|
110
|
+
multi.add(easy)
|
|
111
|
+
multi.perform
|
|
112
|
+
|
|
113
|
+
assert_equal :public, easy.network_policy
|
|
114
|
+
assert_match(/127\.0\.0\.1/, easy.unsafe_destination_error)
|
|
115
|
+
ensure
|
|
116
|
+
multi.close if multi
|
|
117
|
+
end
|
|
118
|
+
|
|
15
119
|
def test_network_policy_defaults_to_none
|
|
16
120
|
easy = Curl::Easy.new
|
|
17
121
|
|
|
@@ -95,6 +199,7 @@ class TestCurbCurlNetworkPolicy < Test::Unit::TestCase
|
|
|
95
199
|
|
|
96
200
|
with_host_redirect_server(allowed_host, allowed_host) do |port, hits|
|
|
97
201
|
Curl.safe! do |config|
|
|
202
|
+
config.network_policy = :none # Isolate host filtering from peer filtering.
|
|
98
203
|
config.allowed_hosts = [allowed_host]
|
|
99
204
|
end
|
|
100
205
|
|
|
@@ -119,6 +224,7 @@ class TestCurbCurlNetworkPolicy < Test::Unit::TestCase
|
|
|
119
224
|
|
|
120
225
|
with_host_redirect_server(allowed_host, blocked_host) do |port, hits|
|
|
121
226
|
Curl.safe! do |config|
|
|
227
|
+
config.network_policy = :none # Isolate host filtering from peer filtering.
|
|
122
228
|
config.allowed_hosts = [allowed_host]
|
|
123
229
|
end
|
|
124
230
|
|
|
@@ -142,6 +248,7 @@ class TestCurbCurlNetworkPolicy < Test::Unit::TestCase
|
|
|
142
248
|
|
|
143
249
|
def test_safe_bang_allowed_cidrs_requires_public_network_policy
|
|
144
250
|
Curl.safe! do |config|
|
|
251
|
+
config.network_policy = :none # Isolate the host/CIDR policy from peer filtering.
|
|
145
252
|
config.allowed_cidrs = ['1.1.1.0/24']
|
|
146
253
|
end
|
|
147
254
|
|
|
@@ -1080,7 +1187,7 @@ class TestCurbCurlNetworkPolicy < Test::Unit::TestCase
|
|
|
1080
1187
|
multi = Curl::Multi.new
|
|
1081
1188
|
|
|
1082
1189
|
multi.add(easy)
|
|
1083
|
-
Curl.safe!
|
|
1190
|
+
Curl.safe! { |config| config.network_policy = :none }
|
|
1084
1191
|
multi.perform
|
|
1085
1192
|
|
|
1086
1193
|
assert_equal [['1']], request_headers
|
data/tests/tc_curl_postfield.rb
CHANGED
|
@@ -253,6 +253,143 @@ class TestCurbCurlPostfieldMultipartCoverage < Test::Unit::TestCase
|
|
|
253
253
|
server_setup
|
|
254
254
|
end
|
|
255
255
|
|
|
256
|
+
[:http_post, :http_patch, :http_put].each do |method|
|
|
257
|
+
[:close, :reset, :initialize, :perform, :http_post, :post_body=].each do |operation|
|
|
258
|
+
[:proc, :coercion].each do |source|
|
|
259
|
+
define_method("test_#{method}_guards_#{operation}_from_#{source}") do
|
|
260
|
+
curl = Curl::Easy.new(TestServlet.url)
|
|
261
|
+
curl.multipart_form_post = true
|
|
262
|
+
action = lambda do |*_|
|
|
263
|
+
if operation == :post_body=
|
|
264
|
+
curl.post_body = 'replacement'
|
|
265
|
+
elsif operation == :http_post
|
|
266
|
+
curl.http_post(Curl::PostField.content('nested', 'payload'))
|
|
267
|
+
else
|
|
268
|
+
curl.__send__(operation)
|
|
269
|
+
end
|
|
270
|
+
'payload'
|
|
271
|
+
end
|
|
272
|
+
field = if source == :proc
|
|
273
|
+
Curl::PostField.content('trigger', &action)
|
|
274
|
+
else
|
|
275
|
+
value = Object.new
|
|
276
|
+
value.define_singleton_method(:to_str, &action)
|
|
277
|
+
Curl::PostField.content('trigger', value)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
error = assert_raise(RuntimeError) do
|
|
281
|
+
curl.__send__(method, Curl::PostField.content('first', 'allocated'), field)
|
|
282
|
+
end
|
|
283
|
+
assert_match(/active curl handle/, error.message)
|
|
284
|
+
|
|
285
|
+
# Both partial forms and guards must unwind before reuse.
|
|
286
|
+
curl.multipart_form_post = false
|
|
287
|
+
curl.perform
|
|
288
|
+
assert_equal 'GET', curl.body_str
|
|
289
|
+
assert_nothing_raised { curl.reset }
|
|
290
|
+
assert_nothing_raised { curl.close }
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
[:content, :file].each do |kind|
|
|
297
|
+
[false, true].each do |dynamic|
|
|
298
|
+
define_method("test_multipart_snapshots_#{kind}_#{dynamic ? 'proc' : 'direct'}") do
|
|
299
|
+
name = +'original_name'
|
|
300
|
+
payload = +"original\0payload"
|
|
301
|
+
field = multipart_field(kind, dynamic, nil, payload)
|
|
302
|
+
field.name = name
|
|
303
|
+
later = Curl::PostField.content('later') do
|
|
304
|
+
Thread.new do
|
|
305
|
+
name.replace('changed_name' * 1000)
|
|
306
|
+
payload.replace('changed_payload' * 1000)
|
|
307
|
+
field.content = nil
|
|
308
|
+
field.name = nil
|
|
309
|
+
GC.start
|
|
310
|
+
GC.verify_compaction_references(double_heap: true, toward: :empty) if GC.respond_to?(:verify_compaction_references)
|
|
311
|
+
end.join
|
|
312
|
+
'later_value'
|
|
313
|
+
end
|
|
314
|
+
curl = Curl::Easy.new(TestServlet.url)
|
|
315
|
+
curl.multipart_form_post = true
|
|
316
|
+
curl.http_post(field, later)
|
|
317
|
+
|
|
318
|
+
assert_include curl.body_str, 'name="original_name"'
|
|
319
|
+
assert_include curl.body_str, "original\0payload"
|
|
320
|
+
assert_include curl.body_str, 'filename="remote.txt"' if kind == :file
|
|
321
|
+
assert_not_include curl.body_str, 'changed_payload'
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def test_multipart_snapshots_before_later_coercions_in_same_field
|
|
327
|
+
name = +'original_name'
|
|
328
|
+
payload = +'original_payload'
|
|
329
|
+
content_type = Object.new
|
|
330
|
+
content_type.define_singleton_method(:to_str) do
|
|
331
|
+
name.replace('changed_name' * 1000)
|
|
332
|
+
payload.replace('changed_payload' * 1000)
|
|
333
|
+
'text/plain'
|
|
334
|
+
end
|
|
335
|
+
field = Curl::PostField.content(name, payload, content_type)
|
|
336
|
+
curl = Curl::Easy.new(TestServlet.url)
|
|
337
|
+
curl.multipart_form_post = true
|
|
338
|
+
curl.http_post(field)
|
|
339
|
+
|
|
340
|
+
assert_include curl.body_str, 'name="original_name"'
|
|
341
|
+
assert_include curl.body_str, 'original_payload'
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Exercise each pointer/length pair with and without a content type.
|
|
345
|
+
[:content, :file].each do |kind|
|
|
346
|
+
[false, true].each do |dynamic|
|
|
347
|
+
[nil, 'text/plain'].each do |content_type|
|
|
348
|
+
variant = "#{kind}_#{dynamic ? 'proc' : 'direct'}_#{content_type ? 'typed' : 'untyped'}"
|
|
349
|
+
|
|
350
|
+
define_method("test_multipart_coerces_#{variant}") do
|
|
351
|
+
payload = "before\0after"
|
|
352
|
+
string_like = Object.new
|
|
353
|
+
string_like.define_singleton_method(:to_str) { payload }
|
|
354
|
+
field = multipart_field(kind, dynamic, content_type, string_like)
|
|
355
|
+
curl = Curl::Easy.new(TestServlet.url)
|
|
356
|
+
curl.multipart_form_post = true
|
|
357
|
+
|
|
358
|
+
curl.http_post(field)
|
|
359
|
+
|
|
360
|
+
assert_include curl.body_str, payload
|
|
361
|
+
assert_equal payload, field.content unless dynamic
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
define_method("test_multipart_rejects_non_string_#{variant}") do
|
|
365
|
+
field = multipart_field(kind, dynamic, content_type, 5)
|
|
366
|
+
curl = Curl::Easy.new(TestServlet.url)
|
|
367
|
+
curl.multipart_form_post = true
|
|
368
|
+
|
|
369
|
+
assert_raise(TypeError) { curl.http_post(field) }
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def multipart_field(kind, dynamic, content_type, value)
|
|
376
|
+
field = if dynamic
|
|
377
|
+
if kind == :file
|
|
378
|
+
Curl::PostField.file('upload', 'remote.txt') { value }
|
|
379
|
+
else
|
|
380
|
+
Curl::PostField.content('value') { value }
|
|
381
|
+
end
|
|
382
|
+
elsif kind == :file
|
|
383
|
+
upload = Curl::PostField.file('upload', 'remote.txt')
|
|
384
|
+
upload.content = value
|
|
385
|
+
upload
|
|
386
|
+
else
|
|
387
|
+
Curl::PostField.content('value', value)
|
|
388
|
+
end
|
|
389
|
+
field.content_type = content_type
|
|
390
|
+
field
|
|
391
|
+
end
|
|
392
|
+
|
|
256
393
|
def test_multipart_content_variants_include_dynamic_and_typed_fields
|
|
257
394
|
curl = Curl::Easy.new(TestServlet.url)
|
|
258
395
|
curl.multipart_form_post = true
|
|
@@ -318,3 +455,42 @@ class TestCurbCurlPostfieldMultipartCoverage < Test::Unit::TestCase
|
|
|
318
455
|
assert_match(/Content-Type: text\/plain/, body)
|
|
319
456
|
end
|
|
320
457
|
end
|
|
458
|
+
|
|
459
|
+
class TestCurbCurlPostfieldInvalidToS < Test::Unit::TestCase
|
|
460
|
+
class BadToS
|
|
461
|
+
def to_s
|
|
462
|
+
42
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def test_to_s_with_non_string_content_to_s_result_does_not_crash
|
|
467
|
+
pf = Curl::PostField.content('name', BadToS.new)
|
|
468
|
+
body = nil
|
|
469
|
+
assert_nothing_raised { body = pf.to_s }
|
|
470
|
+
assert_kind_of String, body
|
|
471
|
+
assert body.start_with?('name=')
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def test_to_s_with_non_string_name_to_s_result_does_not_crash
|
|
475
|
+
pf = Curl::PostField.content(BadToS.new, 'value')
|
|
476
|
+
body = nil
|
|
477
|
+
assert_nothing_raised { body = pf.to_s }
|
|
478
|
+
assert_kind_of String, body
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def test_to_s_with_content_proc_returning_non_string_does_not_crash
|
|
482
|
+
pf = Curl::PostField.content('name') { BadToS.new }
|
|
483
|
+
assert_nothing_raised { pf.to_s }
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def test_to_s_content_proc_raising_error_still_propagates
|
|
487
|
+
pf = Curl::PostField.content('name') { raise 'boom' }
|
|
488
|
+
error = assert_raise(RuntimeError) { pf.to_s }
|
|
489
|
+
assert_equal 'boom', error.message
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def test_to_s_still_coerces_objects_with_valid_to_s
|
|
493
|
+
pf = Curl::PostField.content('name', 42)
|
|
494
|
+
assert_equal 'name=42', pf.to_s
|
|
495
|
+
end
|
|
496
|
+
end
|
data/tests/tc_curl_protocols.rb
CHANGED
|
@@ -104,11 +104,10 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
|
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
def
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
assert_match(/GET/, easy.body_str)
|
|
107
|
+
def test_safe_get_blocks_local_http_destination
|
|
108
|
+
assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
109
|
+
Curl.safe_get(TestServlet.url)
|
|
110
|
+
end
|
|
112
111
|
end
|
|
113
112
|
|
|
114
113
|
def test_safe_bang_denies_file_protocol_for_easy_perform
|
|
@@ -159,8 +158,8 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
|
|
|
159
158
|
end
|
|
160
159
|
end
|
|
161
160
|
|
|
162
|
-
def
|
|
163
|
-
Curl.safe!
|
|
161
|
+
def test_safe_bang_explicit_none_allows_trusted_http_destination
|
|
162
|
+
Curl.safe! { |config| config.network_policy = :none }
|
|
164
163
|
easy = Curl.get(TestServlet.url)
|
|
165
164
|
|
|
166
165
|
assert_equal 200, easy.response_code
|
|
@@ -184,6 +183,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
|
|
|
184
183
|
with_file_redirect_server do |url|
|
|
185
184
|
easy = Curl::Easy.new(url)
|
|
186
185
|
easy.follow_location = true
|
|
186
|
+
stub_public_network_policy_for_local_fixture(easy)
|
|
187
187
|
easy.safe_http!
|
|
188
188
|
|
|
189
189
|
assert_raises Curl::Err::UnsupportedProtocolError do
|
|
@@ -198,6 +198,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
|
|
|
198
198
|
|
|
199
199
|
easy = Curl::Easy.new(url)
|
|
200
200
|
easy.follow_location = true
|
|
201
|
+
stub_public_network_policy_for_local_fixture(easy)
|
|
201
202
|
|
|
202
203
|
assert_raises Curl::Err::UnsupportedProtocolError do
|
|
203
204
|
easy.perform
|
|
@@ -211,6 +212,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
|
|
|
211
212
|
easy.follow_location = true
|
|
212
213
|
easy.ssl_verify_peer = false
|
|
213
214
|
easy.ssl_verify_host = false
|
|
215
|
+
stub_public_network_policy_for_local_fixture(easy)
|
|
214
216
|
easy.safe_http!
|
|
215
217
|
|
|
216
218
|
easy.perform
|
|
@@ -226,6 +228,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
|
|
|
226
228
|
easy.follow_location = true
|
|
227
229
|
easy.ssl_verify_peer = false
|
|
228
230
|
easy.ssl_verify_host = false
|
|
231
|
+
stub_public_network_policy_for_local_fixture(easy)
|
|
229
232
|
easy.safe_http!
|
|
230
233
|
|
|
231
234
|
easy.perform
|
|
@@ -240,6 +243,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
|
|
|
240
243
|
easy = Curl::Easy.new(url)
|
|
241
244
|
easy.follow_location = true
|
|
242
245
|
easy.max_redirects = 2
|
|
246
|
+
stub_public_network_policy_for_local_fixture(easy)
|
|
243
247
|
easy.safe_http!
|
|
244
248
|
|
|
245
249
|
assert_raises Curl::Err::TooManyRedirectsError do
|
|
@@ -252,6 +256,14 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
|
|
|
252
256
|
|
|
253
257
|
private
|
|
254
258
|
|
|
259
|
+
# Isolate protocol/redirect behavior using local fixtures. Destination blocking
|
|
260
|
+
# is exercised without this stub in tc_curl_network_policy.rb.
|
|
261
|
+
def stub_public_network_policy_for_local_fixture(easy)
|
|
262
|
+
def easy.network_policy=(policy)
|
|
263
|
+
raise "unexpected policy: #{policy}" unless policy == :public
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
255
267
|
def with_file_redirect_server
|
|
256
268
|
port_socket = TCPServer.new('127.0.0.1', 0)
|
|
257
269
|
port = port_socket.addr[1]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
|
2
|
+
|
|
3
|
+
class TestCurbTransferGuard < Test::Unit::TestCase
|
|
4
|
+
class InterruptedTransfer < StandardError; end
|
|
5
|
+
|
|
6
|
+
[:perform, :_socket_perform].each do |drive|
|
|
7
|
+
define_method("test_#{drive}_rejects_cross_thread_handle_access") do
|
|
8
|
+
with_waiting_transfer(drive) do |easy, multi, worker, release|
|
|
9
|
+
operations = {
|
|
10
|
+
close: -> { easy.close },
|
|
11
|
+
reset: -> { easy.reset },
|
|
12
|
+
initialize: -> { easy.__send__(:initialize) },
|
|
13
|
+
clone: -> { easy.clone },
|
|
14
|
+
dup: -> { easy.dup },
|
|
15
|
+
setopt: -> { easy.setopt(Curl::CURLOPT_CUSTOMREQUEST, 'DELETE') },
|
|
16
|
+
post_body: -> { easy.post_body = 'replacement' },
|
|
17
|
+
url: -> { easy.url = 'http://127.0.0.1/' },
|
|
18
|
+
timeout: -> { easy.timeout = 1 },
|
|
19
|
+
multi_assignment: -> { easy.multi = nil },
|
|
20
|
+
multi_remove: -> { multi.remove(easy) },
|
|
21
|
+
multi_close: -> { multi.close },
|
|
22
|
+
multi_initialize: -> { multi.__send__(:initialize) },
|
|
23
|
+
multi_add: -> { multi.add(Curl::Easy.new('http://127.0.0.1/')) },
|
|
24
|
+
multi_option: -> { multi.max_connects = 1 }
|
|
25
|
+
}
|
|
26
|
+
operations.each do |name, operation|
|
|
27
|
+
error = assert_raise(RuntimeError, name.to_s, &operation)
|
|
28
|
+
assert_match(/another thread|active Curl::Multi/, error.message, name.to_s)
|
|
29
|
+
end
|
|
30
|
+
assert_equal [easy], multi.requests.values
|
|
31
|
+
|
|
32
|
+
release << true
|
|
33
|
+
assert worker.join(5), 'perform did not finish'
|
|
34
|
+
worker.value
|
|
35
|
+
assert_equal 'ok', easy.body_str
|
|
36
|
+
assert_nothing_raised { easy.clone.close }
|
|
37
|
+
assert_nothing_raised { easy.reset }
|
|
38
|
+
assert_nothing_raised { easy.close }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
define_method("test_#{drive}_releases_guard_after_interruption") do
|
|
43
|
+
with_waiting_transfer(drive) do |easy, multi, worker, release|
|
|
44
|
+
worker.raise(InterruptedTransfer, 'stop')
|
|
45
|
+
assert worker.join(5), 'interrupted perform did not finish'
|
|
46
|
+
assert_kind_of InterruptedTransfer, worker[:transfer_error]
|
|
47
|
+
assert_nothing_raised { multi.remove(easy) }
|
|
48
|
+
assert_nothing_raised { easy.reset }
|
|
49
|
+
assert_nothing_raised { easy.close }
|
|
50
|
+
release << true
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def with_waiting_transfer(drive)
|
|
56
|
+
if drive == :_socket_perform && !Curl::Multi.private_method_defined?(drive)
|
|
57
|
+
omit('socket-action perform path is not available')
|
|
58
|
+
end
|
|
59
|
+
server = TCPServer.new('127.0.0.1', 0)
|
|
60
|
+
started = Queue.new
|
|
61
|
+
release = Queue.new
|
|
62
|
+
server_thread = Thread.new do
|
|
63
|
+
socket = server.accept
|
|
64
|
+
begin
|
|
65
|
+
request = +''
|
|
66
|
+
request << socket.readpartial(4096) until request.include?("\r\n\r\n")
|
|
67
|
+
started << true
|
|
68
|
+
release.pop
|
|
69
|
+
socket.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
|
|
70
|
+
rescue Errno::EPIPE, Errno::ECONNRESET
|
|
71
|
+
# The interruption test intentionally closes the connection early.
|
|
72
|
+
ensure
|
|
73
|
+
socket.close
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
easy = Curl::Easy.new("http://127.0.0.1:#{server.addr[1]}/")
|
|
77
|
+
multi = Curl::Multi.new
|
|
78
|
+
# Exercise reuse after autoclose as well as the active-transfer checks.
|
|
79
|
+
previous_autoclose = Curl::Multi.autoclose
|
|
80
|
+
begin
|
|
81
|
+
Curl::Multi.autoclose = true
|
|
82
|
+
multi.add(Curl::Easy.new($TEST_URL))
|
|
83
|
+
multi.perform
|
|
84
|
+
ensure
|
|
85
|
+
Curl::Multi.autoclose = previous_autoclose
|
|
86
|
+
end
|
|
87
|
+
multi.add(easy)
|
|
88
|
+
worker = Thread.new do
|
|
89
|
+
Thread.current.report_on_exception = false if Thread.current.respond_to?(:report_on_exception=)
|
|
90
|
+
begin
|
|
91
|
+
multi.__send__(drive)
|
|
92
|
+
rescue InterruptedTransfer => error
|
|
93
|
+
# Keep join non-raising so the test can inspect the exception explicitly.
|
|
94
|
+
Thread.current[:transfer_error] = error
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
Timeout.timeout(5) { started.pop }
|
|
98
|
+
yield easy, multi, worker, release
|
|
99
|
+
ensure
|
|
100
|
+
release << true if release
|
|
101
|
+
if worker && worker.alive?
|
|
102
|
+
worker.kill
|
|
103
|
+
worker.join(5)
|
|
104
|
+
end
|
|
105
|
+
multi.close if multi
|
|
106
|
+
server.close if server && !server.closed?
|
|
107
|
+
if server_thread
|
|
108
|
+
server_thread.join(5)
|
|
109
|
+
server_thread.kill if server_thread.alive?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
data/tests/tc_fiber_scheduler.rb
CHANGED
|
@@ -118,7 +118,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
118
118
|
|
|
119
119
|
duration = Time.now - started
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
assert_ran_concurrently(duration)
|
|
122
122
|
assert_equal ITERS, results.size
|
|
123
123
|
assert_equal ITERS.times.map {200}, results
|
|
124
124
|
end
|
|
@@ -149,7 +149,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
149
149
|
|
|
150
150
|
duration = Time.now - started
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
assert_ran_concurrently(duration)
|
|
153
153
|
assert_equal ITERS, results.size
|
|
154
154
|
assert_equal ITERS.times.map {200}, results
|
|
155
155
|
end
|
|
@@ -734,6 +734,97 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
734
734
|
end
|
|
735
735
|
end
|
|
736
736
|
|
|
737
|
+
# Records unblock calls in place of a real scheduler.
|
|
738
|
+
class UnblockRecorder
|
|
739
|
+
attr_reader :unblocked
|
|
740
|
+
|
|
741
|
+
def initialize
|
|
742
|
+
@unblocked = []
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def unblock(blocker, _fiber)
|
|
746
|
+
@unblocked << blocker
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# Enumerating every waiter on every drive-loop pass made release cost
|
|
751
|
+
# O(waiters) per pass; only the queued completions should be visited.
|
|
752
|
+
class NonEnumerableWaiters < Hash
|
|
753
|
+
%i[each each_pair each_value each_key].each do |name|
|
|
754
|
+
define_method(name) { |*| raise 'release_scheduler_waiters must not enumerate all waiters' }
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
def scheduler_test_waiter(scheduler)
|
|
759
|
+
{completed: true, done: false, error: nil, fiber: Fiber.new {}, scheduler: scheduler}
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def test_release_scheduler_waiters_visits_only_queued_completions
|
|
763
|
+
state = Curl.scheduler_state
|
|
764
|
+
recorder = UnblockRecorder.new
|
|
765
|
+
waiters = NonEnumerableWaiters.new
|
|
766
|
+
1_000.times { |i| waiters[i] = scheduler_test_waiter(recorder).merge(completed: false) }
|
|
767
|
+
finished = scheduler_test_waiter(recorder)
|
|
768
|
+
waiters[:finished] = finished
|
|
769
|
+
state[:waiters] = waiters
|
|
770
|
+
state[:completed] << [:finished, finished]
|
|
771
|
+
|
|
772
|
+
Curl.release_scheduler_waiters(state)
|
|
773
|
+
|
|
774
|
+
assert_equal [finished], recorder.unblocked
|
|
775
|
+
assert finished[:done]
|
|
776
|
+
assert_empty state[:completed]
|
|
777
|
+
ensure
|
|
778
|
+
cleanup_scheduler_state
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
# A waiter whose perform already returned (it raised) is unregistered and its
|
|
782
|
+
# fiber may be blocked on something else, so a late completion must not
|
|
783
|
+
# unblock it.
|
|
784
|
+
def test_release_scheduler_waiters_skips_unregistered_waiter
|
|
785
|
+
state = Curl.scheduler_state
|
|
786
|
+
recorder = UnblockRecorder.new
|
|
787
|
+
stale = scheduler_test_waiter(recorder)
|
|
788
|
+
state[:completed] << [:stale, stale]
|
|
789
|
+
|
|
790
|
+
Curl.release_scheduler_waiters(state)
|
|
791
|
+
|
|
792
|
+
assert_empty recorder.unblocked
|
|
793
|
+
assert_equal false, stale[:done]
|
|
794
|
+
assert_empty state[:completed]
|
|
795
|
+
ensure
|
|
796
|
+
cleanup_scheduler_state
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
# The transfer whose callback raised the deferred exception receives that
|
|
800
|
+
# error rather than a normal completion; it stays queued so it is completed
|
|
801
|
+
# normally if the exception source changes.
|
|
802
|
+
def test_release_scheduler_waiters_holds_back_deferred_exception_source
|
|
803
|
+
state = Curl.scheduler_state
|
|
804
|
+
recorder = UnblockRecorder.new
|
|
805
|
+
source = scheduler_test_waiter(recorder)
|
|
806
|
+
sibling = scheduler_test_waiter(recorder)
|
|
807
|
+
state[:waiters][:source] = source
|
|
808
|
+
state[:waiters][:sibling] = sibling
|
|
809
|
+
state[:completed] << [:source, source] << [:sibling, sibling]
|
|
810
|
+
state[:multi].instance_variable_set(:@__curb_deferred_exception_source_id, :source)
|
|
811
|
+
|
|
812
|
+
Curl.release_scheduler_waiters(state)
|
|
813
|
+
|
|
814
|
+
assert_equal [sibling], recorder.unblocked
|
|
815
|
+
assert_equal false, source[:done]
|
|
816
|
+
assert_equal [[:source, source]], state[:completed]
|
|
817
|
+
|
|
818
|
+
state[:multi].remove_instance_variable(:@__curb_deferred_exception_source_id)
|
|
819
|
+
Curl.release_scheduler_waiters(state)
|
|
820
|
+
|
|
821
|
+
assert_equal [sibling, source], recorder.unblocked
|
|
822
|
+
assert source[:done]
|
|
823
|
+
assert_empty state[:completed]
|
|
824
|
+
ensure
|
|
825
|
+
cleanup_scheduler_state
|
|
826
|
+
end
|
|
827
|
+
|
|
737
828
|
def test_drain_scheduler_pending_does_not_drop_work_rejected_during_deferred_abort
|
|
738
829
|
state = Curl.scheduler_state
|
|
739
830
|
easy = Curl::Easy.new("http://127.0.0.1:#{@port}/test")
|
|
@@ -849,6 +940,14 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
|
|
|
849
940
|
false
|
|
850
941
|
end
|
|
851
942
|
|
|
943
|
+
# Wall-clock bounds are not reliable under valgrind, which slows and
|
|
944
|
+
# serializes threads unpredictably; there the requests and their results are
|
|
945
|
+
# still checked under memcheck, only the duration bound is skipped.
|
|
946
|
+
def assert_ran_concurrently(duration)
|
|
947
|
+
return if curb_under_valgrind?
|
|
948
|
+
assert duration < THRESHOLD, "Requests did not run concurrently under fiber scheduler (#{duration}s) which exceeds the expected threshold of: #{THRESHOLD} serial time would be about: #{SERIAL_TIME_WOULD_BE_ABOUT}"
|
|
949
|
+
end
|
|
950
|
+
|
|
852
951
|
def socket_perform_supported?
|
|
853
952
|
Curl::Multi.private_method_defined?(:_socket_perform)
|
|
854
953
|
end
|