curb 1.3.2 → 1.3.4
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/ext/curb.h +3 -3
- data/ext/curb_easy.c +114 -45
- data/ext/curb_multi.c +231 -34
- data/ext/curb_multi.h +3 -0
- data/tests/bug_curb_easy_blocks_ruby_threads.rb +3 -3
- data/tests/bug_follow_redirect_288.rb +7 -7
- data/tests/bug_raise_on_callback.rb +4 -3
- data/tests/helper.rb +7 -0
- data/tests/tc_curl_easy.rb +57 -0
- data/tests/tc_curl_easy_resolve.rb +32 -0
- data/tests/tc_curl_multi.rb +56 -0
- data/tests/tc_fiber_scheduler.rb +1 -1
- data/tests/tc_ftp_options.rb +14 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 91423579fc22ce296ae84abfa27438de27d2e9bf6da58658ce86939aabecfe86
|
|
4
|
+
data.tar.gz: 46f1a051e546934eee05283bdbed84635ae1be3f46adaf48625140f260feb27f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b863797088f6ade8e6a48c3018a7f24b4469fda0daa35cabdacc826452b31c028ba75a2d0fd9bfefeb90817a39421110979d7013b0a573c7b06016f27e7d93fd
|
|
7
|
+
data.tar.gz: 851be0f60e8e7e53db42f2e6b03742c403c55c7d2242282dd4bab8b77f33c4b0eb206fb221c7f26d26dafa2f6fdddd7a0e84369ca94a0afd038510910cf7d5de
|
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.
|
|
32
|
-
#define CURB_VER_NUM
|
|
31
|
+
#define CURB_VERSION "1.3.4"
|
|
32
|
+
#define CURB_VER_NUM 1034
|
|
33
33
|
#define CURB_VER_MAJ 1
|
|
34
34
|
#define CURB_VER_MIN 3
|
|
35
|
-
#define CURB_VER_MIC
|
|
35
|
+
#define CURB_VER_MIC 4
|
|
36
36
|
#define CURB_VER_PATCH 0
|
|
37
37
|
|
|
38
38
|
|
data/ext/curb_easy.c
CHANGED
|
@@ -829,6 +829,10 @@ static VALUE ruby_curl_easy_clone(VALUE self) {
|
|
|
829
829
|
|
|
830
830
|
/* now deep copy */
|
|
831
831
|
newrbce->curl = curl_easy_duphandle(rbce->curl);
|
|
832
|
+
if (!newrbce->curl) {
|
|
833
|
+
free(newrbce);
|
|
834
|
+
rb_raise(rb_eNoMemError, "Failed to duplicate Curl::Easy handle");
|
|
835
|
+
}
|
|
832
836
|
newrbce->curl_headers = (rbce->curl_headers) ? duplicate_curl_slist(rbce->curl_headers) : NULL;
|
|
833
837
|
newrbce->curl_proxy_headers = (rbce->curl_proxy_headers) ? duplicate_curl_slist(rbce->curl_proxy_headers) : NULL;
|
|
834
838
|
newrbce->curl_ftp_commands = (rbce->curl_ftp_commands) ? duplicate_curl_slist(rbce->curl_ftp_commands) : NULL;
|
|
@@ -1367,35 +1371,15 @@ static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
|
|
|
1367
1371
|
ruby_curl_easy *rbce;
|
|
1368
1372
|
CURL *curl;
|
|
1369
1373
|
VALUE upload;
|
|
1374
|
+
VALUE upload_stream = data;
|
|
1370
1375
|
VALUE headers;
|
|
1376
|
+
VALUE infile_size = Qnil;
|
|
1371
1377
|
|
|
1372
1378
|
TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
|
|
1373
1379
|
|
|
1374
|
-
upload = ruby_curl_upload_new(cCurlUpload);
|
|
1375
|
-
ruby_curl_upload_stream_set(upload,data);
|
|
1376
|
-
|
|
1377
|
-
curl = rbce->curl;
|
|
1378
|
-
rb_easy_set("upload", upload); /* keep the upload object alive as long as
|
|
1379
|
-
the easy handle is active or until the upload
|
|
1380
|
-
is complete or terminated... */
|
|
1381
|
-
|
|
1382
|
-
curl_easy_setopt(curl, CURLOPT_NOBODY, 0);
|
|
1383
|
-
curl_easy_setopt(curl, CURLOPT_POST, 0);
|
|
1384
|
-
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, NULL);
|
|
1385
|
-
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0);
|
|
1386
|
-
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
|
|
1387
|
-
curl_easy_setopt(curl, CURLOPT_READFUNCTION, (curl_read_callback)read_data_handler);
|
|
1388
|
-
#ifdef HAVE_CURLOPT_SEEKFUNCTION
|
|
1389
|
-
curl_easy_setopt(curl, CURLOPT_SEEKFUNCTION, (curl_seek_callback)seek_data_handler);
|
|
1390
|
-
#endif
|
|
1391
|
-
curl_easy_setopt(curl, CURLOPT_READDATA, rbce);
|
|
1392
|
-
#ifdef HAVE_CURLOPT_SEEKDATA
|
|
1393
|
-
curl_easy_setopt(curl, CURLOPT_SEEKDATA, rbce);
|
|
1394
|
-
#endif
|
|
1395
|
-
|
|
1396
1380
|
/*
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
1381
|
+
* Validate and prepare Ruby-visible state before mutating the CURL handle.
|
|
1382
|
+
* Several branches below can raise (header type, stat, size, to_s).
|
|
1399
1383
|
*/
|
|
1400
1384
|
if (!rb_easy_nil("headers")) {
|
|
1401
1385
|
if (rb_easy_type_check("headers", T_ARRAY) || rb_easy_type_check("headers", T_STRING)) {
|
|
@@ -1403,43 +1387,80 @@ static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
|
|
|
1403
1387
|
}
|
|
1404
1388
|
}
|
|
1405
1389
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1390
|
+
if (!NIL_P(data) && !rb_respond_to(data, rb_intern("read"))) {
|
|
1391
|
+
if (rb_respond_to(data, rb_intern("to_s"))) {
|
|
1392
|
+
upload_stream = rb_obj_as_string(data);
|
|
1393
|
+
} else {
|
|
1394
|
+
rb_raise(rb_eRuntimeError, "PUT data must respond to read or to_s");
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1408
1397
|
|
|
1409
1398
|
headers = rb_easy_get("headers");
|
|
1410
1399
|
if( headers == Qnil ) {
|
|
1411
1400
|
headers = rb_hash_new();
|
|
1412
1401
|
}
|
|
1413
1402
|
|
|
1414
|
-
if (rb_respond_to(data, rb_intern("read"))) {
|
|
1415
|
-
VALUE stat =
|
|
1416
|
-
if
|
|
1403
|
+
if (!NIL_P(data) && rb_respond_to(data, rb_intern("read"))) {
|
|
1404
|
+
VALUE stat = Qnil;
|
|
1405
|
+
if (rb_respond_to(data, rb_intern("stat"))) {
|
|
1406
|
+
stat = rb_funcall(data, rb_intern("stat"), 0);
|
|
1407
|
+
}
|
|
1408
|
+
if(!NIL_P(stat) && stat != Qfalse && rb_hash_aref(headers, rb_str_new2("Content-Length")) == Qnil) {
|
|
1417
1409
|
VALUE size;
|
|
1418
1410
|
if( rb_hash_aref(headers, rb_str_new2("Expect")) == Qnil ) {
|
|
1419
1411
|
rb_hash_aset(headers, rb_str_new2("Expect"), rb_str_new2(""));
|
|
1420
1412
|
}
|
|
1421
1413
|
size = rb_funcall(stat, rb_intern("size"), 0);
|
|
1422
|
-
|
|
1414
|
+
infile_size = size;
|
|
1423
1415
|
}
|
|
1424
1416
|
else if( rb_hash_aref(headers, rb_str_new2("Content-Length")) == Qnil && rb_hash_aref(headers, rb_str_new2("Transfer-Encoding")) == Qnil ) {
|
|
1425
1417
|
rb_hash_aset(headers, rb_str_new2("Transfer-Encoding"), rb_str_new2("chunked"));
|
|
1426
1418
|
}
|
|
1427
1419
|
else if( rb_hash_aref(headers, rb_str_new2("Content-Length")) ) {
|
|
1428
1420
|
VALUE size = rb_funcall(rb_hash_aref(headers, rb_str_new2("Content-Length")), rb_intern("to_i"), 0);
|
|
1429
|
-
|
|
1421
|
+
infile_size = size;
|
|
1430
1422
|
}
|
|
1431
1423
|
}
|
|
1432
|
-
else if (rb_respond_to(data, rb_intern("to_s"))) {
|
|
1433
|
-
|
|
1424
|
+
else if (!NIL_P(data) && rb_respond_to(data, rb_intern("to_s"))) {
|
|
1425
|
+
infile_size = LONG2NUM(RSTRING_LEN(upload_stream));
|
|
1434
1426
|
if( rb_hash_aref(headers, rb_str_new2("Expect")) == Qnil ) {
|
|
1435
1427
|
rb_hash_aset(headers, rb_str_new2("Expect"), rb_str_new2(""));
|
|
1436
1428
|
}
|
|
1437
1429
|
}
|
|
1430
|
+
else if (NIL_P(data)) {
|
|
1431
|
+
/* Preserve legacy nil handling: configure an upload with no payload. */
|
|
1432
|
+
}
|
|
1438
1433
|
else {
|
|
1439
1434
|
rb_raise(rb_eRuntimeError, "PUT data must respond to read or to_s");
|
|
1440
1435
|
}
|
|
1441
1436
|
rb_easy_set("headers",headers);
|
|
1442
1437
|
|
|
1438
|
+
upload = ruby_curl_upload_new(cCurlUpload);
|
|
1439
|
+
ruby_curl_upload_stream_set(upload, upload_stream);
|
|
1440
|
+
|
|
1441
|
+
curl = rbce->curl;
|
|
1442
|
+
rb_easy_set("upload", upload); /* keep the upload object alive as long as
|
|
1443
|
+
the easy handle is active or until the upload
|
|
1444
|
+
is complete or terminated... */
|
|
1445
|
+
|
|
1446
|
+
curl_easy_setopt(curl, CURLOPT_NOBODY, 0);
|
|
1447
|
+
curl_easy_setopt(curl, CURLOPT_POST, 0);
|
|
1448
|
+
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, NULL);
|
|
1449
|
+
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0);
|
|
1450
|
+
curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
|
|
1451
|
+
curl_easy_setopt(curl, CURLOPT_READFUNCTION, (curl_read_callback)read_data_handler);
|
|
1452
|
+
#ifdef HAVE_CURLOPT_SEEKFUNCTION
|
|
1453
|
+
curl_easy_setopt(curl, CURLOPT_SEEKFUNCTION, (curl_seek_callback)seek_data_handler);
|
|
1454
|
+
#endif
|
|
1455
|
+
curl_easy_setopt(curl, CURLOPT_READDATA, rbce);
|
|
1456
|
+
#ifdef HAVE_CURLOPT_SEEKDATA
|
|
1457
|
+
curl_easy_setopt(curl, CURLOPT_SEEKDATA, rbce);
|
|
1458
|
+
#endif
|
|
1459
|
+
|
|
1460
|
+
if (!NIL_P(infile_size)) {
|
|
1461
|
+
curl_easy_setopt(curl, CURLOPT_INFILESIZE, NUM2LONG(infile_size));
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1443
1464
|
// if we made it this far, all should be well.
|
|
1444
1465
|
return data;
|
|
1445
1466
|
}
|
|
@@ -2754,7 +2775,7 @@ static VALUE cb_each_ftp_command(VALUE ftp_command, VALUE wrap, int _c, const VA
|
|
|
2754
2775
|
TypedData_Get_Struct(wrap, struct curl_slist *, &curl_slist_ptr_type, list);
|
|
2755
2776
|
|
|
2756
2777
|
ftp_command_string = rb_obj_as_string(ftp_command);
|
|
2757
|
-
struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(
|
|
2778
|
+
struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(ftp_command_string));
|
|
2758
2779
|
if (!new_list) {
|
|
2759
2780
|
rb_raise(rb_eNoMemError, "Failed to append to FTP command list");
|
|
2760
2781
|
}
|
|
@@ -2772,7 +2793,7 @@ static VALUE cb_each_resolve(VALUE resolve, VALUE wrap, int _c, const VALUE *_pt
|
|
|
2772
2793
|
TypedData_Get_Struct(wrap, struct curl_slist *, &curl_slist_ptr_type, list);
|
|
2773
2794
|
|
|
2774
2795
|
resolve_string = rb_obj_as_string(resolve);
|
|
2775
|
-
struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(
|
|
2796
|
+
struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(resolve_string));
|
|
2776
2797
|
if (!new_list) {
|
|
2777
2798
|
rb_raise(rb_eNoMemError, "Failed to append to resolve list");
|
|
2778
2799
|
}
|
|
@@ -3153,6 +3174,13 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
|
|
|
3153
3174
|
if (rb_easy_type_check("resolve", T_ARRAY)) {
|
|
3154
3175
|
VALUE wrap = TypedData_Wrap_Struct(rb_cObject, &curl_slist_ptr_type, rslv);
|
|
3155
3176
|
rb_block_call(rb_easy_get("resolve"), rb_intern("each"), 0, NULL, cb_each_resolve, wrap);
|
|
3177
|
+
} else {
|
|
3178
|
+
VALUE resolve_str = rb_obj_as_string(rb_easy_get("resolve"));
|
|
3179
|
+
struct curl_slist *new_list = curl_slist_append(*rslv, StringValuePtr(resolve_str));
|
|
3180
|
+
if (!new_list) {
|
|
3181
|
+
rb_raise(rb_eNoMemError, "Failed to append to resolve list");
|
|
3182
|
+
}
|
|
3183
|
+
*rslv = new_list;
|
|
3156
3184
|
}
|
|
3157
3185
|
|
|
3158
3186
|
if (*rslv) {
|
|
@@ -3209,6 +3237,44 @@ VALUE ruby_curl_easy_cleanup( VALUE self, ruby_curl_easy *rbce ) {
|
|
|
3209
3237
|
return Qnil;
|
|
3210
3238
|
}
|
|
3211
3239
|
|
|
3240
|
+
struct easy_perform_request_restore_args {
|
|
3241
|
+
VALUE self;
|
|
3242
|
+
CURL *curl;
|
|
3243
|
+
ruby_curl_easy *rbce;
|
|
3244
|
+
int clear_customrequest;
|
|
3245
|
+
int clear_nobody;
|
|
3246
|
+
int clear_postfields;
|
|
3247
|
+
};
|
|
3248
|
+
|
|
3249
|
+
static VALUE perform_with_request_restore_body(VALUE argp) {
|
|
3250
|
+
struct easy_perform_request_restore_args *args = (struct easy_perform_request_restore_args *)argp;
|
|
3251
|
+
return rb_funcall(args->self, rb_intern("perform"), 0);
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
static VALUE perform_with_request_restore_ensure(VALUE argp) {
|
|
3255
|
+
struct easy_perform_request_restore_args *args = (struct easy_perform_request_restore_args *)argp;
|
|
3256
|
+
|
|
3257
|
+
if (args->curl) {
|
|
3258
|
+
if (args->clear_nobody) {
|
|
3259
|
+
curl_easy_setopt(args->curl, CURLOPT_NOBODY, 0L);
|
|
3260
|
+
}
|
|
3261
|
+
if (args->clear_customrequest) {
|
|
3262
|
+
curl_easy_setopt(args->curl, CURLOPT_CUSTOMREQUEST, NULL);
|
|
3263
|
+
}
|
|
3264
|
+
if (args->clear_postfields) {
|
|
3265
|
+
curl_easy_setopt(args->curl, CURLOPT_POST, 0L);
|
|
3266
|
+
curl_easy_setopt(args->curl, CURLOPT_POSTFIELDS, NULL);
|
|
3267
|
+
curl_easy_setopt(args->curl, CURLOPT_POSTFIELDSIZE, 0L);
|
|
3268
|
+
curl_easy_setopt(args->curl, CURLOPT_HTTPGET, 1L);
|
|
3269
|
+
if (args->rbce && !NIL_P(args->rbce->opts)) {
|
|
3270
|
+
rb_hash_delete(args->rbce->opts, rb_easy_hkey("postdata_buffer"));
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
return Qnil;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3212
3278
|
/*
|
|
3213
3279
|
* Common implementation of easy.http(verb) and easy.http_delete
|
|
3214
3280
|
*/
|
|
@@ -3241,13 +3307,9 @@ static VALUE ruby_curl_easy_perform_verb_str(VALUE self, const char *verb) {
|
|
|
3241
3307
|
curl_easy_setopt(curl, CURLOPT_POST, 0L);
|
|
3242
3308
|
}
|
|
3243
3309
|
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
if (is_head) {
|
|
3248
|
-
curl_easy_setopt(curl, CURLOPT_NOBODY, 0L);
|
|
3249
|
-
}
|
|
3250
|
-
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, NULL);
|
|
3310
|
+
struct easy_perform_request_restore_args restore_args = { self, curl, rbce, 1, is_head, 0 };
|
|
3311
|
+
retval = rb_ensure(perform_with_request_restore_body, (VALUE)&restore_args,
|
|
3312
|
+
perform_with_request_restore_ensure, (VALUE)&restore_args);
|
|
3251
3313
|
|
|
3252
3314
|
return retval;
|
|
3253
3315
|
}
|
|
@@ -3405,7 +3467,9 @@ static VALUE ruby_curl_easy_perform_post(int argc, VALUE *argv, VALUE self) {
|
|
|
3405
3467
|
ruby_curl_easy_post_body_set(self, post_body);
|
|
3406
3468
|
}
|
|
3407
3469
|
|
|
3408
|
-
|
|
3470
|
+
struct easy_perform_request_restore_args restore_args = { self, curl, rbce, 1, 0, 0 };
|
|
3471
|
+
return rb_ensure(perform_with_request_restore_body, (VALUE)&restore_args,
|
|
3472
|
+
perform_with_request_restore_ensure, (VALUE)&restore_args);
|
|
3409
3473
|
}
|
|
3410
3474
|
}
|
|
3411
3475
|
}
|
|
@@ -3459,7 +3523,9 @@ static VALUE ruby_curl_easy_perform_patch(int argc, VALUE *argv, VALUE self) {
|
|
|
3459
3523
|
if (rb_easy_nil("postdata_buffer")) {
|
|
3460
3524
|
ruby_curl_easy_post_body_set(self, patch_body);
|
|
3461
3525
|
}
|
|
3462
|
-
|
|
3526
|
+
struct easy_perform_request_restore_args restore_args = { self, curl, rbce, 1, 0, 1 };
|
|
3527
|
+
return rb_ensure(perform_with_request_restore_body, (VALUE)&restore_args,
|
|
3528
|
+
perform_with_request_restore_ensure, (VALUE)&restore_args);
|
|
3463
3529
|
}
|
|
3464
3530
|
}
|
|
3465
3531
|
}
|
|
@@ -3510,7 +3576,9 @@ static VALUE ruby_curl_easy_perform_put(int argc, VALUE *argv, VALUE self) {
|
|
|
3510
3576
|
ruby_curl_easy_put_data_set(self, post_body);
|
|
3511
3577
|
}
|
|
3512
3578
|
}
|
|
3513
|
-
|
|
3579
|
+
struct easy_perform_request_restore_args restore_args = { self, curl, rbce, 1, 0, 0 };
|
|
3580
|
+
return rb_ensure(perform_with_request_restore_body, (VALUE)&restore_args,
|
|
3581
|
+
perform_with_request_restore_ensure, (VALUE)&restore_args);
|
|
3514
3582
|
}
|
|
3515
3583
|
|
|
3516
3584
|
|
|
@@ -4577,6 +4645,7 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
|
|
|
4577
4645
|
}
|
|
4578
4646
|
/* Save the list pointer in the ruby_curl_easy structure for cleanup later */
|
|
4579
4647
|
rbce->curl_resolve = list;
|
|
4648
|
+
rb_hash_aset(rbce->opts, rb_easy_hkey("resolve"), val);
|
|
4580
4649
|
curl_easy_setopt(rbce->curl, CURLOPT_RESOLVE, list);
|
|
4581
4650
|
} break;
|
|
4582
4651
|
#endif
|
data/ext/curb_multi.c
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
#include <errno.h>
|
|
30
30
|
#include <fcntl.h>
|
|
31
|
+
#include <stdint.h>
|
|
31
32
|
#include <stdarg.h>
|
|
32
33
|
|
|
33
34
|
/*
|
|
@@ -52,9 +53,7 @@
|
|
|
52
53
|
#include <fcntl.h>
|
|
53
54
|
#endif
|
|
54
55
|
|
|
55
|
-
#if
|
|
56
|
-
#include <stdint.h> /* for intptr_t */
|
|
57
|
-
|
|
56
|
+
#if defined(HAVE_CURL_MULTI_WAIT) && !defined(HAVE_RB_THREAD_FD_SELECT)
|
|
58
57
|
struct wait_args {
|
|
59
58
|
CURLM *handle;
|
|
60
59
|
long timeout_ms;
|
|
@@ -95,6 +94,34 @@ static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy);
|
|
|
95
94
|
static VALUE ruby_curl_multi_mark_closed(VALUE self);
|
|
96
95
|
static VALUE ruby_curl_multi_alloc(VALUE klass);
|
|
97
96
|
static VALUE ruby_curl_multi_initialize(VALUE self);
|
|
97
|
+
static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self);
|
|
98
|
+
#if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
|
|
99
|
+
static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE self);
|
|
100
|
+
#endif
|
|
101
|
+
|
|
102
|
+
static ruby_curl_multi *ruby_curl_multi_pointer_if_compatible(VALUE multi_val) {
|
|
103
|
+
if (NIL_P(multi_val) || !RB_TYPE_P(multi_val, T_DATA)) {
|
|
104
|
+
return NULL;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#if defined(RTYPEDDATA_P) && defined(RTYPEDDATA_TYPE) && defined(RTYPEDDATA_DATA)
|
|
108
|
+
if (!RTYPEDDATA_P(multi_val)) {
|
|
109
|
+
return NULL;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (RTYPEDDATA_TYPE(multi_val) != &ruby_curl_multi_data_type) {
|
|
113
|
+
return NULL;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (ruby_curl_multi *)RTYPEDDATA_DATA(multi_val);
|
|
117
|
+
#else
|
|
118
|
+
if (!rb_typeddata_is_kind_of(multi_val, &ruby_curl_multi_data_type)) {
|
|
119
|
+
return NULL;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return DATA_PTR(multi_val);
|
|
123
|
+
#endif
|
|
124
|
+
}
|
|
98
125
|
|
|
99
126
|
static VALUE callback_exception(VALUE did_raise, VALUE exception) {
|
|
100
127
|
// TODO: we could have an option to enable exception reporting
|
|
@@ -342,6 +369,9 @@ static void ruby_curl_multi_init(ruby_curl_multi *rbcm) {
|
|
|
342
369
|
rbcm->active = 0;
|
|
343
370
|
rbcm->running = 0;
|
|
344
371
|
rbcm->closed = 0;
|
|
372
|
+
rbcm->perform_active = 0;
|
|
373
|
+
rbcm->callback_active = 0;
|
|
374
|
+
rbcm->allow_close_during_perform = 0;
|
|
345
375
|
|
|
346
376
|
if (rbcm->attached) {
|
|
347
377
|
st_free_table(rbcm->attached);
|
|
@@ -537,11 +567,21 @@ VALUE ruby_curl_multi_add(VALUE self, VALUE easy) {
|
|
|
537
567
|
CURLMcode mcode;
|
|
538
568
|
ruby_curl_easy *rbce;
|
|
539
569
|
ruby_curl_multi *rbcm;
|
|
570
|
+
ruby_curl_multi *existing_rbcm;
|
|
540
571
|
|
|
541
572
|
TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
|
|
542
573
|
TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
|
|
543
574
|
ruby_curl_multi_ensure_handle(rbcm);
|
|
544
575
|
|
|
576
|
+
if (rb_curl_multi_has_easy(rbcm, rbce)) {
|
|
577
|
+
return self;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
existing_rbcm = ruby_curl_multi_pointer_if_compatible(rbce->multi);
|
|
581
|
+
if (existing_rbcm && existing_rbcm != rbcm && rb_curl_multi_has_easy(existing_rbcm, rbce)) {
|
|
582
|
+
rb_raise(rb_eRuntimeError, "Cannot add an active Curl::Easy handle to another Curl::Multi");
|
|
583
|
+
}
|
|
584
|
+
|
|
545
585
|
/* setup the easy handle */
|
|
546
586
|
ruby_curl_easy_setup( rbce );
|
|
547
587
|
|
|
@@ -797,6 +837,8 @@ static VALUE rb_curl_multi_run_completion_callbacks(VALUE argp) {
|
|
|
797
837
|
VALUE did_raise = rb_hash_new();
|
|
798
838
|
long redirect_count;
|
|
799
839
|
|
|
840
|
+
args->rbcm->callback_active = 1;
|
|
841
|
+
|
|
800
842
|
easy_callback_error = take_easy_callback_error_if_any(args->easy);
|
|
801
843
|
if (!NIL_P(easy_callback_error)) {
|
|
802
844
|
stash_multi_exception_if_unset(args->self, easy_callback_error, args->easy);
|
|
@@ -877,6 +919,10 @@ static VALUE rb_curl_multi_finish_completion_callbacks(VALUE argp) {
|
|
|
877
919
|
args->rbce->callback_active = 0;
|
|
878
920
|
}
|
|
879
921
|
|
|
922
|
+
if (args->rbcm) {
|
|
923
|
+
args->rbcm->callback_active = 0;
|
|
924
|
+
}
|
|
925
|
+
|
|
880
926
|
if (args->rbce->multi == args->self && !rb_curl_multi_has_easy(args->rbcm, args->rbce)) {
|
|
881
927
|
args->rbce->multi = Qnil;
|
|
882
928
|
}
|
|
@@ -1072,6 +1118,41 @@ static VALUE fiber_io_wait_protected(VALUE argp) {
|
|
|
1072
1118
|
}
|
|
1073
1119
|
#endif
|
|
1074
1120
|
|
|
1121
|
+
#if defined(RB_INTEGER_TYPE_P)
|
|
1122
|
+
#define CURB_INTEGER_P(value) RB_INTEGER_TYPE_P(value)
|
|
1123
|
+
#else
|
|
1124
|
+
#define CURB_INTEGER_P(value) (FIXNUM_P(value) || RB_TYPE_P((value), T_BIGNUM))
|
|
1125
|
+
#endif
|
|
1126
|
+
|
|
1127
|
+
static int multi_socket_wait_events_for_curl_poll(int what) {
|
|
1128
|
+
if (what == CURL_POLL_IN) return RB_WAITFD_IN;
|
|
1129
|
+
if (what == CURL_POLL_OUT) return RB_WAITFD_OUT;
|
|
1130
|
+
if (what == CURL_POLL_INOUT) return RB_WAITFD_IN | RB_WAITFD_OUT;
|
|
1131
|
+
return RB_WAITFD_IN | RB_WAITFD_OUT;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
static int multi_socket_cselect_flags_for_curl_poll(int what) {
|
|
1135
|
+
int flags = 0;
|
|
1136
|
+
|
|
1137
|
+
if (what == CURL_POLL_IN || what == CURL_POLL_INOUT) flags |= CURL_CSELECT_IN;
|
|
1138
|
+
if (what == CURL_POLL_OUT || what == CURL_POLL_INOUT) flags |= CURL_CSELECT_OUT;
|
|
1139
|
+
if (flags == 0) flags = CURL_CSELECT_IN | CURL_CSELECT_OUT;
|
|
1140
|
+
|
|
1141
|
+
return flags;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
static int multi_socket_cselect_flags_for_wait_events(int events) {
|
|
1145
|
+
int flags = 0;
|
|
1146
|
+
|
|
1147
|
+
if (events & RB_WAITFD_IN) flags |= CURL_CSELECT_IN;
|
|
1148
|
+
if (events & RB_WAITFD_OUT) flags |= CURL_CSELECT_OUT;
|
|
1149
|
+
#ifdef RB_WAITFD_PRI
|
|
1150
|
+
if (events & RB_WAITFD_PRI) flags |= CURL_CSELECT_ERR;
|
|
1151
|
+
#endif
|
|
1152
|
+
|
|
1153
|
+
return flags;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1075
1156
|
static void multi_socket_forget_fd(multi_socket_ctx *ctx, int fd) {
|
|
1076
1157
|
st_data_t key = (st_data_t)fd;
|
|
1077
1158
|
st_data_t rec;
|
|
@@ -1144,18 +1225,32 @@ static void rb_fdset_from_sockmap(st_table *map, rb_fdset_t *rfds, rb_fdset_t *w
|
|
|
1144
1225
|
*maxfd_out = a.maxfd;
|
|
1145
1226
|
}
|
|
1146
1227
|
|
|
1147
|
-
struct
|
|
1148
|
-
|
|
1228
|
+
struct ready_fd {
|
|
1229
|
+
int fd;
|
|
1230
|
+
int flags;
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
struct collect_ready_fd_args {
|
|
1234
|
+
rb_fdset_t *r;
|
|
1235
|
+
rb_fdset_t *w;
|
|
1236
|
+
rb_fdset_t *e;
|
|
1237
|
+
struct ready_fd *fds;
|
|
1238
|
+
int capacity;
|
|
1239
|
+
int count;
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
static int collect_ready_fd_i(st_data_t key, st_data_t val, st_data_t argp) {
|
|
1149
1243
|
(void)val;
|
|
1150
|
-
struct
|
|
1244
|
+
struct collect_ready_fd_args *a = (struct collect_ready_fd_args *)argp;
|
|
1151
1245
|
int fd = (int)key;
|
|
1152
1246
|
int flags = 0;
|
|
1153
|
-
if (rb_fd_isset(fd,
|
|
1154
|
-
if (rb_fd_isset(fd,
|
|
1155
|
-
if (rb_fd_isset(fd,
|
|
1156
|
-
if (flags) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1247
|
+
if (rb_fd_isset(fd, a->r)) flags |= CURL_CSELECT_IN;
|
|
1248
|
+
if (rb_fd_isset(fd, a->w)) flags |= CURL_CSELECT_OUT;
|
|
1249
|
+
if (rb_fd_isset(fd, a->e)) flags |= CURL_CSELECT_ERR;
|
|
1250
|
+
if (flags && a->count < a->capacity) {
|
|
1251
|
+
a->fds[a->count].fd = fd;
|
|
1252
|
+
a->fds[a->count].flags = flags;
|
|
1253
|
+
a->count++;
|
|
1159
1254
|
}
|
|
1160
1255
|
return ST_CONTINUE;
|
|
1161
1256
|
}
|
|
@@ -1236,6 +1331,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1236
1331
|
|
|
1237
1332
|
int did_timeout = 0;
|
|
1238
1333
|
int any_ready = 0;
|
|
1334
|
+
int ready_flags = 0;
|
|
1239
1335
|
|
|
1240
1336
|
int handled_wait = 0;
|
|
1241
1337
|
if (count_tracked > 1) {
|
|
@@ -1254,12 +1350,25 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1254
1350
|
any_ready = (rc > 0);
|
|
1255
1351
|
did_timeout = (rc == 0);
|
|
1256
1352
|
if (any_ready) {
|
|
1257
|
-
struct
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1353
|
+
struct ready_fd *ready_fds = ALLOC_N(struct ready_fd, count_tracked);
|
|
1354
|
+
struct collect_ready_fd_args d;
|
|
1355
|
+
int i;
|
|
1356
|
+
d.r = &rfds;
|
|
1357
|
+
d.w = &wfds;
|
|
1358
|
+
d.e = &efds;
|
|
1359
|
+
d.fds = ready_fds;
|
|
1360
|
+
d.capacity = count_tracked;
|
|
1361
|
+
d.count = 0;
|
|
1362
|
+
st_foreach(ctx->sock_map, collect_ready_fd_i, (st_data_t)&d);
|
|
1363
|
+
for (i = 0; i < d.count; i++) {
|
|
1364
|
+
mrc = curl_multi_socket_action(rbcm->handle, (curl_socket_t)d.fds[i].fd, d.fds[i].flags, &rbcm->running);
|
|
1365
|
+
if (mrc != CURLM_OK) {
|
|
1366
|
+
xfree(ready_fds);
|
|
1367
|
+
rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
|
|
1368
|
+
raise_curl_multi_error_exception(mrc);
|
|
1369
|
+
}
|
|
1262
1370
|
}
|
|
1371
|
+
xfree(ready_fds);
|
|
1263
1372
|
}
|
|
1264
1373
|
rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
|
|
1265
1374
|
handled_wait = 1;
|
|
@@ -1270,10 +1379,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1270
1379
|
if (scheduler != Qnil) {
|
|
1271
1380
|
int events = 0;
|
|
1272
1381
|
if (wait_fd >= 0) {
|
|
1273
|
-
|
|
1274
|
-
else if (wait_what == CURL_POLL_OUT) events = RB_WAITFD_OUT;
|
|
1275
|
-
else if (wait_what == CURL_POLL_INOUT) events = RB_WAITFD_IN|RB_WAITFD_OUT;
|
|
1276
|
-
else events = RB_WAITFD_IN|RB_WAITFD_OUT;
|
|
1382
|
+
events = multi_socket_wait_events_for_curl_poll(wait_what);
|
|
1277
1383
|
}
|
|
1278
1384
|
double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
|
|
1279
1385
|
VALUE timeout = rb_float_new(timeout_s);
|
|
@@ -1305,8 +1411,21 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1305
1411
|
did_timeout = 1;
|
|
1306
1412
|
any_ready = 0;
|
|
1307
1413
|
} else {
|
|
1308
|
-
any_ready = (ready != Qfalse);
|
|
1414
|
+
any_ready = (ready != Qfalse && !NIL_P(ready));
|
|
1309
1415
|
did_timeout = !any_ready;
|
|
1416
|
+
if (any_ready) {
|
|
1417
|
+
if (ready == Qtrue) {
|
|
1418
|
+
ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
|
|
1419
|
+
} else if (CURB_INTEGER_P(ready)) {
|
|
1420
|
+
ready_flags = multi_socket_cselect_flags_for_wait_events(NUM2INT(ready));
|
|
1421
|
+
if (ready_flags == 0) {
|
|
1422
|
+
any_ready = 0;
|
|
1423
|
+
did_timeout = 1;
|
|
1424
|
+
}
|
|
1425
|
+
} else {
|
|
1426
|
+
ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1310
1429
|
}
|
|
1311
1430
|
}
|
|
1312
1431
|
}
|
|
@@ -1316,10 +1435,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1316
1435
|
#endif
|
|
1317
1436
|
#if defined(HAVE_RB_WAIT_FOR_SINGLE_FD)
|
|
1318
1437
|
if (!handled_wait && wait_fd >= 0) {
|
|
1319
|
-
int ev =
|
|
1320
|
-
if (wait_what == CURL_POLL_IN) ev = RB_WAITFD_IN;
|
|
1321
|
-
else if (wait_what == CURL_POLL_OUT) ev = RB_WAITFD_OUT;
|
|
1322
|
-
else if (wait_what == CURL_POLL_INOUT) ev = RB_WAITFD_IN|RB_WAITFD_OUT;
|
|
1438
|
+
int ev = multi_socket_wait_events_for_curl_poll(wait_what);
|
|
1323
1439
|
int rc = rb_wait_for_single_fd(wait_fd, ev, &tv);
|
|
1324
1440
|
curb_debugf("[curb.socket] rb_wait_for_single_fd rc=%d fd=%d ev=%d", rc, wait_fd, ev);
|
|
1325
1441
|
if (rc < 0) {
|
|
@@ -1328,6 +1444,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1328
1444
|
}
|
|
1329
1445
|
any_ready = (rc != 0);
|
|
1330
1446
|
did_timeout = (rc == 0);
|
|
1447
|
+
if (any_ready) ready_flags = multi_socket_cselect_flags_for_wait_events(rc);
|
|
1331
1448
|
handled_wait = 1;
|
|
1332
1449
|
}
|
|
1333
1450
|
#endif
|
|
@@ -1351,6 +1468,11 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1351
1468
|
}
|
|
1352
1469
|
any_ready = (rc > 0);
|
|
1353
1470
|
did_timeout = (rc == 0);
|
|
1471
|
+
if (any_ready && wait_fd >= 0) {
|
|
1472
|
+
if (rb_fd_isset(wait_fd, &rfds)) ready_flags |= CURL_CSELECT_IN;
|
|
1473
|
+
if (rb_fd_isset(wait_fd, &wfds)) ready_flags |= CURL_CSELECT_OUT;
|
|
1474
|
+
if (rb_fd_isset(wait_fd, &efds)) ready_flags |= CURL_CSELECT_ERR;
|
|
1475
|
+
}
|
|
1354
1476
|
rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
|
|
1355
1477
|
}
|
|
1356
1478
|
} else { /* count_tracked == 0 */
|
|
@@ -1368,10 +1490,8 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1368
1490
|
if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
|
|
1369
1491
|
} else if (any_ready) {
|
|
1370
1492
|
if (count_tracked == 1 && wait_fd >= 0) {
|
|
1371
|
-
int flags =
|
|
1372
|
-
if (
|
|
1373
|
-
if (wait_what == CURL_POLL_OUT || wait_what == CURL_POLL_INOUT) flags |= CURL_CSELECT_OUT;
|
|
1374
|
-
flags |= CURL_CSELECT_ERR;
|
|
1493
|
+
int flags = ready_flags;
|
|
1494
|
+
if (flags == 0) flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
|
|
1375
1495
|
#if CURB_SOCKET_DEBUG
|
|
1376
1496
|
{
|
|
1377
1497
|
char b[32];
|
|
@@ -1410,12 +1530,16 @@ static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
|
|
|
1410
1530
|
c->ctx->sock_map = NULL;
|
|
1411
1531
|
}
|
|
1412
1532
|
if (c->ctx) {
|
|
1533
|
+
if (!NIL_P(c->ctx->io_cache)) {
|
|
1534
|
+
rb_hash_clear(c->ctx->io_cache);
|
|
1535
|
+
rb_gc_unregister_address(&c->ctx->io_cache);
|
|
1536
|
+
}
|
|
1413
1537
|
c->ctx->io_cache = Qnil;
|
|
1414
1538
|
}
|
|
1415
1539
|
return Qnil;
|
|
1416
1540
|
}
|
|
1417
1541
|
|
|
1418
|
-
VALUE
|
|
1542
|
+
static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE self) {
|
|
1419
1543
|
ruby_curl_multi *rbcm;
|
|
1420
1544
|
VALUE block = Qnil;
|
|
1421
1545
|
rb_scan_args(argc, argv, "0&", &block);
|
|
@@ -1430,6 +1554,7 @@ VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
|
|
|
1430
1554
|
ctx.sock_map = st_init_numtable();
|
|
1431
1555
|
ctx.timeout_ms = -1;
|
|
1432
1556
|
ctx.io_cache = rb_hash_new();
|
|
1557
|
+
rb_gc_register_address(&ctx.io_cache);
|
|
1433
1558
|
|
|
1434
1559
|
/* install socket/timer callbacks */
|
|
1435
1560
|
curl_multi_setopt(rbcm->handle, CURLMOPT_SOCKETFUNCTION, multi_socket_cb);
|
|
@@ -1445,7 +1570,11 @@ VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
|
|
|
1445
1570
|
/* finalize */
|
|
1446
1571
|
rb_curl_multi_read_info(self, rbcm->handle);
|
|
1447
1572
|
rb_curl_multi_yield_if_given(self, block);
|
|
1448
|
-
if (cCurlMutiAutoClose == 1)
|
|
1573
|
+
if (cCurlMutiAutoClose == 1) {
|
|
1574
|
+
rbcm->allow_close_during_perform = 1;
|
|
1575
|
+
rb_funcall(self, rb_intern("_autoclose"), 0);
|
|
1576
|
+
rbcm->allow_close_during_perform = 0;
|
|
1577
|
+
}
|
|
1449
1578
|
|
|
1450
1579
|
return Qtrue;
|
|
1451
1580
|
}
|
|
@@ -1493,6 +1622,14 @@ static VALUE curb_select(void *args) {
|
|
|
1493
1622
|
int rc = select(set->maxfd, set->fdread, set->fdwrite, set->fdexcep, set->tv);
|
|
1494
1623
|
return INT2FIX(rc);
|
|
1495
1624
|
}
|
|
1625
|
+
|
|
1626
|
+
#ifdef HAVE_RB_THREAD_CALL_WITHOUT_GVL
|
|
1627
|
+
static void *curb_select_without_gvl(void *args) {
|
|
1628
|
+
struct _select_set* set = args;
|
|
1629
|
+
int rc = select(set->maxfd, set->fdread, set->fdwrite, set->fdexcep, set->tv);
|
|
1630
|
+
return (void *)(intptr_t)rc;
|
|
1631
|
+
}
|
|
1632
|
+
#endif
|
|
1496
1633
|
#endif
|
|
1497
1634
|
|
|
1498
1635
|
/*
|
|
@@ -1510,7 +1647,7 @@ static VALUE curb_select(void *args) {
|
|
|
1510
1647
|
*
|
|
1511
1648
|
* Run multi handles, looping selecting when data can be transfered
|
|
1512
1649
|
*/
|
|
1513
|
-
VALUE
|
|
1650
|
+
static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
|
|
1514
1651
|
CURLMcode mcode;
|
|
1515
1652
|
ruby_curl_multi *rbcm;
|
|
1516
1653
|
int maxfd, rc = -1;
|
|
@@ -1691,7 +1828,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
|
|
|
1691
1828
|
rb_fd_term(&efds);
|
|
1692
1829
|
}
|
|
1693
1830
|
#elif defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL)
|
|
1694
|
-
rc = (int)(
|
|
1831
|
+
rc = (int)(intptr_t) rb_thread_call_without_gvl(curb_select_without_gvl, &fdset_args, RUBY_UBF_IO, 0);
|
|
1695
1832
|
#elif HAVE_RB_THREAD_BLOCKING_REGION
|
|
1696
1833
|
rc = rb_thread_blocking_region(curb_select, &fdset_args, RUBY_UBF_IO, 0);
|
|
1697
1834
|
#else
|
|
@@ -1725,11 +1862,66 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
|
|
|
1725
1862
|
rb_curl_multi_read_info( self, rbcm->handle );
|
|
1726
1863
|
rb_curl_multi_yield_if_given(self, block);
|
|
1727
1864
|
if (cCurlMutiAutoClose == 1) {
|
|
1865
|
+
rbcm->allow_close_during_perform = 1;
|
|
1728
1866
|
rb_funcall(self, rb_intern("_autoclose"), 0);
|
|
1867
|
+
rbcm->allow_close_during_perform = 0;
|
|
1729
1868
|
}
|
|
1730
1869
|
return Qtrue;
|
|
1731
1870
|
}
|
|
1732
1871
|
|
|
1872
|
+
struct multi_perform_call_args {
|
|
1873
|
+
int argc;
|
|
1874
|
+
VALUE *argv;
|
|
1875
|
+
VALUE self;
|
|
1876
|
+
VALUE result;
|
|
1877
|
+
VALUE (*func)(int, VALUE *, VALUE);
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
static VALUE ruby_curl_multi_perform_guard_body(VALUE argp) {
|
|
1881
|
+
struct multi_perform_call_args *args = (struct multi_perform_call_args *)argp;
|
|
1882
|
+
args->result = args->func(args->argc, args->argv, args->self);
|
|
1883
|
+
return args->result;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
static VALUE ruby_curl_multi_perform_guard_ensure(VALUE self) {
|
|
1887
|
+
ruby_curl_multi *rbcm;
|
|
1888
|
+
TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
|
|
1889
|
+
rbcm->perform_active = 0;
|
|
1890
|
+
rbcm->callback_active = 0;
|
|
1891
|
+
rbcm->allow_close_during_perform = 0;
|
|
1892
|
+
return Qnil;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
static VALUE ruby_curl_multi_with_perform_guard(int argc, VALUE *argv, VALUE self, VALUE (*func)(int, VALUE *, VALUE)) {
|
|
1896
|
+
ruby_curl_multi *rbcm;
|
|
1897
|
+
struct multi_perform_call_args args;
|
|
1898
|
+
|
|
1899
|
+
TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
|
|
1900
|
+
if (rbcm->perform_active) {
|
|
1901
|
+
rb_raise(rb_eRuntimeError, "Cannot recursively perform an active Curl::Multi handle");
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
rbcm->perform_active = 1;
|
|
1905
|
+
args.argc = argc;
|
|
1906
|
+
args.argv = argv;
|
|
1907
|
+
args.self = self;
|
|
1908
|
+
args.result = Qnil;
|
|
1909
|
+
args.func = func;
|
|
1910
|
+
|
|
1911
|
+
return rb_ensure(ruby_curl_multi_perform_guard_body, (VALUE)&args,
|
|
1912
|
+
ruby_curl_multi_perform_guard_ensure, self);
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
|
|
1916
|
+
return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_perform_impl);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
#if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
|
|
1920
|
+
VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
|
|
1921
|
+
return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_socket_perform_impl);
|
|
1922
|
+
}
|
|
1923
|
+
#endif
|
|
1924
|
+
|
|
1733
1925
|
/*
|
|
1734
1926
|
* call-seq:
|
|
1735
1927
|
*
|
|
@@ -1740,6 +1932,11 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
|
|
|
1740
1932
|
VALUE ruby_curl_multi_close(VALUE self) {
|
|
1741
1933
|
ruby_curl_multi *rbcm;
|
|
1742
1934
|
TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
|
|
1935
|
+
|
|
1936
|
+
if ((rbcm->perform_active || rbcm->callback_active) && !rbcm->allow_close_during_perform) {
|
|
1937
|
+
rb_raise(rb_eRuntimeError, "Cannot close an active Curl::Multi handle during perform");
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1743
1940
|
rb_curl_multi_detach_all(rbcm);
|
|
1744
1941
|
|
|
1745
1942
|
if (rbcm->handle) {
|
data/ext/curb_multi.h
CHANGED
|
@@ -4,7 +4,7 @@ class BugTestInstancePostDiffersFromClassPost < Test::Unit::TestCase
|
|
|
4
4
|
include BugTestServerSetupTeardown
|
|
5
5
|
|
|
6
6
|
def setup
|
|
7
|
-
@port =
|
|
7
|
+
@port = unused_local_port
|
|
8
8
|
@response_proc = lambda do|res|
|
|
9
9
|
sleep 0.5
|
|
10
10
|
res.body = "hi"
|
|
@@ -19,7 +19,7 @@ class BugTestInstancePostDiffersFromClassPost < Test::Unit::TestCase
|
|
|
19
19
|
|
|
20
20
|
5.times do |i|
|
|
21
21
|
t = Thread.new do
|
|
22
|
-
c = Curl::Easy.perform(
|
|
22
|
+
c = Curl::Easy.perform("http://127.0.0.1:#{@port}/test")
|
|
23
23
|
c.header_str
|
|
24
24
|
end
|
|
25
25
|
threads << t
|
|
@@ -35,7 +35,7 @@ class BugTestInstancePostDiffersFromClassPost < Test::Unit::TestCase
|
|
|
35
35
|
timer = Time.now
|
|
36
36
|
single_responses = []
|
|
37
37
|
5.times do |i|
|
|
38
|
-
c = Curl::Easy.perform(
|
|
38
|
+
c = Curl::Easy.perform("http://127.0.0.1:#{@port}/test")
|
|
39
39
|
single_responses << c.header_str
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -4,7 +4,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
|
|
|
4
4
|
include BugTestServerSetupTeardown
|
|
5
5
|
|
|
6
6
|
def setup
|
|
7
|
-
@port =
|
|
7
|
+
@port = unused_local_port
|
|
8
8
|
super
|
|
9
9
|
@server.mount_proc("/redirect_to_test") do|req,res|
|
|
10
10
|
res.set_redirect(WEBrick::HTTPStatus::TemporaryRedirect, "/test")
|
|
@@ -13,7 +13,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
|
|
|
13
13
|
|
|
14
14
|
def test_follow_redirect_with_no_redirect
|
|
15
15
|
|
|
16
|
-
c = Curl::Easy.new(
|
|
16
|
+
c = Curl::Easy.new("http://127.0.0.1:#{@port}/test")
|
|
17
17
|
did_call_redirect = false
|
|
18
18
|
c.on_redirect do|x|
|
|
19
19
|
did_call_redirect = true
|
|
@@ -22,7 +22,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
|
|
|
22
22
|
|
|
23
23
|
assert !did_call_redirect, "should reach this point redirect should not have been called"
|
|
24
24
|
|
|
25
|
-
c = Curl::Easy.new(
|
|
25
|
+
c = Curl::Easy.new("http://127.0.0.1:#{@port}/test")
|
|
26
26
|
did_call_redirect = false
|
|
27
27
|
c.on_redirect do|x|
|
|
28
28
|
did_call_redirect = true
|
|
@@ -33,7 +33,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
|
|
|
33
33
|
assert_equal 0, c.redirect_count
|
|
34
34
|
assert !did_call_redirect, "should reach this point redirect should not have been called"
|
|
35
35
|
|
|
36
|
-
c = Curl::Easy.new(
|
|
36
|
+
c = Curl::Easy.new("http://127.0.0.1:#{@port}/redirect_to_test")
|
|
37
37
|
did_call_redirect = false
|
|
38
38
|
c.on_redirect do|x|
|
|
39
39
|
did_call_redirect = true
|
|
@@ -43,7 +43,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
|
|
|
43
43
|
|
|
44
44
|
assert did_call_redirect, "we should have called on_redirect"
|
|
45
45
|
|
|
46
|
-
c = Curl::Easy.new(
|
|
46
|
+
c = Curl::Easy.new("http://127.0.0.1:#{@port}/redirect_to_test")
|
|
47
47
|
did_call_redirect = false
|
|
48
48
|
c.follow_location = true
|
|
49
49
|
# NOTE: while this API is not supported by libcurl e.g. there is no redirect function callback in libcurl we could
|
|
@@ -57,7 +57,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
|
|
|
57
57
|
|
|
58
58
|
assert did_call_redirect, "we should have called on_redirect"
|
|
59
59
|
|
|
60
|
-
c.url =
|
|
60
|
+
c.url = "http://127.0.0.1:#{@port}/test"
|
|
61
61
|
c.perform
|
|
62
62
|
assert_equal 0, c.redirect_count
|
|
63
63
|
assert_equal 200, c.response_code
|
|
@@ -65,7 +65,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
|
|
|
65
65
|
puts "checking for raise support"
|
|
66
66
|
did_raise = false
|
|
67
67
|
begin
|
|
68
|
-
c = Curl::Easy.new(
|
|
68
|
+
c = Curl::Easy.new("http://127.0.0.1:#{@port}/redirect_to_test")
|
|
69
69
|
did_call_redirect = false
|
|
70
70
|
c.on_redirect do|x|
|
|
71
71
|
raise "raise"
|
|
@@ -4,16 +4,17 @@ class BugRaiseOnCallback < Test::Unit::TestCase
|
|
|
4
4
|
include BugTestServerSetupTeardown
|
|
5
5
|
|
|
6
6
|
def setup
|
|
7
|
-
@port =
|
|
7
|
+
@port = unused_local_port
|
|
8
8
|
super
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def test_on_complte
|
|
12
|
-
|
|
12
|
+
url = "http://127.0.0.1:#{@port}/test"
|
|
13
|
+
c = Curl::Easy.new(url)
|
|
13
14
|
did_raise = false
|
|
14
15
|
begin
|
|
15
16
|
c.on_complete do|x|
|
|
16
|
-
assert_equal
|
|
17
|
+
assert_equal url, x.url
|
|
17
18
|
raise "error complete" # this will get swallowed
|
|
18
19
|
end
|
|
19
20
|
c.perform
|
data/tests/helper.rb
CHANGED
|
@@ -206,6 +206,13 @@ class TestServlet < WEBrick::HTTPServlet::AbstractServlet
|
|
|
206
206
|
end
|
|
207
207
|
|
|
208
208
|
module BugTestServerSetupTeardown
|
|
209
|
+
def unused_local_port
|
|
210
|
+
socket = TCPServer.new('127.0.0.1', 0)
|
|
211
|
+
socket.addr[1]
|
|
212
|
+
ensure
|
|
213
|
+
socket.close if socket
|
|
214
|
+
end
|
|
215
|
+
|
|
209
216
|
def setup
|
|
210
217
|
@port ||= 9992
|
|
211
218
|
@server = WEBrick::HTTPServer.new(:Port => @port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
|
data/tests/tc_curl_easy.rb
CHANGED
|
@@ -123,6 +123,50 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
|
123
123
|
assert_equal "", easy.body_str.to_s
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
+
def test_head_request_restores_easy_state_after_callback_exception
|
|
127
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
128
|
+
easy.on_complete { raise "head complete blew up" }
|
|
129
|
+
|
|
130
|
+
error = assert_raise(Curl::Err::AbortedByCallbackError) { easy.http("HEAD") }
|
|
131
|
+
assert_equal "head complete blew up", error.message
|
|
132
|
+
|
|
133
|
+
easy.on_complete { |_curl| }
|
|
134
|
+
easy.perform
|
|
135
|
+
|
|
136
|
+
assert_equal "GET", easy.body_str
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def test_patch_request_does_not_leak_custom_method_to_next_perform
|
|
140
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
141
|
+
|
|
142
|
+
easy.http_patch("a=b")
|
|
143
|
+
assert_equal "PATCH\na=b", easy.body_str
|
|
144
|
+
|
|
145
|
+
easy.perform
|
|
146
|
+
assert_equal "GET", easy.body_str
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def test_put_request_does_not_leak_custom_method_to_next_perform
|
|
150
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
151
|
+
|
|
152
|
+
easy.http_put("payload")
|
|
153
|
+
assert_equal "PUT\npayload", easy.body_str
|
|
154
|
+
|
|
155
|
+
easy.perform
|
|
156
|
+
assert_equal "GET", easy.body_str
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def test_failed_put_data_setup_does_not_leave_easy_in_upload_mode
|
|
160
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
161
|
+
easy.headers = ["X-Test: true"]
|
|
162
|
+
|
|
163
|
+
assert_raise(RuntimeError) { easy.put_data = "payload" }
|
|
164
|
+
|
|
165
|
+
easy.headers = {}
|
|
166
|
+
easy.perform
|
|
167
|
+
assert_equal "GET", easy.body_str
|
|
168
|
+
end
|
|
169
|
+
|
|
126
170
|
def test_perform_releases_failed_implicit_multi_without_follow_up_easy_perform
|
|
127
171
|
Curl::Easy.flush_deferred_multi_closes
|
|
128
172
|
assert_equal 0, Curl::Easy.deferred_multi_closes.length
|
|
@@ -1376,6 +1420,15 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
|
1376
1420
|
assert_match(/message$/, curl.body_str)
|
|
1377
1421
|
end
|
|
1378
1422
|
|
|
1423
|
+
def test_put_data_from_to_s_object
|
|
1424
|
+
curl = Curl::Easy.new(TestServlet.url)
|
|
1425
|
+
curl.put_data = :message
|
|
1426
|
+
|
|
1427
|
+
curl.perform
|
|
1428
|
+
|
|
1429
|
+
assert_equal "PUT\nmessage", curl.body_str
|
|
1430
|
+
end
|
|
1431
|
+
|
|
1379
1432
|
def test_put_data_read_exception_sets_result_and_releases_callback_state
|
|
1380
1433
|
error_class = Class.new(StandardError)
|
|
1381
1434
|
reader_class = Class.new do
|
|
@@ -1507,6 +1560,10 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
|
1507
1560
|
# header value and encoding; skip the NTLM-specific assertion.
|
|
1508
1561
|
return
|
|
1509
1562
|
end
|
|
1563
|
+
# curl 8.20.0 disables NTLM by default and plans to remove it in
|
|
1564
|
+
# September 2026; libcurl silently downgrades to Basic when NTLM is
|
|
1565
|
+
# unavailable, so skip the NTLM-specific assertion in that case.
|
|
1566
|
+
return unless Curl.ntlm?
|
|
1510
1567
|
curl = Curl::Easy.new(TestServlet.url)
|
|
1511
1568
|
curl.username = "foo"
|
|
1512
1569
|
curl.password = "bar"
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
|
2
2
|
|
|
3
3
|
class TestCurbCurlEasyResolve < Test::Unit::TestCase
|
|
4
|
+
include TestServerMethods
|
|
5
|
+
|
|
4
6
|
def setup
|
|
7
|
+
server_setup
|
|
5
8
|
@easy = Curl::Easy.new
|
|
6
9
|
end
|
|
7
10
|
|
|
@@ -13,4 +16,33 @@ class TestCurbCurlEasyResolve < Test::Unit::TestCase
|
|
|
13
16
|
def test_empty_resolve
|
|
14
17
|
assert_equal @easy.resolve, nil
|
|
15
18
|
end
|
|
19
|
+
|
|
20
|
+
def test_setopt_resolve_persists_across_performs
|
|
21
|
+
host = "curb-setopt-resolve.invalid"
|
|
22
|
+
mapping = "#{host}:#{TestServlet.port}:127.0.0.1"
|
|
23
|
+
|
|
24
|
+
@easy.url = "http://#{host}:#{TestServlet.port}#{TestServlet.path}"
|
|
25
|
+
@easy.dns_cache_timeout = 0
|
|
26
|
+
@easy.set(:resolve, [mapping])
|
|
27
|
+
|
|
28
|
+
@easy.perform
|
|
29
|
+
assert_match(/GET/, @easy.body_str)
|
|
30
|
+
|
|
31
|
+
@easy.perform
|
|
32
|
+
assert_match(/GET/, @easy.body_str)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_resolve_entries_can_be_objects_that_convert_to_string
|
|
36
|
+
host = "curb-resolve-object.invalid"
|
|
37
|
+
mapping = "#{host}:#{TestServlet.port}:127.0.0.1"
|
|
38
|
+
entry = Object.new
|
|
39
|
+
entry.define_singleton_method(:to_s) { mapping }
|
|
40
|
+
|
|
41
|
+
@easy.url = "http://#{host}:#{TestServlet.port}#{TestServlet.path}"
|
|
42
|
+
@easy.dns_cache_timeout = 0
|
|
43
|
+
@easy.resolve = [entry]
|
|
44
|
+
|
|
45
|
+
@easy.perform
|
|
46
|
+
assert_match(/GET/, @easy.body_str)
|
|
47
|
+
end
|
|
16
48
|
end
|
data/tests/tc_curl_multi.rb
CHANGED
|
@@ -133,6 +133,62 @@ class TestCurbCurlMulti < Test::Unit::TestCase
|
|
|
133
133
|
Curl::Multi.autoclose = false
|
|
134
134
|
end
|
|
135
135
|
|
|
136
|
+
def test_close_inside_completion_callback_is_rejected_without_invalidating_multi
|
|
137
|
+
multi = Curl::Multi.new
|
|
138
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
139
|
+
|
|
140
|
+
easy.on_complete { multi.close }
|
|
141
|
+
multi.add(easy)
|
|
142
|
+
|
|
143
|
+
error = assert_raise(Curl::Err::AbortedByCallbackError) { multi.perform }
|
|
144
|
+
assert_match(/Cannot close an active Curl::Multi handle/, error.message)
|
|
145
|
+
assert multi.idle?
|
|
146
|
+
assert_equal({}, multi.requests)
|
|
147
|
+
|
|
148
|
+
easy.on_complete { |_curl| }
|
|
149
|
+
multi.add(easy)
|
|
150
|
+
multi.perform
|
|
151
|
+
|
|
152
|
+
assert_equal "GET", easy.body_str
|
|
153
|
+
ensure
|
|
154
|
+
multi.close if defined?(multi) && multi
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_close_inside_perform_block_is_rejected_without_invalidating_multi
|
|
158
|
+
multi = Curl::Multi.new
|
|
159
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
160
|
+
multi.add(easy)
|
|
161
|
+
|
|
162
|
+
error = assert_raise(RuntimeError) do
|
|
163
|
+
multi.perform { multi.close }
|
|
164
|
+
end
|
|
165
|
+
assert_match(/Cannot close an active Curl::Multi handle/, error.message)
|
|
166
|
+
|
|
167
|
+
assert_nothing_raised { multi.close }
|
|
168
|
+
multi = nil
|
|
169
|
+
ensure
|
|
170
|
+
multi.close if defined?(multi) && multi
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def test_active_easy_cannot_be_added_to_another_multi
|
|
174
|
+
first_multi = Curl::Multi.new
|
|
175
|
+
second_multi = Curl::Multi.new
|
|
176
|
+
easy = Curl::Easy.new(TestServlet.url)
|
|
177
|
+
|
|
178
|
+
first_multi.add(easy)
|
|
179
|
+
|
|
180
|
+
error = assert_raise(RuntimeError) { second_multi.add(easy) }
|
|
181
|
+
assert_match(/active Curl::Easy/, error.message)
|
|
182
|
+
assert_same first_multi, easy.multi
|
|
183
|
+
assert_equal easy, first_multi.requests[easy.object_id]
|
|
184
|
+
|
|
185
|
+
assert_nothing_raised { first_multi.close }
|
|
186
|
+
first_multi = nil
|
|
187
|
+
ensure
|
|
188
|
+
first_multi.close if defined?(first_multi) && first_multi
|
|
189
|
+
second_multi.close if defined?(second_multi) && second_multi
|
|
190
|
+
end
|
|
191
|
+
|
|
136
192
|
def test_close_makes_multi_unusable
|
|
137
193
|
multi = Curl::Multi.new
|
|
138
194
|
multi.close
|
data/tests/tc_fiber_scheduler.rb
CHANGED
data/tests/tc_ftp_options.rb
CHANGED
|
@@ -22,5 +22,18 @@ class TestCurbFtpOptions < Test::Unit::TestCase
|
|
|
22
22
|
assert_kind_of(Array, c.ftp_commands)
|
|
23
23
|
assert_equal ["PWD", "CWD /"], c.ftp_commands
|
|
24
24
|
end
|
|
25
|
-
end
|
|
26
25
|
|
|
26
|
+
def test_ftp_command_entries_can_be_objects_that_convert_to_string
|
|
27
|
+
command = Object.new
|
|
28
|
+
command.define_singleton_method(:to_s) { "PWD" }
|
|
29
|
+
|
|
30
|
+
c = Curl::Easy.new($TEST_URL)
|
|
31
|
+
c.ftp_commands = [command]
|
|
32
|
+
m = Curl::Multi.new
|
|
33
|
+
|
|
34
|
+
assert_nothing_raised { m.add(c) }
|
|
35
|
+
ensure
|
|
36
|
+
m.remove(c) if defined?(m) && m && m.requests[c.object_id]
|
|
37
|
+
m.close if defined?(m) && m
|
|
38
|
+
end
|
|
39
|
+
end
|
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
|
+
version: 1.3.4
|
|
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-
|
|
11
|
+
date: 2026-05-12 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
|