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.
@@ -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
- ".localhost\tTRUE\t/\tFALSE\t#{expires_file.to_time.to_i}\tpermanent\t84",
192
- ".localhost\tTRUE\t/\tFALSE\t#{expires_file.to_time.to_i}\tpermanent_file\t84",
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
- ".localhost\tTRUE\t/\tFALSE\t0\tsession\t840",
195
- ".localhost\tTRUE\t/\tFALSE\t0\tsession_file\t840",
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
- ".localhost\tTRUE\t/\tFALSE\t#{expires_file.to_time.to_i}\tpermanent\t84",
208
- ".localhost\tTRUE\t/\tFALSE\t#{expires_file.to_time.to_i}\tpermanent_file\t84",
209
- ".localhost\tTRUE\t/\tFALSE\t0\tsession\t840",
210
- ".localhost\tTRUE\t/\tFALSE\t0\tsession_file\t840",
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
- socket.write(response)
201
- socket.close
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}/"
@@ -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