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.
@@ -104,11 +104,10 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
104
104
  end
105
105
  end
106
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)
107
+ def test_safe_get_blocks_local_http_destination
108
+ assert_raise(Curl::Err::UnsafeDestinationError) do
109
+ Curl.safe_get(TestServlet.url)
110
+ end
112
111
  end
113
112
 
114
113
  def test_safe_bang_denies_file_protocol_for_easy_perform
@@ -159,8 +158,8 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
159
158
  end
160
159
  end
161
160
 
162
- def test_safe_bang_allows_http_protocol
163
- Curl.safe!
161
+ def test_safe_bang_explicit_none_allows_trusted_http_destination
162
+ Curl.safe! { |config| config.network_policy = :none }
164
163
  easy = Curl.get(TestServlet.url)
165
164
 
166
165
  assert_equal 200, easy.response_code
@@ -184,6 +183,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
184
183
  with_file_redirect_server do |url|
185
184
  easy = Curl::Easy.new(url)
186
185
  easy.follow_location = true
186
+ stub_public_network_policy_for_local_fixture(easy)
187
187
  easy.safe_http!
188
188
 
189
189
  assert_raises Curl::Err::UnsupportedProtocolError do
@@ -198,6 +198,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
198
198
 
199
199
  easy = Curl::Easy.new(url)
200
200
  easy.follow_location = true
201
+ stub_public_network_policy_for_local_fixture(easy)
201
202
 
202
203
  assert_raises Curl::Err::UnsupportedProtocolError do
203
204
  easy.perform
@@ -211,6 +212,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
211
212
  easy.follow_location = true
212
213
  easy.ssl_verify_peer = false
213
214
  easy.ssl_verify_host = false
215
+ stub_public_network_policy_for_local_fixture(easy)
214
216
  easy.safe_http!
215
217
 
216
218
  easy.perform
@@ -226,6 +228,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
226
228
  easy.follow_location = true
227
229
  easy.ssl_verify_peer = false
228
230
  easy.ssl_verify_host = false
231
+ stub_public_network_policy_for_local_fixture(easy)
229
232
  easy.safe_http!
230
233
 
231
234
  easy.perform
@@ -240,6 +243,7 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
240
243
  easy = Curl::Easy.new(url)
241
244
  easy.follow_location = true
242
245
  easy.max_redirects = 2
246
+ stub_public_network_policy_for_local_fixture(easy)
243
247
  easy.safe_http!
244
248
 
245
249
  assert_raises Curl::Err::TooManyRedirectsError do
@@ -252,6 +256,14 @@ class TestCurbCurlProtocols < Test::Unit::TestCase
252
256
 
253
257
  private
254
258
 
259
+ # Isolate protocol/redirect behavior using local fixtures. Destination blocking
260
+ # is exercised without this stub in tc_curl_network_policy.rb.
261
+ def stub_public_network_policy_for_local_fixture(easy)
262
+ def easy.network_policy=(policy)
263
+ raise "unexpected policy: #{policy}" unless policy == :public
264
+ end
265
+ end
266
+
255
267
  def with_file_redirect_server
256
268
  port_socket = TCPServer.new('127.0.0.1', 0)
257
269
  port = port_socket.addr[1]
