curb 1.3.7 → 1.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12fe9b3726ab9e6bb2a7369ebae117d8a2da19db382acd1b182af4b8cd19d4cb
4
- data.tar.gz: 05a07a992383c5c956d037770215bb2bb8795cc3062bad6ca2ec84c0389dd880
3
+ metadata.gz: 9f3088ba29db67ea56c9587898267648a704c8acbcb9e95cb58318c3464deb12
4
+ data.tar.gz: 40134cd41f0704c2aaa3b65ca6507987f6e8bc755c4530c9361ccd25f4831c17
5
5
  SHA512:
6
- metadata.gz: 3f0016de5762e46f6b1f1ef3a588c82a41a8a3b1f8a3d3f351626279fa053539ec7d6ca35a8a9792ed5c9dd6e06c550d2b9ba7b900cffbb60af09f260e603662
7
- data.tar.gz: c26cfe8e0c03bf30dda075c6abb916e4e1d66205f61ebe54d2db73c1359e3c8def512163393280874730769ffad6fc96c24aad8e5f79a5d586e1b43640ed6b9f
6
+ metadata.gz: 29ffbf5bb4301db5729aff433a938251622c8483a29f5fa0f5344b52573048964c9863d00d254b42d01ff172291a1b469ea10f1fa9757b58fe4743a66c7078a5
7
+ data.tar.gz: d2e21e48891f398af31a7b2dd8d99a2465f82bb97d9caecda8ac4cb69fb599e894c3172bc56a6106a28d7761b03743fc78ee80701b3051ac673b7b6f1ed27ea9
data/README.md CHANGED
@@ -205,7 +205,7 @@ end
205
205
  `curb` is a libcurl binding and intentionally supports protocols beyond HTTP.
206
206
  Do not pass untrusted URLs to `Curl.get`, `Curl::Easy.new`, or related raw
207
207
  helpers without application-level validation. For user-supplied URLs, enable the
208
- process-wide safety policy before making requests:
208
+ safety policy for the current Ractor before making requests:
209
209
 
