curb 1.3.5 → 1.3.6
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/README.md +57 -0
- data/Rakefile +8 -3
- data/doc.rb +48 -8
- data/ext/curb.c +24 -0
- data/ext/curb.h +3 -3
- data/ext/curb_easy.c +1378 -55
- data/ext/curb_easy.h +26 -0
- data/ext/curb_errors.c +2 -0
- data/ext/curb_errors.h +1 -0
- data/ext/curb_multi.c +48 -2
- data/ext/curb_multi.h +1 -0
- data/ext/extconf.rb +8 -0
- data/lib/curl/download.rb +160 -0
- data/lib/curl/easy.rb +113 -13
- data/lib/curl/multi.rb +172 -39
- data/lib/curl.rb +471 -11
- data/tests/bug_poison.rb +29 -0
- data/tests/tc_curl_download.rb +86 -0
- data/tests/tc_curl_easy.rb +76 -0
- data/tests/tc_curl_maxfilesize.rb +201 -1
- data/tests/tc_curl_multi.rb +258 -0
- data/tests/tc_curl_network_policy.rb +1475 -0
- data/tests/tc_curl_protocols.rb +351 -0
- data/tests/tc_fiber_scheduler.rb +41 -0
- metadata +7 -2
|
@@ -0,0 +1,1475 @@
|
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
|
2
|
+
|
|
3
|
+
class TestCurbCurlNetworkPolicy < Test::Unit::TestCase
|
|
4
|
+
include TestServerMethods
|
|
5
|
+
|
|
6
|
+
def setup
|
|
7
|
+
server_setup
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def teardown
|
|
11
|
+
Curl.__send__(:clear_safe!) if Curl.respond_to?(:clear_safe!, true)
|
|
12
|
+
super
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_network_policy_defaults_to_none
|
|
16
|
+
easy = Curl::Easy.new
|
|
17
|
+
|
|
18
|
+
assert_equal :none, easy.network_policy
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_network_policy_rejects_unknown_policy
|
|
22
|
+
easy = Curl::Easy.new
|
|
23
|
+
|
|
24
|
+
assert_raise(ArgumentError) do
|
|
25
|
+
easy.network_policy = :private
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_safe_bang_allowed_hosts_rejects_unlisted_initial_host
|
|
30
|
+
Curl.safe! do |config|
|
|
31
|
+
config.allowed_hosts = ['allowed.example']
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
easy = Curl::Easy.new('http://blocked.example/')
|
|
35
|
+
|
|
36
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
37
|
+
Curl.__send__(:apply_safety!, easy)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
assert_match(/host allowlist/, error.message)
|
|
41
|
+
assert_match(/blocked\.example/, error.message)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_safe_bang_allowed_hosts_accepts_matching_initial_host
|
|
45
|
+
Curl.safe! do |config|
|
|
46
|
+
config.allowed_hosts = ['ALLOWED.example.']
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
easy = Curl::Easy.new('http://allowed.example/path')
|
|
50
|
+
|
|
51
|
+
Curl.__send__(:apply_safety!, easy)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_safe_bang_allowed_hosts_strips_ports_without_scheme
|
|
55
|
+
Curl.safe! do |config|
|
|
56
|
+
config.allowed_hosts = ['api.example:443', '[2001:db8::1]:443']
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
easy = Curl::Easy.new('http://api.example/path')
|
|
60
|
+
Curl.__send__(:apply_safety!, easy)
|
|
61
|
+
|
|
62
|
+
assert_equal ['api.example', '2001:db8::1'], easy.allowed_hosts
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_safety_allowlists_are_deduplicated_after_normalization
|
|
66
|
+
require_public_network_policy!
|
|
67
|
+
|
|
68
|
+
Curl.safe! do |config|
|
|
69
|
+
config.network_policy = :public
|
|
70
|
+
config.allowed_hosts = ['API.example.', 'api.example', 'https://api.example/path']
|
|
71
|
+
config.allowed_cidrs = ['1.1.1.0/24', '1.1.1.0/24']
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
easy = Curl::Easy.new('http://api.example/')
|
|
75
|
+
Curl.__send__(:apply_safety!, easy)
|
|
76
|
+
|
|
77
|
+
assert_equal ['api.example'], easy.allowed_hosts
|
|
78
|
+
assert_equal ['1.1.1.0/24'], easy.allowed_cidrs
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_easy_allowlists_are_deduplicated_after_normalization
|
|
82
|
+
easy = Curl::Easy.new
|
|
83
|
+
|
|
84
|
+
easy.allowed_hosts = ['API.example.', 'api.example', 'https://api.example/path']
|
|
85
|
+
easy.allowed_cidrs = ['1.1.1.0/24', '1.1.1.0/24']
|
|
86
|
+
|
|
87
|
+
assert_equal ['api.example'], easy.allowed_hosts
|
|
88
|
+
assert_equal ['1.1.1.0/24'], easy.allowed_cidrs
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_safe_bang_allowed_hosts_allows_same_host_redirect
|
|
92
|
+
omit('redirect-aware host allowlists require CURLOPT_PREREQFUNCTION') unless Curl.const_defined?(:CURLOPT_PREREQFUNCTION)
|
|
93
|
+
|
|
94
|
+
allowed_host = 'curb-allowed.test'
|
|
95
|
+
|
|
96
|
+
with_host_redirect_server(allowed_host, allowed_host) do |port, hits|
|
|
97
|
+
Curl.safe! do |config|
|
|
98
|
+
config.allowed_hosts = [allowed_host]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
easy = Curl::Easy.new("http://#{allowed_host}:#{port}/redirect-to-target")
|
|
102
|
+
easy.resolve = [resolve_entry(allowed_host, port, '127.0.0.1')]
|
|
103
|
+
easy.follow_location = true
|
|
104
|
+
|
|
105
|
+
easy.perform
|
|
106
|
+
|
|
107
|
+
assert_equal 200, easy.response_code
|
|
108
|
+
assert_equal 'target', easy.body_str
|
|
109
|
+
assert_equal 1, hits[:redirect]
|
|
110
|
+
assert_equal 1, hits[:target]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def test_safe_bang_allowed_hosts_blocks_redirect_to_unlisted_host
|
|
115
|
+
omit('redirect-aware host allowlists require CURLOPT_PREREQFUNCTION') unless Curl.const_defined?(:CURLOPT_PREREQFUNCTION)
|
|
116
|
+
|
|
117
|
+
allowed_host = 'curb-allowed.test'
|
|
118
|
+
blocked_host = 'curb-blocked.test'
|
|
119
|
+
|
|
120
|
+
with_host_redirect_server(allowed_host, blocked_host) do |port, hits|
|
|
121
|
+
Curl.safe! do |config|
|
|
122
|
+
config.allowed_hosts = [allowed_host]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
easy = Curl::Easy.new("http://#{allowed_host}:#{port}/redirect-to-target")
|
|
126
|
+
easy.resolve = [
|
|
127
|
+
resolve_entry(allowed_host, port, '127.0.0.1'),
|
|
128
|
+
resolve_entry(blocked_host, port, '127.0.0.1')
|
|
129
|
+
]
|
|
130
|
+
easy.follow_location = true
|
|
131
|
+
|
|
132
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
133
|
+
easy.perform
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
assert_match(/host allowlist/, error.message)
|
|
137
|
+
assert_match(/#{Regexp.escape(blocked_host)}/, error.message)
|
|
138
|
+
assert_equal 1, hits[:redirect]
|
|
139
|
+
assert_equal 0, hits[:target], 'blocked redirected host should not receive a request'
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_safe_bang_allowed_cidrs_requires_public_network_policy
|
|
144
|
+
Curl.safe! do |config|
|
|
145
|
+
config.allowed_cidrs = ['1.1.1.0/24']
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
easy = Curl::Easy.new('http://1.1.1.1/')
|
|
149
|
+
|
|
150
|
+
assert_raise(ArgumentError) do
|
|
151
|
+
Curl.__send__(:apply_safety!, easy)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def test_allowed_cidrs_rejects_invalid_ranges
|
|
156
|
+
easy = Curl::Easy.new
|
|
157
|
+
|
|
158
|
+
assert_raise(ArgumentError) do
|
|
159
|
+
easy.allowed_cidrs = ['not-a-cidr']
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
assert_raise(ArgumentError) do
|
|
163
|
+
Curl.safe! { |config| config.allowed_cidrs = ['1.1.1.1/33'] }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def test_clone_preserves_allowed_hosts_for_native_prereq_checks
|
|
168
|
+
omit('redirect-aware host allowlists require CURLOPT_PREREQFUNCTION') unless Curl.const_defined?(:CURLOPT_PREREQFUNCTION)
|
|
169
|
+
|
|
170
|
+
allowed_host = 'curb-clone-allowed.test'
|
|
171
|
+
blocked_host = 'curb-clone-blocked.test'
|
|
172
|
+
original = Curl::Easy.new("http://#{allowed_host}:#{TestServlet.port}#{TestServlet.path}")
|
|
173
|
+
original.allowed_hosts = [allowed_host]
|
|
174
|
+
|
|
175
|
+
clone = original.clone
|
|
176
|
+
clone.url = "http://#{blocked_host}:#{TestServlet.port}#{TestServlet.path}"
|
|
177
|
+
clone.resolve = [resolve_entry(blocked_host, TestServlet.port, '127.0.0.1')]
|
|
178
|
+
clone.connect_timeout_ms = 50
|
|
179
|
+
|
|
180
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
181
|
+
clone.perform
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
assert_match(/host allowlist/, error.message)
|
|
185
|
+
assert_match(/#{Regexp.escape(blocked_host)}/, error.message)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def test_clone_preserves_allowed_cidrs_for_native_public_policy_checks
|
|
189
|
+
require_public_network_policy!
|
|
190
|
+
|
|
191
|
+
original = Curl::Easy.new('http://1.1.1.1:81/')
|
|
192
|
+
original.network_policy = :public
|
|
193
|
+
original.allowed_cidrs = ['8.8.8.0/24']
|
|
194
|
+
original.connect_timeout_ms = 50
|
|
195
|
+
original.timeout_ms = 100
|
|
196
|
+
|
|
197
|
+
clone = original.clone
|
|
198
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
199
|
+
clone.perform
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
assert_match(/outside allowed CIDR ranges/, error.message)
|
|
203
|
+
assert_match(/1\.1\.1\.1/, error.message)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def test_allowed_cidrs_set_before_public_network_policy_are_enforced
|
|
207
|
+
require_public_network_policy!
|
|
208
|
+
|
|
209
|
+
easy = Curl::Easy.new('http://1.1.1.1:81/')
|
|
210
|
+
easy.allowed_cidrs = ['8.8.8.0/24']
|
|
211
|
+
easy.network_policy = :public
|
|
212
|
+
easy.connect_timeout_ms = 50
|
|
213
|
+
easy.timeout_ms = 100
|
|
214
|
+
|
|
215
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
216
|
+
easy.perform
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
assert_match(/outside allowed CIDR ranges/, error.message)
|
|
220
|
+
assert_match(/1\.1\.1\.1/, error.message)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def test_public_network_policy_blocks_loopback_destination
|
|
224
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
225
|
+
require_public_network_policy!(easy)
|
|
226
|
+
|
|
227
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
228
|
+
easy.perform
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
assert_match(/127\.0\.0\.1/, error.message)
|
|
232
|
+
assert_match(/public network policy/, error.message)
|
|
233
|
+
assert_match(/127\.0\.0\.1/, easy.unsafe_destination_error)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def test_public_network_policy_does_not_reuse_pre_policy_loopback_connection
|
|
237
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
238
|
+
|
|
239
|
+
easy.perform
|
|
240
|
+
assert_equal 200, easy.response_code
|
|
241
|
+
assert_match(/GET/, easy.body_str)
|
|
242
|
+
|
|
243
|
+
require_public_network_policy!(easy)
|
|
244
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
245
|
+
easy.perform
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
assert_match(/127\.0\.0\.1/, error.message)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def test_public_network_policy_reuse_controls_clear_when_policy_is_disabled
|
|
252
|
+
omit('CURLOPT_FRESH_CONNECT is not available') unless Curl.const_defined?(:CURLOPT_FRESH_CONNECT)
|
|
253
|
+
omit('CURLOPT_FORBID_REUSE is not available') unless Curl.const_defined?(:CURLOPT_FORBID_REUSE)
|
|
254
|
+
|
|
255
|
+
previous_autoclose = Curl::Multi.autoclose
|
|
256
|
+
Curl::Multi.autoclose = false
|
|
257
|
+
|
|
258
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
259
|
+
require_public_network_policy!(easy)
|
|
260
|
+
|
|
261
|
+
assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
262
|
+
easy.perform
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
easy.network_policy = :none
|
|
266
|
+
easy.perform
|
|
267
|
+
assert_equal 1, easy.num_connects
|
|
268
|
+
|
|
269
|
+
easy.perform
|
|
270
|
+
assert_equal 0, easy.num_connects
|
|
271
|
+
ensure
|
|
272
|
+
easy.close if defined?(easy) && easy
|
|
273
|
+
Curl::Multi.autoclose = previous_autoclose if defined?(previous_autoclose)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def test_default_network_policy_preserves_explicit_forbid_reuse
|
|
277
|
+
omit('CURLOPT_FORBID_REUSE is not available') unless Curl.const_defined?(:CURLOPT_FORBID_REUSE)
|
|
278
|
+
|
|
279
|
+
previous_autoclose = Curl::Multi.autoclose
|
|
280
|
+
Curl::Multi.autoclose = false
|
|
281
|
+
|
|
282
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
283
|
+
easy.setopt(Curl::CURLOPT_FORBID_REUSE, 1)
|
|
284
|
+
|
|
285
|
+
easy.perform
|
|
286
|
+
assert_equal 1, easy.num_connects
|
|
287
|
+
|
|
288
|
+
easy.perform
|
|
289
|
+
assert_equal 1, easy.num_connects
|
|
290
|
+
ensure
|
|
291
|
+
easy.close if defined?(easy) && easy
|
|
292
|
+
Curl::Multi.autoclose = previous_autoclose if defined?(previous_autoclose)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def test_public_network_policy_disable_preserves_explicit_forbid_reuse
|
|
296
|
+
omit('CURLOPT_FORBID_REUSE is not available') unless Curl.const_defined?(:CURLOPT_FORBID_REUSE)
|
|
297
|
+
|
|
298
|
+
previous_autoclose = Curl::Multi.autoclose
|
|
299
|
+
Curl::Multi.autoclose = false
|
|
300
|
+
|
|
301
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
302
|
+
easy.setopt(Curl::CURLOPT_FORBID_REUSE, 1)
|
|
303
|
+
require_public_network_policy!(easy)
|
|
304
|
+
|
|
305
|
+
assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
306
|
+
easy.perform
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
easy.network_policy = :none
|
|
310
|
+
easy.perform
|
|
311
|
+
assert_equal 1, easy.num_connects
|
|
312
|
+
|
|
313
|
+
easy.perform
|
|
314
|
+
assert_equal 1, easy.num_connects
|
|
315
|
+
ensure
|
|
316
|
+
easy.close if defined?(easy) && easy
|
|
317
|
+
Curl::Multi.autoclose = previous_autoclose if defined?(previous_autoclose)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def test_public_network_policy_blocks_ipv4_unsafe_matrix
|
|
321
|
+
require_public_network_policy!
|
|
322
|
+
|
|
323
|
+
unsafe_addresses = {
|
|
324
|
+
'unspecified' => '0.0.0.0',
|
|
325
|
+
'private_10' => '10.0.0.1',
|
|
326
|
+
'private_172' => '172.16.0.1',
|
|
327
|
+
'private_192' => '192.168.0.1',
|
|
328
|
+
'shared_carrier' => '100.64.0.1',
|
|
329
|
+
'cloud_metadata' => '100.100.100.200',
|
|
330
|
+
'loopback' => '127.0.0.1',
|
|
331
|
+
'link_local_metadata' => '169.254.169.254',
|
|
332
|
+
'ietf_assignments' => '192.0.0.8',
|
|
333
|
+
'benchmarking' => '198.18.0.1',
|
|
334
|
+
'documentation_1' => '192.0.2.1',
|
|
335
|
+
'documentation_2' => '198.51.100.1',
|
|
336
|
+
'documentation_3' => '203.0.113.1',
|
|
337
|
+
'multicast' => '224.0.0.1',
|
|
338
|
+
'reserved' => '240.0.0.1'
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
unsafe_addresses.each do |label, address|
|
|
342
|
+
error = assert_resolved_destination_blocked(address, label)
|
|
343
|
+
|
|
344
|
+
assert_match(/#{Regexp.escape(address)}/, error.message)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def test_public_network_policy_blocks_ipv6_unsafe_matrix
|
|
349
|
+
require_public_network_policy!
|
|
350
|
+
omit('IPv6 sockets are not available on this platform') unless Socket.const_defined?(:AF_INET6)
|
|
351
|
+
|
|
352
|
+
unsafe_addresses = {
|
|
353
|
+
'unspecified' => '::',
|
|
354
|
+
'loopback' => '::1',
|
|
355
|
+
'unique_local_fc' => 'fc00::1',
|
|
356
|
+
'unique_local_fd' => 'fd12:3456::1',
|
|
357
|
+
'link_local' => 'fe80::1',
|
|
358
|
+
'site_local' => 'fec0::1',
|
|
359
|
+
'multicast' => 'ff02::1',
|
|
360
|
+
'documentation' => '2001:db8::1',
|
|
361
|
+
'benchmarking' => '2001:2::1',
|
|
362
|
+
'six_to_four' => '2002::1',
|
|
363
|
+
'ipv4_mapped_loopback' => '::ffff:127.0.0.1',
|
|
364
|
+
'ipv4_mapped_private' => '::ffff:10.0.0.1',
|
|
365
|
+
'ipv4_mapped_metadata' => '::ffff:169.254.169.254',
|
|
366
|
+
'ipv4_compatible_private' => '::192.168.0.1'
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
unsafe_addresses.each do |label, address|
|
|
370
|
+
assert_resolved_destination_blocked(address, label)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def test_public_network_policy_blocks_nat64_well_known_embedded_unsafe_ipv4_peers
|
|
375
|
+
require_public_network_policy!
|
|
376
|
+
omit('IPv6 sockets are not available on this platform') unless Socket.const_defined?(:AF_INET6)
|
|
377
|
+
|
|
378
|
+
unsafe_addresses = {
|
|
379
|
+
'nat64_well_known_loopback' => '64:ff9b::7f00:1',
|
|
380
|
+
'nat64_well_known_private' => '64:ff9b::a00:1'
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
unsafe_addresses.each do |label, address|
|
|
384
|
+
error = assert_resolved_destination_blocked(address, label)
|
|
385
|
+
|
|
386
|
+
assert_match(/#{Regexp.escape(address)}/, error.message)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def test_public_network_policy_blocks_nat64_local_use_translation_prefix
|
|
391
|
+
require_public_network_policy!
|
|
392
|
+
omit('IPv6 sockets are not available on this platform') unless Socket.const_defined?(:AF_INET6)
|
|
393
|
+
|
|
394
|
+
address = '64:ff9b:1::203:4'
|
|
395
|
+
error = assert_resolved_destination_blocked(address, 'nat64_local_use')
|
|
396
|
+
|
|
397
|
+
assert_match(/#{Regexp.escape(address)}/, error.message)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def test_public_network_policy_uses_canonical_ipv6_diagnostics
|
|
401
|
+
require_public_network_policy!
|
|
402
|
+
omit('IPv6 sockets are not available on this platform') unless Socket.const_defined?(:AF_INET6)
|
|
403
|
+
|
|
404
|
+
error = assert_resolved_destination_blocked('2001:db8::1', 'documentation_canonical')
|
|
405
|
+
|
|
406
|
+
assert_match(/2001:db8::1/, error.message)
|
|
407
|
+
refute_match(/2001:0db8:0000:0000:0000:0000:0000:0001/, error.message)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def test_public_network_policy_allows_representative_ipv4_public_peer
|
|
411
|
+
assert_public_destination_not_blocked('1.1.1.1')
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def test_public_network_policy_allowed_cidrs_allows_matching_public_peer
|
|
415
|
+
require_public_network_policy!
|
|
416
|
+
|
|
417
|
+
Curl.safe! do |config|
|
|
418
|
+
config.network_policy = :public
|
|
419
|
+
config.allowed_cidrs = ['1.1.1.0/24']
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
easy = Curl::Easy.new('http://1.1.1.1:81/')
|
|
423
|
+
easy.connect_timeout_ms = 50
|
|
424
|
+
easy.timeout_ms = 100
|
|
425
|
+
|
|
426
|
+
perform_without_unsafe_destination(easy, '1.1.1.1')
|
|
427
|
+
ensure
|
|
428
|
+
Curl.__send__(:clear_safe!) if Curl.respond_to?(:clear_safe!, true)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def test_public_network_policy_allowed_cidrs_blocks_public_peer_outside_allowlist
|
|
432
|
+
require_public_network_policy!
|
|
433
|
+
|
|
434
|
+
Curl.safe! do |config|
|
|
435
|
+
config.network_policy = :public
|
|
436
|
+
config.allowed_cidrs = ['8.8.8.0/24']
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
easy = Curl::Easy.new('http://1.1.1.1:81/')
|
|
440
|
+
easy.connect_timeout_ms = 50
|
|
441
|
+
easy.timeout_ms = 100
|
|
442
|
+
|
|
443
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
444
|
+
easy.perform
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
assert_match(/outside allowed CIDR ranges/, error.message)
|
|
448
|
+
assert_match(/1\.1\.1\.1/, error.message)
|
|
449
|
+
ensure
|
|
450
|
+
Curl.__send__(:clear_safe!) if Curl.respond_to?(:clear_safe!, true)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def test_public_network_policy_allowed_cidrs_do_not_permit_unsafe_peer
|
|
454
|
+
require_public_network_policy!
|
|
455
|
+
|
|
456
|
+
Curl.safe! do |config|
|
|
457
|
+
config.network_policy = :public
|
|
458
|
+
config.allowed_cidrs = ['127.0.0.0/8']
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
462
|
+
Curl.get(TestServlet.url)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
assert_match(/unsafe destination/, error.message)
|
|
466
|
+
assert_match(/127\.0\.0\.1/, error.message)
|
|
467
|
+
ensure
|
|
468
|
+
Curl.__send__(:clear_safe!) if Curl.respond_to?(:clear_safe!, true)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def test_public_network_policy_allows_representative_ipv6_public_peer
|
|
472
|
+
omit('IPv6 sockets are not available on this platform') unless Socket.const_defined?(:AF_INET6)
|
|
473
|
+
|
|
474
|
+
assert_public_destination_not_blocked('2606:4700:4700::1111')
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def test_public_network_policy_allowed_cidrs_allows_matching_ipv6_public_peer
|
|
478
|
+
require_public_network_policy!
|
|
479
|
+
omit('IPv6 sockets are not available on this platform') unless Socket.const_defined?(:AF_INET6)
|
|
480
|
+
|
|
481
|
+
Curl.safe! do |config|
|
|
482
|
+
config.network_policy = :public
|
|
483
|
+
config.allowed_cidrs = ['2606:4700:4700::/48']
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
easy = Curl::Easy.new('http://[2606:4700:4700::1111]:81/')
|
|
487
|
+
easy.connect_timeout_ms = 50
|
|
488
|
+
easy.timeout_ms = 100
|
|
489
|
+
|
|
490
|
+
perform_without_unsafe_destination(easy, '2606:4700:4700::1111')
|
|
491
|
+
ensure
|
|
492
|
+
Curl.__send__(:clear_safe!) if Curl.respond_to?(:clear_safe!, true)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def test_safe_bang_public_network_policy_blocks_loopback_destination
|
|
496
|
+
require_public_network_policy!
|
|
497
|
+
|
|
498
|
+
Curl.safe! do |config|
|
|
499
|
+
config.network_policy = :public
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
503
|
+
Curl.get(TestServlet.url)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
assert_match(/127\.0\.0\.1/, error.message)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def test_public_network_policy_disables_explicit_proxy
|
|
510
|
+
require_public_network_policy!
|
|
511
|
+
|
|
512
|
+
Curl.safe! do |config|
|
|
513
|
+
config.network_policy = :public
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
517
|
+
easy.proxy_url = 'http://127.0.0.1:3128'
|
|
518
|
+
easy.proxy_tunnel = true
|
|
519
|
+
|
|
520
|
+
Curl.__send__(:apply_safety!, easy)
|
|
521
|
+
|
|
522
|
+
assert_equal '', easy.proxy_url
|
|
523
|
+
assert_equal false, easy.proxy_tunnel?
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def test_public_network_policy_allows_explicit_proxy_host_allowlist
|
|
527
|
+
require_public_network_policy!
|
|
528
|
+
|
|
529
|
+
Curl.safe! do |config|
|
|
530
|
+
config.network_policy = :public
|
|
531
|
+
config.allowed_proxy_hosts = ['proxy.example:3128']
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
easy = Curl::Easy.new('http://1.1.1.1/')
|
|
535
|
+
easy.proxy_url = 'http://user:pass@PROXY.example.:3128'
|
|
536
|
+
|
|
537
|
+
Curl.__send__(:apply_safety!, easy)
|
|
538
|
+
|
|
539
|
+
assert_equal 'http://user:pass@PROXY.example.:3128', easy.proxy_url
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def test_direct_public_network_policy_ignores_explicit_proxy
|
|
543
|
+
proxy_server = TCPServer.new('127.0.0.1', 0)
|
|
544
|
+
proxy_port = proxy_server.addr[1]
|
|
545
|
+
proxy_connections = []
|
|
546
|
+
proxy_thread = record_proxy_connections(proxy_server, proxy_connections)
|
|
547
|
+
|
|
548
|
+
easy = Curl::Easy.new('http://1.1.1.1:81/')
|
|
549
|
+
require_public_network_policy!(easy)
|
|
550
|
+
easy.proxy_url = "http://127.0.0.1:#{proxy_port}"
|
|
551
|
+
easy.proxy_tunnel = true
|
|
552
|
+
easy.connect_timeout_ms = 50
|
|
553
|
+
easy.timeout_ms = 100
|
|
554
|
+
|
|
555
|
+
perform_without_unsafe_destination(easy, '1.1.1.1')
|
|
556
|
+
sleep 0.05
|
|
557
|
+
|
|
558
|
+
assert_nil easy.unsafe_destination_error
|
|
559
|
+
assert_empty proxy_connections, 'direct public network policy should disable explicit proxies'
|
|
560
|
+
ensure
|
|
561
|
+
proxy_server.close if defined?(proxy_server) && proxy_server
|
|
562
|
+
proxy_thread.join(1) if defined?(proxy_thread) && proxy_thread
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def test_public_network_policy_rejects_explicit_proxy_outside_allowlist
|
|
566
|
+
require_public_network_policy!
|
|
567
|
+
|
|
568
|
+
Curl.safe! do |config|
|
|
569
|
+
config.network_policy = :public
|
|
570
|
+
config.allowed_proxy_hosts = ['proxy.example']
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
easy = Curl::Easy.new('http://1.1.1.1/')
|
|
574
|
+
easy.proxy_url = 'http://blocked.example:3128'
|
|
575
|
+
|
|
576
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
577
|
+
Curl.__send__(:apply_safety!, easy)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
assert_match(/proxy allowlist/, error.message)
|
|
581
|
+
assert_match(/blocked\.example/, error.message)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def test_public_network_policy_proxy_allowlist_still_ignores_environment_proxy_without_explicit_proxy
|
|
585
|
+
require_public_network_policy!
|
|
586
|
+
proxy_server = TCPServer.new('127.0.0.1', 0)
|
|
587
|
+
proxy_port = proxy_server.addr[1]
|
|
588
|
+
proxy_connections = []
|
|
589
|
+
proxy_thread = record_proxy_connections(proxy_server, proxy_connections)
|
|
590
|
+
|
|
591
|
+
with_proxy_environment("http://127.0.0.1:#{proxy_port}") do
|
|
592
|
+
Curl.safe! do |config|
|
|
593
|
+
config.network_policy = :public
|
|
594
|
+
config.allowed_proxy_hosts = ['proxy.example']
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
easy = Curl::Easy.new('http://1.1.1.1:81/')
|
|
598
|
+
easy.connect_timeout_ms = 50
|
|
599
|
+
easy.timeout_ms = 100
|
|
600
|
+
|
|
601
|
+
perform_without_unsafe_destination(easy, '1.1.1.1')
|
|
602
|
+
sleep 0.05
|
|
603
|
+
|
|
604
|
+
assert_equal '', easy.proxy_url
|
|
605
|
+
assert_empty proxy_connections, 'proxy allowlist should not permit environment proxies without an explicit proxy'
|
|
606
|
+
end
|
|
607
|
+
ensure
|
|
608
|
+
proxy_server.close if defined?(proxy_server) && proxy_server
|
|
609
|
+
proxy_thread.join(1) if defined?(proxy_thread) && proxy_thread
|
|
610
|
+
Curl.__send__(:clear_safe!) if Curl.respond_to?(:clear_safe!, true)
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def test_direct_public_network_policy_ignores_proxy_environment_variables
|
|
614
|
+
proxy_server = TCPServer.new('127.0.0.1', 0)
|
|
615
|
+
proxy_port = proxy_server.addr[1]
|
|
616
|
+
proxy_connections = []
|
|
617
|
+
proxy_thread = record_proxy_connections(proxy_server, proxy_connections)
|
|
618
|
+
|
|
619
|
+
with_proxy_environment("http://127.0.0.1:#{proxy_port}") do
|
|
620
|
+
easy = Curl::Easy.new('http://1.1.1.1:81/')
|
|
621
|
+
require_public_network_policy!(easy)
|
|
622
|
+
easy.connect_timeout_ms = 50
|
|
623
|
+
easy.timeout_ms = 100
|
|
624
|
+
|
|
625
|
+
perform_without_unsafe_destination(easy, '1.1.1.1')
|
|
626
|
+
sleep 0.05
|
|
627
|
+
|
|
628
|
+
assert_nil easy.unsafe_destination_error
|
|
629
|
+
assert_empty proxy_connections, 'direct public network policy should disable environment proxies'
|
|
630
|
+
end
|
|
631
|
+
ensure
|
|
632
|
+
proxy_server.close if defined?(proxy_server) && proxy_server
|
|
633
|
+
proxy_thread.join(1) if defined?(proxy_thread) && proxy_thread
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def test_public_network_policy_ignores_proxy_environment_variables
|
|
637
|
+
require_public_network_policy!
|
|
638
|
+
proxy_server = TCPServer.new('127.0.0.1', 0)
|
|
639
|
+
proxy_port = proxy_server.addr[1]
|
|
640
|
+
proxy_connections = []
|
|
641
|
+
proxy_thread = record_proxy_connections(proxy_server, proxy_connections)
|
|
642
|
+
|
|
643
|
+
with_proxy_environment("http://127.0.0.1:#{proxy_port}") do
|
|
644
|
+
Curl.safe! do |config|
|
|
645
|
+
config.network_policy = :public
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
easy = Curl::Easy.new('http://1.1.1.1:81/')
|
|
649
|
+
easy.connect_timeout_ms = 50
|
|
650
|
+
easy.timeout_ms = 100
|
|
651
|
+
|
|
652
|
+
perform_without_unsafe_destination(easy, '1.1.1.1')
|
|
653
|
+
sleep 0.05
|
|
654
|
+
|
|
655
|
+
assert_nil easy.unsafe_destination_error
|
|
656
|
+
assert_empty proxy_connections, 'public network policy should disable environment proxies'
|
|
657
|
+
end
|
|
658
|
+
ensure
|
|
659
|
+
proxy_server.close if defined?(proxy_server) && proxy_server
|
|
660
|
+
proxy_thread.join(1) if defined?(proxy_thread) && proxy_thread
|
|
661
|
+
Curl.__send__(:clear_safe!) if Curl.respond_to?(:clear_safe!, true)
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def test_public_network_policy_rejects_resolve_override
|
|
665
|
+
require_public_network_policy!
|
|
666
|
+
|
|
667
|
+
Curl.safe! do |config|
|
|
668
|
+
config.network_policy = :public
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
672
|
+
easy.resolve = ["example.test:80:127.0.0.1"]
|
|
673
|
+
|
|
674
|
+
assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
675
|
+
Curl.__send__(:apply_safety!, easy)
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def test_public_network_policy_can_allow_resolve_override_before_peer_check
|
|
680
|
+
require_public_network_policy!
|
|
681
|
+
|
|
682
|
+
Curl.safe! do |config|
|
|
683
|
+
config.network_policy = :public
|
|
684
|
+
config.allow_resolve = true
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
688
|
+
easy.resolve = ["example.test:80:127.0.0.1"]
|
|
689
|
+
|
|
690
|
+
Curl.__send__(:apply_safety!, easy)
|
|
691
|
+
|
|
692
|
+
assert_equal ["example.test:80:127.0.0.1"], easy.resolve
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def test_public_network_policy_rejects_connect_to_override
|
|
696
|
+
omit('CURLOPT_CONNECT_TO not supported by this libcurl') unless Curl.const_defined?(:CURLOPT_CONNECT_TO)
|
|
697
|
+
require_public_network_policy!
|
|
698
|
+
|
|
699
|
+
Curl.safe! do |config|
|
|
700
|
+
config.network_policy = :public
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
easy = Curl::Easy.new('http://example.test/')
|
|
704
|
+
easy.connect_to = ['example.test:80:127.0.0.1:80']
|
|
705
|
+
|
|
706
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
707
|
+
Curl.__send__(:apply_safety!, easy)
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
assert_match(/connect_to overrides/, error.message)
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def test_public_network_policy_can_allow_connect_to_override_before_peer_check
|
|
714
|
+
omit('CURLOPT_CONNECT_TO not supported by this libcurl') unless Curl.const_defined?(:CURLOPT_CONNECT_TO)
|
|
715
|
+
require_public_network_policy!
|
|
716
|
+
|
|
717
|
+
Curl.safe! do |config|
|
|
718
|
+
config.network_policy = :public
|
|
719
|
+
config.allow_connect_to = true
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
mapping = ['example.test:80:127.0.0.1:80']
|
|
723
|
+
easy = Curl::Easy.new('http://example.test/')
|
|
724
|
+
easy.connect_to = mapping
|
|
725
|
+
|
|
726
|
+
Curl.__send__(:apply_safety!, easy)
|
|
727
|
+
|
|
728
|
+
assert_equal mapping, easy.connect_to
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
def test_public_network_policy_blocks_connect_to_private_peer_when_override_is_allowed
|
|
732
|
+
omit('CURLOPT_CONNECT_TO not supported by this libcurl') unless Curl.const_defined?(:CURLOPT_CONNECT_TO)
|
|
733
|
+
require_public_network_policy!
|
|
734
|
+
|
|
735
|
+
Curl.safe! do |config|
|
|
736
|
+
config.network_policy = :public
|
|
737
|
+
config.allow_connect_to = true
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
host = 'curb-connect-to.invalid'
|
|
741
|
+
port = TestServlet.port
|
|
742
|
+
easy = Curl::Easy.new("http://#{host}:#{port}#{TestServlet.path}")
|
|
743
|
+
easy.connect_to = ["#{host}:#{port}:127.0.0.1:#{port}"]
|
|
744
|
+
easy.connect_timeout_ms = 50
|
|
745
|
+
|
|
746
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
747
|
+
easy.perform
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
assert_match(/127\.0\.0\.1/, error.message)
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
def test_public_network_policy_rejects_doh_url_override
|
|
754
|
+
omit('CURLOPT_DOH_URL not supported by this libcurl') unless Curl.const_defined?(:CURLOPT_DOH_URL)
|
|
755
|
+
require_public_network_policy!
|
|
756
|
+
|
|
757
|
+
Curl.safe! do |config|
|
|
758
|
+
config.network_policy = :public
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
easy = Curl::Easy.new('http://example.test/')
|
|
762
|
+
easy.doh_url = 'https://dns.example/dns-query'
|
|
763
|
+
|
|
764
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
765
|
+
Curl.__send__(:apply_safety!, easy)
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
assert_match(/DoH URL overrides/, error.message)
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def test_public_network_policy_rejects_dns_servers_override
|
|
772
|
+
omit('CURLOPT_DNS_SERVERS not supported by this libcurl') unless Curl.const_defined?(:CURLOPT_DNS_SERVERS)
|
|
773
|
+
require_public_network_policy!
|
|
774
|
+
|
|
775
|
+
easy = Curl::Easy.new('http://example.test/')
|
|
776
|
+
easy.network_policy = :public
|
|
777
|
+
easy.setopt(Curl::CURLOPT_DNS_SERVERS, '127.0.0.1')
|
|
778
|
+
|
|
779
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
780
|
+
easy.perform
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
assert_match(/DNS server overrides/, error.message)
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def test_public_network_policy_can_allow_doh_url_override_before_peer_check
|
|
787
|
+
omit('CURLOPT_DOH_URL not supported by this libcurl') unless Curl.const_defined?(:CURLOPT_DOH_URL)
|
|
788
|
+
require_public_network_policy!
|
|
789
|
+
|
|
790
|
+
Curl.safe! do |config|
|
|
791
|
+
config.network_policy = :public
|
|
792
|
+
config.allow_doh = true
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
easy = Curl::Easy.new('http://example.test/')
|
|
796
|
+
easy.doh_url = 'https://dns.example/dns-query'
|
|
797
|
+
|
|
798
|
+
Curl.__send__(:apply_safety!, easy)
|
|
799
|
+
|
|
800
|
+
assert_equal 'https://dns.example/dns-query', easy.doh_url
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def test_doh_url_nil_clears_native_doh_url
|
|
804
|
+
omit('CURLOPT_DOH_URL not supported by this libcurl') unless Curl.const_defined?(:CURLOPT_DOH_URL)
|
|
805
|
+
omit('CURLOPT_DOH_SSL_VERIFYPEER not supported by this libcurl') unless Curl.const_defined?(:CURLOPT_DOH_SSL_VERIFYPEER)
|
|
806
|
+
omit('CURLOPT_DOH_SSL_VERIFYHOST not supported by this libcurl') unless Curl.const_defined?(:CURLOPT_DOH_SSL_VERIFYHOST)
|
|
807
|
+
|
|
808
|
+
host = 'curb-doh-clear.invalid'
|
|
809
|
+
with_rebinding_doh_server(host, '127.0.0.1') do |doh_url, doh_state|
|
|
810
|
+
easy = Curl::Easy.new("http://#{host}:81/")
|
|
811
|
+
configure_live_doh_easy(easy, doh_url)
|
|
812
|
+
easy.doh_url = nil
|
|
813
|
+
|
|
814
|
+
begin
|
|
815
|
+
easy.perform
|
|
816
|
+
rescue Curl::Err::CurlError
|
|
817
|
+
nil
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
assert_equal [], doh_a_answers(doh_state)
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
def test_public_network_policy_blocks_resolved_private_peer_when_resolve_override_is_allowed
|
|
825
|
+
require_public_network_policy!
|
|
826
|
+
|
|
827
|
+
Curl.safe! do |config|
|
|
828
|
+
config.network_policy = :public
|
|
829
|
+
config.allow_resolve = true
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
host = 'curb-dns-rebind.invalid'
|
|
833
|
+
easy = Curl::Easy.new("http://#{host}:80/")
|
|
834
|
+
easy.resolve = [resolve_entry(host, 80, '127.0.0.1')]
|
|
835
|
+
easy.connect_timeout_ms = 50
|
|
836
|
+
|
|
837
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
838
|
+
easy.perform
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
assert_match(/127\.0\.0\.1/, error.message)
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
def test_public_network_policy_rechecks_same_host_when_resolved_address_changes
|
|
845
|
+
require_public_network_policy!
|
|
846
|
+
|
|
847
|
+
Curl.safe! do |config|
|
|
848
|
+
config.network_policy = :public
|
|
849
|
+
config.allow_resolve = true
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
host = 'curb-dns-rebind.invalid'
|
|
853
|
+
easy = Curl::Easy.new("http://#{host}:81/")
|
|
854
|
+
easy.connect_timeout_ms = 50
|
|
855
|
+
easy.timeout_ms = 100
|
|
856
|
+
|
|
857
|
+
easy.resolve = [resolve_entry(host, 81, '1.1.1.1')]
|
|
858
|
+
perform_without_unsafe_destination(easy, '1.1.1.1')
|
|
859
|
+
assert_nil easy.unsafe_destination_error
|
|
860
|
+
|
|
861
|
+
easy.resolve = [resolve_entry(host, 81, '127.0.0.1')]
|
|
862
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
863
|
+
easy.perform
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
assert_match(/127\.0\.0\.1/, error.message)
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def test_live_public_redirect_to_private_peer_is_blocked_by_resolved_peer_policy
|
|
870
|
+
live_network_required!
|
|
871
|
+
require_public_network_policy!
|
|
872
|
+
|
|
873
|
+
with_private_target_server('/private-redirect-target') do |port, hits|
|
|
874
|
+
private_host = 'curb-private-redirect.invalid'
|
|
875
|
+
private_url = "http://#{private_host}:#{port}/private-redirect-target"
|
|
876
|
+
|
|
877
|
+
Curl.safe! do |config|
|
|
878
|
+
config.network_policy = :public
|
|
879
|
+
config.allow_resolve = true
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
easy = Curl::Easy.new(live_redirect_url_for(private_url))
|
|
883
|
+
easy.follow_location = true
|
|
884
|
+
easy.resolve = [resolve_entry(private_host, port, '127.0.0.1')]
|
|
885
|
+
easy.connect_timeout_ms = live_connect_timeout_ms
|
|
886
|
+
easy.timeout_ms = live_timeout_ms
|
|
887
|
+
|
|
888
|
+
error = nil
|
|
889
|
+
begin
|
|
890
|
+
easy.perform
|
|
891
|
+
rescue Curl::Err::UnsafeDestinationError => e
|
|
892
|
+
error = e
|
|
893
|
+
rescue Curl::Err::CurlError => e
|
|
894
|
+
if easy.respond_to?(:redirect_count) && easy.redirect_count.to_i == 0
|
|
895
|
+
omit("live redirect endpoint unavailable before redirect: #{e.class}: #{e.message}")
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
flunk("expected redirected private peer to be reported unsafe, got #{e.class}: #{e.message}")
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
assert_equal 0, hits[:target], 'private redirect target should not receive a request'
|
|
902
|
+
if error.nil? && easy.respond_to?(:redirect_count) && easy.redirect_count.to_i == 0
|
|
903
|
+
omit("live redirect endpoint did not issue a redirect, response code #{easy.response_code}")
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
assert_not_nil error, 'expected redirected private peer to be blocked'
|
|
907
|
+
assert_match(/public network policy/, error.message)
|
|
908
|
+
assert_match(/127\.0\.0\.1/, error.message)
|
|
909
|
+
end
|
|
910
|
+
ensure
|
|
911
|
+
Curl.__send__(:clear_safe!) if Curl.respond_to?(:clear_safe!, true)
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
def test_public_network_policy_blocks_private_peer_after_live_dns_answer_changes
|
|
915
|
+
omit('live DNS rebinding test requires CURLOPT_DOH_URL') unless Curl.const_defined?(:CURLOPT_DOH_URL)
|
|
916
|
+
omit('live DNS rebinding test requires CURLOPT_DOH_SSL_VERIFYPEER') unless Curl.const_defined?(:CURLOPT_DOH_SSL_VERIFYPEER)
|
|
917
|
+
omit('live DNS rebinding test requires CURLOPT_DOH_SSL_VERIFYHOST') unless Curl.const_defined?(:CURLOPT_DOH_SSL_VERIFYHOST)
|
|
918
|
+
require_public_network_policy!
|
|
919
|
+
|
|
920
|
+
host = 'curb-doh-rebind.invalid'
|
|
921
|
+
with_rebinding_doh_server(host, '1.1.1.1') do |doh_url, doh_state|
|
|
922
|
+
Curl.safe! do |config|
|
|
923
|
+
config.network_policy = :public
|
|
924
|
+
config.allow_doh = true
|
|
925
|
+
config.allowed_cidrs = ['8.8.8.0/24']
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
first = Curl::Easy.new("http://#{host}/dns-rebind-target")
|
|
929
|
+
configure_live_doh_easy(first, doh_url)
|
|
930
|
+
|
|
931
|
+
first_error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
932
|
+
first.perform
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
assert_include doh_a_answers(doh_state), '1.1.1.1'
|
|
936
|
+
assert_match(/outside allowed CIDR ranges/, first_error.message)
|
|
937
|
+
assert_match(/1\.1\.1\.1/, first_error.message)
|
|
938
|
+
|
|
939
|
+
set_doh_answer(doh_state, '127.0.0.1')
|
|
940
|
+
second = Curl::Easy.new("http://#{host}/dns-rebind-target")
|
|
941
|
+
configure_live_doh_easy(second, doh_url)
|
|
942
|
+
|
|
943
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
944
|
+
second.perform
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
assert_include doh_a_answers(doh_state), '127.0.0.1'
|
|
948
|
+
assert_match(/public network policy/, error.message)
|
|
949
|
+
assert_match(/127\.0\.0\.1/, error.message)
|
|
950
|
+
end
|
|
951
|
+
ensure
|
|
952
|
+
Curl.__send__(:clear_safe!) if Curl.respond_to?(:clear_safe!, true)
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
def test_public_network_policy_rejects_unix_socket_override
|
|
956
|
+
omit('CURLOPT_UNIX_SOCKET_PATH not supported by this libcurl') unless Curl.const_defined?(:CURLOPT_UNIX_SOCKET_PATH)
|
|
957
|
+
require_public_network_policy!
|
|
958
|
+
|
|
959
|
+
Curl.safe! do |config|
|
|
960
|
+
config.network_policy = :public
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
964
|
+
easy.setopt(Curl::CURLOPT_UNIX_SOCKET_PATH, '/tmp/curb-test.sock')
|
|
965
|
+
|
|
966
|
+
assert_raise(Curl::Err::UnsafeDestinationError) do
|
|
967
|
+
Curl.__send__(:apply_safety!, easy)
|
|
968
|
+
end
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
def test_public_network_policy_can_allow_unix_socket_override
|
|
972
|
+
omit('CURLOPT_UNIX_SOCKET_PATH not supported by this libcurl') unless Curl.const_defined?(:CURLOPT_UNIX_SOCKET_PATH)
|
|
973
|
+
omit('Unix socket transfers are not supported on Windows runners') if WINDOWS
|
|
974
|
+
omit('Unix domain sockets are not available on this platform') unless defined?(UNIXServer)
|
|
975
|
+
require_public_network_policy!
|
|
976
|
+
require 'tmpdir'
|
|
977
|
+
|
|
978
|
+
socket_path = File.join(Dir.tmpdir, "curb-test-#{$$}-#{object_id}.sock")
|
|
979
|
+
FileUtils.rm_f(socket_path)
|
|
980
|
+
server = UNIXServer.new(socket_path)
|
|
981
|
+
server_thread = Thread.new do
|
|
982
|
+
socket = server.accept
|
|
983
|
+
begin
|
|
984
|
+
socket.readpartial(4096)
|
|
985
|
+
rescue EOFError
|
|
986
|
+
end
|
|
987
|
+
socket.write("HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nunix socket")
|
|
988
|
+
socket.close
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
Curl.safe! do |config|
|
|
992
|
+
config.network_policy = :public
|
|
993
|
+
config.allow_unix_socket = true
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
easy = Curl::Easy.new('http://unix.test/')
|
|
997
|
+
easy.setopt(Curl::CURLOPT_UNIX_SOCKET_PATH, socket_path)
|
|
998
|
+
easy.connect_timeout_ms = 500
|
|
999
|
+
easy.timeout_ms = 1_000
|
|
1000
|
+
easy.perform
|
|
1001
|
+
|
|
1002
|
+
assert_equal 200, easy.response_code
|
|
1003
|
+
assert_equal 'unix socket', easy.body_str
|
|
1004
|
+
ensure
|
|
1005
|
+
server.close if defined?(server) && server && !server.closed?
|
|
1006
|
+
if defined?(server_thread) && server_thread
|
|
1007
|
+
server_thread.join(1)
|
|
1008
|
+
server_thread.kill if server_thread.alive?
|
|
1009
|
+
end
|
|
1010
|
+
FileUtils.rm_f(socket_path) if defined?(socket_path) && socket_path
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
def test_multi_records_public_network_policy_block
|
|
1014
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
1015
|
+
require_public_network_policy!(easy)
|
|
1016
|
+
multi = Curl::Multi.new
|
|
1017
|
+
|
|
1018
|
+
multi.add(easy)
|
|
1019
|
+
multi.perform
|
|
1020
|
+
|
|
1021
|
+
assert_match(/127\.0\.0\.1/, easy.unsafe_destination_error)
|
|
1022
|
+
ensure
|
|
1023
|
+
multi.close if defined?(multi) && multi
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
def test_multi_reinstalls_public_policy_callbacks_when_safe_enabled_after_add
|
|
1027
|
+
require_public_network_policy!
|
|
1028
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
1029
|
+
multi = Curl::Multi.new
|
|
1030
|
+
|
|
1031
|
+
multi.add(easy)
|
|
1032
|
+
Curl.safe! do |config|
|
|
1033
|
+
config.network_policy = :public
|
|
1034
|
+
end
|
|
1035
|
+
multi.perform
|
|
1036
|
+
|
|
1037
|
+
assert_match(/127\.0\.0\.1/, easy.unsafe_destination_error)
|
|
1038
|
+
assert_equal 7, easy.last_result
|
|
1039
|
+
ensure
|
|
1040
|
+
multi.close if defined?(multi) && multi
|
|
1041
|
+
end
|
|
1042
|
+
|
|
1043
|
+
def test_multi_reinstalls_public_policy_callbacks_when_easy_policy_enabled_after_add
|
|
1044
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
1045
|
+
multi = Curl::Multi.new
|
|
1046
|
+
|
|
1047
|
+
multi.add(easy)
|
|
1048
|
+
require_public_network_policy!(easy)
|
|
1049
|
+
multi.perform
|
|
1050
|
+
|
|
1051
|
+
assert_match(/127\.0\.0\.1/, easy.unsafe_destination_error)
|
|
1052
|
+
assert_equal 7, easy.last_result
|
|
1053
|
+
ensure
|
|
1054
|
+
multi.close if defined?(multi) && multi
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
def test_multi_reinstalls_host_allowlist_callback_when_allowed_hosts_set_after_add
|
|
1058
|
+
omit('redirect-aware host allowlists require CURLOPT_PREREQFUNCTION') unless Curl.const_defined?(:CURLOPT_PREREQFUNCTION)
|
|
1059
|
+
|
|
1060
|
+
blocked_host = 'curb-late-blocked.test'
|
|
1061
|
+
easy = Curl::Easy.new("http://#{blocked_host}:#{TestServlet.port}#{TestServlet.path}")
|
|
1062
|
+
easy.resolve = [resolve_entry(blocked_host, TestServlet.port, '127.0.0.1')]
|
|
1063
|
+
multi = Curl::Multi.new
|
|
1064
|
+
|
|
1065
|
+
multi.add(easy)
|
|
1066
|
+
easy.allowed_hosts = ['curb-late-allowed.test']
|
|
1067
|
+
multi.perform
|
|
1068
|
+
|
|
1069
|
+
assert_match(/host allowlist/, easy.unsafe_destination_error)
|
|
1070
|
+
assert_match(/#{Regexp.escape(blocked_host)}/, easy.unsafe_destination_error)
|
|
1071
|
+
assert_not_equal 0, easy.last_result
|
|
1072
|
+
ensure
|
|
1073
|
+
multi.close if defined?(multi) && multi
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
def test_multi_safety_refresh_does_not_duplicate_slist_backed_options
|
|
1077
|
+
with_header_recording_server do |url, request_headers|
|
|
1078
|
+
easy = Curl::Easy.new(url)
|
|
1079
|
+
easy.headers['X-Test'] = '1'
|
|
1080
|
+
multi = Curl::Multi.new
|
|
1081
|
+
|
|
1082
|
+
multi.add(easy)
|
|
1083
|
+
Curl.safe!
|
|
1084
|
+
multi.perform
|
|
1085
|
+
|
|
1086
|
+
assert_equal [['1']], request_headers
|
|
1087
|
+
ensure
|
|
1088
|
+
multi.close if defined?(multi) && multi
|
|
1089
|
+
end
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
def test_multi_does_not_reuse_pre_policy_loopback_connection
|
|
1093
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
1094
|
+
multi = Curl::Multi.new
|
|
1095
|
+
|
|
1096
|
+
multi.add(easy)
|
|
1097
|
+
multi.perform
|
|
1098
|
+
assert_equal 200, easy.response_code
|
|
1099
|
+
assert_match(/GET/, easy.body_str)
|
|
1100
|
+
|
|
1101
|
+
require_public_network_policy!(easy)
|
|
1102
|
+
multi.add(easy)
|
|
1103
|
+
multi.perform
|
|
1104
|
+
|
|
1105
|
+
assert_match(/127\.0\.0\.1/, easy.unsafe_destination_error)
|
|
1106
|
+
assert_equal 7, easy.last_result
|
|
1107
|
+
ensure
|
|
1108
|
+
multi.close if defined?(multi) && multi
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
def test_multi_isolates_allowed_and_blocked_public_policy_handles
|
|
1112
|
+
allowed = Curl::Easy.new(TestServlet.url)
|
|
1113
|
+
blocked = Curl::Easy.new(TestServlet.url)
|
|
1114
|
+
require_public_network_policy!(blocked)
|
|
1115
|
+
multi = Curl::Multi.new
|
|
1116
|
+
events = []
|
|
1117
|
+
|
|
1118
|
+
allowed.on_complete { events << [:allowed, :complete] }
|
|
1119
|
+
allowed.on_success do |easy|
|
|
1120
|
+
events << [:allowed, :success, easy.response_code]
|
|
1121
|
+
end
|
|
1122
|
+
allowed.on_failure do |_easy, error|
|
|
1123
|
+
events << [:allowed, :failure, error.first]
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
blocked.on_complete { events << [:blocked, :complete] }
|
|
1127
|
+
blocked.on_success do |easy|
|
|
1128
|
+
events << [:blocked, :success, easy.response_code]
|
|
1129
|
+
end
|
|
1130
|
+
blocked.on_failure do |_easy, error|
|
|
1131
|
+
events << [:blocked, :failure, error.first]
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
multi.add(allowed)
|
|
1135
|
+
multi.add(blocked)
|
|
1136
|
+
multi.perform
|
|
1137
|
+
|
|
1138
|
+
assert_equal 200, allowed.response_code
|
|
1139
|
+
assert_match(/GET/, allowed.body_str)
|
|
1140
|
+
assert_nil allowed.unsafe_destination_error
|
|
1141
|
+
assert_match(/127\.0\.0\.1/, blocked.unsafe_destination_error)
|
|
1142
|
+
assert_equal 7, blocked.last_result
|
|
1143
|
+
|
|
1144
|
+
assert_equal [:allowed, :complete], events.find { |event| event[0] == :allowed && event[1] == :complete }
|
|
1145
|
+
assert_equal [:allowed, :success, 200], events.find { |event| event[0] == :allowed && event[1] == :success }
|
|
1146
|
+
assert_nil events.find { |event| event[0] == :allowed && event[1] == :failure }
|
|
1147
|
+
assert_equal [:blocked, :complete], events.find { |event| event[0] == :blocked && event[1] == :complete }
|
|
1148
|
+
assert_equal [:blocked, :failure, Curl::Err::ConnectionFailedError],
|
|
1149
|
+
events.find { |event| event[0] == :blocked && event[1] == :failure }
|
|
1150
|
+
assert_nil events.find { |event| event[0] == :blocked && event[1] == :success }
|
|
1151
|
+
ensure
|
|
1152
|
+
multi.close if defined?(multi) && multi
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
private
|
|
1156
|
+
|
|
1157
|
+
def require_public_network_policy!(easy = Curl::Easy.new)
|
|
1158
|
+
easy.network_policy = :public
|
|
1159
|
+
easy
|
|
1160
|
+
rescue NotImplementedError
|
|
1161
|
+
omit('network_policy=:public requires CURLOPT_OPENSOCKETFUNCTION support')
|
|
1162
|
+
end
|
|
1163
|
+
|
|
1164
|
+
def assert_resolved_destination_blocked(address, label)
|
|
1165
|
+
host = "curb-#{label.tr('_', '-')}.invalid"
|
|
1166
|
+
easy = Curl::Easy.new("http://#{host}:80/")
|
|
1167
|
+
require_public_network_policy!(easy)
|
|
1168
|
+
easy.resolve = [resolve_entry(host, 80, address)]
|
|
1169
|
+
easy.connect_timeout_ms = 50
|
|
1170
|
+
|
|
1171
|
+
error = assert_raise(Curl::Err::UnsafeDestinationError, "expected #{address} to be blocked") do
|
|
1172
|
+
easy.perform
|
|
1173
|
+
end
|
|
1174
|
+
|
|
1175
|
+
assert_match(/public network policy/, error.message)
|
|
1176
|
+
assert_match(/public network policy/, easy.unsafe_destination_error)
|
|
1177
|
+
error
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
def resolve_entry(host, port, address)
|
|
1181
|
+
resolved_address = address.include?(':') ? "[#{address}]" : address
|
|
1182
|
+
"#{host}:#{port}:#{resolved_address}"
|
|
1183
|
+
end
|
|
1184
|
+
|
|
1185
|
+
def assert_public_destination_not_blocked(address)
|
|
1186
|
+
require_public_network_policy!
|
|
1187
|
+
|
|
1188
|
+
Curl.safe! do |config|
|
|
1189
|
+
config.network_policy = :public
|
|
1190
|
+
end
|
|
1191
|
+
|
|
1192
|
+
host = address.include?(':') ? "[#{address}]" : address
|
|
1193
|
+
easy = Curl::Easy.new("http://#{host}:81/")
|
|
1194
|
+
easy.connect_timeout_ms = 50
|
|
1195
|
+
easy.timeout_ms = 100
|
|
1196
|
+
|
|
1197
|
+
perform_without_unsafe_destination(easy, address)
|
|
1198
|
+
rescue Curl::Err::UnsafeDestinationError => e
|
|
1199
|
+
flunk("expected #{address} to be treated as public, got #{e.class}: #{e.message}")
|
|
1200
|
+
ensure
|
|
1201
|
+
Curl.__send__(:clear_safe!) if Curl.respond_to?(:clear_safe!, true)
|
|
1202
|
+
end
|
|
1203
|
+
|
|
1204
|
+
def perform_without_unsafe_destination(easy, address)
|
|
1205
|
+
easy.perform
|
|
1206
|
+
rescue Curl::Err::UnsafeDestinationError => e
|
|
1207
|
+
flunk("expected #{address} to be treated as public, got #{e.class}: #{e.message}")
|
|
1208
|
+
rescue Curl::Err::CurlError
|
|
1209
|
+
assert_nil easy.unsafe_destination_error
|
|
1210
|
+
end
|
|
1211
|
+
|
|
1212
|
+
def record_proxy_connections(server, connections)
|
|
1213
|
+
Thread.new do
|
|
1214
|
+
loop do
|
|
1215
|
+
readable = IO.select([server], nil, nil, 0.05)
|
|
1216
|
+
next unless readable
|
|
1217
|
+
|
|
1218
|
+
socket = server.accept_nonblock(exception: false)
|
|
1219
|
+
next unless socket.is_a?(TCPSocket)
|
|
1220
|
+
|
|
1221
|
+
connections << true
|
|
1222
|
+
socket.close
|
|
1223
|
+
rescue IOError, Errno::EBADF, Errno::ENOTSOCK
|
|
1224
|
+
break
|
|
1225
|
+
end
|
|
1226
|
+
end
|
|
1227
|
+
end
|
|
1228
|
+
|
|
1229
|
+
def with_proxy_environment(proxy_url)
|
|
1230
|
+
keys = %w[http_proxy HTTP_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY]
|
|
1231
|
+
old_values = keys.each_with_object({}) { |key, values| values[key] = ENV[key] }
|
|
1232
|
+
|
|
1233
|
+
ENV['http_proxy'] = proxy_url
|
|
1234
|
+
ENV['HTTP_PROXY'] = proxy_url
|
|
1235
|
+
ENV['all_proxy'] = proxy_url
|
|
1236
|
+
ENV['ALL_PROXY'] = proxy_url
|
|
1237
|
+
ENV.delete('no_proxy')
|
|
1238
|
+
ENV.delete('NO_PROXY')
|
|
1239
|
+
|
|
1240
|
+
yield
|
|
1241
|
+
ensure
|
|
1242
|
+
old_values.each do |key, value|
|
|
1243
|
+
if value.nil?
|
|
1244
|
+
ENV.delete(key)
|
|
1245
|
+
else
|
|
1246
|
+
ENV[key] = value
|
|
1247
|
+
end
|
|
1248
|
+
end
|
|
1249
|
+
end
|
|
1250
|
+
|
|
1251
|
+
def live_network_required!
|
|
1252
|
+
omit('set CURB_LIVE_NETWORK_TESTS=1 to run live network security tests') unless ENV['CURB_LIVE_NETWORK_TESTS'] == '1'
|
|
1253
|
+
end
|
|
1254
|
+
|
|
1255
|
+
def live_redirect_url_for(target_url)
|
|
1256
|
+
template = ENV['CURB_LIVE_REDIRECT_URL_TEMPLATE'] || 'https://httpbin.org/redirect-to?url={url}'
|
|
1257
|
+
omit('CURB_LIVE_REDIRECT_URL_TEMPLATE must include {url}') unless template.include?('{url}')
|
|
1258
|
+
|
|
1259
|
+
template.gsub('{url}', URI.encode_www_form_component(target_url))
|
|
1260
|
+
end
|
|
1261
|
+
|
|
1262
|
+
def live_connect_timeout_ms
|
|
1263
|
+
[(ENV['CURB_LIVE_CONNECT_TIMEOUT_MS'] || 1_000).to_i, 1].max
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
def live_timeout_ms
|
|
1267
|
+
[(ENV['CURB_LIVE_TIMEOUT_MS'] || 5_000).to_i, 1].max
|
|
1268
|
+
end
|
|
1269
|
+
|
|
1270
|
+
def configure_live_doh_easy(easy, doh_url)
|
|
1271
|
+
easy.resolve_mode = :ipv4
|
|
1272
|
+
easy.dns_cache_timeout = 0
|
|
1273
|
+
easy.connect_timeout_ms = live_connect_timeout_ms
|
|
1274
|
+
easy.timeout_ms = live_timeout_ms
|
|
1275
|
+
easy.doh_url = doh_url
|
|
1276
|
+
easy.setopt(Curl::CURLOPT_DOH_SSL_VERIFYPEER, 0)
|
|
1277
|
+
easy.setopt(Curl::CURLOPT_DOH_SSL_VERIFYHOST, 0)
|
|
1278
|
+
end
|
|
1279
|
+
|
|
1280
|
+
def with_rebinding_doh_server(host, initial_address)
|
|
1281
|
+
require 'webrick/https'
|
|
1282
|
+
require 'openssl'
|
|
1283
|
+
|
|
1284
|
+
omit('DoH rebinding fixture requires OpenSSL') unless defined?(OpenSSL::PKey::RSA)
|
|
1285
|
+
|
|
1286
|
+
port_socket = TCPServer.new('127.0.0.1', 0)
|
|
1287
|
+
port = port_socket.addr[1]
|
|
1288
|
+
port_socket.close
|
|
1289
|
+
|
|
1290
|
+
expected_host = host.downcase
|
|
1291
|
+
state = {
|
|
1292
|
+
:address => initial_address,
|
|
1293
|
+
:answers => [],
|
|
1294
|
+
:queries => [],
|
|
1295
|
+
:mutex => Mutex.new
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
cert, key = doh_certificate
|
|
1299
|
+
server = WEBrick::HTTPServer.new(:BindAddress => '127.0.0.1',
|
|
1300
|
+
:Port => port,
|
|
1301
|
+
:Logger => WEBRICK_TEST_LOG,
|
|
1302
|
+
:AccessLog => [],
|
|
1303
|
+
:SSLEnable => true,
|
|
1304
|
+
:SSLCertificate => cert,
|
|
1305
|
+
:SSLPrivateKey => key)
|
|
1306
|
+
server.mount_proc('/dns-query') do |req, res|
|
|
1307
|
+
res['Content-Type'] = 'application/dns-message'
|
|
1308
|
+
res.body = dns_response(req.body.b, expected_host, state)
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1311
|
+
thread = Thread.new(server) { |srv| srv.start }
|
|
1312
|
+
wait_for_server_ready(port, thread: thread)
|
|
1313
|
+
|
|
1314
|
+
yield "https://127.0.0.1:#{port}/dns-query", state
|
|
1315
|
+
ensure
|
|
1316
|
+
server.shutdown if defined?(server) && server
|
|
1317
|
+
thread.join(server_startup_timeout) if defined?(thread) && thread
|
|
1318
|
+
thread.kill if defined?(thread) && thread&.alive?
|
|
1319
|
+
end
|
|
1320
|
+
|
|
1321
|
+
def set_doh_answer(state, address)
|
|
1322
|
+
state[:mutex].synchronize { state[:address] = address }
|
|
1323
|
+
end
|
|
1324
|
+
|
|
1325
|
+
def doh_a_answers(state)
|
|
1326
|
+
state[:mutex].synchronize { state[:answers].dup }
|
|
1327
|
+
end
|
|
1328
|
+
|
|
1329
|
+
def doh_certificate
|
|
1330
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
|
1331
|
+
cert = OpenSSL::X509::Certificate.new
|
|
1332
|
+
cert.version = 2
|
|
1333
|
+
cert.serial = 1
|
|
1334
|
+
cert.subject = OpenSSL::X509::Name.parse('/CN=127.0.0.1')
|
|
1335
|
+
cert.issuer = cert.subject
|
|
1336
|
+
cert.public_key = key.public_key
|
|
1337
|
+
cert.not_before = Time.now - 60
|
|
1338
|
+
cert.not_after = Time.now + 3600
|
|
1339
|
+
|
|
1340
|
+
extensions = OpenSSL::X509::ExtensionFactory.new
|
|
1341
|
+
extensions.subject_certificate = cert
|
|
1342
|
+
extensions.issuer_certificate = cert
|
|
1343
|
+
cert.add_extension(extensions.create_extension('basicConstraints', 'CA:FALSE', true))
|
|
1344
|
+
cert.add_extension(extensions.create_extension('subjectAltName', 'IP:127.0.0.1', false))
|
|
1345
|
+
cert.sign(key, OpenSSL::Digest::SHA256.new)
|
|
1346
|
+
|
|
1347
|
+
[cert, key]
|
|
1348
|
+
end
|
|
1349
|
+
|
|
1350
|
+
def dns_response(packet, expected_host, state)
|
|
1351
|
+
question = dns_question(packet)
|
|
1352
|
+
return nil unless question
|
|
1353
|
+
|
|
1354
|
+
name, qtype, qclass, question_bytes = question
|
|
1355
|
+
answer_address = nil
|
|
1356
|
+
state[:mutex].synchronize do
|
|
1357
|
+
state[:queries] << [name, qtype, qclass]
|
|
1358
|
+
if name == expected_host && qtype == 1 && qclass == 1
|
|
1359
|
+
answer_address = state[:address]
|
|
1360
|
+
state[:answers] << answer_address
|
|
1361
|
+
end
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
answer = answer_address && dns_a_answer(answer_address)
|
|
1365
|
+
[packet.unpack1('n'), 0x8180, 1, answer ? 1 : 0, 0, 0].pack('nnnnnn') +
|
|
1366
|
+
question_bytes +
|
|
1367
|
+
(answer || ''.b)
|
|
1368
|
+
end
|
|
1369
|
+
|
|
1370
|
+
def dns_question(packet)
|
|
1371
|
+
return nil if packet.bytesize < 12
|
|
1372
|
+
|
|
1373
|
+
offset = 12
|
|
1374
|
+
labels = []
|
|
1375
|
+
loop do
|
|
1376
|
+
length = packet.getbyte(offset)
|
|
1377
|
+
return nil unless length
|
|
1378
|
+
|
|
1379
|
+
offset += 1
|
|
1380
|
+
break if length.zero?
|
|
1381
|
+
return nil if length & 0xc0 != 0
|
|
1382
|
+
return nil if offset + length > packet.bytesize
|
|
1383
|
+
|
|
1384
|
+
labels << packet.byteslice(offset, length)
|
|
1385
|
+
offset += length
|
|
1386
|
+
end
|
|
1387
|
+
|
|
1388
|
+
return nil if offset + 4 > packet.bytesize
|
|
1389
|
+
|
|
1390
|
+
qtype, qclass = packet.byteslice(offset, 4).unpack('nn')
|
|
1391
|
+
[labels.join('.').downcase, qtype, qclass, packet.byteslice(12, offset + 4 - 12)]
|
|
1392
|
+
end
|
|
1393
|
+
|
|
1394
|
+
def dns_a_answer(address)
|
|
1395
|
+
octets = address.split('.').map { |octet| Integer(octet, 10) }
|
|
1396
|
+
return nil unless octets.length == 4 && octets.all? { |octet| octet.between?(0, 255) }
|
|
1397
|
+
|
|
1398
|
+
[0xc00c, 1, 1, 0, 4].pack('nnnNn') + octets.pack('C4')
|
|
1399
|
+
end
|
|
1400
|
+
|
|
1401
|
+
def with_private_target_server(path)
|
|
1402
|
+
port_socket = TCPServer.new('127.0.0.1', 0)
|
|
1403
|
+
port = port_socket.addr[1]
|
|
1404
|
+
port_socket.close
|
|
1405
|
+
|
|
1406
|
+
hits = Hash.new(0)
|
|
1407
|
+
server = WEBrick::HTTPServer.new(:BindAddress => '127.0.0.1', :Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
|
|
1408
|
+
server.mount_proc(path) do |_req, res|
|
|
1409
|
+
hits[:target] += 1
|
|
1410
|
+
res['Content-Type'] = 'text/plain'
|
|
1411
|
+
res.body = 'private target'
|
|
1412
|
+
end
|
|
1413
|
+
|
|
1414
|
+
server_thread = Thread.new(server) { |srv| srv.start }
|
|
1415
|
+
wait_for_server_ready(port, thread: server_thread)
|
|
1416
|
+
|
|
1417
|
+
yield port, hits
|
|
1418
|
+
ensure
|
|
1419
|
+
server.shutdown if defined?(server) && server
|
|
1420
|
+
server_thread.join(server_startup_timeout) if defined?(server_thread) && server_thread
|
|
1421
|
+
server_thread.kill if defined?(server_thread) && server_thread&.alive?
|
|
1422
|
+
end
|
|
1423
|
+
|
|
1424
|
+
def with_host_redirect_server(_allowed_host, target_host)
|
|
1425
|
+
port_socket = TCPServer.new('127.0.0.1', 0)
|
|
1426
|
+
port = port_socket.addr[1]
|
|
1427
|
+
port_socket.close
|
|
1428
|
+
|
|
1429
|
+
hits = Hash.new(0)
|
|
1430
|
+
server = WEBrick::HTTPServer.new(:BindAddress => '127.0.0.1', :Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
|
|
1431
|
+
server.mount_proc('/redirect-to-target') do |_req, res|
|
|
1432
|
+
hits[:redirect] += 1
|
|
1433
|
+
res.status = 302
|
|
1434
|
+
res['Location'] = "http://#{target_host}:#{port}/target"
|
|
1435
|
+
res.body = 'redirect'
|
|
1436
|
+
end
|
|
1437
|
+
server.mount_proc('/target') do |_req, res|
|
|
1438
|
+
hits[:target] += 1
|
|
1439
|
+
res['Content-Type'] = 'text/plain'
|
|
1440
|
+
res.body = 'target'
|
|
1441
|
+
end
|
|
1442
|
+
|
|
1443
|
+
server_thread = Thread.new(server) { |srv| srv.start }
|
|
1444
|
+
wait_for_server_ready(port, thread: server_thread)
|
|
1445
|
+
|
|
1446
|
+
yield port, hits
|
|
1447
|
+
ensure
|
|
1448
|
+
server.shutdown if defined?(server) && server
|
|
1449
|
+
server_thread.join(server_startup_timeout) if defined?(server_thread) && server_thread
|
|
1450
|
+
server_thread.kill if defined?(server_thread) && server_thread&.alive?
|
|
1451
|
+
end
|
|
1452
|
+
|
|
1453
|
+
def with_header_recording_server
|
|
1454
|
+
port_socket = TCPServer.new('127.0.0.1', 0)
|
|
1455
|
+
port = port_socket.addr[1]
|
|
1456
|
+
port_socket.close
|
|
1457
|
+
|
|
1458
|
+
request_headers = []
|
|
1459
|
+
server = WEBrick::HTTPServer.new(:BindAddress => '127.0.0.1', :Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
|
|
1460
|
+
server.mount_proc('/headers') do |req, res|
|
|
1461
|
+
request_headers << req.header.fetch('x-test', []).dup
|
|
1462
|
+
res['Content-Type'] = 'text/plain'
|
|
1463
|
+
res.body = 'ok'
|
|
1464
|
+
end
|
|
1465
|
+
|
|
1466
|
+
server_thread = Thread.new(server) { |srv| srv.start }
|
|
1467
|
+
wait_for_server_ready(port, thread: server_thread)
|
|
1468
|
+
|
|
1469
|
+
yield "http://127.0.0.1:#{port}/headers", request_headers
|
|
1470
|
+
ensure
|
|
1471
|
+
server.shutdown if defined?(server) && server
|
|
1472
|
+
server_thread.join(server_startup_timeout) if defined?(server_thread) && server_thread
|
|
1473
|
+
server_thread.kill if defined?(server_thread) && server_thread&.alive?
|
|
1474
|
+
end
|
|
1475
|
+
end
|