@@ -0,0 +1,112 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+
3
+ class TestCurbTransferGuard < Test::Unit::TestCase
4
+ class InterruptedTransfer < StandardError; end
5
+
6
+ [:perform, :_socket_perform].each do |drive|
7
+ define_method("test_#{drive}_rejects_cross_thread_handle_access") do
8
+ with_waiting_transfer(drive) do |easy, multi, worker, release|
9
+ operations = {
10
+ close: -> { easy.close },
11
+ reset: -> { easy.reset },
12
+ initialize: -> { easy.__send__(:initialize) },
13
+ clone: -> { easy.clone },
14
+ dup: -> { easy.dup },
15
+ setopt: -> { easy.setopt(Curl::CURLOPT_CUSTOMREQUEST, 'DELETE') },
16
+ post_body: -> { easy.post_body = 'replacement' },
17
+ url: -> { easy.url = 'http://127.0.0.1/' },
18
+ timeout: -> { easy.timeout = 1 },
19
+ multi_assignment: -> { easy.multi = nil },
20
+ multi_remove: -> { multi.remove(easy) },
21
+ multi_close: -> { multi.close },
22
+ multi_initialize: -> { multi.__send__(:initialize) },
23
+ multi_add: -> { multi.add(Curl::Easy.new('http://127.0.0.1/')) },
24
+ multi_option: -> { multi.max_connects = 1 }
25
+ }
26
+ operations.each do |name, operation|
27
+ error = assert_raise(RuntimeError, name.to_s, &operation)
28
+ assert_match(/another thread|active Curl::Multi/, error.message, name.to_s)
29
+ end
30
+ assert_equal [easy], multi.requests.values
31
+
32
+ release << true
33
+ assert worker.join(5), 'perform did not finish'
34
+ worker.value
35
+ assert_equal 'ok', easy.body_str
36
+ assert_nothing_raised { easy.clone.close }
37
+ assert_nothing_raised { easy.reset }
38
+ assert_nothing_raised { easy.close }
39
+ end
40
+ end
41
+
42
+ define_method("test_#{drive}_releases_guard_after_interruption") do
43
+ with_waiting_transfer(drive) do |easy, multi, worker, release|
44
+ worker.raise(InterruptedTransfer, 'stop')
45
+ assert worker.join(5), 'interrupted perform did not finish'
46
+ assert_kind_of InterruptedTransfer, worker[:transfer_error]
47
+ assert_nothing_raised { multi.remove(easy) }
48
+ assert_nothing_raised { easy.reset }
49
+ assert_nothing_raised { easy.close }
50
+ release << true
51
+ end
52
+ end
53
+ end
54
+
55
+ def with_waiting_transfer(drive)
56
+ if drive == :_socket_perform && !Curl::Multi.private_method_defined?(drive)
57
+ omit('socket-action perform path is not available')
58
+ end
59
+ server = TCPServer.new('127.0.0.1', 0)
60
+ started = Queue.new
61
+ release = Queue.new
62
+ server_thread = Thread.new do
63
+ socket = server.accept
64
+ begin
65
+ request = +''
66
+ request << socket.readpartial(4096) until request.include?("\r\n\r\n")
67
+ started << true
68
+ release.pop
69
+ socket.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok")
70
+ rescue Errno::EPIPE, Errno::ECONNRESET
71
+ # The interruption test intentionally closes the connection early.
72
+ ensure
73
+ socket.close
74
+ end
75
+ end
76
+ easy = Curl::Easy.new("http://127.0.0.1:#{server.addr[1]}/")
77
+ multi = Curl::Multi.new
78
+ # Exercise reuse after autoclose as well as the active-transfer checks.
79
+ previous_autoclose = Curl::Multi.autoclose
80
+ begin
81
+ Curl::Multi.autoclose = true
82
+ multi.add(Curl::Easy.new($TEST_URL))
83
+ multi.perform
84
+ ensure
85
+ Curl::Multi.autoclose = previous_autoclose
86
+ end
87
+ multi.add(easy)
88
+ worker = Thread.new do
89
+ Thread.current.report_on_exception = false if Thread.current.respond_to?(:report_on_exception=)
90
+ begin
91
+ multi.__send__(drive)
92
+ rescue InterruptedTransfer => error
93
+ # Keep join non-raising so the test can inspect the exception explicitly.
94
+ Thread.current[:transfer_error] = error
95
+ end
96
+ end
97
+ Timeout.timeout(5) { started.pop }
98
+ yield easy, multi, worker, release
99
+ ensure
100
+ release << true if release
101
+ if worker && worker.alive?
102
+ worker.kill
103
+ worker.join(5)
104
+ end
105
+ multi.close if multi
106
+ server.close if server && !server.closed?
107
+ if server_thread
108
+ server_thread.join(5)
109
+ server_thread.kill if server_thread.alive?
110
+ end
111
+ end
112
+ end
data/tests/tc_ractor.rb CHANGED
@@ -30,7 +30,10 @@ class TestCurbRactor < Test::Unit::TestCase
30
30
 
31
31
  workers = RACTOR_WORKERS.times.map do |worker_id|
32
32
  Ractor.new(url, worker_id, REQUESTS_PER_WORKER) do |target, id, request_count|
