curb 1.3.7 → 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.
data/ext/curb_multi.h CHANGED
@@ -17,16 +17,24 @@ typedef struct {
17
17
  int running;
18
18
  char closed;
19
19
  char perform_active;
20
+ VALUE perform_thread; /* Owner while a drive loop may release the GVL. */
20
21
  char callback_active;
21
22
  char allow_close_during_perform;
22
23
  CURLM *handle;
23
24
  struct st_table *attached;
25
+ /* Bumped on every add and stamped onto the easy (multi_attachment_generation),
26
+ * so "attached after point X" is a comparison instead of a table snapshot.
27
+ * Monotonic for the object's lifetime; never reset. */
28
+ unsigned long attachment_sequence;
24
29
  } ruby_curl_multi;
25
30
 
26
31
  extern VALUE cCurlMulti;
27
32
  extern const rb_data_type_t ruby_curl_multi_data_type;
28
33
 
29
34
  void init_curb_multi();
35
+ int rb_curl_multi_easy_performing(void *rbce_ptr);
36
+ void rb_curl_multi_check_easy_thread(void *rbce_ptr);
37
+ void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy);
30
38
  void rb_curl_multi_forget_easy(ruby_curl_multi *rbcm, void *rbce_ptr);
31
39
  CURLMcode rb_curl_multi_detach_easy(ruby_curl_multi *rbcm, void *rbce_ptr);
32
40
 
data/ext/curb_postfield.c CHANGED
@@ -20,128 +20,117 @@ VALUE cCurlPostField;
20
20
 
21
21
  /* ================= APPEND FORM FUNC ================ */
22
22
 
