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.
- checksums.yaml +4 -4
- data/ChangeLog.md +432 -0
- data/README.md +62 -5
- data/Rakefile +12 -1
- data/ext/curb.c +17 -4
- data/ext/curb.h +4 -4
- data/ext/curb_easy.c +344 -191
- data/ext/curb_easy.h +6 -0
- data/ext/curb_macros.h +16 -0
- data/ext/curb_multi.c +479 -103
- data/ext/curb_multi.h +8 -0
- data/ext/curb_postfield.c +134 -145
- data/ext/curb_postfield.h +10 -1
- data/ext/extconf.rb +6 -0
- data/lib/curl/easy.rb +28 -12
- data/lib/curl/multi.rb +1 -1
- data/lib/curl.rb +58 -19
- data/tests/helper.rb +122 -3
- data/tests/tc_curl_easy.rb +175 -4
- data/tests/tc_curl_easy_cookielist.rb +18 -12
- data/tests/tc_curl_maxfilesize.rb +109 -5
- data/tests/tc_curl_multi.rb +29 -0
- data/tests/tc_curl_multi_poll.rb +223 -0
- data/tests/tc_curl_multi_socket_poller.rb +374 -0
- data/tests/tc_curl_native_coverage.rb +57 -0
- data/tests/tc_curl_network_policy.rb +108 -1
- data/tests/tc_curl_postfield.rb +176 -0
- data/tests/tc_curl_protocols.rb +19 -7
- data/tests/tc_curl_transfer_guard.rb +112 -0
- data/tests/tc_fiber_scheduler.rb +101 -2
- data/tests/tc_ractor.rb +141 -0
- metadata +11 -2
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
|
-
/*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
39
|
-
return if
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
deferred_closes = (state[:deferred_multi_closes] ||= [])
|
|
49
58
|
|
|
50
59
|
if all_threads
|
|
51
|
-
|
|
60
|
+
deferred_closes.shift(deferred_closes.length)
|
|
52
61
|
else
|
|
53
62
|
owner = Thread.current
|
|
54
63
|
remaining = []
|
|
55
64
|
current = []
|
|
56
65
|
|
|
57
|
-
|
|
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
|
-
|
|
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)
|
|
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
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 =
|
|
21
|
-
@network_policy =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
!!(
|
|
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
|
-
|
|
259
|
+
ractor_local_state[:safety_generation] ||= 0
|
|
244
260
|
end
|
|
245
261
|
|
|
246
262
|
def self.bump_safety_generation!
|
|
247
|
-
|
|
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 :
|
|
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
|
-
|
|
504
|
+
completed = state[:completed]
|
|
505
|
+
return if completed.empty?
|
|
485
506
|
|
|
486
|
-
|
|
487
|
-
|
|
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)
|
|
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][
|
|
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
|
|