33
- Curl.safe! { |config| config.protocols = [:http] }
33
+ Curl.safe! do |config|
34
+ config.protocols = [:http]
35
+ config.network_policy = :none # This concurrency fixture uses localhost.
36
+ end
34
37
  Curl::Multi.default_timeout = 25 + id
35
38
  Curl::Multi.autoclose = true
36
39
 
@@ -76,6 +79,50 @@ class TestCurbRactor < Test::Unit::TestCase
76
79
  Curl::Multi.autoclose = original_autoclose if defined?(original_autoclose)
77
80
  end
78
81
 
82
+ def test_multipart_snapshots_are_private_to_parallel_ractors
83
+ omit_unless_curb_ractor_safe
84
+ worker_count = Integer(ENV.fetch('CURB_MULTIPART_RACTORS', '4'))
85
+ request_count = Integer(ENV.fetch('CURB_MULTIPART_REQUESTS', '5'))
86
+ workers = worker_count.times.map do |worker_id|
87
+ Ractor.new(TestServlet.url, worker_id, request_count) do |url, id, count|
88
+ easy = Curl::Easy.new(url)
89
+ easy.multipart_form_post = true
90
+ count.times do |iteration|
91
+ name = +"field_#{id}_#{iteration}"
92
+ payload = "payload_#{id}_#{iteration}\0" + ('x' * 1024)
93
+ expected_name = name.dup
94
+ expected_payload = payload.dup
95
+ direct = Curl::PostField.content(name, payload, 'application/octet-stream')
96
+ file = Curl::PostField.file('upload', 'payload.bin') { payload }
97
+ later = Curl::PostField.content('later') do
98
+ name.replace('changed' * 1000)
99
+ payload.replace('changed' * 1000)
100
+ # Ruby #21537: explicit GC in a Windows Ractor can deadlock
101
+ # the VM barrier. Keep parallel mutation coverage on Windows;
102
+ # single-Ractor compaction is covered by the PostField tests.
103
+ if iteration % 10 == 0 && !WINDOWS
104
+ GC.start
105
+ GC.compact if GC.respond_to?(:compact)
106
+ end
107
+ 'done'
108
+ end
109
+ easy.http_post(direct, file, later)
110
+ body = easy.body_str
111
+ unless easy.response_code == 200 &&
112
+ body.include?("name=\"#{expected_name}\"") &&
113
+ body.include?('filename="payload.bin"') &&
114
+ body.scan(expected_payload).length == 2 &&
115
+ !body.include?('changed')
116
+ raise "Multipart snapshot changed in Ractor #{id}, request #{iteration}"
117
+ end
118
+ end
119
+ easy.close
120
+ count
121
+ end
122
+ end
123
+ assert_equal Array.new(worker_count, request_count), workers.map { |worker| ractor_value(worker) }
124
+ end
125
+
79
126
  private
80
127
 
81
128
  def omit_unless_curb_ractor_safe
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.4.0
4
+ version: 1.4.1
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-09-27 00:00:00.000000000 Z
11
+ date: 2026-10-21 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
@@ -21,6 +21,7 @@ extra_rdoc_files:
21
21
  - LICENSE
22
22
  - README.md
23
23
  files:
24
+ - ChangeLog.md
24
25
  - LICENSE
25
26
  - README.md
26
27
  - Rakefile
@@ -84,6 +85,7 @@ files:
84
85
  - tests/tc_curl_network_policy.rb
85
86
  - tests/tc_curl_postfield.rb
86
87
  - tests/tc_curl_protocols.rb
88
+ - tests/tc_curl_transfer_guard.rb
87
89
  - tests/tc_fiber_scheduler.rb
88
90
  - tests/tc_ftp_options.rb
89
91
  - tests/tc_gc_compact.rb
@@ -157,6 +159,7 @@ test_files:
157
159
  - tests/tc_curl_network_policy.rb
158
160
  - tests/tc_curl_postfield.rb
159
161
  - tests/tc_curl_protocols.rb
162
+ - tests/tc_curl_transfer_guard.rb
160
163
  - tests/tc_fiber_scheduler.rb
161
164
  - tests/tc_ftp_options.rb
162
165
  - tests/tc_gc_compact.rb