23
- /* This gets called by the post method on Curl::Easy for each postfield
24
- * supplied in the arguments. It's job is to add the supplied field to
25
- * the list that's being built for a perform.
26
- *
27
- * THIS FUNC MODIFIES ITS ARGUMENTS. See curl_formadd(3) for details.
28
- */
29
- void append_to_form(VALUE self,
30
- struct curl_httppost **first,
31
- struct curl_httppost **last) {
23
+ /* Snapshot before invoking further Ruby coercions. Native storage is private
24
+ * to this request and cannot move under GC or require VM-global GC roots. */
25
+ static ruby_curl_form_string *form_string_snapshot(VALUE value, ruby_curl_form_string **strings) {
26
+ ruby_curl_form_string *entry;
27
+ long length;
28
+ StringValue(value);
29
+ length = RSTRING_LEN(value);
30
+ /* One allocation for the list entry, payload and terminating NUL. */
31
+ entry = ruby_xmalloc(sizeof(*entry) + (size_t)length + 1);
32
+ entry->data = (char *)(entry + 1);
33
+ entry->length = length;
34
+ memcpy(entry->data, RSTRING_PTR(value), (size_t)length);
35
+ entry->data[length] = '\0';
36
+ entry->next = *strings;
37
+ *strings = entry;
38
+ RB_GC_GUARD(value);
39
+ return entry;
40
+ }
41
+
42
+ static void free_form_strings_until(ruby_curl_form_string **strings,
43
+ ruby_curl_form_string *stop) {
44
+ while (*strings != stop) {
45
+ ruby_curl_form_string *next = (*strings)->next;
46
+ xfree(*strings);
47
+ *strings = next;
48
+ }
49
+ }
50
+
51
+ void free_form_strings(ruby_curl_form_string *strings) {
52
+ free_form_strings_until(&strings, NULL);
53
+ }
54
+
55
+ void append_to_form(VALUE self,
56
+ struct curl_httppost **first,
57
+ struct curl_httppost **last,
58
+ ruby_curl_form_string **strings) {
32
59
  ruby_curl_postfield *rbcpf;
33
60
  CURLFORMcode result = -1;
34
-
61
+ ruby_curl_form_string *name, *content = NULL, *remote = NULL, *local = NULL, *type = NULL;
62
+ ruby_curl_form_string *previous_strings = *strings;
63
+ int is_file;
64
+ struct curl_forms form[8];
65
+ int n = 0;
66
+
35
67
  TypedData_Get_Struct(self, ruby_curl_postfield, &ruby_curl_postfield_data_type, rbcpf);
36
-
37
- if (rbcpf->name == Qnil) {
68
+ if (NIL_P(rbcpf->name)) {
38
69
  rb_raise(eCurlErrInvalidPostField, "Cannot post unnamed field");
39
- } else {
40
- if ((rbcpf->local_file != Qnil) || (rbcpf->remote_file != Qnil)) {
41
- // is a file upload field
42
- if (rbcpf->content_proc != Qnil) {
43
- // with content proc
44
- rbcpf->buffer_str = rb_funcall(rbcpf->content_proc, idCall, 1, self);
45
-
46
- if (rbcpf->remote_file == Qnil) {
47
- rb_raise(eCurlErrInvalidPostField, "Cannot post file upload field with no filename");
48
- } else {
49
- if (rbcpf->content_type == Qnil) {
50
- result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name),
51
- CURLFORM_BUFFER, StringValuePtr(rbcpf->remote_file),
52
- CURLFORM_BUFFERPTR, StringValuePtr(rbcpf->buffer_str),
53
- CURLFORM_BUFFERLENGTH, RSTRING_LEN(rbcpf->buffer_str),
54
- CURLFORM_END);
55
- } else {
56
- result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name),
57
- CURLFORM_BUFFER, StringValuePtr(rbcpf->remote_file),
58
- CURLFORM_BUFFERPTR, StringValuePtr(rbcpf->buffer_str),
59
- CURLFORM_BUFFERLENGTH, RSTRING_LEN(rbcpf->buffer_str),
60
- CURLFORM_CONTENTTYPE, StringValuePtr(rbcpf->content_type),
61
- CURLFORM_END);
62
- }
63
- }
64
- } else if (rbcpf->content != Qnil) {
65
- // with content
66
- if (rbcpf->remote_file == Qnil) {
67
- rb_raise(eCurlErrInvalidPostField, "Cannot post file upload field with no filename");
68
- } else {
69
- if (rbcpf->content_type == Qnil) {
70
- result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name),
71
- CURLFORM_BUFFER, StringValuePtr(rbcpf->remote_file),
72
- CURLFORM_BUFFERPTR, StringValuePtr(rbcpf->content),
73
- CURLFORM_BUFFERLENGTH, RSTRING_LEN(rbcpf->content),
74
- CURLFORM_END);
75
- } else {
76
- result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name),
77
- CURLFORM_BUFFER, StringValuePtr(rbcpf->remote_file),
78
- CURLFORM_BUFFERPTR, StringValuePtr(rbcpf->content),
79
- CURLFORM_BUFFERLENGTH, RSTRING_LEN(rbcpf->content),
80
- CURLFORM_CONTENTTYPE, StringValuePtr(rbcpf->content_type),
81
- CURLFORM_END);
82
- }
83
- }
84
- } else if (rbcpf->local_file != Qnil) {
85
- // with local filename
86
- if (rbcpf->local_file == Qnil) {
87
- rb_raise(eCurlErrInvalidPostField, "Cannot post file upload field no filename");
88
- } else {
89
- if (rbcpf->remote_file == Qnil) {
90
- rbcpf->remote_file = rbcpf->local_file;
91
- }
92
-
93
- if (rbcpf->content_type == Qnil) {
94
- result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name),
95
- CURLFORM_FILE, StringValuePtr(rbcpf->local_file),
96
- CURLFORM_FILENAME, StringValuePtr(rbcpf->remote_file),
97
- CURLFORM_END);
98
- } else {
99
- result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name),
100
- CURLFORM_FILE, StringValuePtr(rbcpf->local_file),
101
- CURLFORM_FILENAME, StringValuePtr(rbcpf->remote_file),
102
- CURLFORM_CONTENTTYPE, StringValuePtr(rbcpf->content_type),
103
- CURLFORM_END);
104
- }
105
- }
106
- } else {
107
- rb_raise(eCurlErrInvalidPostField, "Cannot post file upload field with no data");
70
+ }
71
+ is_file = !NIL_P(rbcpf->local_file) || !NIL_P(rbcpf->remote_file);
72
+ if (!NIL_P(rbcpf->content_proc)) {
73
+ rbcpf->buffer_str = rb_funcall(rbcpf->content_proc, idCall, 1, self);
74
+ StringValue(rbcpf->buffer_str);
75
+ content = form_string_snapshot(rbcpf->buffer_str, strings);
76
+ } else if (!NIL_P(rbcpf->content)) {
77
+ StringValue(rbcpf->content);
78
+ content = form_string_snapshot(rbcpf->content, strings);
79
+ }
80
+
81
+ name = form_string_snapshot(rbcpf->name, strings);
82
+ if (is_file) {
83
+ if (content != NULL) {
84
+ if (NIL_P(rbcpf->remote_file)) {
85
+ rb_raise(eCurlErrInvalidPostField, "Cannot post file upload field with no filename");
108
86
  }
87
+ } else if (!NIL_P(rbcpf->local_file)) {
88
+ local = form_string_snapshot(rbcpf->local_file, strings);
89
+ if (NIL_P(rbcpf->remote_file)) rbcpf->remote_file = rbcpf->local_file;
109
90
  } else {
110
- // is a content field
111
- if (rbcpf->content_proc != Qnil) {
112
- rbcpf->buffer_str = rb_funcall(rbcpf->content_proc, idCall, 1, self);
113
-
114
- if (rbcpf->content_type == Qnil) {
115
- result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name),
116
- CURLFORM_PTRCONTENTS, StringValuePtr(rbcpf->buffer_str),
117
- CURLFORM_CONTENTSLENGTH, RSTRING_LEN(rbcpf->buffer_str),
118
- CURLFORM_END);
119
- } else {
120
- result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name),
121
- CURLFORM_PTRCONTENTS, StringValuePtr(rbcpf->buffer_str),
122
- CURLFORM_CONTENTSLENGTH, RSTRING_LEN(rbcpf->buffer_str),
123
- CURLFORM_CONTENTTYPE, StringValuePtr(rbcpf->content_type),
124
- CURLFORM_END);
125
- }
126
- } else if (rbcpf->content != Qnil) {
127
- if (rbcpf->content_type == Qnil) {
128
- result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name),
129
- CURLFORM_PTRCONTENTS, StringValuePtr(rbcpf->content),
130
- CURLFORM_CONTENTSLENGTH, RSTRING_LEN(rbcpf->content),
131
- CURLFORM_END);
132
- } else {
133
- result = curl_formadd(first, last, CURLFORM_PTRNAME, StringValuePtr(rbcpf->name),
134
- CURLFORM_PTRCONTENTS, StringValuePtr(rbcpf->content),
135
- CURLFORM_CONTENTSLENGTH, RSTRING_LEN(rbcpf->content),
136
- CURLFORM_CONTENTTYPE, StringValuePtr(rbcpf->content_type),
137
- CURLFORM_END);
138
- }
139
- } else {
140
- rb_raise(eCurlErrInvalidPostField, "Cannot post content field with no data");
141
- }
91
+ rb_raise(eCurlErrInvalidPostField, "Cannot post file upload field with no data");
142
92
  }
