curb 1.4.0 → 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 +447 -0
- data/README.md +39 -8
- data/ext/curb.h +3 -3
- data/ext/curb_easy.c +567 -186
- data/ext/curb_easy.h +10 -0
- data/ext/curb_macros.h +16 -0
- data/ext/curb_multi.c +83 -23
- data/ext/curb_multi.h +4 -0
- data/ext/curb_postfield.c +103 -114
- data/ext/curb_postfield.h +10 -1
- data/ext/extconf.rb +1 -0
- data/lib/curl/easy.rb +24 -4
- data/lib/curl/multi.rb +31 -9
- data/lib/curl.rb +78 -15
- 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 +304 -0
- data/tests/tc_curl_easy_cookielist.rb +10 -8
- data/tests/tc_curl_maxfilesize.rb +109 -5
- data/tests/tc_curl_multi.rb +32 -0
- data/tests/tc_curl_native_coverage.rb +57 -0
- data/tests/tc_curl_network_policy.rb +133 -33
- data/tests/tc_curl_postfield.rb +137 -0
- data/tests/tc_curl_protocols.rb +19 -7
- data/tests/tc_curl_security_hardening.rb +386 -0
- data/tests/tc_curl_transfer_guard.rb +112 -0
- data/tests/tc_fiber_scheduler.rb +46 -6
- data/tests/tc_ractor.rb +52 -1
- metadata +7 -2
data/tests/tc_curl_easy.rb
CHANGED
|
@@ -10,6 +10,122 @@ class FooBadToS
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
class TestCurbCurlEasy < Test::Unit::TestCase
|
|
13
|
+
CONNECTION_INFO_METHODS = %i[
|
|
14
|
+
last_effective_url response_code code primary_ip http_connect_code file_time
|
|
15
|
+
total_time name_lookup_time connect_time app_connect_time pre_transfer_time
|
|
16
|
+
start_transfer_time redirect_time redirect_count redirect_url downloaded_bytes
|
|
17
|
+
uploaded_bytes download_speed upload_speed header_size request_size
|
|
18
|
+
ssl_verify_result downloaded_content_length uploaded_content_length content_type
|
|
19
|
+
os_errno num_connects cookielist ftp_entry_path
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
def test_uninitialized_handles_reject_native_operations
|
|
23
|
+
subclass = Class.new(Curl::Easy) { def initialize; end }
|
|
24
|
+
[Curl::Easy.allocate, subclass.new].each do |easy|
|
|
25
|
+
methods = CONNECTION_INFO_METHODS + %i[reset close clone dup perform]
|
|
26
|
+
methods.each do |method|
|
|
27
|
+
next unless easy.respond_to?(method)
|
|
28
|
+
error = assert_raise(Curl::Err::FailedInitError, method.to_s) { easy.public_send(method) }
|
|
29
|
+
assert_match(/not initialized/, error.message)
|
|
30
|
+
end
|
|
31
|
+
[[:escape, "x"], [:unescape, "x"], [:setopt, Curl::CURLOPT_VERBOSE, true],
|
|
32
|
+
[:http_post, "x"], [:http_put, "x"], [:http_patch, "x"]].each do |args|
|
|
33
|
+
assert_raise(Curl::Err::FailedInitError, args.first.to_s) { easy.public_send(*args) }
|
|
34
|
+
end
|
|
35
|
+
multi = Curl::Multi.new
|
|
36
|
+
begin
|
|
37
|
+
assert_raise(Curl::Err::FailedInitError) { multi.add(easy) }
|
|
38
|
+
assert_raise(Curl::Err::FailedInitError) { multi.send(:_remove, easy) }
|
|
39
|
+
ensure
|
|
40
|
+
multi.close
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_allocated_handle_can_be_initialized
|
|
46
|
+
easy = Curl::Easy.allocate
|
|
47
|
+
easy.send(:initialize, $TEST_URL)
|
|
48
|
+
easy.perform
|
|
49
|
+
# libcurl normalizes file:///C:/ to file://C:/ on Windows.
|
|
50
|
+
expected_url = WINDOWS ? $TEST_URL.sub(%r{\Afile:///}, "file://") : $TEST_URL
|
|
51
|
+
assert_equal expected_url, easy.last_effective_url
|
|
52
|
+
assert_match(/^# DO NOT REMOVE THIS COMMENT/, easy.body_str)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_connection_info_before_perform
|
|
56
|
+
easy = Curl::Easy.new
|
|
57
|
+
CONNECTION_INFO_METHODS.each do |method|
|
|
58
|
+
next unless easy.respond_to?(method)
|
|
59
|
+
assert_nothing_raised(method.to_s) { easy.public_send(method) }
|
|
60
|
+
end
|
|
61
|
+
assert_nil easy.last_effective_url
|
|
62
|
+
assert_nil easy.content_type
|
|
63
|
+
assert_equal 0, easy.response_code
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_reinitialize_detaches_previous_handle
|
|
67
|
+
easy = Curl::Easy.new($TEST_URL)
|
|
68
|
+
easy.headers = { "X-Old" => "old" }
|
|
69
|
+
multi = Curl::Multi.new
|
|
70
|
+
multi.add(easy)
|
|
71
|
+
easy.send(:initialize, $TEST_URL)
|
|
72
|
+
assert_nil easy.multi
|
|
73
|
+
assert_equal({}, easy.headers)
|
|
74
|
+
multi.perform
|
|
75
|
+
easy.perform
|
|
76
|
+
assert_match(/^# DO NOT REMOVE THIS COMMENT/, easy.body_str)
|
|
77
|
+
multi.add(easy)
|
|
78
|
+
multi.perform
|
|
79
|
+
assert_match(/^# DO NOT REMOVE THIS COMMENT/, easy.body_str)
|
|
80
|
+
ensure
|
|
81
|
+
multi.close if multi
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
[:initialize, :close, :reset].each do |operation|
|
|
85
|
+
define_method("test_#{operation}_removes_multi_bookkeeping_before_readd") do
|
|
86
|
+
easy = Curl::Easy.new($TEST_URL)
|
|
87
|
+
multi = Curl::Multi.new
|
|
88
|
+
multi.add(easy)
|
|
89
|
+
easy.__send__(operation)
|
|
90
|
+
|
|
91
|
+
assert_nil easy.multi
|
|
92
|
+
assert_empty multi.requests
|
|
93
|
+
assert_empty multi.__send__(:__curb_native_safety_signatures)
|
|
94
|
+
|
|
95
|
+
easy.url = $TEST_URL
|
|
96
|
+
multi.add(easy)
|
|
97
|
+
assert_equal [easy], multi.requests.values
|
|
98
|
+
multi.perform
|
|
99
|
+
assert_match(/^# DO NOT REMOVE THIS COMMENT/, easy.body_str)
|
|
100
|
+
ensure
|
|
101
|
+
multi.close if multi
|
|
102
|
+
easy.close if easy
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_reinitialize_during_callback_is_blocked
|
|
107
|
+
easy = Curl::Easy.new($TEST_URL)
|
|
108
|
+
easy.on_body do |data|
|
|
109
|
+
error = assert_raise(RuntimeError) { easy.send(:initialize, $TEST_URL) }
|
|
110
|
+
assert_match(/Cannot initialize an active curl handle/, error.message)
|
|
111
|
+
data.bytesize
|
|
112
|
+
end
|
|
113
|
+
easy.perform
|
|
114
|
+
easy.send(:initialize, $TEST_URL)
|
|
115
|
+
easy.perform
|
|
116
|
+
assert_match(/^# DO NOT REMOVE THIS COMMENT/, easy.body_str)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_reinitialize_during_argument_coercion_is_blocked
|
|
120
|
+
easy = Curl::Easy.new($TEST_URL)
|
|
121
|
+
field = Object.new
|
|
122
|
+
field.define_singleton_method(:to_s) { easy.send(:initialize, $TEST_URL); "a=b" }
|
|
123
|
+
error = assert_raise(RuntimeError) { easy.http_post(field) }
|
|
124
|
+
assert_match(/Cannot initialize an active curl handle/, error.message)
|
|
125
|
+
easy.perform
|
|
126
|
+
assert_match(/^# DO NOT REMOVE THIS COMMENT/, easy.body_str)
|
|
127
|
+
end
|
|
128
|
+
|
|
13
129
|
def test_global_reset
|
|
14
130
|
Curl.get($TEST_URL)
|
|
15
131
|
# in a Timeout block you should reset the thread current handle
|
|
@@ -63,6 +179,32 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
|
63
179
|
assert_match(/GET/, http.body)
|
|
64
180
|
end
|
|
65
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
|
+
|
|
66
208
|
def test_curlopt_stderr_with_file
|
|
67
209
|
# does not work with Tempfile directly
|
|
68
210
|
path = Tempfile.new('curb_test_curlopt_stderr').path
|
|
@@ -123,6 +265,92 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
|
123
265
|
tmp.close! if defined?(tmp) && tmp
|
|
124
266
|
end
|
|
125
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
|
+
|
|
126
354
|
def test_head_request_no_body_and_no_timeout
|
|
127
355
|
# Ensure a HEAD request completes and does not attempt to read a body
|
|
128
356
|
# even when the server advertises a Content-Length.
|
|
@@ -2003,4 +2231,80 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
|
2003
2231
|
server_setup
|
|
2004
2232
|
end
|
|
2005
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
|
+
|
|
2006
2310
|
end
|
|
@@ -186,13 +186,15 @@ class TestCurbCurlEasyCookielist < Test::Unit::TestCase
|
|
|
186
186
|
cookiefile = File.join(Dir.tmpdir, 'curl_test_cookiefile')
|
|
187
187
|
assert !File.exist?(cookiefile)
|
|
188
188
|
begin
|
|
189
|
+
# Use host-only file cookies: libpsl builds normalize localhost domain
|
|
190
|
+
# cookies on file load, but COOKIELIST imports retain tail matching.
|
|
189
191
|
cookielist = [
|
|
190
192
|
# Won't be updated, added instead
|
|
191
|
-
"
|
|
192
|
-
"
|
|
193
|
+
"localhost\tFALSE\t/\tFALSE\t#{expires_file.to_time.to_i}\tpermanent\t84",
|
|
194
|
+
"localhost\tFALSE\t/\tFALSE\t#{expires_file.to_time.to_i}\tpermanent_file\t84",
|
|
193
195
|
# Won't be updated, added instead
|
|
194
|
-
"
|
|
195
|
-
"
|
|
196
|
+
"localhost\tFALSE\t/\tFALSE\t0\tsession\t840",
|
|
197
|
+
"localhost\tFALSE\t/\tFALSE\t0\tsession_file\t840",
|
|
196
198
|
'',
|
|
197
199
|
]
|
|
198
200
|
File.write(cookiefile, cookielist.join("\n"))
|
|
@@ -204,10 +206,10 @@ class TestCurbCurlEasyCookielist < Test::Unit::TestCase
|
|
|
204
206
|
expected_cookielist = [
|
|
205
207
|
".localhost\tTRUE\t/\tFALSE\t#{expires.to_time.to_i}\tpermanent\t42",
|
|
206
208
|
".localhost\tTRUE\t/\tFALSE\t0\tsession\t420",
|
|
207
|
-
"
|
|
208
|
-
"
|
|
209
|
-
"
|
|
210
|
-
"
|
|
209
|
+
"localhost\tFALSE\t/\tFALSE\t#{expires_file.to_time.to_i}\tpermanent\t84",
|
|
210
|
+
"localhost\tFALSE\t/\tFALSE\t#{expires_file.to_time.to_i}\tpermanent_file\t84",
|
|
211
|
+
"localhost\tFALSE\t/\tFALSE\t0\tsession\t840",
|
|
212
|
+
"localhost\tFALSE\t/\tFALSE\t0\tsession_file\t840",
|
|
211
213
|
]
|
|
212
214
|
assert_equal expected_cookielist, easy.cookielist
|
|
213
215
|
easy.perform
|
|
@@ -10,6 +10,94 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
10
10
|
super
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def test_safe_body_limit_defaults_and_raw_requests
|
|
14
|
+
assert_equal 10 * 1024 * 1024, Curl::SafetyConfig.new.max_body_bytes
|
|
15
|
+
assert_nil @easy.max_body_bytes
|
|
16
|
+
stub_public_network_policy_for_local_fixture(@easy)
|
|
17
|
+
@easy.safe_http!
|
|
18
|
+
assert_equal Curl::SafetyConfig::DEFAULT_MAX_BODY_BYTES, @easy.max_body_bytes
|
|
19
|
+
@easy.max_body_bytes = nil
|
|
20
|
+
Curl.__send__(:apply_safety!, @easy)
|
|
21
|
+
assert_equal Curl::SafetyConfig::DEFAULT_MAX_BODY_BYTES, @easy.max_body_bytes
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
[:global, :helper, :handle].each do |mode|
|
|
25
|
+
define_method("test_#{mode}_default_body_limit_rejects_oversized_response") do
|
|
26
|
+
payload = 'x' * (Curl::SafetyConfig::DEFAULT_MAX_BODY_BYTES + 1)
|
|
27
|
+
with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: #{payload.bytesize}\r\n\r\n#{payload}") do |url|
|
|
28
|
+
assert_body_limit_error do
|
|
29
|
+
if mode == :helper
|
|
30
|
+
Curl.safe_get(url) { |easy| stub_public_network_policy_for_local_fixture(easy) }
|
|
31
|
+
else
|
|
32
|
+
Curl.safe! if mode == :global
|
|
33
|
+
@easy.url = url
|
|
34
|
+
stub_public_network_policy_for_local_fixture(@easy)
|
|
35
|
+
@easy.safe_http! if mode == :handle
|
|
36
|
+
@easy.perform
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_default_body_limit_bounds_callback_delivery_without_content_length
|
|
44
|
+
limit = Curl::SafetyConfig::DEFAULT_MAX_BODY_BYTES
|
|
45
|
+
payload = 'x' * (limit + 1)
|
|
46
|
+
received = 0
|
|
47
|
+
with_raw_http_response("HTTP/1.0 200 OK\r\n\r\n#{payload}") do |url|
|
|
48
|
+
@easy.url = url
|
|
49
|
+
stub_public_network_policy_for_local_fixture(@easy)
|
|
50
|
+
@easy.safe_http!
|
|
51
|
+
@easy.on_body { |data| received += data.bytesize; data.bytesize }
|
|
52
|
+
assert_body_limit_error { @easy.perform }
|
|
53
|
+
end
|
|
54
|
+
assert_operator received, :<=, limit
|
|
55
|
+
assert_operator received, :>, 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def test_global_body_limit_can_be_explicitly_disabled
|
|
59
|
+
Curl.safe! { |config| config.max_body_bytes = nil }
|
|
60
|
+
stub_public_network_policy_for_local_fixture(@easy)
|
|
61
|
+
@easy.safe_http!
|
|
62
|
+
assert_nil @easy.max_body_bytes
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_safe_helper_accepts_explicit_larger_limit_and_unlimited
|
|
66
|
+
[20 * 1024 * 1024, nil, 0].each do |limit|
|
|
67
|
+
with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok") do |url|
|
|
68
|
+
easy = Curl.safe_get(url, :max_body_bytes => limit) do |handle|
|
|
69
|
+
stub_public_network_policy_for_local_fixture(handle)
|
|
70
|
+
end
|
|
71
|
+
assert_equal(limit == 0 ? nil : limit, easy.max_body_bytes)
|
|
72
|
+
assert_equal 'ok', easy.body_str
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_safe_helper_inherits_global_body_limit
|
|
78
|
+
Curl.safe! { |config| config.max_body_bytes = 20 * 1024 * 1024 }
|
|
79
|
+
stub_public_network_policy_for_local_fixture(@easy)
|
|
80
|
+
@easy.safe_http!
|
|
81
|
+
assert_equal 20 * 1024 * 1024, @easy.max_body_bytes
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_safe_helper_cannot_weaken_global_body_limit
|
|
85
|
+
Curl.safe! { |config| config.max_body_bytes = 10 }
|
|
86
|
+
[100, nil, 0].each do |limit|
|
|
87
|
+
with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: 64\r\n\r\n#{'x' * 64}") do |url|
|
|
88
|
+
assert_body_limit_error do
|
|
89
|
+
Curl.safe_get(url, :max_body_bytes => limit) do |handle|
|
|
90
|
+
stub_public_network_policy_for_local_fixture(handle)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_safe_http_rejects_negative_body_limit
|
|
98
|
+
assert_raise(ArgumentError) { @easy.safe_http!(:max_body_bytes => -1) }
|
|
99
|
+
end
|
|
100
|
+
|
|
13
101
|
def test_maxfilesize_setopt
|
|
14
102
|
@easy.set(Curl::CURLOPT_MAXFILESIZE, 5000000)
|
|
15
103
|
@easy.set(Curl::CURLOPT_MAXFILESIZE_LARGE, 5000000) if Curl.const_defined?(:CURLOPT_MAXFILESIZE_LARGE)
|
|
@@ -41,7 +129,7 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
41
129
|
def test_safe_get_accepts_max_body_bytes_as_second_argument
|
|
42
130
|
with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: 64\r\n\r\n#{'x' * 64}") do |url|
|
|
43
131
|
assert_body_limit_error do
|
|
44
|
-
Curl.safe_get(url, :max_body_bytes => 10)
|
|
132
|
+
Curl.safe_get(url, :max_body_bytes => 10) { |easy| stub_public_network_policy_for_local_fixture(easy) }
|
|
45
133
|
end
|
|
46
134
|
end
|
|
47
135
|
end
|
|
@@ -50,6 +138,7 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
50
138
|
with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: 64\r\n\r\n#{'x' * 64}") do |url|
|
|
51
139
|
assert_body_limit_error do
|
|
52
140
|
Curl.safe_get(url, {}, :max_body_bytes => 10) do |easy|
|
|
141
|
+
stub_public_network_policy_for_local_fixture(easy)
|
|
53
142
|
easy.max_body_bytes = nil
|
|
54
143
|
end
|
|
55
144
|
end
|
|
@@ -63,6 +152,7 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
63
152
|
end
|
|
64
153
|
|
|
65
154
|
@easy.url = url
|
|
155
|
+
stub_public_network_policy_for_local_fixture(@easy)
|
|
66
156
|
|
|
67
157
|
assert_body_limit_error do
|
|
68
158
|
@easy.perform
|
|
@@ -78,6 +168,7 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
78
168
|
|
|
79
169
|
assert_body_limit_error do
|
|
80
170
|
Curl.get(url) do |easy|
|
|
171
|
+
stub_public_network_policy_for_local_fixture(easy)
|
|
81
172
|
easy.max_body_bytes = nil
|
|
82
173
|
end
|
|
83
174
|
end
|
|
@@ -91,7 +182,7 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
91
182
|
end
|
|
92
183
|
|
|
93
184
|
assert_body_limit_error do
|
|
94
|
-
Curl.safe_get(url, :max_body_bytes => 10)
|
|
185
|
+
Curl.safe_get(url, :max_body_bytes => 10) { |easy| stub_public_network_policy_for_local_fixture(easy) }
|
|
95
186
|
end
|
|
96
187
|
end
|
|
97
188
|
end
|
|
@@ -106,7 +197,7 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
106
197
|
|
|
107
198
|
def test_safe_head_does_not_apply_body_cap_to_content_length
|
|
108
199
|
with_raw_http_response("HTTP/1.1 200 OK\r\nContent-Length: 65536\r\n\r\n") do |url|
|
|
109
|
-
easy = Curl.safe_head(url, :max_body_bytes => 1)
|
|
200
|
+
easy = Curl.safe_head(url, :max_body_bytes => 1) { |handle| stub_public_network_policy_for_local_fixture(handle) }
|
|
110
201
|
|
|
111
202
|
assert_equal 200, easy.response_code
|
|
112
203
|
assert_equal '', easy.body_str
|
|
@@ -182,6 +273,14 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
182
273
|
|
|
183
274
|
private
|
|
184
275
|
|
|
276
|
+
# Keep body-limit integration tests local; real destination enforcement is
|
|
277
|
+
# covered separately in tc_curl_network_policy.rb.
|
|
278
|
+
def stub_public_network_policy_for_local_fixture(easy)
|
|
279
|
+
def easy.network_policy=(policy)
|
|
280
|
+
raise "unexpected policy: #{policy}" unless policy == :public
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
185
284
|
def assert_body_limit_error
|
|
186
285
|
assert_raise(Curl::Err::FileSizeExceededError) do
|
|
187
286
|
yield
|
|
@@ -197,8 +296,13 @@ class TestCurbCurlMaxFileSize < Test::Unit::TestCase
|
|
|
197
296
|
socket.readpartial(4096)
|
|
198
297
|
rescue EOFError
|
|
199
298
|
end
|
|
200
|
-
|
|
201
|
-
|
|
299
|
+
begin
|
|
300
|
+
socket.write(response)
|
|
301
|
+
rescue Errno::EPIPE, Errno::ECONNRESET
|
|
302
|
+
# Expected when the client aborts at the body limit.
|
|
303
|
+
ensure
|
|
304
|
+
socket.close
|
|
305
|
+
end
|
|
202
306
|
end
|
|
203
307
|
|
|
204
308
|
yield "http://127.0.0.1:#{port}/"
|
data/tests/tc_curl_multi.rb
CHANGED
|
@@ -799,6 +799,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
|
|
|
799
799
|
end
|
|
800
800
|
|
|
801
801
|
def test_native_safety_signatures_are_pruned_after_perform
|
|
802
|
+
Curl.safe! { |config| config.network_policy = :none }
|
|
802
803
|
m = Curl::Multi.new
|
|
803
804
|
easy = Curl::Easy.new($TEST_URL)
|
|
804
805
|
|
|
@@ -811,6 +812,37 @@ class TestCurbCurlMulti < Test::Unit::TestCase
|
|
|
811
812
|
|
|
812
813
|
assert_equal 0, m.requests.length
|
|
813
814
|
assert_empty signatures
|
|
815
|
+
ensure
|
|
816
|
+
m.close if m
|
|
817
|
+
Curl.__send__(:clear_safe!)
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
def test_unconstrained_easy_skips_safety_signature_bookkeeping
|
|
821
|
+
m = Curl::Multi.new
|
|
822
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
823
|
+
|
|
824
|
+
m.add(easy)
|
|
825
|
+
assert_empty m.__send__(:__curb_native_safety_signatures)
|
|
826
|
+
|
|
827
|
+
m.perform
|
|
828
|
+
assert_equal 200, easy.response_code
|
|
829
|
+
ensure
|
|
830
|
+
m.close if m
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
def test_native_policy_set_after_unconstrained_add_is_applied
|
|
834
|
+
m = Curl::Multi.new
|
|
835
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
836
|
+
m.add(easy)
|
|
837
|
+
begin
|
|
838
|
+
easy.network_policy = :public
|
|
839
|
+
rescue NotImplementedError
|
|
840
|
+
omit('network_policy=:public requires CURLOPT_OPENSOCKETFUNCTION support')
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
m.perform
|
|
844
|
+
|
|
845
|
+
assert_match(/127\.0\.0\.1/, easy.unsafe_destination_error)
|
|
814
846
|
ensure
|
|
815
847
|
m.close if m
|
|
816
848
|
end
|
|
@@ -106,6 +106,63 @@ class TestCurbCurlNativeCoverage < Test::Unit::TestCase
|
|
|
106
106
|
easy.close if defined?(easy) && easy
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
[:clone, :dup].each do |copy_method|
|
|
110
|
+
[:on_body, :on_header, :on_progress, :on_debug, :on_complete].each do |callback|
|
|
111
|
+
define_method("test_#{copy_method}_during_multipart_#{callback}_detaches_form") do
|
|
112
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
113
|
+
easy.multipart_form_post = true
|
|
114
|
+
easy.verbose = true if callback == :on_debug
|
|
115
|
+
copies = []
|
|
116
|
+
easy.public_send(callback) do |*args|
|
|
117
|
+
if copies.empty?
|
|
118
|
+
# The public option can change while the native form is attached.
|
|
119
|
+
easy.multipart_form_post = false
|
|
120
|
+
copies << easy.public_send(copy_method)
|
|
121
|
+
end
|
|
122
|
+
[:on_body, :on_header].include?(callback) ? args.first.bytesize : true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
easy.http_post(Curl::PostField.content('original', 'temporary-form'))
|
|
126
|
+
assert_equal 1, copies.size, "#{callback} must run during the multipart request"
|
|
127
|
+
copy = copies.first
|
|
128
|
+
copy.public_send(callback) # Remove the inherited callback.
|
|
129
|
+
copy.verbose = false
|
|
130
|
+
|
|
131
|
+
# Plain perform must not access or resend the original's freed form.
|
|
132
|
+
copy.perform
|
|
133
|
+
easy.public_send(callback)
|
|
134
|
+
easy.verbose = false
|
|
135
|
+
easy.perform
|
|
136
|
+
assert_equal easy.body_str, copy.body_str
|
|
137
|
+
assert_no_match(/temporary-form|name="original"/, copy.body_str)
|
|
138
|
+
|
|
139
|
+
copy.multipart_form_post = true
|
|
140
|
+
copy.http_post(Curl::PostField.content('replacement', 'fresh-form'))
|
|
141
|
+
assert_match(/name="replacement"/, copy.body_str)
|
|
142
|
+
assert_match(/fresh-form/, copy.body_str)
|
|
143
|
+
assert_no_match(/temporary-form/, copy.body_str)
|
|
144
|
+
ensure
|
|
145
|
+
copies&.each(&:close)
|
|
146
|
+
easy&.close
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
define_method("test_#{copy_method}_preserves_post_after_multipart_cleanup") do
|
|
151
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
152
|
+
easy.multipart_form_post = true
|
|
153
|
+
easy.http_post(Curl::PostField.content('original', 'temporary-form'))
|
|
154
|
+
easy.multipart_form_post = false
|
|
155
|
+
easy.post_body = 'ordinary=post'
|
|
156
|
+
|
|
157
|
+
copy = easy.public_send(copy_method)
|
|
158
|
+
copy.perform
|
|
159
|
+
assert_equal "POST\nordinary=post", copy.body_str
|
|
160
|
+
ensure
|
|
161
|
+
copy&.close
|
|
162
|
+
easy&.close
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
109
166
|
def test_native_accessors_round_trip
|
|
110
167
|
easy = Curl::Easy.new(TestServlet.url)
|
|
111
168
|
|