curb 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ChangeLog.md +16 -1
- data/README.md +17 -6
- data/ext/curb.h +4 -4
- data/ext/curb_easy.c +302 -34
- data/ext/curb_easy.h +5 -0
- data/ext/curb_multi.c +32 -15
- data/ext/extconf.rb +1 -0
- data/lib/curl/easy.rb +15 -2
- data/lib/curl/multi.rb +30 -8
- data/lib/curl.rb +69 -8
- data/tests/bug_curb_easy_blocks_ruby_threads.rb +1 -1
- data/tests/bug_follow_redirect_288.rb +1 -1
- data/tests/bug_poison.rb +1 -1
- data/tests/bug_raise_on_callback.rb +1 -1
- data/tests/helper.rb +9 -8
- data/tests/tc_curl_easy.rb +188 -0
- data/tests/tc_curl_multi.rb +32 -0
- data/tests/tc_curl_network_policy.rb +25 -32
- data/tests/tc_curl_security_hardening.rb +386 -0
- data/tests/tc_fiber_scheduler.rb +46 -6
- data/tests/tc_ractor.rb +5 -1
- metadata +4 -2
data/ext/curb_easy.c
CHANGED
|
@@ -650,6 +650,93 @@ static int curb_host_allowlist_prereq(void *clientp,
|
|
|
650
650
|
}
|
|
651
651
|
#endif
|
|
652
652
|
|
|
653
|
+
#if defined(CURB_HAVE_OPENSOCKET_NETWORK_POLICY) && defined(HAVE_CURLOPT_DOH_URL)
|
|
654
|
+
/* libcurl's DoH probe transfers do not inherit the socket policy callback,
|
|
655
|
+
* so under :public the DoH endpoint must be vetted before the transfer.
|
|
656
|
+
* Only public IP literals are accepted: a hostname would be resolved outside
|
|
657
|
+
* the policy, leaving a DNS-rebinding window. The host is taken from
|
|
658
|
+
* libcurl's own URL parser so it matches what libcurl will connect to. */
|
|
659
|
+
static void curb_check_public_doh_url(ruby_curl_easy *rbce, const char *doh_url) {
|
|
660
|
+
CURLU *parsed_url;
|
|
661
|
+
char *host = NULL;
|
|
662
|
+
char literal[INET6_ADDRSTRLEN + 3];
|
|
663
|
+
char *address = literal;
|
|
664
|
+
size_t len;
|
|
665
|
+
unsigned char ip[16];
|
|
666
|
+
int parsed = 0;
|
|
667
|
+
|
|
668
|
+
parsed_url = curl_url();
|
|
669
|
+
if (!parsed_url) {
|
|
670
|
+
rb_raise(rb_eNoMemError, "Failed to allocate URL parser for DoH URL check");
|
|
671
|
+
}
|
|
672
|
+
if (curl_url_set(parsed_url, CURLUPART_URL, doh_url, 0) == CURLUE_OK &&
|
|
673
|
+
curl_url_get(parsed_url, CURLUPART_HOST, &host, 0) == CURLUE_OK && host) {
|
|
674
|
+
len = strlen(host);
|
|
675
|
+
if (len < sizeof(literal)) {
|
|
676
|
+
memcpy(literal, host, len + 1);
|
|
677
|
+
parsed = 1;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (host) curl_free(host);
|
|
681
|
+
curl_url_cleanup(parsed_url);
|
|
682
|
+
|
|
683
|
+
if (!parsed) {
|
|
684
|
+
rb_raise(eCurlErrUnsafeDestination,
|
|
685
|
+
"DoH URL must use a public IP address under public network policy");
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
len = strlen(address);
|
|
689
|
+
if (len >= 2 && address[0] == '[' && address[len - 1] == ']') {
|
|
690
|
+
address[len - 1] = '\0';
|
|
691
|
+
address++;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (inet_pton(AF_INET, address, ip) == 1) {
|
|
695
|
+
if (curb_ipv4_is_unsafe_destination(ip) || !curb_cidr_rules_match(rbce, CURB_CIDR_FAMILY_IPV4, ip)) {
|
|
696
|
+
rb_raise(eCurlErrUnsafeDestination,
|
|
697
|
+
"DoH URL address %s is not allowed by public network policy", address);
|
|
698
|
+
}
|
|
699
|
+
} else if (inet_pton(AF_INET6, address, ip) == 1) {
|
|
700
|
+
if (curb_ipv6_is_unsafe_destination(ip) || !curb_cidr_rules_match(rbce, CURB_CIDR_FAMILY_IPV6, ip)) {
|
|
701
|
+
rb_raise(eCurlErrUnsafeDestination,
|
|
702
|
+
"DoH URL address %s is not allowed by public network policy", address);
|
|
703
|
+
}
|
|
704
|
+
} else {
|
|
705
|
+
rb_raise(eCurlErrUnsafeDestination,
|
|
706
|
+
"DoH URL must use a public IP address under public network policy (got host %s)", address);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
#endif
|
|
710
|
+
|
|
711
|
+
#ifdef CURB_HAVE_OPENSOCKET_NETWORK_POLICY
|
|
712
|
+
/* file:// opens no socket, so the public policy's socket check never sees
|
|
713
|
+
* it. Without a protocol allowlist, reject it up front; libcurl's default
|
|
714
|
+
* redirect protocols already exclude file. The scheme comes from libcurl's
|
|
715
|
+
* parser when available. */
|
|
716
|
+
static void curb_check_public_url_scheme(const char *url) {
|
|
717
|
+
int is_file = 0;
|
|
718
|
+
#if LIBCURL_VERSION_NUM >= 0x073e00
|
|
719
|
+
CURLU *parsed_url = curl_url();
|
|
720
|
+
char *scheme = NULL;
|
|
721
|
+
if (!parsed_url) {
|
|
722
|
+
rb_raise(rb_eNoMemError, "Failed to allocate URL parser for scheme check");
|
|
723
|
+
}
|
|
724
|
+
if (curl_url_set(parsed_url, CURLUPART_URL, url, CURLU_GUESS_SCHEME | CURLU_NON_SUPPORT_SCHEME) == CURLUE_OK &&
|
|
725
|
+
curl_url_get(parsed_url, CURLUPART_SCHEME, &scheme, 0) == CURLUE_OK && scheme) {
|
|
726
|
+
is_file = STRCASECMP(scheme, "file") == 0;
|
|
727
|
+
}
|
|
728
|
+
if (scheme) curl_free(scheme);
|
|
729
|
+
curl_url_cleanup(parsed_url);
|
|
730
|
+
#else
|
|
731
|
+
while (*url == ' ' || *url == '\t') url++;
|
|
732
|
+
is_file = STRNCASECMP(url, "file:", 5) == 0;
|
|
733
|
+
#endif
|
|
734
|
+
if (is_file) {
|
|
735
|
+
rb_raise(eCurlErrUnsafeDestination, "file:// URLs are not allowed by public network policy");
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
#endif
|
|
739
|
+
|
|
653
740
|
#ifdef CURB_HAVE_OPENSOCKET_NETWORK_POLICY
|
|
654
741
|
static curl_socket_t curb_public_network_opensocket(void *clientp, curlsocktype purpose, struct curl_sockaddr *address) {
|
|
655
742
|
ruby_curl_easy *rbce = (ruby_curl_easy *)clientp;
|
|
@@ -704,6 +791,14 @@ static curl_socket_t curb_public_network_opensocket(void *clientp, curlsocktype
|
|
|
704
791
|
}
|
|
705
792
|
#endif
|
|
706
793
|
|
|
794
|
+
/* Anything other than an IP socket (e.g. a Unix domain socket configured
|
|
795
|
+
* after the policy was applied) has no address to vet. Refuse it unless
|
|
796
|
+
* Unix sockets were explicitly allowed. */
|
|
797
|
+
if (!checked_destination_address && rbce && !rbce->allow_unix_socket) {
|
|
798
|
+
curb_store_destination_error(rbce, "(non-IP socket)", "is not allowed");
|
|
799
|
+
return CURL_SOCKET_BAD;
|
|
800
|
+
}
|
|
801
|
+
|
|
707
802
|
if (!checked_destination_address && rbce && rbce->network_allowed_cidr_rule_count > 0) {
|
|
708
803
|
curb_store_destination_error(rbce, "unknown", "outside allowed CIDR ranges");
|
|
709
804
|
return CURL_SOCKET_BAD;
|
|
@@ -879,6 +974,13 @@ static int ruby_curl_easy_body_limit_exceeded(ruby_curl_easy *rbce, size_t total
|
|
|
879
974
|
return 0;
|
|
880
975
|
}
|
|
881
976
|
|
|
977
|
+
/* Initial capacity for the default body/header buffers. rb_str_buf_cat grows
|
|
978
|
+
* them geometrically, so starting small costs a few reallocations at most,
|
|
979
|
+
* while a large fixed reservation stays attached to every returned body_str
|
|
980
|
+
* (a 2 KB response used to keep 32 KB alive). */
|
|
981
|
+
#define CURB_DEFAULT_BODY_BUFFER_CAPA 0
|
|
982
|
+
#define CURB_DEFAULT_HEADER_BUFFER_CAPA 0
|
|
983
|
+
|
|
882
984
|
/* Default body handler appends to easy.body_data buffer */
|
|
883
985
|
static size_t default_body_handler(char *stream,
|
|
884
986
|
size_t size,
|
|
@@ -893,7 +995,7 @@ static size_t default_body_handler(char *stream,
|
|
|
893
995
|
}
|
|
894
996
|
|
|
895
997
|
if (NIL_P(out)) {
|
|
896
|
-
out = rb_easy_set("body_data", rb_str_buf_new(
|
|
998
|
+
out = rb_easy_set("body_data", rb_str_buf_new(CURB_DEFAULT_BODY_BUFFER_CAPA));
|
|
897
999
|
}
|
|
898
1000
|
rb_str_buf_cat(out, stream, total);
|
|
899
1001
|
return total;
|
|
@@ -908,7 +1010,7 @@ static size_t default_header_handler(char *stream,
|
|
|
908
1010
|
size_t total = size * nmemb;
|
|
909
1011
|
VALUE out = rb_easy_get("header_data");
|
|
910
1012
|
if (NIL_P(out)) {
|
|
911
|
-
out = rb_easy_set("header_data", rb_str_buf_new(
|
|
1013
|
+
out = rb_easy_set("header_data", rb_str_buf_new(CURB_DEFAULT_HEADER_BUFFER_CAPA));
|
|
912
1014
|
}
|
|
913
1015
|
rb_str_buf_cat(out, stream, total);
|
|
914
1016
|
return total;
|
|
@@ -1316,6 +1418,36 @@ static void ruby_curl_easy_detach(VALUE self, ruby_curl_easy *rbce) {
|
|
|
1316
1418
|
rbce->multi = Qnil;
|
|
1317
1419
|
}
|
|
1318
1420
|
|
|
1421
|
+
/* Release the curb-owned CURLOPT_STDERR stream. Callers must first stop
|
|
1422
|
+
* libcurl from using it (cleanup, reset, or setopt of a replacement). */
|
|
1423
|
+
static void ruby_curl_easy_close_stderr_file(ruby_curl_easy *rbce) {
|
|
1424
|
+
FILE *fp = rbce->stderr_file;
|
|
1425
|
+
if (fp) {
|
|
1426
|
+
rbce->stderr_file = NULL;
|
|
1427
|
+
fclose(fp);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/* Open a stream curb owns on a duplicate of fd, so its lifetime is
|
|
1432
|
+
* independent of the Ruby IO it came from. */
|
|
1433
|
+
static FILE *ruby_curl_easy_dup_stderr_file(int fd) {
|
|
1434
|
+
FILE *fp;
|
|
1435
|
+
int dup_fd = rb_cloexec_dup(fd);
|
|
1436
|
+
if (dup_fd < 0) {
|
|
1437
|
+
rb_sys_fail("dup(CURLOPT_STDERR)");
|
|
1438
|
+
}
|
|
1439
|
+
rb_update_max_fd(dup_fd);
|
|
1440
|
+
/* "w" on an existing descriptor neither truncates nor changes O_APPEND. */
|
|
1441
|
+
fp = fdopen(dup_fd, "w");
|
|
1442
|
+
if (!fp) {
|
|
1443
|
+
int saved_errno = errno;
|
|
1444
|
+
close(dup_fd);
|
|
1445
|
+
errno = saved_errno;
|
|
1446
|
+
rb_sys_fail("fdopen(CURLOPT_STDERR)");
|
|
1447
|
+
}
|
|
1448
|
+
return fp;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1319
1451
|
static void ruby_curl_easy_free(ruby_curl_easy *rbce) {
|
|
1320
1452
|
if (!rbce) {
|
|
1321
1453
|
return;
|
|
@@ -1362,6 +1494,8 @@ static void ruby_curl_easy_free(ruby_curl_easy *rbce) {
|
|
|
1362
1494
|
curl_easy_cleanup(rbce->curl);
|
|
1363
1495
|
rbce->curl = NULL;
|
|
1364
1496
|
}
|
|
1497
|
+
/* After cleanup: libcurl may log while closing connections. */
|
|
1498
|
+
ruby_curl_easy_close_stderr_file(rbce);
|
|
1365
1499
|
|
|
1366
1500
|
rbce->self = Qnil;
|
|
1367
1501
|
}
|
|
@@ -1431,6 +1565,15 @@ ruby_curl_easy *ruby_curl_easy_get_handle(VALUE self) {
|
|
|
1431
1565
|
return rbce;
|
|
1432
1566
|
}
|
|
1433
1567
|
|
|
1568
|
+
/* Entry points that hand libcurl new request state (bodies, verbs, setup)
|
|
1569
|
+
* must not run while libcurl is still using the old state: replacing it
|
|
1570
|
+
* mid-transfer frees buffers libcurl is reading (use-after-free). */
|
|
1571
|
+
static ruby_curl_easy *ruby_curl_easy_get_idle_handle(VALUE self) {
|
|
1572
|
+
ruby_curl_easy *rbce = ruby_curl_easy_get_handle(self);
|
|
1573
|
+
ruby_curl_easy_check_idle(rbce);
|
|
1574
|
+
return rbce;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1434
1577
|
/* Keep libcurl's typed getinfo arguments while checking every result. */
|
|
1435
1578
|
#define CURB_GETINFO(handle, info, output) do { \
|
|
1436
1579
|
CURLcode info_result = curl_easy_getinfo(handle, info, output); \
|
|
@@ -1457,6 +1600,8 @@ static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
|
|
|
1457
1600
|
rbce->network_allowed_hosts = NULL;
|
|
1458
1601
|
rbce->network_allowed_cidr_rule_count = 0;
|
|
1459
1602
|
rbce->network_allowed_host_count = 0;
|
|
1603
|
+
/* Callers that may hold a stream close it before zeroing. */
|
|
1604
|
+
rbce->stderr_file = NULL;
|
|
1460
1605
|
|
|
1461
1606
|
/* various-typed opts */
|
|
1462
1607
|
rbce->local_port = 0;
|
|
@@ -1466,6 +1611,8 @@ static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
|
|
|
1466
1611
|
rbce->http_auth_types = 0;
|
|
1467
1612
|
rbce->proxy_auth_types = 0;
|
|
1468
1613
|
rbce->max_redirs = -1;
|
|
1614
|
+
rbce->max_redirs_set = 0;
|
|
1615
|
+
rbce->protocols_set = 0;
|
|
1469
1616
|
rbce->timeout = 0;
|
|
1470
1617
|
rbce->timeout_ms = 0;
|
|
1471
1618
|
rbce->connect_timeout = 0;
|
|
@@ -1632,19 +1779,24 @@ static VALUE duplicate_upload(VALUE upload) {
|
|
|
1632
1779
|
static VALUE ruby_curl_easy_clone(VALUE self) {
|
|
1633
1780
|
ruby_curl_easy *rbce, *newrbce;
|
|
1634
1781
|
|
|
1782
|
+
FILE *clone_stderr_file = NULL;
|
|
1783
|
+
|
|
1635
1784
|
rbce = ruby_curl_easy_get_handle(self);
|
|
1636
1785
|
|
|
1637
|
-
|
|
1638
|
-
if (
|
|
1639
|
-
|
|
1786
|
+
/* Duplicate first: this can raise, and nothing needs unwinding yet. */
|
|
1787
|
+
if (rbce->stderr_file) {
|
|
1788
|
+
clone_stderr_file = ruby_curl_easy_dup_stderr_file(fileno(rbce->stderr_file));
|
|
1640
1789
|
}
|
|
1790
|
+
|
|
1791
|
+
newrbce = ALLOC(ruby_curl_easy);
|
|
1641
1792
|
/* shallow copy */
|
|
1642
1793
|
memcpy(newrbce, rbce, sizeof(ruby_curl_easy));
|
|
1643
1794
|
|
|
1644
1795
|
/* now deep copy */
|
|
1645
1796
|
newrbce->curl = curl_easy_duphandle(rbce->curl);
|
|
1646
1797
|
if (!newrbce->curl) {
|
|
1647
|
-
|
|
1798
|
+
xfree(newrbce);
|
|
1799
|
+
if (clone_stderr_file) fclose(clone_stderr_file);
|
|
1648
1800
|
rb_raise(rb_eNoMemError, "Failed to duplicate Curl::Easy handle");
|
|
1649
1801
|
}
|
|
1650
1802
|
/* duphandle borrows HTTPPOST, whose form is freed when the original
|
|
@@ -1654,7 +1806,8 @@ static VALUE ruby_curl_easy_clone(VALUE self) {
|
|
|
1654
1806
|
CURLcode ecode = curl_easy_setopt(newrbce->curl, CURLOPT_HTTPPOST, NULL);
|
|
1655
1807
|
if (ecode != CURLE_OK) {
|
|
1656
1808
|
curl_easy_cleanup(newrbce->curl);
|
|
1657
|
-
|
|
1809
|
+
xfree(newrbce);
|
|
1810
|
+
if (clone_stderr_file) fclose(clone_stderr_file);
|
|
1658
1811
|
raise_curl_easy_error_exception(ecode);
|
|
1659
1812
|
}
|
|
1660
1813
|
newrbce->multipart_form_active = 0;
|
|
@@ -1669,6 +1822,13 @@ static VALUE ruby_curl_easy_clone(VALUE self) {
|
|
|
1669
1822
|
newrbce->network_allowed_hosts = NULL;
|
|
1670
1823
|
newrbce->network_allowed_host_count = 0;
|
|
1671
1824
|
|
|
1825
|
+
/* duphandle copied the original's CURLOPT_STDERR pointer; give the clone
|
|
1826
|
+
* its own stream so neither handle can outlive the other's. */
|
|
1827
|
+
newrbce->stderr_file = clone_stderr_file;
|
|
1828
|
+
if (clone_stderr_file) {
|
|
1829
|
+
curl_easy_setopt(newrbce->curl, CURLOPT_STDERR, clone_stderr_file);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1672
1832
|
/* A cloned easy should not retain ownership reference to the original multi. */
|
|
1673
1833
|
newrbce->multi = Qnil;
|
|
1674
1834
|
newrbce->callback_error = Qnil;
|
|
@@ -1789,6 +1949,8 @@ static VALUE ruby_curl_easy_reset(VALUE self) {
|
|
|
1789
1949
|
|
|
1790
1950
|
ruby_curl_easy_cleanup(self, rbce);
|
|
1791
1951
|
curl_easy_reset(rbce->curl);
|
|
1952
|
+
/* reset restored libcurl's default stderr, so ours is unused now. */
|
|
1953
|
+
ruby_curl_easy_close_stderr_file(rbce);
|
|
1792
1954
|
ruby_curl_easy_zero(rbce);
|
|
1793
1955
|
rbce->self = self;
|
|
1794
1956
|
|
|
@@ -2151,7 +2313,7 @@ static VALUE ruby_curl_easy_post_body_set_with_mode_body(VALUE argp) {
|
|
|
2151
2313
|
char *data;
|
|
2152
2314
|
long len;
|
|
2153
2315
|
|
|
2154
|
-
rbce =
|
|
2316
|
+
rbce = ruby_curl_easy_get_idle_handle(self);
|
|
2155
2317
|
|
|
2156
2318
|
curl = rbce->curl;
|
|
2157
2319
|
|
|
@@ -2247,7 +2409,7 @@ static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
|
|
|
2247
2409
|
VALUE headers;
|
|
2248
2410
|
VALUE infile_size = Qnil;
|
|
2249
2411
|
|
|
2250
|
-
rbce =
|
|
2412
|
+
rbce = ruby_curl_easy_get_idle_handle(self);
|
|
2251
2413
|
|
|
2252
2414
|
/*
|
|
2253
2415
|
* Validate and prepare Ruby-visible state before mutating the CURL handle.
|
|
@@ -2744,14 +2906,27 @@ static VALUE ruby_curl_easy_proxy_auth_types_get(VALUE self) {
|
|
|
2744
2906
|
* easy.max_redirects = fixnum or nil => fixnum or nil
|
|
2745
2907
|
*
|
|
2746
2908
|
* Set the maximum number of redirections to follow in the following +perform+
|
|
2747
|
-
* calls. Set to
|
|
2748
|
-
*
|
|
2909
|
+
* calls. Set to -1 to allow an infinite number. When nil (the default), at
|
|
2910
|
+
* most 30 redirects are followed, matching libcurl 8.3+ on every libcurl
|
|
2911
|
+
* version. Setting this option only makes sense if +follow_location+ is also
|
|
2912
|
+
* set true.
|
|
2749
2913
|
*
|
|
2750
2914
|
* With libcurl >= 7.15.1, setting this to 0 will cause libcurl to refuse any
|
|
2751
2915
|
* redirect.
|
|
2752
2916
|
*/
|
|
2753
2917
|
static VALUE ruby_curl_easy_max_redirects_set(VALUE self, VALUE max_redirs) {
|
|
2754
|
-
|
|
2918
|
+
ruby_curl_easy *rbce;
|
|
2919
|
+
|
|
2920
|
+
TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
|
|
2921
|
+
ruby_curl_easy_check_access(rbce);
|
|
2922
|
+
if (NIL_P(max_redirs)) {
|
|
2923
|
+
rbce->max_redirs = -1;
|
|
2924
|
+
rbce->max_redirs_set = 0;
|
|
2925
|
+
} else {
|
|
2926
|
+
rbce->max_redirs = NUM2LONG(max_redirs);
|
|
2927
|
+
rbce->max_redirs_set = 1;
|
|
2928
|
+
}
|
|
2929
|
+
return max_redirs;
|
|
2755
2930
|
}
|
|
2756
2931
|
|
|
2757
2932
|
/*
|
|
@@ -3318,7 +3493,7 @@ static VALUE ruby_curl_easy_use_netrc_q(VALUE self) {
|
|
|
3318
3493
|
*/
|
|
3319
3494
|
static VALUE ruby_curl_easy_autoreferer_set(VALUE self, VALUE autoreferer) {
|
|
3320
3495
|
ruby_curl_easy *rbce;
|
|
3321
|
-
rbce =
|
|
3496
|
+
rbce = ruby_curl_easy_get_idle_handle(self);
|
|
3322
3497
|
|
|
3323
3498
|
if (Qtrue == autoreferer) {
|
|
3324
3499
|
curl_easy_setopt(rbce->curl, CURLOPT_AUTOREFERER, 1);
|
|
@@ -3819,6 +3994,37 @@ static VALUE ruby_curl_easy_on_debug_set(int argc, VALUE *argv, VALUE self) {
|
|
|
3819
3994
|
|
|
3820
3995
|
/* =================== PERFORM =====================*/
|
|
3821
3996
|
|
|
3997
|
+
/* CR or LF in a header line would let the value inject extra headers or
|
|
3998
|
+
* split the request; libcurl sends header strings verbatim. NUL would
|
|
3999
|
+
* silently truncate the line. */
|
|
4000
|
+
static void curb_check_header_line_len(const char *ptr, long len, const char *what) {
|
|
4001
|
+
if (memchr(ptr, '\r', (size_t)len) || memchr(ptr, '\n', (size_t)len) || memchr(ptr, '\0', (size_t)len)) {
|
|
4002
|
+
rb_raise(rb_eArgError, "%s must not contain CR, LF, or NUL characters", what);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
static void curb_check_header_line(VALUE str, const char *what) {
|
|
4007
|
+
curb_check_header_line_len(RSTRING_PTR(str), RSTRING_LEN(str), what);
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
/* A String for the option stored under key, converting like rb_easy_get_str. */
|
|
4011
|
+
static VALUE curb_easy_opt_string(ruby_curl_easy *rbce, const char *key) {
|
|
4012
|
+
VALUE value = rb_hash_aref(rbce->opts, rb_easy_hkey(key));
|
|
4013
|
+
return RB_TYPE_P(value, T_STRING) ? value : rb_str_to_str(value);
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
static int curb_slist_has_header(const struct curl_slist *list, const char *name) {
|
|
4017
|
+
size_t name_len = strlen(name);
|
|
4018
|
+
for (; list; list = list->next) {
|
|
4019
|
+
const char *line = list->data;
|
|
4020
|
+
if (line && STRNCASECMP(line, name, name_len) == 0 &&
|
|
4021
|
+
(line[name_len] == ':' || line[name_len] == ';')) {
|
|
4022
|
+
return 1;
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
return 0;
|
|
4026
|
+
}
|
|
4027
|
+
|
|
3822
4028
|
/***********************************************
|
|
3823
4029
|
* This is an rb_iterate callback used to set up http headers.
|
|
3824
4030
|
*/
|
|
@@ -3850,6 +4056,8 @@ static VALUE cb_each_http_header(VALUE header, VALUE wrap, int _c, const VALUE *
|
|
|
3850
4056
|
|
|
3851
4057
|
//rb_p(header_str);
|
|
3852
4058
|
|
|
4059
|
+
StringValue(header_str);
|
|
4060
|
+
curb_check_header_line(header_str, "HTTP header");
|
|
3853
4061
|
struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(header_str));
|
|
3854
4062
|
if (!new_list) {
|
|
3855
4063
|
rb_raise(rb_eNoMemError, "Failed to append to header list");
|
|
@@ -3886,6 +4094,8 @@ static VALUE cb_each_http_proxy_header(VALUE proxy_header, VALUE wrap, int _c, c
|
|
|
3886
4094
|
|
|
3887
4095
|
//rb_p(header_str);
|
|
3888
4096
|
|
|
4097
|
+
StringValue(proxy_header_str);
|
|
4098
|
+
curb_check_header_line(proxy_header_str, "HTTP proxy header");
|
|
3889
4099
|
struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(proxy_header_str));
|
|
3890
4100
|
if (!new_list) {
|
|
3891
4101
|
rb_raise(rb_eNoMemError, "Failed to append to proxy header list");
|
|
@@ -4031,6 +4241,17 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
|
|
|
4031
4241
|
curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, rbce->forbid_reuse_set ? rbce->forbid_reuse : 0L);
|
|
4032
4242
|
#endif
|
|
4033
4243
|
}
|
|
4244
|
+
/* With a protocol allowlist, libcurl enforces it (UnsupportedProtocolError)
|
|
4245
|
+
* and an explicit :file entry is a deliberate opt-in. */
|
|
4246
|
+
if (rbce->network_policy == CURB_NETWORK_POLICY_PUBLIC && !rbce->protocols_set) {
|
|
4247
|
+
curb_check_public_url_scheme(StringValueCStr(url));
|
|
4248
|
+
}
|
|
4249
|
+
#ifdef HAVE_CURLOPT_DOH_URL
|
|
4250
|
+
/* After the CIDR rules above are prepared. */
|
|
4251
|
+
if (rbce->network_policy == CURB_NETWORK_POLICY_PUBLIC && !rb_easy_nil("doh_url")) {
|
|
4252
|
+
curb_check_public_doh_url(rbce, rb_easy_get_str("doh_url"));
|
|
4253
|
+
}
|
|
4254
|
+
#endif
|
|
4034
4255
|
#endif
|
|
4035
4256
|
|
|
4036
4257
|
// network stuff and auth
|
|
@@ -4097,7 +4318,7 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
|
|
|
4097
4318
|
/* clear out the body_data if it was set */
|
|
4098
4319
|
rb_easy_del("body_data");
|
|
4099
4320
|
} else {
|
|
4100
|
-
rb_easy_set("body_data", rb_str_buf_new(
|
|
4321
|
+
rb_easy_set("body_data", rb_str_buf_new(CURB_DEFAULT_BODY_BUFFER_CAPA));
|
|
4101
4322
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)&default_body_handler);
|
|
4102
4323
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, rbce);
|
|
4103
4324
|
}
|
|
@@ -4108,7 +4329,7 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
|
|
|
4108
4329
|
/* clear out the header_data if it was set */
|
|
4109
4330
|
rb_easy_del("header_data");
|
|
4110
4331
|
} else {
|
|
4111
|
-
rb_easy_set("header_data", rb_str_buf_new(
|
|
4332
|
+
rb_easy_set("header_data", rb_str_buf_new(CURB_DEFAULT_HEADER_BUFFER_CAPA));
|
|
4112
4333
|
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, (curl_write_callback)&default_header_handler);
|
|
4113
4334
|
curl_easy_setopt(curl, CURLOPT_HEADERDATA, rbce);
|
|
4114
4335
|
}
|
|
@@ -4152,14 +4373,20 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
|
|
|
4152
4373
|
|
|
4153
4374
|
curl_easy_setopt(curl, CURLOPT_HEADER, rbce->header_in_body);
|
|
4154
4375
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, rbce->follow_location);
|
|
4376
|
+
/* Unset: cap at 30, libcurl's default since 8.3.0. Passing curb's old
|
|
4377
|
+
* default of -1 made redirects unlimited on every libcurl version. */
|
|
4378
|
+
if (!rbce->max_redirs_set) {
|
|
4379
|
+
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 30L);
|
|
4380
|
+
} else {
|
|
4155
4381
|
#if LIBCURL_VERSION_NUM == 0x081000
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4382
|
+
/* Work around 8.16.0 regression that clamps -1 (infinite) to zero */
|
|
4383
|
+
if (rbce->max_redirs >= 0) {
|
|
4384
|
+
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, rbce->max_redirs);
|
|
4385
|
+
}
|
|
4160
4386
|
#else
|
|
4161
|
-
|
|
4387
|
+
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, rbce->max_redirs);
|
|
4162
4388
|
#endif
|
|
4389
|
+
}
|
|
4163
4390
|
|
|
4164
4391
|
curl_easy_setopt(curl, CURLOPT_HTTPPROXYTUNNEL,
|
|
4165
4392
|
public_policy_disables_proxy ? 0L : rbce->proxy_tunnel);
|
|
@@ -4275,9 +4502,7 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
|
|
|
4275
4502
|
curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");
|
|
4276
4503
|
}
|
|
4277
4504
|
|
|
4278
|
-
|
|
4279
|
-
curl_easy_setopt(curl, CURLOPT_COOKIE, rb_easy_get_str("cookies"));
|
|
4280
|
-
}
|
|
4505
|
+
/* cookies= is applied after the HTTP headers below. */
|
|
4281
4506
|
|
|
4282
4507
|
/* Set up HTTPS cert handling if necessary */
|
|
4283
4508
|
if (!rb_easy_nil("cert")) {
|
|
@@ -4322,7 +4547,9 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
|
|
|
4322
4547
|
|
|
4323
4548
|
/* Set the user-agent string if specified */
|
|
4324
4549
|
if (!rb_easy_nil("useragent")) {
|
|
4325
|
-
|
|
4550
|
+
VALUE useragent = curb_easy_opt_string(rbce, "useragent");
|
|
4551
|
+
curb_check_header_line(useragent, "useragent");
|
|
4552
|
+
curl_easy_setopt(curl, CURLOPT_USERAGENT, RSTRING_PTR(useragent));
|
|
4326
4553
|
}
|
|
4327
4554
|
|
|
4328
4555
|
/* Setup can be rerun for safety-policy changes while a handle is attached.
|
|
@@ -4339,6 +4566,7 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
|
|
|
4339
4566
|
rb_block_call(rb_easy_get("headers"), rb_intern("each"), 0, NULL, cb_each_http_header, wrap);
|
|
4340
4567
|
} else {
|
|
4341
4568
|
VALUE headers_str = rb_obj_as_string(rb_easy_get("headers"));
|
|
4569
|
+
curb_check_header_line(headers_str, "HTTP header");
|
|
4342
4570
|
struct curl_slist *new_list = curl_slist_append(*hdrs, StringValuePtr(headers_str));
|
|
4343
4571
|
if (!new_list) {
|
|
4344
4572
|
rb_raise(rb_eNoMemError, "Failed to append to headers list");
|
|
@@ -4351,6 +4579,28 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
|
|
|
4351
4579
|
}
|
|
4352
4580
|
}
|
|
4353
4581
|
|
|
4582
|
+
/* cookies=: libcurl sends CURLOPT_COOKIE to every host a redirect reaches,
|
|
4583
|
+
* but drops a custom Cookie header on cross-host redirects (7.83+). So
|
|
4584
|
+
* send it as a header, unless the cookie engine is on: libcurl would then
|
|
4585
|
+
* emit a second Cookie line, so CURLOPT_COOKIE is kept to merge them. */
|
|
4586
|
+
curl_easy_setopt(curl, CURLOPT_COOKIE, NULL);
|
|
4587
|
+
if (!rb_easy_nil("cookies")) {
|
|
4588
|
+
VALUE cookies = curb_easy_opt_string(rbce, "cookies");
|
|
4589
|
+
curb_check_header_line(cookies, "cookies");
|
|
4590
|
+
if (rbce->enable_cookies || rbce->cookielist_engine_enabled) {
|
|
4591
|
+
curl_easy_setopt(curl, CURLOPT_COOKIE, RSTRING_PTR(cookies));
|
|
4592
|
+
} else if (RSTRING_LEN(cookies) > 0 && !curb_slist_has_header(*hdrs, "Cookie")) {
|
|
4593
|
+
/* An explicit Cookie header already wins over cookies=, as before. */
|
|
4594
|
+
VALUE cookie_header = rb_str_plus(rb_str_new_cstr("Cookie: "), cookies);
|
|
4595
|
+
struct curl_slist *new_list = curl_slist_append(*hdrs, StringValueCStr(cookie_header));
|
|
4596
|
+
if (!new_list) {
|
|
4597
|
+
rb_raise(rb_eNoMemError, "Failed to append to headers list");
|
|
4598
|
+
}
|
|
4599
|
+
*hdrs = new_list;
|
|
4600
|
+
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *hdrs);
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4354
4604
|
#ifdef HAVE_CURLOPT_PROXYHEADER
|
|
4355
4605
|
/* Setup HTTP proxy headers if necessary */
|
|
4356
4606
|
curl_easy_setopt(curl, CURLOPT_PROXYHEADER, NULL); // XXX: maybe we shouldn't be clearing this?
|
|
@@ -4361,6 +4611,7 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
|
|
|
4361
4611
|
rb_block_call(rb_easy_get("proxy_headers"), rb_intern("each"), 0, NULL, cb_each_http_proxy_header, wrap);
|
|
4362
4612
|
} else {
|
|
4363
4613
|
VALUE proxy_headers_str = rb_obj_as_string(rb_easy_get("proxy_headers"));
|
|
4614
|
+
curb_check_header_line(proxy_headers_str, "HTTP proxy header");
|
|
4364
4615
|
struct curl_slist *new_list = curl_slist_append(*phdrs, StringValuePtr(proxy_headers_str));
|
|
4365
4616
|
if (!new_list) {
|
|
4366
4617
|
rb_raise(rb_eNoMemError, "Failed to append to proxy headers list");
|
|
@@ -4440,7 +4691,7 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
|
|
|
4440
4691
|
static VALUE ruby_curl_easy_setup_self(VALUE self) {
|
|
4441
4692
|
ruby_curl_easy *rbce;
|
|
4442
4693
|
|
|
4443
|
-
rbce =
|
|
4694
|
+
rbce = ruby_curl_easy_get_idle_handle(self);
|
|
4444
4695
|
ruby_curl_easy_setup(rbce);
|
|
4445
4696
|
|
|
4446
4697
|
return self;
|
|
@@ -4546,11 +4797,13 @@ static VALUE ruby_curl_easy_perform_verb_str(VALUE self, const char *verb) {
|
|
|
4546
4797
|
CURL *curl;
|
|
4547
4798
|
VALUE retval;
|
|
4548
4799
|
|
|
4549
|
-
rbce =
|
|
4800
|
+
rbce = ruby_curl_easy_get_idle_handle(self);
|
|
4550
4801
|
curl = rbce->curl;
|
|
4551
4802
|
|
|
4552
4803
|
memset(rbce->err_buf, 0, CURL_ERROR_SIZE);
|
|
4553
4804
|
|
|
4805
|
+
if (verb) curb_check_header_line_len(verb, (long)strlen(verb), "HTTP verb");
|
|
4806
|
+
|
|
4554
4807
|
/* Use method override and adjust related options for special verbs. */
|
|
4555
4808
|
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, verb);
|
|
4556
4809
|
|
|
@@ -4734,7 +4987,7 @@ static VALUE ruby_curl_easy_perform_post(int argc, VALUE *argv, VALUE self) {
|
|
|
4734
4987
|
|
|
4735
4988
|
rb_scan_args(argc, argv, "*", &args_ary);
|
|
4736
4989
|
|
|
4737
|
-
rbce =
|
|
4990
|
+
rbce = ruby_curl_easy_get_idle_handle(self);
|
|
4738
4991
|
curl = rbce->curl;
|
|
4739
4992
|
|
|
4740
4993
|
memset(rbce->err_buf, 0, CURL_ERROR_SIZE);
|
|
@@ -4793,7 +5046,7 @@ static VALUE ruby_curl_easy_perform_patch(int argc, VALUE *argv, VALUE self) {
|
|
|
4793
5046
|
VALUE args_ary;
|
|
4794
5047
|
|
|
4795
5048
|
rb_scan_args(argc, argv, "*", &args_ary);
|
|
4796
|
-
rbce =
|
|
5049
|
+
rbce = ruby_curl_easy_get_idle_handle(self);
|
|
4797
5050
|
curl = rbce->curl;
|
|
4798
5051
|
|
|
4799
5052
|
/* Clear the error buffer */
|
|
@@ -4842,7 +5095,7 @@ static VALUE ruby_curl_easy_perform_put(int argc, VALUE *argv, VALUE self) {
|
|
|
4842
5095
|
VALUE args_ary;
|
|
4843
5096
|
|
|
4844
5097
|
rb_scan_args(argc, argv, "*", &args_ary);
|
|
4845
|
-
rbce =
|
|
5098
|
+
rbce = ruby_curl_easy_get_idle_handle(self);
|
|
4846
5099
|
curl = rbce->curl;
|
|
4847
5100
|
|
|
4848
5101
|
memset(rbce->err_buf, 0, CURL_ERROR_SIZE);
|
|
@@ -5747,6 +6000,7 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
|
|
|
5747
6000
|
CURB_OBJECT_HSETTER(ruby_curl_easy, url);
|
|
5748
6001
|
} break;
|
|
5749
6002
|
case CURLOPT_CUSTOMREQUEST:
|
|
6003
|
+
if (!NIL_P(val)) curb_check_header_line(StringValue(val), "custom request method");
|
|
5750
6004
|
curl_easy_setopt(rbce->curl, CURLOPT_CUSTOMREQUEST, NIL_P(val) ? NULL : StringValueCStr(val));
|
|
5751
6005
|
break;
|
|
5752
6006
|
case CURLOPT_HTTP_VERSION: {
|
|
@@ -5822,6 +6076,7 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
|
|
|
5822
6076
|
#ifdef HAVE_CURLOPT_REQUEST_TARGET
|
|
5823
6077
|
case CURLOPT_REQUEST_TARGET: {
|
|
5824
6078
|
/* Forward request-target directly to libcurl as a string. */
|
|
6079
|
+
if (!NIL_P(val)) curb_check_header_line(StringValue(val), "request target");
|
|
5825
6080
|
curl_easy_setopt(rbce->curl, CURLOPT_REQUEST_TARGET, NIL_P(val) ? NULL : StringValueCStr(val));
|
|
5826
6081
|
} break;
|
|
5827
6082
|
#endif
|
|
@@ -5978,23 +6233,36 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
|
|
|
5978
6233
|
curl_easy_setopt(rbce->curl, CURLOPT_HAPROXYPROTOCOL, NUM2LONG(val));
|
|
5979
6234
|
break;
|
|
5980
6235
|
#endif
|
|
5981
|
-
case CURLOPT_STDERR:
|
|
6236
|
+
case CURLOPT_STDERR: {
|
|
5982
6237
|
// libcurl requires raw FILE pointer and this should be IO object in Ruby.
|
|
5983
6238
|
// Tempfile or StringIO won't work.
|
|
6239
|
+
FILE *stderr_file;
|
|
5984
6240
|
Check_Type(val, T_FILE);
|
|
5985
6241
|
GetOpenFile(val, open_f_ptr);
|
|
5986
|
-
|
|
5987
|
-
/*
|
|
5988
|
-
*
|
|
6242
|
+
rb_io_check_writable(open_f_ptr);
|
|
6243
|
+
/* Hand libcurl a stream curb owns on a duplicate descriptor. Passing
|
|
6244
|
+
* the IO's own FILE* left libcurl writing through freed memory once the
|
|
6245
|
+
* caller closed the IO, including after Multi#add. */
|
|
6246
|
+
#ifdef HAVE_RB_IO_DESCRIPTOR
|
|
6247
|
+
stderr_file = ruby_curl_easy_dup_stderr_file(rb_io_descriptor(val));
|
|
6248
|
+
#else
|
|
6249
|
+
stderr_file = ruby_curl_easy_dup_stderr_file(open_f_ptr->fd);
|
|
6250
|
+
#endif
|
|
6251
|
+
curl_easy_setopt(rbce->curl, CURLOPT_STDERR, stderr_file);
|
|
6252
|
+
ruby_curl_easy_close_stderr_file(rbce);
|
|
6253
|
+
rbce->stderr_file = stderr_file;
|
|
6254
|
+
/* Kept so completion can flush the caller's own buffered output. */
|
|
5989
6255
|
rb_easy_set("stderr_io", val);
|
|
5990
|
-
break;
|
|
6256
|
+
} break;
|
|
5991
6257
|
case CURLOPT_PROTOCOLS:
|
|
5992
6258
|
case CURLOPT_REDIR_PROTOCOLS:
|
|
5993
6259
|
curl_easy_setopt(rbce->curl, option, NUM2LONG(val));
|
|
6260
|
+
if (option == CURLOPT_PROTOCOLS) rbce->protocols_set = 1;
|
|
5994
6261
|
break;
|
|
5995
6262
|
#ifdef HAVE_CURLOPT_PROTOCOLS_STR
|
|
5996
6263
|
case CURLOPT_PROTOCOLS_STR:
|
|
5997
6264
|
curl_easy_setopt(rbce->curl, CURLOPT_PROTOCOLS_STR, NIL_P(val) ? NULL : StringValueCStr(val));
|
|
6265
|
+
rbce->protocols_set = !NIL_P(val);
|
|
5998
6266
|
break;
|
|
5999
6267
|
#endif
|
|
6000
6268
|
#ifdef HAVE_CURLOPT_REDIR_PROTOCOLS_STR
|
data/ext/curb_easy.h
CHANGED
|
@@ -65,6 +65,8 @@ typedef struct {
|
|
|
65
65
|
long http_auth_types;
|
|
66
66
|
long proxy_auth_types;
|
|
67
67
|
long max_redirs;
|
|
68
|
+
char max_redirs_set; /* user configured max_redirs (nil = libcurl-style default) */
|
|
69
|
+
char protocols_set; /* CURLOPT_PROTOCOLS(_STR) allowlist configured */
|
|
68
70
|
unsigned long timeout;
|
|
69
71
|
unsigned long timeout_ms;
|
|
70
72
|
unsigned long connect_timeout;
|
|
@@ -113,6 +115,9 @@ typedef struct {
|
|
|
113
115
|
struct curl_slist *curl_connect_to;
|
|
114
116
|
curb_cidr_rule *network_allowed_cidr_rules;
|
|
115
117
|
char **network_allowed_hosts;
|
|
118
|
+
/* CURLOPT_STDERR target. curb owns this FILE* (on a duplicated descriptor)
|
|
119
|
+
* so closing or collecting the caller's IO cannot free it under libcurl. */
|
|
120
|
+
FILE *stderr_file;
|
|
116
121
|
|
|
117
122
|
/* attachment_sequence of the multi at the time this easy was last added. */
|
|
118
123
|
unsigned long multi_attachment_generation;
|