curb 1.4.0 → 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.
@@ -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
@@ -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}/"
@@ -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
 
@@ -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
@@ -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