curb 1.3.4 → 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.
@@ -1,3 +1,5 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+
1
3
  class TestCurbCurlProtocols < Test::Unit::TestCase
2
4
  include TestServerMethods
3
5
 
@@ -8,6 +10,11 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
8
10
  server_setup
9
11
  end
10
12
 
13
+ def teardown
14
+ Curl.__send__(:clear_safe!) if Curl.respond_to?(:clear_safe!, true)
15
+ super
16
+ end
17
+
11
18
  def test_protocol_allowed
12
19
  @easy.set :url, "http://127.0.0.1:9129/this_file_does_not_exist.html"
13
20
  @easy.perform
@@ -34,4 +41,348 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
34
41
  @easy.perform
35
42
  end
36
43
  end
44
+
45
+ def test_protocols_str_setopt_supported
46
+ omit('CURLOPT_PROTOCOLS_STR not supported by this libcurl') unless Curl.const_defined?(:CURLOPT_PROTOCOLS_STR)
47
+
48
+ @easy.set :protocols_str, 'http,https'
49
+ @easy.set :url, "http://127.0.0.1:9129/this_file_does_not_exist.html"
50
+ @easy.perform
51
+ assert_equal 404, @easy.response_code
52
+ end
53
+
54
+ def test_redir_protocols_str_setopt_supported
55
+ omit('CURLOPT_REDIR_PROTOCOLS_STR not supported by this libcurl') unless Curl.const_defined?(:CURLOPT_REDIR_PROTOCOLS_STR)
56
+
57
+ @easy.set :url, TestServlet.url + "/redirect"
58
+ @easy.set :redir_protocols_str, 'https'
59
+ assert_raises Curl::Err::UnsupportedProtocolError do
60
+ @easy.perform
61
+ end
62
+ end
63
+
64
+ def test_allowed_protocols_denies_file_protocol
65
+ easy = Curl::Easy.new($TEST_URL)
66
+ easy.allowed_protocols = [:http, :https]
67
+
68
+ assert_raises Curl::Err::UnsupportedProtocolError do
69
+ easy.perform
70
+ end
71
+ end
72
+
73
+ def test_safe_http_denies_file_protocol
74
+ easy = Curl::Easy.new($TEST_URL)
75
+ easy.safe_http!
76
+
77
+ assert_raises Curl::Err::UnsupportedProtocolError do
78
+ easy.perform
79
+ end
80
+ end
81
+
82
+ def test_safe_http_override_is_cleared_by_close
83
+ easy = Curl::Easy.new($TEST_URL)
84
+ easy.safe_http!
85
+ easy.close
86
+ easy.url = $TEST_URL
87
+
88
+ easy.perform
89
+
90
+ assert_match(/TESTMODEL/, easy.body_str)
91
+ end
92
+
93
+ def test_safe_get_denies_file_protocol
94
+ assert_raises Curl::Err::UnsupportedProtocolError do
95
+ Curl.safe_get($TEST_URL)
96
+ end
97
+ end
98
+
99
+ def test_safe_get_reapplies_protocols_after_user_block
100
+ assert_raises Curl::Err::UnsupportedProtocolError do
101
+ Curl.safe_get($TEST_URL) do |easy|
102
+ easy.allowed_protocols = [:file]
103
+ end
104
+ end
105
+ end
106
+
107
+ def test_safe_get_allows_http_protocol
108
+ easy = Curl.safe_get(TestServlet.url)
109
+
110
+ assert_equal 200, easy.response_code
111
+ assert_match(/GET/, easy.body_str)
112
+ end
113
+
114
+ def test_safe_bang_denies_file_protocol_for_easy_perform
115
+ Curl.safe!
116
+ easy = Curl::Easy.new($TEST_URL)
117
+
118
+ assert_raises Curl::Err::UnsupportedProtocolError do
119
+ easy.perform
120
+ end
121
+ end
122
+
123
+ def test_safe_bang_denies_file_protocol_for_shortcut_helpers
124
+ Curl.safe!
125
+
126
+ assert_raises Curl::Err::UnsupportedProtocolError do
127
+ Curl.get($TEST_URL)
128
+ end
129
+ end
130
+
131
+ def test_safe_bang_reapplies_protocols_after_shortcut_block
132
+ Curl.safe!
133
+
134
+ assert_raises Curl::Err::UnsupportedProtocolError do
135
+ Curl.get($TEST_URL) do |easy|
136
+ easy.allowed_protocols = [:file]
137
+ end
138
+ end
139
+ end
140
+
141
+ def test_safe_bang_allows_configured_protocols
142
+ Curl.safe! do |config|
143
+ config.protocols = [:http, :file]
144
+ end
145
+
146
+ easy = Curl::Easy.new($TEST_URL)
147
+ easy.perform
148
+
149
+ assert_match(/DO NOT REMOVE THIS COMMENT/, easy.body_str)
150
+ end
151
+
152
+ def test_safe_get_remains_http_safe_with_broader_global_policy
153
+ Curl.safe! do |config|
154
+ config.protocols = [:http, :file]
155
+ end
156
+
157
+ assert_raises Curl::Err::UnsupportedProtocolError do
158
+ Curl.safe_get($TEST_URL)
159
+ end
160
+ end
161
+
162
+ def test_safe_bang_allows_http_protocol
163
+ Curl.safe!
164
+ easy = Curl.get(TestServlet.url)
165
+
166
+ assert_equal 200, easy.response_code
167
+ assert_match(/GET/, easy.body_str)
168
+ end
169
+
170
+ def test_safe_bang_applies_to_multi_perform
171
+ Curl.safe!
172
+ easy = Curl::Easy.new($TEST_URL)
173
+ multi = Curl::Multi.new
174
+
175
+ multi.add(easy)
176
+ multi.perform
177
+
178
+ assert_equal Curl::Err::UnsupportedProtocolError, Curl::Easy.error(easy.last_result).first
179
+ ensure
180
+ multi.close if defined?(multi) && multi
181
+ end
182
+
183
+ def test_safe_http_denies_file_redirect
184
+ with_file_redirect_server do |url|
185
+ easy = Curl::Easy.new(url)
186
+ easy.follow_location = true
187
+ easy.safe_http!
188
+
189
+ assert_raises Curl::Err::UnsupportedProtocolError do
190
+ easy.perform
191
+ end
192
+ end
193
+ end
194
+
195
+ def test_safe_bang_denies_file_redirect
196
+ with_file_redirect_server do |url|
197
+ Curl.safe!
198
+
199
+ easy = Curl::Easy.new(url)
200
+ easy.follow_location = true
201
+
202
+ assert_raises Curl::Err::UnsupportedProtocolError do
203
+ easy.perform
204
+ end
205
+ end
206
+ end
207
+
208
+ def test_safe_http_allows_http_to_https_redirect
209
+ with_http_https_redirect_servers(:http, :https) do |url|
210
+ easy = Curl::Easy.new(url)
211
+ easy.follow_location = true
212
+ easy.ssl_verify_peer = false
213
+ easy.ssl_verify_host = false
214
+ easy.safe_http!
215
+
216
+ easy.perform
217
+
218
+ assert_equal 200, easy.response_code
219
+ assert_equal 'https target', easy.body_str
220
+ end
221
+ end
222
+
223
+ def test_safe_http_allows_https_to_http_redirect
224
+ with_http_https_redirect_servers(:https, :http) do |url|
225
+ easy = Curl::Easy.new(url)
226
+ easy.follow_location = true
227
+ easy.ssl_verify_peer = false
228
+ easy.ssl_verify_host = false
229
+ easy.safe_http!
230
+
231
+ easy.perform
232
+
233
+ assert_equal 200, easy.response_code
234
+ assert_equal 'http target', easy.body_str
235
+ end
236
+ end
237
+
238
+ def test_safe_http_honors_redirect_limit
239
+ with_redirect_loop_server do |url, hits|
240
+ easy = Curl::Easy.new(url)
241
+ easy.follow_location = true
242
+ easy.max_redirects = 2
243
+ easy.safe_http!
244
+
245
+ assert_raises Curl::Err::TooManyRedirectsError do
246
+ easy.perform
247
+ end
248
+
249
+ assert_operator hits[:loop], :>=, 2
250
+ end
251
+ end
252
+
253
+ private
254
+
255
+ def with_file_redirect_server
256
+ port_socket = TCPServer.new('127.0.0.1', 0)
257
+ port = port_socket.addr[1]
258
+ port_socket.close
259
+
260
+ server = WEBrick::HTTPServer.new(:Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
261
+ server.mount_proc('/redirect-file') do |_req, res|
262
+ res.status = 302
263
+ res['Location'] = $TEST_URL
264
+ res.body = 'redirect'
265
+ end
266
+
267
+ thread = Thread.new(server) { |srv| srv.start }
268
+ wait_for_server_ready(port, thread: thread)
269
+
270
+ yield "http://127.0.0.1:#{port}/redirect-file"
271
+ ensure
272
+ server.shutdown if defined?(server) && server
273
+ thread.join(server_shutdown_timeout) if defined?(thread) && thread
274
+ if defined?(thread) && thread&.alive?
275
+ thread.kill
276
+ thread.join(server_shutdown_timeout)
277
+ end
278
+ end
279
+
280
+ def with_redirect_loop_server
281
+ port_socket = TCPServer.new('127.0.0.1', 0)
282
+ port = port_socket.addr[1]
283
+ port_socket.close
284
+
285
+ hits = Hash.new(0)
286
+ server = WEBrick::HTTPServer.new(:Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
287
+ server.mount_proc('/loop') do |_req, res|
288
+ hits[:loop] += 1
289
+ res.status = 302
290
+ res['Location'] = '/loop'
291
+ res.body = 'redirect'
292
+ end
293
+
294
+ thread = Thread.new(server) { |srv| srv.start }
295
+ wait_for_server_ready(port, thread: thread)
296
+
297
+ yield "http://127.0.0.1:#{port}/loop", hits
298
+ ensure
299
+ server.shutdown if defined?(server) && server
300
+ thread.join(server_shutdown_timeout) if defined?(thread) && thread
301
+ if defined?(thread) && thread&.alive?
302
+ thread.kill
303
+ thread.join(server_shutdown_timeout)
304
+ end
305
+ end
306
+
307
+ def with_http_https_redirect_servers(initial_scheme, target_scheme)
308
+ require 'webrick/https'
309
+ require 'openssl'
310
+
311
+ omit('HTTPS redirect fixtures require OpenSSL') unless defined?(OpenSSL::PKey::RSA)
312
+
313
+ initial_server = nil
314
+ target_server = nil
315
+ initial_thread = nil
316
+ target_thread = nil
317
+
318
+ initial_port = unused_redirect_port
319
+ target_port = unused_redirect_port
320
+ target_server = redirect_matrix_server(target_scheme, target_port)
321
+ target_server.mount_proc('/target') do |_req, res|
322
+ res['Content-Type'] = 'text/plain'
323
+ res.body = "#{target_scheme} target"
324
+ end
325
+
326
+ target_thread = Thread.new(target_server) { |srv| srv.start }
327
+ wait_for_server_ready(target_port, thread: target_thread)
328
+
329
+ initial_server = redirect_matrix_server(initial_scheme, initial_port)
330
+ initial_server.mount_proc('/redirect') do |_req, res|
331
+ res.status = 302
332
+ res['Location'] = "#{target_scheme}://127.0.0.1:#{target_port}/target"
333
+ res.body = 'redirect'
334
+ end
335
+
336
+ initial_thread = Thread.new(initial_server) { |srv| srv.start }
337
+ wait_for_server_ready(initial_port, thread: initial_thread)
338
+
339
+ yield "#{initial_scheme}://127.0.0.1:#{initial_port}/redirect"
340
+ ensure
341
+ initial_server.shutdown if initial_server
342
+ target_server.shutdown if target_server
343
+ initial_thread.join(server_shutdown_timeout) if initial_thread
344
+ target_thread.join(server_shutdown_timeout) if target_thread
345
+ [initial_thread, target_thread].compact.each do |thread|
346
+ next unless thread.alive?
347
+
348
+ thread.kill
349
+ thread.join(server_shutdown_timeout)
350
+ end
351
+ end
352
+
353
+ def redirect_matrix_server(scheme, port)
354
+ options = {:BindAddress => '127.0.0.1', :Port => port, :Logger => WEBRICK_TEST_LOG, :AccessLog => []}
355
+ return WEBrick::HTTPServer.new(options) if scheme == :http
356
+
357
+ cert, key = redirect_matrix_certificate
358
+ WEBrick::HTTPServer.new(options.merge(:SSLEnable => true, :SSLCertificate => cert, :SSLPrivateKey => key))
359
+ end
360
+
361
+ def redirect_matrix_certificate
362
+ key = OpenSSL::PKey::RSA.new(2048)
363
+ cert = OpenSSL::X509::Certificate.new
364
+ cert.version = 2
365
+ cert.serial = 1
366
+ cert.subject = OpenSSL::X509::Name.parse('/CN=127.0.0.1')
367
+ cert.issuer = cert.subject
368
+ cert.public_key = key.public_key
369
+ cert.not_before = Time.now - 60
370
+ cert.not_after = Time.now + 3600
371
+
372
+ extensions = OpenSSL::X509::ExtensionFactory.new
373
+ extensions.subject_certificate = cert
374
+ extensions.issuer_certificate = cert
375
+ cert.add_extension(extensions.create_extension('basicConstraints', 'CA:FALSE', true))
376
+ cert.add_extension(extensions.create_extension('subjectAltName', 'IP:127.0.0.1', false))
377
+ cert.sign(key, OpenSSL::Digest::SHA256.new)
378
+
379
+ [cert, key]
380
+ end
381
+
382
+ def unused_redirect_port
383
+ socket = TCPServer.new('127.0.0.1', 0)
384
+ socket.addr[1]
385
+ ensure
386
+ socket.close if socket
387
+ end
37
388
  end
@@ -15,10 +15,11 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
15
15
  include TestServerMethods
16
16
 
17
17
  class RecordingScheduler
18
- attr_reader :io_wait_events
18
+ attr_reader :io_wait_events, :io_select_calls
19
19
 
20
20
  def initialize
21
21
  @io_wait_events = []
22
+ @io_select_calls = 0
22
23
  end
23
24
 
24
25
  def fiber(&block)
@@ -31,7 +32,7 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
31
32
 
32
33
  readers = (events & IO::READABLE) != 0 ? [io] : nil
33
34
  writers = (events & IO::WRITABLE) != 0 ? [io] : nil
34
- readable, writable = IO.select(readers, writers, nil, timeout)
35
+ readable, writable = blocking_io { IO.select(readers, writers, nil, timeout) }
35
36
 
36
37
  ready = 0
37
38
  ready |= IO::READABLE if readable && !readable.empty?
@@ -39,6 +40,11 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
39
40
  ready.zero? ? false : ready
40
41
  end
41
42
 
43
+ def io_select(readers, writers, excepts, timeout = nil)
44
+ @io_select_calls += 1
45
+ blocking_io { IO.select(readers, writers, excepts, timeout) }
46
+ end
47
+
42
48
  def kernel_sleep(duration = nil)
43
49
  sleep(duration || 0)
44
50
  end
@@ -56,6 +62,16 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
56
62
 
57
63
  def fiber_interrupt(*)
58
64
  end
65
+
66
+ private
67
+
68
+ def blocking_io(&block)
69
+ if Fiber.respond_to?(:blocking)
70
+ Fiber.blocking(&block)
71
+ else
72
+ block.call
73
+ end
74
+ end
59
75
  end
60
76
 
61
77
  ITERS = 4
@@ -208,9 +224,11 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
208
224
  end
209
225
 
210
226
  assert_equal 200, result
211
- assert_operator scheduler.io_wait_events.length, :>=, 1
212
- assert scheduler.io_wait_events.all? { |events| events.is_a?(Integer) }
213
- assert scheduler.io_wait_events.any? { |events| (events & (IO::READABLE | IO::WRITABLE)) != 0 }
227
+ assert_operator scheduler.io_wait_events.length + scheduler.io_select_calls, :>=, 1
228
+ unless scheduler.io_wait_events.empty?
229
+ assert scheduler.io_wait_events.all? { |events| events.is_a?(Integer) }
230
+ assert scheduler.io_wait_events.any? { |events| (events & (IO::READABLE | IO::WRITABLE)) != 0 }
231
+ end
214
232
  end
215
233
 
216
234
  def test_multi_reuse_after_scheduler_perform
@@ -431,6 +449,47 @@ class TestCurbFiberScheduler < Test::Unit::TestCase
431
449
  end
432
450
  end
433
451
 
452
+ def test_easy_perform_isolates_public_network_policy_block_under_scheduler
453
+ if skip_no_async
454
+ return
455
+ end
456
+
457
+ with_ephemeral_http_server do |port, hits|
458
+ results = {}
459
+ details = {}
460
+
461
+ async_run do |top|
462
+ blocked = top.async do
463
+ easy = Curl::Easy.new("http://127.0.0.1:#{port}/fast")
464
+ easy.network_policy = :public
465
+ easy.perform
466
+ results[:blocked] = :returned
467
+ rescue => e
468
+ results[:blocked] = e
469
+ details[:blocked_error] = easy.unsafe_destination_error if defined?(easy) && easy
470
+ end
471
+
472
+ successful = top.async do
473
+ easy = Curl::Easy.new("http://127.0.0.1:#{port}/slow")
474
+ easy.perform
475
+ results[:successful] = easy.response_code
476
+ rescue => e
477
+ results[:successful] = e
478
+ end
479
+
480
+ blocked.wait
481
+ successful.wait
482
+ end
483
+
484
+ assert_equal 200, results[:successful], "scheduler peer without public policy should return normally"
485
+ assert_kind_of Curl::Err::UnsafeDestinationError, results[:blocked]
486
+ assert_match(/127\.0\.0\.1/, results[:blocked].message)
487
+ assert_match(/127\.0\.0\.1/, details[:blocked_error])
488
+ assert_equal 0, hits[:fast], "blocked public-policy peer should not reach the server"
489
+ assert_equal 1, hits[:slow]
490
+ end
491
+ end
492
+
434
493
  def test_drain_scheduler_pending_does_not_drop_work_rejected_during_deferred_abort
435
494
  state = Curl.scheduler_state
436
495
  easy = Curl::Easy.new("http://127.0.0.1:#{@port}/test")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: curb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.4
4
+ version: 1.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ross Bamford
8
8
  - Todd A. Fisher
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-12 00:00:00.000000000 Z
11
+ date: 2026-06-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Curb (probably CUrl-RuBy or something) provides Ruby-language bindings
14
14
  for the libcurl(3), a fully-featured client-side URL transfer library. cURL and
@@ -42,6 +42,7 @@ files:
42
42
  - ext/extconf.rb
43
43
  - lib/curb.rb
44
44
  - lib/curl.rb
45
+ - lib/curl/download.rb
45
46
  - lib/curl/easy.rb
46
47
  - lib/curl/multi.rb
47
48
  - tests/alltests.rb
@@ -56,6 +57,7 @@ files:
56
57
  - tests/bug_issue_post_redirect.rb
57
58
  - tests/bug_issue_spnego.rb
58
59
  - tests/bug_multi_segfault.rb
60
+ - tests/bug_poison.rb
59
61
  - tests/bug_postfields_crash.rb
60
62
  - tests/bug_postfields_crash2.rb
61
63
  - tests/bug_raise_on_callback.rb
@@ -76,6 +78,7 @@ files:
76
78
  - tests/tc_curl_maxfilesize.rb
77
79
  - tests/tc_curl_multi.rb
78
80
  - tests/tc_curl_native_coverage.rb
81
+ - tests/tc_curl_network_policy.rb
79
82
  - tests/tc_curl_postfield.rb
80
83
  - tests/tc_curl_protocols.rb
81
84
  - tests/tc_fiber_scheduler.rb
@@ -107,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
110
  - !ruby/object:Gem::Version
108
111
  version: '0'
109
112
  requirements: []
110
- rubygems_version: 4.0.6
113
+ rubygems_version: 4.0.10
111
114
  specification_version: 4
112
115
  summary: Ruby libcurl bindings
113
116
  test_files:
@@ -123,6 +126,7 @@ test_files:
123
126
  - tests/bug_issue_post_redirect.rb
124
127
  - tests/bug_issue_spnego.rb
125
128
  - tests/bug_multi_segfault.rb
129
+ - tests/bug_poison.rb
126
130
  - tests/bug_postfields_crash.rb
127
131
  - tests/bug_postfields_crash2.rb
128
132
  - tests/bug_raise_on_callback.rb
@@ -143,6 +147,7 @@ test_files:
143
147
  - tests/tc_curl_maxfilesize.rb
144
148
  - tests/tc_curl_multi.rb
145
149
  - tests/tc_curl_native_coverage.rb
150
+ - tests/tc_curl_network_policy.rb
146
151
  - tests/tc_curl_postfield.rb
147
152
  - tests/tc_curl_protocols.rb
148
153
  - tests/tc_fiber_scheduler.rb