93
+ remote = form_string_snapshot(rbcpf->remote_file, strings);
94
+ } else if (content == NULL) {
95
+ rb_raise(eCurlErrInvalidPostField, "Cannot post content field with no data");
143
96
  }
144
-
97
+ if (!NIL_P(rbcpf->content_type)) {
98
+ type = form_string_snapshot(rbcpf->content_type, strings);
99
+ }
100
+
101
+ /* libcurl copies metadata but borrows the private payload for the request.
102
+ * This avoids a second form-owned copy of ordinary field contents. */
103
+ form[n].option = CURLFORM_COPYNAME;
104
+ form[n++].value = name->data;
105
+ if (is_file) {
106
+ form[n].option = content == NULL ? CURLFORM_FILE : CURLFORM_BUFFER;
107
+ form[n++].value = (content == NULL ? local : remote)->data;
108
+ if (content == NULL) {
109
+ form[n].option = CURLFORM_FILENAME;
110
+ form[n++].value = remote->data;
111
+ }
112
+ }
113
+ if (type != NULL) {
114
+ form[n].option = CURLFORM_CONTENTTYPE;
115
+ form[n++].value = type->data;
116
+ }
117
+ form[n].option = CURLFORM_END;
118
+ form[n].value = NULL;
119
+
120
+ if (content == NULL) {
121
+ result = curl_formadd(first, last, CURLFORM_ARRAY, form, CURLFORM_END);
122
+ } else {
123
+ result = curl_formadd(first, last, CURLFORM_ARRAY, form,
124
+ is_file ? CURLFORM_BUFFERPTR : CURLFORM_PTRCONTENTS,
125
+ content->data,
126
+ is_file ? CURLFORM_BUFFERLENGTH : CURLFORM_CONTENTSLENGTH,
127
+ content->length, CURLFORM_END);
128
+ }
129
+
130
+ /* Metadata was copied by curl_formadd. Retain only the payload, if any,
131
+ * which was the first snapshot allocated for this field. */
132
+ free_form_strings_until(strings, content ? content : previous_strings);
133
+
145
134
  if (result != 0) {
146
135
  const char *reason = NULL;
147
136
 
@@ -468,7 +457,10 @@ static VALUE ruby_curl_postfield_to_str(VALUE self) {
468
457
  name = rbcpf->name;
469
458
  if (TYPE(name) != T_STRING) {
470
459
  if (rb_respond_to(name, rb_intern("to_s")))
471
- name = rb_funcall(name, rb_intern("to_s"), 0);
460
+ /* rb_obj_as_string always yields a String, even if #to_s misbehaves
461
+ * and returns a non-String; a raw rb_funcall result could later be
462
+ * handed to RSTRING_PTR/RSTRING_LEN and crash. */
463
+ name = rb_obj_as_string(name);
472
464
  else
473
465
  name = Qnil;
474
466
  }
@@ -481,6 +473,33 @@ static VALUE ruby_curl_postfield_to_str(VALUE self) {
481
473
  /* Force field name to UTF-8 before escaping */
482
474
  VALUE name_utf8 = rb_str_export_to_enc(name, rb_utf8_encoding());
483
475
 
476
+ /* Resolve and coerce the content before creating the curl handle: the
477
+ * content proc, #to_s and the UTF-8 export can all raise, which would
478
+ * otherwise leak the handle. */
479
+ VALUE tmpcontent = Qnil;
480
+ if (rbcpf->content_proc != Qnil) {
481
+ tmpcontent = rb_funcall(rbcpf->content_proc, idCall, 1, self);
482
+ } else if (rbcpf->content != Qnil) {
483
+ tmpcontent = rbcpf->content;
484
+ } else if (rbcpf->local_file != Qnil) {
485
+ tmpcontent = rbcpf->local_file;
486
+ } else if (rbcpf->remote_file != Qnil) {
487
+ tmpcontent = rbcpf->remote_file;
488
+ } else {
489
+ tmpcontent = rb_str_new2("");
490
+ }
491
+ if (TYPE(tmpcontent) != T_STRING) {
492
+ if (rb_respond_to(tmpcontent, rb_intern("to_s")))
493
+ /* rb_obj_as_string always yields a String, even if #to_s misbehaves */
494
+ tmpcontent = rb_obj_as_string(tmpcontent);
495
+ else
496
+ rb_raise(rb_eRuntimeError,
497
+ "postfield(%"PRIsVALUE") is not a string and does not respond_to to_s",
498
+ name);
499
+ }
500
+ /* Force content to UTF-8 before escaping */
501
+ VALUE content_utf8 = rb_str_export_to_enc(tmpcontent, rb_utf8_encoding());
502
+
484
503
  #ifdef HAVE_CURL_EASY_ESCAPE
485
504
  curl_handle = curl_easy_init();
486
505
  if (!curl_handle) {
@@ -499,38 +518,8 @@ static VALUE ruby_curl_postfield_to_str(VALUE self) {
499
518
  #endif
500
519
 
501
520
  VALUE escd_name = rb_str_new2(tmpchrs);
502
- #ifdef HAVE_CURL_EASY_ESCAPE
503
- curl_free(tmpchrs);
504
- #else
505
521
  curl_free(tmpchrs);
506
- #endif
507
522
 
508
- VALUE tmpcontent = Qnil;
509
- if (rbcpf->content_proc != Qnil) {
510
- tmpcontent = rb_funcall(rbcpf->content_proc, idCall, 1, self);
511
- } else if (rbcpf->content != Qnil) {
512
- tmpcontent = rbcpf->content;
513
- } else if (rbcpf->local_file != Qnil) {
514
- tmpcontent = rbcpf->local_file;
515
- } else if (rbcpf->remote_file != Qnil) {
516
- tmpcontent = rbcpf->remote_file;
517
- } else {
518
- tmpcontent = rb_str_new2("");
519
- }
520
- if (TYPE(tmpcontent) != T_STRING) {
521
- if (rb_respond_to(tmpcontent, rb_intern("to_s")))
522
- tmpcontent = rb_funcall(tmpcontent, rb_intern("to_s"), 0);
523
- else {
524
- #ifdef HAVE_CURL_EASY_ESCAPE
525
- curl_easy_cleanup(curl_handle);
526
- #endif
527
- rb_raise(rb_eRuntimeError,
528
- "postfield(%s) is not a string and does not respond_to to_s",
529
- RSTRING_PTR(escd_name));
530
- }
531
- }
532
- /* Force content to UTF-8 before escaping */
533
- VALUE content_utf8 = rb_str_export_to_enc(tmpcontent, rb_utf8_encoding());
534
523
  #ifdef HAVE_CURL_EASY_ESCAPE
535
524
  tmpchrs = curl_easy_escape(curl_handle, StringValuePtr(content_utf8), (int)RSTRING_LEN(content_utf8));
536
525
  if (!tmpchrs) {
data/ext/curb_postfield.h CHANGED
@@ -32,9 +32,18 @@ typedef struct {
32
32
  extern VALUE cCurlPostField;
33
33
  extern const rb_data_type_t ruby_curl_postfield_data_type;
34
34
 
35
+ typedef struct ruby_curl_form_string {
36
+ struct ruby_curl_form_string *next;
37
+ long length;
38
+ char *data;
39
+ } ruby_curl_form_string;
40
+
41
+ void free_form_strings(ruby_curl_form_string *strings);
42
+
35
43
  void append_to_form(VALUE self,
36
44
  struct curl_httppost **first,
37
- struct curl_httppost **last);
45
+ struct curl_httppost **last,
46
+ ruby_curl_form_string **strings);
38
47
 
39
48
  void init_curb_postfield();
40
49
 
data/ext/extconf.rb CHANGED
@@ -260,6 +260,7 @@ have_constant "curl_version_idn"
260
260
  have_constant "curl_version_sspi"
261
261
  have_constant "curl_version_conv"
262
262
  have_constant "curl_version_http2"
263
+ have_constant "curl_version_threadsafe"
263
264
  have_constant "curlproxy_http"
264
265
  have_constant "curlproxy_socks4"
265
266
  have_constant "curlproxy_socks4a"
@@ -704,6 +705,7 @@ have_func('rb_fiber_scheduler_current', 'ruby/fiber/scheduler.h')
704
705
  have_func('rb_fiber_scheduler_io_wait', 'ruby/fiber/scheduler.h')
705
706
  have_func('rb_fiber_scheduler_io_select', 'ruby/fiber/scheduler.h')
706
707
  have_func('rb_fiber_scheduler_kernel_sleep', 'ruby/fiber/scheduler.h')
708
+ have_func('rb_ext_ractor_safe')
707
709
  have_func('rb_io_stdio_file')
708
710
  have_func('curl_multi_wait')
709
711
  have_func('curl_multi_socket_action')
@@ -714,6 +716,10 @@ have_func('curl_multi_socket')
714
716
  have_func('curl_multi_timer_callback')
715
717
  have_constant 'curlmopt_socketfunction'
716
718
  have_constant 'curlmopt_timerfunction'
719
+ # Kernel event queues let the socket-action loop wait on one descriptor for
720
+ # every libcurl socket under a fiber scheduler.
721
+ have_header('sys/epoll.h') && have_func('epoll_create1', 'sys/epoll.h')
722
+ have_header('sys/event.h') && have_func('kqueue', ['sys/types.h', 'sys/event.h'])
717
723
  have_func('curl_easy_duphandle')
718
724
 
719
725
  # Optional: enable verbose socket-action debug logging.
data/lib/curl/easy.rb CHANGED
@@ -3,13 +3,20 @@ require 'curl/download'
3
3
  module Curl
4
4
  class Easy
5
5
  class << self
6
+ def deferred_multi_close_state
7
+ Curl.__send__(:ractor_local_state)
8
+ end
9
+ private :deferred_multi_close_state
10
+
6
11
  def deferred_multi_close_mutex
7
- @deferred_multi_close_mutex ||= Mutex.new
12
+ state = deferred_multi_close_state
13
+ state[:deferred_multi_close_mutex] ||= Mutex.new
8
14
  end
9
15
 
10
16
  def deferred_multi_closes
17
+ state = deferred_multi_close_state
11
18
  deferred_multi_close_mutex.synchronize do
12
- (@deferred_multi_closes ||= []).dup
19
+ (state[:deferred_multi_closes] ||= []).dup
13
20
  end
14
21
  end
15
22
 
@@ -34,27 +41,29 @@ module Curl
34
41
  end
35
42
 
36
43
  def defer_multi_close(multi, easy, owner: Thread.current)
44
+ state = deferred_multi_close_state
37
45
  deferred_multi_close_mutex.synchronize do
38
- @deferred_multi_closes ||= []
39
- return if @deferred_multi_closes.any? { |entry| entry[:multi].equal?(multi) }
46
+ state[:deferred_multi_closes] ||= []
47
+ return if state[:deferred_multi_closes].any? { |entry| entry[:multi].equal?(multi) }
40
48
 
41
49
  multi.instance_variable_set(:@deferred_close, true)
42
- @deferred_multi_closes << { multi: multi, easy: easy, owner: owner }
50
+ state[:deferred_multi_closes] << { multi: multi, easy: easy, owner: owner }
43
51
  end
44
52
  end
45
53
 
46
54
  def flush_deferred_multi_closes(all_threads: false)
55
+ state = deferred_multi_close_state
47
56
  pending = deferred_multi_close_mutex.synchronize do
48
- @deferred_multi_closes ||= []
57
+ deferred_closes = (state[:deferred_multi_closes] ||= [])
49
58
 
50
59
  if all_threads
51
- @deferred_multi_closes.shift(@deferred_multi_closes.length)
60
+ deferred_closes.shift(deferred_closes.length)
52
61
  else
53
62
  owner = Thread.current
54
63
  remaining = []
55
64
  current = []
56
65
 
57
- @deferred_multi_closes.each do |entry|
66
+ deferred_closes.each do |entry|
58
67
  if entry[:owner].equal?(owner)
59
68
  current << entry
60
69
  else
@@ -62,7 +71,7 @@ module Curl
62
71
  end
63
72
  end
64
73
 
65
- @deferred_multi_closes = remaining
74
+ state[:deferred_multi_closes] = remaining
66
75
  current
67
76
  end
68
77
  end
@@ -175,10 +184,16 @@ module Curl
175
184
  set_protocol_allowlist('CURLOPT_REDIR_PROTOCOLS_STR', 'CURLOPT_REDIR_PROTOCOLS', protocols)
176
185
  end
177
186
 
178
- def safe_http!
187
+ def safe_http!(options = {})
188
+ options = Curl.__send__(:safe_http_options, options)
189
+ config = Curl.__send__(:ractor_local_state)[:safety_config] || Curl::SafetyConfig.new
190
+ limit = Curl::SafetyConfig.new
191
+ limit.max_body_bytes = options.fetch(:max_body_bytes, config.max_body_bytes)
179
192
  __curb_set_safety_override!(
180
193
  :protocols => [:http, :https],
181
- :redirect_protocols => [:http, :https]
194
+ :redirect_protocols => [:http, :https],
195
+ :network_policy => :public,
196
+ :max_body_bytes => limit.max_body_bytes
182
197
  )
183
198
  end
184
199
 
@@ -188,7 +203,8 @@ module Curl
188
203
  @__curb_safety_override = (defined?(@__curb_safety_override) && @__curb_safety_override) ? @__curb_safety_override.dup : {}
189
204
  @__curb_safety_override[:protocols] = Array(options[:protocols]).map { |protocol| protocol.to_s.downcase.to_sym } if options.key?(:protocols)
190
205
  @__curb_safety_override[:redirect_protocols] = Array(options[:redirect_protocols]).map { |protocol| protocol.to_s.downcase.to_sym } if options.key?(:redirect_protocols)
191
- @__curb_safety_override[:max_body_bytes] = options[:max_body_bytes] if options.key?(:max_body_bytes) && options[:max_body_bytes]
206
+ @__curb_safety_override[:max_body_bytes] = options[:max_body_bytes] if options.key?(:max_body_bytes)
207
+ @__curb_safety_override[:network_policy] = options[:network_policy] if options.key?(:network_policy)
192
208
  @__curb_safety_override_generation = __curb_safety_override_generation + 1
193
209
  Curl.__send__(:apply_safety!, self) if Curl.respond_to?(:apply_safety!, true)
194
210
  self
data/lib/curl/multi.rb CHANGED
@@ -454,9 +454,9 @@ module Curl
454
454
 
455
455
  def remove(easy)
456
456
  return self if !requests[easy.object_id]
457
+ _remove(easy)
457
458
  requests.delete(easy.object_id)
458
459
  __curb_native_safety_signatures.delete(easy.object_id)
459
- _remove(easy)
460
460
  self
461
461
  end
462
462
 
data/lib/curl.rb CHANGED
@@ -8,8 +8,22 @@ require 'uri'
8
8
 
9
9
  # expose shortcut methods
10
10
  module Curl
11
+ # Module/class instance variables cannot be read or written by non-main
12
+ # Ractors, even when the value itself would otherwise be safe. Keep curb's
13
+ # mutable Ruby-side configuration in storage owned by the current Ractor.
14
+ # Older Rubies retain the historical process-wide state.
15
+ def self.ractor_local_state
16
+ if defined?(Ractor) && Ractor.respond_to?(:current) &&
17
+ Ractor.current.respond_to?(:[]) && Ractor.current.respond_to?(:[]=)
18
+ Ractor.current[:__curb_ractor_local_state] ||= {}
19
+ else
20
+ @legacy_local_state ||= {}
21
+ end
22
+ end
23
+
11
24
  class SafetyConfig
12
25
  DEFAULT_PROTOCOLS = [:http, :https].freeze
26
+ DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024
13
27
 
14
28
  attr_reader :max_body_bytes, :network_policy
15
29
  attr_accessor :allow_proxies, :allow_resolve, :allow_connect_to, :allow_doh, :allow_unix_socket
@@ -17,8 +31,8 @@ module Curl
17
31
  def initialize
18
32
  @protocols = DEFAULT_PROTOCOLS
19
33
  @redirect_protocols = nil
20
- @max_body_bytes = nil
21
- @network_policy = nil
34
+ @max_body_bytes = DEFAULT_MAX_BODY_BYTES
35
+ @network_policy = :public
22
36
  @allowed_hosts = nil
23
37
  @allowed_proxy_hosts = nil
24
38
  @allowed_cidrs = nil
@@ -176,12 +190,12 @@ module Curl
176
190
  def self.safe!
177
191
  config = SafetyConfig.new
178
192
  yield config if block_given?
179
- @safety_config = config
193
+ ractor_local_state[:safety_config] = config
180
194
  bump_safety_generation!
181
195
  end
182
196
 
183
197
  def self.apply_safety!(easy)
184
- config = @safety_config
198
+ config = ractor_local_state[:safety_config]
185
199
  override = safety_override_for(easy)
186
200
  return easy unless config || override
187
201
 
@@ -204,9 +218,11 @@ module Curl
204
218
  apply_allowed_hosts!(easy, config.allowed_hosts) if config&.allowed_hosts
205
219
  apply_allowed_cidrs!(easy, config) if config&.allowed_cidrs
206
220
 
207
- if config&.network_policy
208
- easy.network_policy = config.network_policy
209
- apply_public_network_policy_controls!(easy, config) if config.network_policy == :public
221
+ network_policy = config&.network_policy
222
+ network_policy = :public if override && override[:network_policy] == :public
223
+ if network_policy
224
+ easy.network_policy = network_policy
225
+ apply_public_network_policy_controls!(easy, config || SafetyConfig.new) if network_policy == :public
210
226
  end
211
227
 
212
228
  apply_max_body_bytes!(easy, config.max_body_bytes) if config&.max_body_bytes
@@ -215,12 +231,12 @@ module Curl
215
231
  end
216
232
 
217
233
  def self.clear_safe!
218
- @safety_config = nil
234
+ ractor_local_state[:safety_config] = nil
219
235
  bump_safety_generation!
220
236
  end
221
237
 
222
238
  def self.safety_active_for?(easy)
223
- !!(@safety_config || safety_override_for(easy))
239
+ !!(ractor_local_state[:safety_config] || safety_override_for(easy))
224
240
  end
225
241
 
226
242
  def self.safety_signature_for(easy)
@@ -240,11 +256,11 @@ module Curl
240
256
  end
241
257
 
242
258
  def self.safety_generation
243
- @safety_generation ||= 0
259
+ ractor_local_state[:safety_generation] ||= 0
244
260
  end
245
261
 
246
262
  def self.bump_safety_generation!
247
- @safety_generation = safety_generation + 1
263
+ ractor_local_state[:safety_generation] = safety_generation + 1
248
264
  end
249
265
 
250
266
  def self.safety_protocol_intersection(base_protocols, override_protocols)
@@ -395,7 +411,8 @@ module Curl
395
411
  easy.set(Curl::CURLOPT_NOPROXY, "*") if Curl.const_defined?(:CURLOPT_NOPROXY)
396
412
  end
397
413
 
398
- private_class_method :apply_safety!, :clear_safe!, :safety_active_for?,
414
+ private_class_method :ractor_local_state,
415
+ :apply_safety!, :clear_safe!, :safety_active_for?,
399
416
  :safety_signature_for, :safety_override_for,
400
417
  :safety_generation, :bump_safety_generation!,
401
418
  :safety_protocol_intersection,
@@ -480,23 +497,44 @@ module Curl
480
497
  end
481
498
  end
482
499
 
500
+ # Wakes the waiters whose transfers finished since the last call. Completions
501
+ # are queued by the on_complete wrapper in perform_with_scheduler, so each
502
+ # drive-loop pass costs O(new completions) rather than O(all waiters).
483
503
  def self.release_scheduler_waiters(state)
484
- source_id = deferred_exception_source_id(state)
504
+ completed = state[:completed]
505
+ return if completed.empty?
485
506
 
486
- state[:waiters].each do |easy_id, waiter|
487
- next if source_id == easy_id
507
+ source_id = deferred_exception_source_id(state)
508
+ retained = nil
509
+
510
+ until completed.empty?
511
+ easy_id, waiter = completed.shift
512
+ # A waiter whose perform already returned (it raised) is no longer
513
+ # blocked on this completion; waking its fiber would be spurious.
514
+ next unless state[:waiters][easy_id].equal?(waiter)
515
+
516
+ if source_id == easy_id
517
+ # The transfer that raised the deferred exception gets that error via
518
+ # release_scheduler_error. Keep it queued in case the source changes.
519
+ (retained ||= []) << [easy_id, waiter]
520
+ next
521
+ end
488
522
 
489
- complete_scheduler_waiter(waiter) if waiter[:completed]
523
+ complete_scheduler_waiter(waiter)
490
524
  end
525
+
526
+ completed.concat(retained) if retained
491
527
  end
492
528
 
493
529
  def self.perform_with_scheduler(easy)
494
530
  state = scheduler_state
531
+ easy_id = easy.object_id
495
532
  waiter = {completed: false, done: false, error: nil, fiber: nil, scheduler: Fiber.scheduler}
496
- state[:waiters][easy.object_id] = waiter
533
+ state[:waiters][easy_id] = waiter
497
534
  previous_complete = easy.on_complete do |completed_easy|
498
535
  previous_complete.call(completed_easy) if previous_complete
499
536
  waiter[:completed] = true
537
+ state[:completed] << [easy_id, waiter]
500
538
  end
501
539
 
502
540
  state[:pending] << easy
@@ -533,6 +571,8 @@ module Curl
533
571
  driver_running: false,
534
572
  error: nil,
535
573
  waiters: {},
574
+ # [easy object_id, waiter] pairs completed but not yet woken
575
+ completed: [],
536
576
  }
537
577
  Thread.current.thread_variable_set(:curb_scheduler_state, state)
538
578
  state
@@ -616,8 +656,7 @@ module Curl
616
656
 
617
657
  http(verb, url, post_body, put_data) do |handle|
618
658
  yield handle if block
619
- handle.safe_http!
620
- handle.max_body_bytes = options[:max_body_bytes] if options.key?(:max_body_bytes)
659
+ handle.safe_http!(options)
621
660
  end
622
661
  end
623
662