210
210
  ```ruby
211
211
  Curl.safe! do |config|
@@ -257,6 +257,42 @@ configured. For untrusted or large responses, use `on_body`, `download`, and/or
257
257
  `max_body_bytes` is enforced for downloads as well as buffered responses and
258
258
  custom body callbacks.
259
259
 
260
+ ## Ractor support
261
+
262
+ On Ruby 3.0+, curb can perform requests from multiple Ractors when it is built
263
+ against a libcurl that reports the `CURL_VERSION_THREADSAFE` capability
264
+ (libcurl 7.84.0 or newer). Check `Curl::RACTOR_SAFE` at runtime. Builds that do
265
+ not meet those requirements continue to work in the main Ractor but do not
266
+ advertise native Ractor safety.
267
+
268
+ Create `Curl::Easy` and `Curl::Multi` handles inside the Ractor that uses them;
269
+ do not operate on the same native handle concurrently from different Ractors.
270
+ Safety configuration, `Curl::Multi.default_timeout`, `Curl::Multi.autoclose`,
271
+ and deferred cleanup queues are isolated per Ractor. Call `Curl.safe!` in each
272
+ Ractor that needs the safety policy:
273
+
274
+ ```ruby
275
+ raise "this curb build is not Ractor-safe" unless Curl::RACTOR_SAFE
276
+
277
+ workers = 4.times.map do
278
+ Ractor.new do
279
+ Curl.safe! { |config| config.protocols = [:http, :https] }
280
+ easy = Curl::Easy.new("https://example.com/")
281
+ easy.perform
282
+ easy.response_code
283
+ end
284
+ end
285
+
286
+ # Ruby 4.0 uses Ractor#value; earlier Ractor releases use Ractor#take.
287
+ statuses = workers.map do |worker|
288
+ worker.respond_to?(:value) ? worker.value : worker.take
289
+ end
290
+ ```
291
+
292
+ Curb enables `CURLOPT_NOSIGNAL` on newly initialized and reset Easy handles by
293
+ default so separate handles can be used safely from parallel Ruby execution.
294
+ Applications can still override the option explicitly when required.
295
+
260
296
  ## You will need
261
297
 
262
298
  * A working Ruby installation (`2.0.0+` will work but `2.1+` preferred) (it's possible it still works with 1.8.7 but you'd have to tell me if not...)
@@ -278,6 +314,7 @@ an explicit CI target.
278
314
 
279
315
  | Gem Version | Release Date | libcurl versions |
280
316
  | ----------- | -------------- | ----------------- |
317
+ | 1.4.0 | Sep 27, 2026 | 7.58 – 8.21.0 |
281
318
  | 1.3.7 | Jul 10, 2026 | 7.58 – 8.21.0 |
282
319
  | 1.3.6 | Jun 17, 2026 | 7.58 – 8.20.0 |
283
320
  | 1.3.5 | May 14, 2026 | 7.58 – 8.20.0 |
data/Rakefile CHANGED
@@ -126,7 +126,18 @@ end
126
126
 
127
127
  RubyMemcheck.config(**ruby_memcheck_config)
128
128
  namespace :test do
129
- RubyMemcheck::TestTask.new(valgrind: :compile) do|t|
129
+ # Ruby pools fiber stacks next to each other, about 1MB apart. Valgrind
130
+ # treats a stack-pointer jump smaller than --max-stackframe (2MB) as an
131
+ # ordinary call or return rather than a switch to another stack, and marks
132
+ # the memory in between, which can hold another fiber's stack, as
133
+ # inaccessible. Later reads of that fiber's stack (turning a block into a
134
+ # Proc, GC marking suspended fibers) are then reported as invalid. Spacing
135
+ # fiber stacks wider than that lets valgrind recognize every fiber switch.
136
+ task :valgrind_env do
137
+ ENV['RUBY_FIBER_MACHINE_STACK_SIZE'] ||= (4 * 1024 * 1024).to_s
138
+ end
139
+
140
+ RubyMemcheck::TestTask.new(valgrind: [:compile, :valgrind_env]) do|t|
130
141
  t.test_files = FileList['tests/tc_*.rb']
131
142
  t.verbose = false
132
143
  end
data/ext/curb.c CHANGED
@@ -243,25 +243,38 @@ static void finalize_curb_core(VALUE data) {
243
243
 
244
244
  void Init_curb_core() {
245
245
  curl_version_info_data *ver;
246
- VALUE curlver, curllongver, curlvernum;
246
+ VALUE curbver, curlver, curllongver, curlvernum;
247
+ int ractor_safe = 0;
247
248
 
248
249
  curl_global_init(CURL_GLOBAL_ALL);
249
250
  rb_set_end_proc(finalize_curb_core, Qnil);
250
251
  ver = curl_version_info(CURLVERSION_NOW);
251
252
 
253
+ #if defined(HAVE_RB_EXT_RACTOR_SAFE) && defined(HAVE_CURL_VERSION_THREADSAFE)
254
+ /* Ractors execute native methods in parallel. Only opt in when both Ruby
255
+ * provides the safety marker and libcurl advertises thread-safe global
256
+ * initialization. Individual Curl handles remain Ractor-owned. */
257
+ if (ver && (ver->features & CURL_VERSION_THREADSAFE)) {
258
+ rb_ext_ractor_safe(1);
259
+ ractor_safe = 1;
260
+ }
261
+ #endif
262
+
252
263
  mCurl = rb_define_module("Curl");
253
264
 
254
- curlver = rb_str_new2(ver->version);
255
- curllongver = rb_str_new2(curl_version());
265
+ curbver = rb_obj_freeze(rb_str_new2(CURB_VERSION));
266
+ curlver = rb_obj_freeze(rb_str_new2(ver->version));
267
+ curllongver = rb_obj_freeze(rb_str_new2(curl_version()));
256
268
  curlvernum = LONG2NUM(LIBCURL_VERSION_NUM);
257
269
 
258
- rb_define_const(mCurl, "CURB_VERSION", rb_str_new2(CURB_VERSION));
270
+ rb_define_const(mCurl, "CURB_VERSION", curbver);
259
271
  rb_define_const(mCurl, "VERSION", curlver);
260
272
  rb_define_const(mCurl, "CURL_VERSION", curlver);
261
273
  rb_define_const(mCurl, "VERNUM", curlvernum);
262
274
  rb_define_const(mCurl, "CURL_VERNUM", curlvernum);
263
275
  rb_define_const(mCurl, "LONG_VERSION", curllongver);
264
276
  rb_define_const(mCurl, "CURL_LONG_VERSION", curllongver);
277
+ rb_define_const(mCurl, "RACTOR_SAFE", ractor_safe ? Qtrue : Qfalse);
265
278
 
266
279
  /* Passed to on_debug handler to indicate that the data is informational text. */
267
280
  rb_define_const(mCurl, "CURLINFO_TEXT", LONG2NUM(CURLINFO_TEXT));
data/ext/curb.h CHANGED
@@ -28,11 +28,11 @@
28
28
  #include "curb_macros.h"
29
29
 
30
30
  // These should be managed from the Rake 'release' task.
31
- #define CURB_VERSION "1.3.7"
32
- #define CURB_VER_NUM 1037
31
+ #define CURB_VERSION "1.4.0"
32
+ #define CURB_VER_NUM 1040
33
33
  #define CURB_VER_MAJ 1
34
- #define CURB_VER_MIN 3
35
- #define CURB_VER_MIC 7
34
+ #define CURB_VER_MIN 4
35
+ #define CURB_VER_MIC 0
36
36
  #define CURB_VER_PATCH 0
37
37
 
38
38
 
data/ext/curb_easy.c CHANGED
@@ -66,6 +66,17 @@ static struct curl_slist *duplicate_curl_slist(struct curl_slist *list);
66
66
  static size_t proc_data_handler(char *stream, size_t size, size_t nmemb, VALUE proc);
67
67
  static int curb_array_includes_string(VALUE list, VALUE value);
68
68
 
69
+ static void curb_easy_apply_thread_defaults(ruby_curl_easy *rbce) {
70
+ #ifdef HAVE_CURLOPT_NOSIGNAL
71
+ CURLcode ecode = curl_easy_setopt(rbce->curl, CURLOPT_NOSIGNAL, 1L);
72
+ if (ecode != CURLE_OK) {
73
+ raise_curl_easy_error_exception(ecode);
74
+ }
75
+ #else
76
+ (void)rbce;
77
+ #endif
78
+ }
79
+
69
80
  /* ================== CURL HANDLER FUNCS ==============*/
70
81
 
71
82
  static int curb_ipv4_is_unsafe_destination(const unsigned char *ip) {
@@ -1501,6 +1512,7 @@ static VALUE ruby_curl_easy_initialize(int argc, VALUE *argv, VALUE self) {
1501
1512
  ruby_curl_easy_zero(rbce);
1502
1513
  rbce->self = self;
1503
1514
 
1515
+ curb_easy_apply_thread_defaults(rbce);
1504
1516
  curl_easy_setopt(rbce->curl, CURLOPT_ERRORBUFFER, &rbce->err_buf);
1505
1517
 
1506
1518
  rb_easy_set("url", url);
@@ -1659,6 +1671,7 @@ static VALUE ruby_curl_easy_close(VALUE self) {
1659
1671
  ruby_curl_easy_zero(rbce);
1660
1672
  rbce->self = self;
1661
1673
 
1674
+ curb_easy_apply_thread_defaults(rbce);
1662
1675
  curl_easy_setopt(rbce->curl, CURLOPT_ERRORBUFFER, rbce->err_buf);
1663
1676
 
1664
1677
  /* give the new curl handle a reference back to the ruby object */
@@ -1703,6 +1716,7 @@ static VALUE ruby_curl_easy_reset(VALUE self) {
1703
1716
  ruby_curl_easy_zero(rbce);
1704
1717
  rbce->self = self;
1705
1718
 
1719
+ curb_easy_apply_thread_defaults(rbce);
1706
1720
  curl_easy_setopt(rbce->curl, CURLOPT_ERRORBUFFER, &rbce->err_buf);
1707
1721
 
1708
1722
  /* reset clobbers the private setting, so reset it to self */
@@ -2552,6 +2566,41 @@ static VALUE ruby_curl_easy_proxy_type_get(VALUE self) {
2552
2566
  (!strncmp("anysafe",node,7)) ? CURLAUTH_ANYSAFE : \
2553
2567
  (!strncmp("any",node,3)) ? CURLAUTH_ANY : 0
2554
2568
  #endif
2569
+ /*
2570
+ * Convert the arguments to http_auth_types= / proxy_auth_types= into a
2571
+ * CURLAUTH bitmask. Accepts nil, an Integer mask, or one or more auth type
2572
+ * names (symbols/strings), either as separate args or as a single Array.
2573
+ */
2574
+ static long ruby_curl_easy_auth_types_to_mask(VALUE args_ary) {
2575
+ long i, len;
2576
+ char* node = NULL;
2577
+ long mask = 0;
2578
+ VALUE first;
2579
+
2580
+ /* easy.http_auth_types = [:basic, :digest] arrives as a single Array arg */
2581
+ if (RARRAY_LEN(args_ary) == 1 && RB_TYPE_P(rb_ary_entry(args_ary,0), T_ARRAY)) {
2582
+ args_ary = rb_ary_entry(args_ary,0);
2583
+ }
2584
+
2585
+ len = RARRAY_LEN(args_ary);
2586
+ first = rb_ary_entry(args_ary,0);
2587
+
2588
+ if (len == 1 && (first == Qnil || TYPE(first) == T_FIXNUM || TYPE(first) == T_BIGNUM)) {
2589
+ return first == Qnil ? 0 : NUM2LONG(first);
2590
+ }
2591
+
2592
+ // we could have multiple values, but they should be symbols
2593
+ // rb_obj_as_string always yields a String, even if to_s misbehaves;
2594
+ // RSTRING_PTR on a raw rb_funcall result would crash if #to_s returned
2595
+ // a non-String (e.g. a Fixnum, whose VALUE isn't a real pointer).
2596
+ for( i = 0; i < len; ++i ) {
2597
+ VALUE node_str = rb_obj_as_string(rb_ary_entry(args_ary,i));
2598
+ node = StringValuePtr(node_str);
2599
+ mask |= CURL_HTTPAUTH_STR_TO_NUM(node);
2600
+ }
2601
+ return mask;
2602
+ }
2603
+
2555
2604
  /*
2556
2605
  * call-seq:
2557
2606
  * easy.http_auth_types = fixnum or nil => fixnum or nil
@@ -2561,37 +2610,14 @@ static VALUE ruby_curl_easy_proxy_type_get(VALUE self) {
2561
2610
  * +perform+ calls. This is a bitmap made by ORing together the
2562
2611
  * Curl::CURLAUTH constants.
2563
2612
  */
2564
- static VALUE ruby_curl_easy_http_auth_types_set(int argc, VALUE *argv, VALUE self) {//VALUE self, VALUE http_auth_types) {
2613
+ static VALUE ruby_curl_easy_http_auth_types_set(int argc, VALUE *argv, VALUE self) {
2565
2614
  ruby_curl_easy *rbce;
2566
2615
  VALUE args_ary;
2567
- long i, len;
2568
- char* node = NULL;
2569
- long mask = 0;
2570
2616
 
2571
2617
  rb_scan_args(argc, argv, "*", &args_ary);
2572
2618
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2573
2619
 
2574
- len = RARRAY_LEN(args_ary);
2575
-
2576
- if (len == 1 && (rb_ary_entry(args_ary,0) == Qnil || TYPE(rb_ary_entry(args_ary,0)) == T_FIXNUM ||
2577
- TYPE(rb_ary_entry(args_ary,0)) == T_BIGNUM)) {
2578
- if (rb_ary_entry(args_ary,0) == Qnil) {
2579
- rbce->http_auth_types = 0;
2580
- }
2581
- else {
2582
- rbce->http_auth_types = NUM2LONG(rb_ary_entry(args_ary,0));
2583
- }
2584
- }
2585
- else {
2586
- // we could have multiple values, but they should be symbols
2587
- node = RSTRING_PTR(rb_funcall(rb_ary_entry(args_ary,0),rb_intern("to_s"),0));
2588
- mask = CURL_HTTPAUTH_STR_TO_NUM(node);
2589
- for( i = 1; i < len; ++i ) {
2590
- node = RSTRING_PTR(rb_funcall(rb_ary_entry(args_ary,i),rb_intern("to_s"),0));
2591
- mask |= CURL_HTTPAUTH_STR_TO_NUM(node);
2592
- }
2593
- rbce->http_auth_types = mask;
2594
- }
2620
+ rbce->http_auth_types = ruby_curl_easy_auth_types_to_mask(args_ary);
2595
2621
  return LONG2NUM(rbce->http_auth_types);
2596
2622
  }
2597
2623
 
@@ -2609,13 +2635,21 @@ static VALUE ruby_curl_easy_http_auth_types_get(VALUE self) {
2609
2635
  /*
2610
2636
  * call-seq:
2611
2637
  * easy.proxy_auth_types = fixnum or nil => fixnum or nil
2638
+ * easy.proxy_auth_types = [:basic,:digest,:digest_ie,:gssnegotiate, :ntlm, :any, :anysafe]
2612
2639
  *
2613
2640
  * Set the proxy authentication types that may be used for the following
2614
2641
  * +perform+ calls. This is a bitmap made by ORing together the
2615
2642
  * Curl::CURLAUTH constants.
2616
2643
  */
2617
- static VALUE ruby_curl_easy_proxy_auth_types_set(VALUE self, VALUE proxy_auth_types) {
2618
- CURB_IMMED_SETTER(ruby_curl_easy, proxy_auth_types, 0);
2644
+ static VALUE ruby_curl_easy_proxy_auth_types_set(int argc, VALUE *argv, VALUE self) {
2645
+ ruby_curl_easy *rbce;
2646
+ VALUE args_ary;
2647
+
2648
+ rb_scan_args(argc, argv, "*", &args_ary);
2649
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2650
+
2651
+ rbce->proxy_auth_types = ruby_curl_easy_auth_types_to_mask(args_ary);
2652
+ return LONG2NUM(rbce->proxy_auth_types);
2619
2653
  }
2620
2654
 
2621
2655
  /*
@@ -6067,8 +6101,9 @@ static VALUE ruby_curl_easy_escape(VALUE self, VALUE svalue) {
6067
6101
 
6068
6102
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
6069
6103
 
6070
- /* NOTE: make sure the value is a string, if not call to_s */
6071
- if( rb_type(str) != T_STRING ) { str = rb_funcall(str,rb_intern("to_s"),0); }
6104
+ /* NOTE: make sure the value is a string; rb_obj_as_string always yields a
6105
+ * String, even if the object's to_s misbehaves and returns a non-String. */
6106
+ if( rb_type(str) != T_STRING ) { str = rb_obj_as_string(str); }
6072
6107
 
6073
6108
  #if (LIBCURL_VERSION_NUM >= 0x070f04)
6074
6109
  result = (char*)curl_easy_escape(rbce->curl, StringValuePtr(str), (int)RSTRING_LEN(str));
@@ -6098,6 +6133,11 @@ static VALUE ruby_curl_easy_unescape(VALUE self, VALUE str) {
6098
6133
 
6099
6134
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
6100
6135
 
6136
+ /* Coerce as its own statement before use below - StringValuePtr and
6137
+ * RSTRING_LEN are sibling args to the same call and C does not guarantee
6138
+ * left-to-right evaluation order between them. */
6139
+ StringValue(str);
6140
+
6101
6141
  #if (LIBCURL_VERSION_NUM >= 0x070f04)
6102
6142
  result = (char*)curl_easy_unescape(rbce->curl, StringValuePtr(str), (int)RSTRING_LEN(str), &rlen);
6103
6143
  #else
@@ -6132,7 +6172,7 @@ void init_curb_easy() {
6132
6172
  idNetworkPolicyNone = rb_intern("none");
6133
6173
  idNetworkPolicyPublic = rb_intern("public");
6134
6174
 
6135
- rbstrAmp = rb_str_new2("&");
6175
+ rbstrAmp = rb_obj_freeze(rb_str_new2("&"));
6136
6176
  rb_global_variable(&rbstrAmp);
6137
6177
 
6138
6178
  cCurlEasy = rb_define_class_under(mCurl, "Easy", rb_cObject);
@@ -6203,7 +6243,7 @@ void init_curb_easy() {
6203
6243
  rb_define_method(cCurlEasy, "proxy_type", ruby_curl_easy_proxy_type_get, 0);
6204
6244
  rb_define_method(cCurlEasy, "http_auth_types=", ruby_curl_easy_http_auth_types_set, -1);
6205
6245
  rb_define_method(cCurlEasy, "http_auth_types", ruby_curl_easy_http_auth_types_get, 0);
6206
- rb_define_method(cCurlEasy, "proxy_auth_types=", ruby_curl_easy_proxy_auth_types_set, 1);
6246
+ rb_define_method(cCurlEasy, "proxy_auth_types=", ruby_curl_easy_proxy_auth_types_set, -1);
6207
6247
  rb_define_method(cCurlEasy, "proxy_auth_types", ruby_curl_easy_proxy_auth_types_get, 0);
6208
6248
  rb_define_method(cCurlEasy, "max_redirects=", ruby_curl_easy_max_redirects_set, 1);
6209
6249
  rb_define_method(cCurlEasy, "max_redirects", ruby_curl_easy_max_redirects_get, 0);
data/ext/curb_easy.h CHANGED
@@ -112,6 +112,7 @@ typedef struct {
112
112
  curb_cidr_rule *network_allowed_cidr_rules;
113
113
  char **network_allowed_hosts;
114
114
 
115
+ /* attachment_sequence of the multi at the time this easy was last added. */
115
116
  unsigned long multi_attachment_generation;
116
117
  curl_off_t downloaded_body_bytes;
117
118
  size_t network_allowed_cidr_rule_count;