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.
- checksums.yaml +4 -4
- data/ChangeLog.md +432 -0
- data/README.md +25 -5
- data/ext/curb.h +3 -3
- data/ext/curb_easy.c +274 -161
- data/ext/curb_easy.h +5 -0
- data/ext/curb_macros.h +16 -0
- data/ext/curb_multi.c +51 -8
- data/ext/curb_multi.h +4 -0
- data/ext/curb_postfield.c +103 -114
- data/ext/curb_postfield.h +10 -1
- data/lib/curl/easy.rb +10 -3
- data/lib/curl/multi.rb +1 -1
- data/lib/curl.rb +9 -7
- data/tests/tc_curl_easy.rb +116 -0
- data/tests/tc_curl_easy_cookielist.rb +10 -8
- data/tests/tc_curl_maxfilesize.rb +109 -5
- data/tests/tc_curl_native_coverage.rb +57 -0
- data/tests/tc_curl_network_policy.rb +108 -1
- data/tests/tc_curl_postfield.rb +137 -0
- data/tests/tc_curl_protocols.rb +19 -7
- data/tests/tc_curl_transfer_guard.rb +112 -0
- data/tests/tc_ractor.rb +48 -1
- metadata +5 -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
|
|
@@ -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}/"
|
|
@@ -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
|
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
|