curb 1.4.0 → 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.
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;
@@ -747,7 +842,7 @@ VALUE rb_curl_easy_take_callback_error(ruby_curl_easy *rbce) {
747
842
  static VALUE ruby_curl_easy_take_callback_error(VALUE self) {
748
843
  ruby_curl_easy *rbce = NULL;
749
844
 
750
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
845
+ rbce = ruby_curl_easy_get_handle(self);
751
846
  return rb_curl_easy_take_callback_error(rbce);
752
847
  }
753
848
 
@@ -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(32768));
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(16384));
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;
@@ -1303,6 +1405,49 @@ static ruby_curl_multi *ruby_curl_multi_pointer_if_compatible(VALUE multi_val) {
1303
1405
  #endif
1304
1406
  }
1305
1407
 
1408
+ /* Explicit lifecycle operations can update Ruby bookkeeping; the GC free
1409
+ * path below must remain allocation-free. */
1410
+ static void ruby_curl_easy_detach(VALUE self, ruby_curl_easy *rbce) {
1411
+ VALUE multi = rbce->multi;
1412
+ ruby_curl_multi *rbcm = ruby_curl_multi_pointer_if_compatible(multi);
1413
+ if (rbcm) {
1414
+ CURLMcode result = rb_curl_multi_detach_easy(rbcm, rbce);
1415
+ if (result != CURLM_OK) raise_curl_multi_error_exception(result);
1416
+ rb_curl_multi_remove_request_reference(multi, self);
1417
+ }
1418
+ rbce->multi = Qnil;
1419
+ }
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
+
1306
1451
  static void ruby_curl_easy_free(ruby_curl_easy *rbce) {
1307
1452
  if (!rbce) {
1308
1453
  return;
@@ -1319,10 +1464,7 @@ static void ruby_curl_easy_free(ruby_curl_easy *rbce) {
1319
1464
  * libcurl retaining a dangling pointer to a soon-to-be cleaned-up easy
1320
1465
  * handle. This path runs during GC, so it must not invoke Ruby APIs that
1321
1466
  * can allocate or raise. */
1322
- if (rbcm->handle && rbce->curl) {
1323
- curl_multi_remove_handle(rbcm->handle, rbce->curl);
1324
- }
1325
- rb_curl_multi_forget_easy(rbcm, rbce);
1467
+ rb_curl_multi_detach_easy(rbcm, rbce);
1326
1468
  }
1327
1469
  }
1328
1470
 
@@ -1352,6 +1494,8 @@ static void ruby_curl_easy_free(ruby_curl_easy *rbce) {
1352
1494
  curl_easy_cleanup(rbce->curl);
1353
1495
  rbce->curl = NULL;
1354
1496
  }
1497
+ /* After cleanup: libcurl may log while closing connections. */
1498
+ ruby_curl_easy_close_stderr_file(rbce);
1355
1499
 
1356
1500
  rbce->self = Qnil;
1357
1501
  }
@@ -1396,6 +1540,48 @@ const rb_data_type_t ruby_curl_easy_data_type = {
1396
1540
  };
1397
1541
 
1398
1542
 
1543
+ void ruby_curl_easy_check_access(ruby_curl_easy *rbce) {
1544
+ if (rbce->multipart_building) {
1545
+ rb_raise(rb_eRuntimeError, "Cannot access an active curl handle during multipart construction");
1546
+ }
1547
+ rb_curl_multi_check_easy_thread(rbce);
1548
+ }
1549
+
1550
+ static void ruby_curl_easy_check_idle(ruby_curl_easy *rbce) {
1551
+ ruby_curl_easy_check_access(rbce);
1552
+ if (rb_curl_multi_easy_performing(rbce)) {
1553
+ rb_raise(rb_eRuntimeError, "Cannot modify an active curl handle during perform");
1554
+ }
1555
+ }
1556
+
1557
+ /* Public entry points must not pass an uninitialized handle to libcurl. */
1558
+ ruby_curl_easy *ruby_curl_easy_get_handle(VALUE self) {
1559
+ ruby_curl_easy *rbce;
1560
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
1561
+ if (!rbce->curl) {
1562
+ rb_raise(eCurlErrFailedInit, "Curl::Easy handle is not initialized");
1563
+ }
1564
+ ruby_curl_easy_check_access(rbce);
1565
+ return rbce;
1566
+ }
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
+
1577
+ /* Keep libcurl's typed getinfo arguments while checking every result. */
1578
+ #define CURB_GETINFO(handle, info, output) do { \
1579
+ CURLcode info_result = curl_easy_getinfo(handle, info, output); \
1580
+ if (info_result != CURLE_OK) { \
1581
+ raise_curl_easy_error_exception(info_result); \
1582
+ } \
1583
+ } while (0)
1584
+
1399
1585
  /* ================= ALLOC METHODS ====================*/
1400
1586
 
1401
1587
  static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
@@ -1414,6 +1600,8 @@ static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
1414
1600
  rbce->network_allowed_hosts = NULL;
1415
1601
  rbce->network_allowed_cidr_rule_count = 0;
1416
1602
  rbce->network_allowed_host_count = 0;
1603
+ /* Callers that may hold a stream close it before zeroing. */
1604
+ rbce->stderr_file = NULL;
1417
1605
 
1418
1606
  /* various-typed opts */
1419
1607
  rbce->local_port = 0;
@@ -1423,6 +1611,8 @@ static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
1423
1611
  rbce->http_auth_types = 0;
1424
1612
  rbce->proxy_auth_types = 0;
1425
1613
  rbce->max_redirs = -1;
1614
+ rbce->max_redirs_set = 0;
1615
+ rbce->protocols_set = 0;
1426
1616
  rbce->timeout = 0;
1427
1617
  rbce->timeout_ms = 0;
1428
1618
  rbce->connect_timeout = 0;
@@ -1451,6 +1641,8 @@ static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
1451
1641
  rbce->unrestricted_auth = 0;
1452
1642
  rbce->verbose = 0;
1453
1643
  rbce->multipart_form_post = 0;
1644
+ rbce->multipart_form_active = 0;
1645
+ rbce->multipart_building = 0;
1454
1646
  rbce->enable_cookies = 0;
1455
1647
  rbce->cookielist_engine_enabled = 0;
1456
1648
  rbce->ignore_content_length = 0;
@@ -1500,6 +1692,16 @@ static VALUE ruby_curl_easy_initialize(int argc, VALUE *argv, VALUE self) {
1500
1692
 
1501
1693
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
1502
1694
 
1695
+ if (rbce->callback_active || rbce->native_active) {
1696
+ rb_raise(rb_eRuntimeError, "Cannot initialize an active curl handle");
1697
+ }
1698
+
1699
+ ruby_curl_easy_check_idle(rbce);
1700
+ ruby_curl_easy_detach(self, rbce);
1701
+
1702
+ /* Detach and release any previous handle and its owned option lists. */
1703
+ ruby_curl_easy_free(rbce);
1704
+
1503
1705
  /* handler */
1504
1706
  rbce->curl = curl_easy_init();
1505
1707
  if (!rbce->curl) {
@@ -1571,25 +1773,45 @@ static VALUE duplicate_upload(VALUE upload) {
1571
1773
  *
1572
1774
  * Clone this Curl::Easy instance, creating a new instance.
1573
1775
  * This method duplicates the underlying CURL* handle.
1776
+ * A temporary multipart form attached during a request is not copied;
1777
+ * supply new PostFields to send a multipart request with the clone.
1574
1778
  */
1575
1779
  static VALUE ruby_curl_easy_clone(VALUE self) {
1576
1780
  ruby_curl_easy *rbce, *newrbce;
1577
1781
 
1578
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
1782
+ FILE *clone_stderr_file = NULL;
1579
1783
 
1580
- newrbce = ALLOC(ruby_curl_easy);
1581
- if (!newrbce) {
1582
- rb_raise(rb_eNoMemError, "Failed to allocate memory for Curl::Easy clone");
1784
+ rbce = ruby_curl_easy_get_handle(self);
1785
+
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));
1583
1789
  }
1790
+
1791
+ newrbce = ALLOC(ruby_curl_easy);
1584
1792
  /* shallow copy */
1585
1793
  memcpy(newrbce, rbce, sizeof(ruby_curl_easy));
1586
1794
 
1587
1795
  /* now deep copy */
1588
1796
  newrbce->curl = curl_easy_duphandle(rbce->curl);
1589
1797
  if (!newrbce->curl) {
1590
- free(newrbce);
1798
+ xfree(newrbce);
1799
+ if (clone_stderr_file) fclose(clone_stderr_file);
1591
1800
  rb_raise(rb_eNoMemError, "Failed to duplicate Curl::Easy handle");
1592
1801
  }
1802
+ /* duphandle borrows HTTPPOST, whose form is freed when the original
1803
+ * perform unwinds. Only reset it when attached: setting HTTPPOST also
1804
+ * changes request options and must not affect ordinary POST/upload clones. */
1805
+ if (newrbce->multipart_form_active) {
1806
+ CURLcode ecode = curl_easy_setopt(newrbce->curl, CURLOPT_HTTPPOST, NULL);
1807
+ if (ecode != CURLE_OK) {
1808
+ curl_easy_cleanup(newrbce->curl);
1809
+ xfree(newrbce);
1810
+ if (clone_stderr_file) fclose(clone_stderr_file);
1811
+ raise_curl_easy_error_exception(ecode);
1812
+ }
1813
+ newrbce->multipart_form_active = 0;
1814
+ }
1593
1815
  newrbce->curl_headers = (rbce->curl_headers) ? duplicate_curl_slist(rbce->curl_headers) : NULL;
1594
1816
  newrbce->curl_proxy_headers = (rbce->curl_proxy_headers) ? duplicate_curl_slist(rbce->curl_proxy_headers) : NULL;
1595
1817
  newrbce->curl_ftp_commands = (rbce->curl_ftp_commands) ? duplicate_curl_slist(rbce->curl_ftp_commands) : NULL;
@@ -1600,9 +1822,17 @@ static VALUE ruby_curl_easy_clone(VALUE self) {
1600
1822
  newrbce->network_allowed_hosts = NULL;
1601
1823
  newrbce->network_allowed_host_count = 0;
1602
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
+
1603
1832
  /* A cloned easy should not retain ownership reference to the original multi. */
1604
1833
  newrbce->multi = Qnil;
1605
1834
  newrbce->callback_error = Qnil;
1835
+ newrbce->callback_active = 0;
1606
1836
  newrbce->unsafe_destination_blocked = 0;
1607
1837
  memset(newrbce->unsafe_destination_error, 0, CURL_ERROR_SIZE);
1608
1838
  newrbce->native_active = 0;
@@ -1648,7 +1878,7 @@ static VALUE ruby_curl_easy_close(VALUE self) {
1648
1878
  CURLcode ecode;
1649
1879
  ruby_curl_easy *rbce;
1650
1880
 
1651
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
1881
+ rbce = ruby_curl_easy_get_handle(self);
1652
1882
 
1653
1883
  if (rbce->callback_active) {
1654
1884
  rb_raise(rb_eRuntimeError, "Cannot close an active curl handle within a callback");
@@ -1658,6 +1888,9 @@ static VALUE ruby_curl_easy_close(VALUE self) {
1658
1888
  rb_raise(rb_eRuntimeError, "Cannot close an active curl handle during native operation");
1659
1889
  }
1660
1890
 
1891
+ ruby_curl_easy_check_idle(rbce);
1892
+ ruby_curl_easy_detach(self, rbce);
1893
+
1661
1894
  ruby_curl_easy_free(rbce);
1662
1895
 
1663
1896
  /* reinit the handle */
@@ -1699,7 +1932,7 @@ static VALUE ruby_curl_easy_reset(VALUE self) {
1699
1932
  CURLcode ecode;
1700
1933
  ruby_curl_easy *rbce;
1701
1934
  VALUE opts_dup;
1702
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
1935
+ rbce = ruby_curl_easy_get_handle(self);
1703
1936
 
1704
1937
  if (rbce->callback_active) {
1705
1938
  rb_raise(rb_eRuntimeError, "Cannot close an active curl handle within a callback");
@@ -1709,10 +1942,15 @@ static VALUE ruby_curl_easy_reset(VALUE self) {
1709
1942
  rb_raise(rb_eRuntimeError, "Cannot reset an active curl handle during native operation");
1710
1943
  }
1711
1944
 
1945
+ ruby_curl_easy_check_idle(rbce);
1946
+ ruby_curl_easy_detach(self, rbce);
1947
+
1712
1948
  opts_dup = rb_funcall(rbce->opts, rb_intern("dup"), 0);
1713
1949
 
1714
1950
  ruby_curl_easy_cleanup(self, rbce);
1715
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);
1716
1954
  ruby_curl_easy_zero(rbce);
1717
1955
  rbce->self = self;
1718
1956
 
@@ -1790,7 +2028,7 @@ static VALUE ruby_curl_easy_proxy_headers_set(VALUE self, VALUE proxy_headers) {
1790
2028
  static VALUE ruby_curl_easy_headers_get(VALUE self) {
1791
2029
  ruby_curl_easy *rbce;
1792
2030
  VALUE headers;
1793
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2031
+ rbce = ruby_curl_easy_get_handle(self);
1794
2032
  headers = rb_easy_get("headers");//rb_hash_aref(rbce->opts, rb_intern("headers"));
1795
2033
  if (headers == Qnil) { headers = rb_easy_set("headers", rb_hash_new()); }
1796
2034
  return headers;
@@ -1825,7 +2063,7 @@ static VALUE ruby_curl_easy_headers_get(VALUE self) {
1825
2063
  static VALUE ruby_curl_easy_proxy_headers_get(VALUE self) {
1826
2064
  ruby_curl_easy *rbce;
1827
2065
  VALUE proxy_headers;
1828
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2066
+ rbce = ruby_curl_easy_get_handle(self);
1829
2067
  proxy_headers = rb_easy_get("proxy_headers");//rb_hash_aref(rbce->opts, rb_intern("proxy_headers"));
1830
2068
  if (proxy_headers == Qnil) { proxy_headers = rb_easy_set("proxy_headers", rb_hash_new()); }
1831
2069
  return proxy_headers;
@@ -2075,7 +2313,7 @@ static VALUE ruby_curl_easy_post_body_set_with_mode_body(VALUE argp) {
2075
2313
  char *data;
2076
2314
  long len;
2077
2315
 
2078
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2316
+ rbce = ruby_curl_easy_get_idle_handle(self);
2079
2317
 
2080
2318
  curl = rbce->curl;
2081
2319
 
@@ -2134,7 +2372,7 @@ static VALUE ruby_curl_easy_post_body_set_with_mode(VALUE self, VALUE post_body,
2134
2372
  ruby_curl_easy *rbce;
2135
2373
  struct post_body_set_args args = { self, post_body, force_http_get_on_nil };
2136
2374
 
2137
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2375
+ rbce = ruby_curl_easy_get_handle(self);
2138
2376
  ruby_curl_easy_enter_native(rbce);
2139
2377
 
2140
2378
  return rb_ensure(ruby_curl_easy_post_body_set_with_mode_body, (VALUE)&args,
@@ -2171,7 +2409,7 @@ static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
2171
2409
  VALUE headers;
2172
2410
  VALUE infile_size = Qnil;
2173
2411
 
2174
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2412
+ rbce = ruby_curl_easy_get_idle_handle(self);
2175
2413
 
2176
2414
  /*
2177
2415
  * Validate and prepare Ruby-visible state before mutating the CURL handle.
@@ -2356,7 +2594,7 @@ static VALUE ruby_curl_easy_dns_servers_get(VALUE self) {
2356
2594
  static VALUE ruby_curl_easy_allow_unix_socket_set(VALUE self, VALUE allow) {
2357
2595
  ruby_curl_easy *rbce;
2358
2596
 
2359
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2597
+ rbce = ruby_curl_easy_get_handle(self);
2360
2598
  rbce->allow_unix_socket = RTEST(allow) ? 1 : 0;
2361
2599
 
2362
2600
  return allow;
@@ -2365,7 +2603,7 @@ static VALUE ruby_curl_easy_allow_unix_socket_set(VALUE self, VALUE allow) {
2365
2603
  static VALUE ruby_curl_easy_allow_proxy_set(VALUE self, VALUE allow) {
2366
2604
  ruby_curl_easy *rbce;
2367
2605
 
2368
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2606
+ rbce = ruby_curl_easy_get_handle(self);
2369
2607
  rbce->allow_proxy = RTEST(allow) ? 1 : 0;
2370
2608
 
2371
2609
  return allow;
@@ -2384,7 +2622,7 @@ static VALUE ruby_curl_easy_allowed_cidrs_set(VALUE self, VALUE cidrs) {
2384
2622
  VALUE normalized;
2385
2623
  VALUE stored;
2386
2624
 
2387
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2625
+ rbce = ruby_curl_easy_get_handle(self);
2388
2626
  normalized = curb_normalize_cidr_list(cidrs);
2389
2627
  stored = curb_dup_string_array(normalized);
2390
2628
  rb_hash_aset(rbce->opts, rb_easy_hkey("allowed_cidrs"), stored);
@@ -2405,7 +2643,7 @@ static VALUE ruby_curl_easy_allowed_cidrs_get(VALUE self) {
2405
2643
  ruby_curl_easy *rbce;
2406
2644
  VALUE cidrs;
2407
2645
 
2408
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2646
+ rbce = ruby_curl_easy_get_handle(self);
2409
2647
  cidrs = rb_hash_aref(rbce->opts, rb_easy_hkey("allowed_cidrs"));
2410
2648
 
2411
2649
  return curb_dup_string_array(cidrs);
@@ -2424,7 +2662,7 @@ static VALUE ruby_curl_easy_allowed_hosts_set(VALUE self, VALUE hosts) {
2424
2662
  VALUE normalized;
2425
2663
  VALUE stored;
2426
2664
 
2427
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2665
+ rbce = ruby_curl_easy_get_handle(self);
2428
2666
  normalized = curb_normalize_host_list(hosts);
2429
2667
  stored = curb_dup_string_array(normalized);
2430
2668
  rb_hash_aset(rbce->opts, rb_easy_hkey("allowed_hosts"), stored);
@@ -2441,7 +2679,7 @@ static VALUE ruby_curl_easy_allowed_hosts_get(VALUE self) {
2441
2679
  ruby_curl_easy *rbce;
2442
2680
  VALUE hosts;
2443
2681
 
2444
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2682
+ rbce = ruby_curl_easy_get_handle(self);
2445
2683
  hosts = rb_hash_aref(rbce->opts, rb_easy_hkey("allowed_hosts"));
2446
2684
 
2447
2685
  return curb_dup_string_array(hosts);
@@ -2615,7 +2853,7 @@ static VALUE ruby_curl_easy_http_auth_types_set(int argc, VALUE *argv, VALUE sel
2615
2853
  VALUE args_ary;
2616
2854
 
2617
2855
  rb_scan_args(argc, argv, "*", &args_ary);
2618
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2856
+ rbce = ruby_curl_easy_get_handle(self);
2619
2857
 
2620
2858
  rbce->http_auth_types = ruby_curl_easy_auth_types_to_mask(args_ary);
2621
2859
  return LONG2NUM(rbce->http_auth_types);
@@ -2646,7 +2884,7 @@ static VALUE ruby_curl_easy_proxy_auth_types_set(int argc, VALUE *argv, VALUE se
2646
2884
  VALUE args_ary;
2647
2885
 
2648
2886
  rb_scan_args(argc, argv, "*", &args_ary);
2649
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2887
+ rbce = ruby_curl_easy_get_handle(self);
2650
2888
 
2651
2889
  rbce->proxy_auth_types = ruby_curl_easy_auth_types_to_mask(args_ary);
2652
2890
  return LONG2NUM(rbce->proxy_auth_types);
@@ -2668,14 +2906,27 @@ static VALUE ruby_curl_easy_proxy_auth_types_get(VALUE self) {
2668
2906
  * easy.max_redirects = fixnum or nil => fixnum or nil
2669
2907
  *
2670
2908
  * Set the maximum number of redirections to follow in the following +perform+
2671
- * calls. Set to nil or -1 allow an infinite number (the default). Setting this
2672
- * option only makes sense if +follow_location+ is also set true.
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.
2673
2913
  *
2674
2914
  * With libcurl >= 7.15.1, setting this to 0 will cause libcurl to refuse any
2675
2915
  * redirect.
2676
2916
  */
2677
2917
  static VALUE ruby_curl_easy_max_redirects_set(VALUE self, VALUE max_redirs) {
2678
- CURB_IMMED_SETTER(ruby_curl_easy, max_redirs, -1);
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;
2679
2930
  }
2680
2931
 
2681
2932
  /*
@@ -2708,7 +2959,7 @@ static VALUE ruby_curl_easy_max_redirects_get(VALUE self) {
2708
2959
  */
2709
2960
  static VALUE ruby_curl_easy_timeout_set(VALUE self, VALUE timeout_s) {
2710
2961
  ruby_curl_easy *rbce;
2711
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2962
+ rbce = ruby_curl_easy_get_handle(self);
2712
2963
 
2713
2964
  if (Qnil == timeout_s || NUM2DBL(timeout_s) <= 0.0) {
2714
2965
  rbce->timeout_ms = 0;
@@ -2731,7 +2982,7 @@ static VALUE ruby_curl_easy_timeout_set(VALUE self, VALUE timeout_s) {
2731
2982
  */
2732
2983
  static VALUE ruby_curl_easy_timeout_get(VALUE self) {
2733
2984
  ruby_curl_easy *rbce;
2734
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2985
+ rbce = ruby_curl_easy_get_handle(self);
2735
2986
  return DBL2NUM(rbce->timeout_ms / 1000.0);
2736
2987
  }
2737
2988
 
@@ -2749,7 +3000,7 @@ static VALUE ruby_curl_easy_timeout_get(VALUE self) {
2749
3000
  */
2750
3001
  static VALUE ruby_curl_easy_timeout_ms_set(VALUE self, VALUE timeout_ms) {
2751
3002
  ruby_curl_easy *rbce;
2752
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3003
+ rbce = ruby_curl_easy_get_handle(self);
2753
3004
 
2754
3005
  if (Qnil == timeout_ms || NUM2DBL(timeout_ms) <= 0.0) {
2755
3006
  rbce->timeout_ms = 0;
@@ -2769,7 +3020,7 @@ static VALUE ruby_curl_easy_timeout_ms_set(VALUE self, VALUE timeout_ms) {
2769
3020
  */
2770
3021
  static VALUE ruby_curl_easy_timeout_ms_get(VALUE self) {
2771
3022
  ruby_curl_easy *rbce;
2772
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3023
+ rbce = ruby_curl_easy_get_handle(self);
2773
3024
  return LONG2NUM(rbce->timeout_ms);
2774
3025
  }
2775
3026
 
@@ -3242,7 +3493,7 @@ static VALUE ruby_curl_easy_use_netrc_q(VALUE self) {
3242
3493
  */
3243
3494
  static VALUE ruby_curl_easy_autoreferer_set(VALUE self, VALUE autoreferer) {
3244
3495
  ruby_curl_easy *rbce;
3245
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3496
+ rbce = ruby_curl_easy_get_idle_handle(self);
3246
3497
 
3247
3498
  if (Qtrue == autoreferer) {
3248
3499
  curl_easy_setopt(rbce->curl, CURLOPT_AUTOREFERER, 1);
@@ -3399,7 +3650,7 @@ static VALUE ruby_curl_easy_ignore_content_length_q(VALUE self) {
3399
3650
  static VALUE ruby_curl_easy_resolve_mode(VALUE self) {
3400
3651
  ruby_curl_easy *rbce;
3401
3652
  unsigned short rm;
3402
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3653
+ rbce = ruby_curl_easy_get_handle(self);
3403
3654
 
3404
3655
  rm = rbce->resolve_mode;
3405
3656
 
@@ -3431,7 +3682,7 @@ static VALUE ruby_curl_easy_resolve_mode_set(VALUE self, VALUE resolve_mode) {
3431
3682
  } else {
3432
3683
  ruby_curl_easy *rbce;
3433
3684
  ID resolve_mode_id;
3434
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3685
+ rbce = ruby_curl_easy_get_handle(self);
3435
3686
 
3436
3687
  resolve_mode_id = rb_to_id(resolve_mode);
3437
3688
 
@@ -3460,7 +3711,7 @@ static VALUE ruby_curl_easy_resolve_mode_set(VALUE self, VALUE resolve_mode) {
3460
3711
  */
3461
3712
  static VALUE ruby_curl_easy_network_policy_get(VALUE self) {
3462
3713
  ruby_curl_easy *rbce;
3463
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3714
+ rbce = ruby_curl_easy_get_handle(self);
3464
3715
 
3465
3716
  switch (rbce->network_policy) {
3466
3717
  case CURB_NETWORK_POLICY_PUBLIC:
@@ -3484,7 +3735,7 @@ static VALUE ruby_curl_easy_network_policy_set(VALUE self, VALUE network_policy)
3484
3735
  ruby_curl_easy *rbce;
3485
3736
  ID network_policy_id;
3486
3737
 
3487
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3738
+ rbce = ruby_curl_easy_get_handle(self);
3488
3739
 
3489
3740
  if (NIL_P(network_policy)) {
3490
3741
  rbce->network_policy = CURB_NETWORK_POLICY_NONE;
@@ -3522,7 +3773,7 @@ static VALUE ruby_curl_easy_network_policy_set(VALUE self, VALUE network_policy)
3522
3773
  */
3523
3774
  static VALUE ruby_curl_easy_unsafe_destination_error_get(VALUE self) {
3524
3775
  ruby_curl_easy *rbce;
3525
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3776
+ rbce = ruby_curl_easy_get_handle(self);
3526
3777
 
3527
3778
  if (rbce->unsafe_destination_blocked && rbce->unsafe_destination_error[0]) {
3528
3779
  return rb_str_new2(rbce->unsafe_destination_error);
@@ -3540,7 +3791,7 @@ static VALUE ruby_curl_easy_unsafe_destination_error_get(VALUE self) {
3540
3791
  */
3541
3792
  static VALUE ruby_curl_easy_unix_socket_path_get(VALUE self) {
3542
3793
  ruby_curl_easy *rbce;
3543
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3794
+ rbce = ruby_curl_easy_get_handle(self);
3544
3795
 
3545
3796
  return rb_easy_get("unix_socket_path");
3546
3797
  }
@@ -3559,7 +3810,7 @@ static VALUE ruby_curl_easy_http_version_set(VALUE self, VALUE version) {
3559
3810
  ruby_curl_easy *rbce;
3560
3811
  long http_version;
3561
3812
 
3562
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3813
+ rbce = ruby_curl_easy_get_handle(self);
3563
3814
 
3564
3815
  if (NIL_P(version)) {
3565
3816
  http_version = CURL_HTTP_VERSION_NONE;
@@ -3581,7 +3832,7 @@ static VALUE ruby_curl_easy_http_version_set(VALUE self, VALUE version) {
3581
3832
  static VALUE ruby_curl_easy_http_version_get(VALUE self) {
3582
3833
  ruby_curl_easy *rbce;
3583
3834
 
3584
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3835
+ rbce = ruby_curl_easy_get_handle(self);
3585
3836
 
3586
3837
  return LONG2NUM(rbce->http_version);
3587
3838
  }
@@ -3743,6 +3994,37 @@ static VALUE ruby_curl_easy_on_debug_set(int argc, VALUE *argv, VALUE self) {
3743
3994
 
3744
3995
  /* =================== PERFORM =====================*/
3745
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
+
3746
4028
  /***********************************************
3747
4029
  * This is an rb_iterate callback used to set up http headers.
3748
4030
  */
@@ -3774,6 +4056,8 @@ static VALUE cb_each_http_header(VALUE header, VALUE wrap, int _c, const VALUE *
3774
4056
 
3775
4057
  //rb_p(header_str);
3776
4058
 
4059
+ StringValue(header_str);
4060
+ curb_check_header_line(header_str, "HTTP header");
3777
4061
  struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(header_str));
3778
4062
  if (!new_list) {
3779
4063
  rb_raise(rb_eNoMemError, "Failed to append to header list");
@@ -3810,6 +4094,8 @@ static VALUE cb_each_http_proxy_header(VALUE proxy_header, VALUE wrap, int _c, c
3810
4094
 
3811
4095
  //rb_p(header_str);
3812
4096
 
4097
+ StringValue(proxy_header_str);
4098
+ curb_check_header_line(proxy_header_str, "HTTP proxy header");
3813
4099
  struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(proxy_header_str));
3814
4100
  if (!new_list) {
3815
4101
  rb_raise(rb_eNoMemError, "Failed to append to proxy header list");
@@ -3955,6 +4241,17 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
3955
4241
  curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, rbce->forbid_reuse_set ? rbce->forbid_reuse : 0L);
3956
4242
  #endif
3957
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
3958
4255
  #endif
3959
4256
 
3960
4257
  // network stuff and auth
@@ -4021,7 +4318,7 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
4021
4318
  /* clear out the body_data if it was set */
4022
4319
  rb_easy_del("body_data");
4023
4320
  } else {
4024
- rb_easy_set("body_data", rb_str_buf_new(32768));
4321
+ rb_easy_set("body_data", rb_str_buf_new(CURB_DEFAULT_BODY_BUFFER_CAPA));
4025
4322
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)&default_body_handler);
4026
4323
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, rbce);
4027
4324
  }
@@ -4032,7 +4329,7 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
4032
4329
  /* clear out the header_data if it was set */
4033
4330
  rb_easy_del("header_data");
4034
4331
  } else {
4035
- rb_easy_set("header_data", rb_str_buf_new(16384));
4332
+ rb_easy_set("header_data", rb_str_buf_new(CURB_DEFAULT_HEADER_BUFFER_CAPA));
4036
4333
  curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, (curl_write_callback)&default_header_handler);
4037
4334
  curl_easy_setopt(curl, CURLOPT_HEADERDATA, rbce);
4038
4335
  }
@@ -4076,14 +4373,20 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
4076
4373
 
4077
4374
  curl_easy_setopt(curl, CURLOPT_HEADER, rbce->header_in_body);
4078
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 {
4079
4381
  #if LIBCURL_VERSION_NUM == 0x081000
4080
- /* Work around 8.16.0 regression that clamps -1 (infinite) to zero */
4081
- if (rbce->max_redirs >= 0) {
4082
- curl_easy_setopt(curl, CURLOPT_MAXREDIRS, rbce->max_redirs);
4083
- }
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
+ }
4084
4386
  #else
4085
- curl_easy_setopt(curl, CURLOPT_MAXREDIRS, rbce->max_redirs);
4387
+ curl_easy_setopt(curl, CURLOPT_MAXREDIRS, rbce->max_redirs);
4086
4388
  #endif
4389
+ }
4087
4390
 
4088
4391
  curl_easy_setopt(curl, CURLOPT_HTTPPROXYTUNNEL,
4089
4392
  public_policy_disables_proxy ? 0L : rbce->proxy_tunnel);
@@ -4199,9 +4502,7 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
4199
4502
  curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");
4200
4503
  }
4201
4504
 
4202
- if (!rb_easy_nil("cookies")) {
4203
- curl_easy_setopt(curl, CURLOPT_COOKIE, rb_easy_get_str("cookies"));
4204
- }
4505
+ /* cookies= is applied after the HTTP headers below. */
4205
4506
 
4206
4507
  /* Set up HTTPS cert handling if necessary */
4207
4508
  if (!rb_easy_nil("cert")) {
@@ -4246,7 +4547,9 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
4246
4547
 
4247
4548
  /* Set the user-agent string if specified */
4248
4549
  if (!rb_easy_nil("useragent")) {
4249
- curl_easy_setopt(curl, CURLOPT_USERAGENT, rb_easy_get_str("useragent"));
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));
4250
4553
  }
4251
4554
 
4252
4555
  /* Setup can be rerun for safety-policy changes while a handle is attached.
@@ -4263,6 +4566,7 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
4263
4566
  rb_block_call(rb_easy_get("headers"), rb_intern("each"), 0, NULL, cb_each_http_header, wrap);
4264
4567
  } else {
4265
4568
  VALUE headers_str = rb_obj_as_string(rb_easy_get("headers"));
4569
+ curb_check_header_line(headers_str, "HTTP header");
4266
4570
  struct curl_slist *new_list = curl_slist_append(*hdrs, StringValuePtr(headers_str));
4267
4571
  if (!new_list) {
4268
4572
  rb_raise(rb_eNoMemError, "Failed to append to headers list");
@@ -4275,6 +4579,28 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
4275
4579
  }
4276
4580
  }
4277
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
+
4278
4604
  #ifdef HAVE_CURLOPT_PROXYHEADER
4279
4605
  /* Setup HTTP proxy headers if necessary */
4280
4606
  curl_easy_setopt(curl, CURLOPT_PROXYHEADER, NULL); // XXX: maybe we shouldn't be clearing this?
@@ -4285,6 +4611,7 @@ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
4285
4611
  rb_block_call(rb_easy_get("proxy_headers"), rb_intern("each"), 0, NULL, cb_each_http_proxy_header, wrap);
4286
4612
  } else {
4287
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");
4288
4615
  struct curl_slist *new_list = curl_slist_append(*phdrs, StringValuePtr(proxy_headers_str));
4289
4616
  if (!new_list) {
4290
4617
  rb_raise(rb_eNoMemError, "Failed to append to proxy headers list");
@@ -4364,7 +4691,7 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
4364
4691
  static VALUE ruby_curl_easy_setup_self(VALUE self) {
4365
4692
  ruby_curl_easy *rbce;
4366
4693
 
4367
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
4694
+ rbce = ruby_curl_easy_get_idle_handle(self);
4368
4695
  ruby_curl_easy_setup(rbce);
4369
4696
 
4370
4697
  return self;
@@ -4470,11 +4797,13 @@ static VALUE ruby_curl_easy_perform_verb_str(VALUE self, const char *verb) {
4470
4797
  CURL *curl;
4471
4798
  VALUE retval;
4472
4799
 
4473
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
4800
+ rbce = ruby_curl_easy_get_idle_handle(self);
4474
4801
  curl = rbce->curl;
4475
4802
 
4476
4803
  memset(rbce->err_buf, 0, CURL_ERROR_SIZE);
4477
4804
 
4805
+ if (verb) curb_check_header_line_len(verb, (long)strlen(verb), "HTTP verb");
4806
+
4478
4807
  /* Use method override and adjust related options for special verbs. */
4479
4808
  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, verb);
4480
4809
 
@@ -4535,6 +4864,8 @@ struct easy_form_perform_args {
4535
4864
  struct curl_httppost *last;
4536
4865
  int clear_customrequest;
4537
4866
  int form_set_on_curl;
4867
+ ruby_curl_form_string *strings;
4868
+ ruby_curl_easy *rbce;
4538
4869
  };
4539
4870
 
4540
4871
  struct easy_join_args {
@@ -4556,15 +4887,16 @@ static VALUE join_easy_arguments(ruby_curl_easy *rbce, VALUE args_ary) {
4556
4887
 
4557
4888
  static void append_multipart_form_argument(VALUE arg,
4558
4889
  struct curl_httppost **first,
4559
- struct curl_httppost **last) {
4890
+ struct curl_httppost **last,
4891
+ ruby_curl_form_string **strings) {
4560
4892
  if (rb_obj_is_instance_of(arg, cCurlPostField)) {
4561
- append_to_form(arg, first, last);
4893
+ append_to_form(arg, first, last, strings);
4562
4894
  } else if (rb_type(arg) == T_ARRAY) {
4563
4895
  long j, argv_len = RARRAY_LEN(arg);
4564
4896
  for (j = 0; j < argv_len; ++j) {
4565
4897
  VALUE field = rb_ary_entry(arg, j);
4566
4898
  if (rb_obj_is_instance_of(field, cCurlPostField)) {
4567
- append_to_form(field, first, last);
4899
+ append_to_form(field, first, last, strings);
4568
4900
  } else {
4569
4901
  rb_raise(eCurlErrInvalidPostField,
4570
4902
  "You must use PostFields only with multipart form posts");
@@ -4579,23 +4911,35 @@ static void append_multipart_form_argument(VALUE arg,
4579
4911
  static VALUE build_and_perform_multipart_form(VALUE argp) {
4580
4912
  struct easy_form_perform_args *args = (struct easy_form_perform_args *)argp;
4581
4913
  int i;
4914
+ ruby_curl_easy *rbce;
4915
+
4916
+ rbce = ruby_curl_easy_get_handle(args->self);
4917
+ args->rbce = rbce;
4918
+ ruby_curl_easy_enter_native(rbce);
4919
+ rbce->multipart_building = 1;
4582
4920
 
4583
4921
  for (i = 0; i < args->argc; i++) {
4584
- append_multipart_form_argument(args->argv[i], &args->first, &args->last);
4922
+ append_multipart_form_argument(args->argv[i], &args->first, &args->last, &args->strings);
4585
4923
  }
4586
4924
 
4587
4925
  curl_easy_setopt(args->curl, CURLOPT_POST, 0);
4588
4926
  curl_easy_setopt(args->curl, CURLOPT_HTTPPOST, args->first);
4589
4927
  args->form_set_on_curl = 1;
4928
+ rbce->multipart_form_active = 1;
4590
4929
 
4930
+ rbce->multipart_building = 0;
4591
4931
  return call_easy_perform(args->self);
4592
4932
  }
4593
4933
 
4594
4934
  static VALUE ensure_free_form_post(VALUE argp) {
4595
4935
  struct easy_form_perform_args *args = (struct easy_form_perform_args *)argp;
4936
+ if (args->rbce) args->rbce->multipart_building = 0;
4596
4937
  if (args->curl) {
4597
4938
  if (args->form_set_on_curl) {
4939
+ ruby_curl_easy *rbce;
4940
+ rbce = ruby_curl_easy_get_handle(args->self);
4598
4941
  curl_easy_setopt(args->curl, CURLOPT_HTTPPOST, NULL);
4942
+ rbce->multipart_form_active = 0;
4599
4943
  }
4600
4944
  if (args->clear_customrequest) {
4601
4945
  curl_easy_setopt(args->curl, CURLOPT_CUSTOMREQUEST, NULL);
@@ -4606,6 +4950,9 @@ static VALUE ensure_free_form_post(VALUE argp) {
4606
4950
  args->first = NULL;
4607
4951
  }
4608
4952
  args->last = NULL;
4953
+ free_form_strings(args->strings);
4954
+ args->strings = NULL;
4955
+ if (args->rbce) ruby_curl_easy_leave_native((VALUE)args->rbce);
4609
4956
  return Qnil;
4610
4957
  }
4611
4958
 
@@ -4640,7 +4987,7 @@ static VALUE ruby_curl_easy_perform_post(int argc, VALUE *argv, VALUE self) {
4640
4987
 
4641
4988
  rb_scan_args(argc, argv, "*", &args_ary);
4642
4989
 
4643
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
4990
+ rbce = ruby_curl_easy_get_idle_handle(self);
4644
4991
  curl = rbce->curl;
4645
4992
 
4646
4993
  memset(rbce->err_buf, 0, CURL_ERROR_SIZE);
@@ -4649,7 +4996,7 @@ static VALUE ruby_curl_easy_perform_post(int argc, VALUE *argv, VALUE self) {
4649
4996
 
4650
4997
  if (rbce->multipart_form_post) {
4651
4998
  VALUE ret;
4652
- struct easy_form_perform_args perform_args = { self, curl, argc, argv, NULL, NULL, 0, 0 };
4999
+ struct easy_form_perform_args perform_args = { self, curl, argc, argv, NULL, NULL, 0, 0, NULL, NULL };
4653
5000
  ret = rb_ensure(build_and_perform_multipart_form, (VALUE)&perform_args, ensure_free_form_post, (VALUE)&perform_args);
4654
5001
 
4655
5002
  return ret;
@@ -4699,7 +5046,7 @@ static VALUE ruby_curl_easy_perform_patch(int argc, VALUE *argv, VALUE self) {
4699
5046
  VALUE args_ary;
4700
5047
 
4701
5048
  rb_scan_args(argc, argv, "*", &args_ary);
4702
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5049
+ rbce = ruby_curl_easy_get_idle_handle(self);
4703
5050
  curl = rbce->curl;
4704
5051
 
4705
5052
  /* Clear the error buffer */
@@ -4710,7 +5057,7 @@ static VALUE ruby_curl_easy_perform_patch(int argc, VALUE *argv, VALUE self) {
4710
5057
 
4711
5058
  if (rbce->multipart_form_post) {
4712
5059
  VALUE ret;
4713
- struct easy_form_perform_args perform_args = { self, curl, argc, argv, NULL, NULL, 1, 0 };
5060
+ struct easy_form_perform_args perform_args = { self, curl, argc, argv, NULL, NULL, 1, 0, NULL, NULL };
4714
5061
  ret = rb_ensure(build_and_perform_multipart_form, (VALUE)&perform_args, ensure_free_form_post, (VALUE)&perform_args);
4715
5062
  return ret;
4716
5063
  } else {
@@ -4748,7 +5095,7 @@ static VALUE ruby_curl_easy_perform_put(int argc, VALUE *argv, VALUE self) {
4748
5095
  VALUE args_ary;
4749
5096
 
4750
5097
  rb_scan_args(argc, argv, "*", &args_ary);
4751
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5098
+ rbce = ruby_curl_easy_get_idle_handle(self);
4752
5099
  curl = rbce->curl;
4753
5100
 
4754
5101
  memset(rbce->err_buf, 0, CURL_ERROR_SIZE);
@@ -4768,7 +5115,7 @@ static VALUE ruby_curl_easy_perform_put(int argc, VALUE *argv, VALUE self) {
4768
5115
  /* Otherwise, if multipart_form_post is true, use multipart logic */
4769
5116
  else if (rbce->multipart_form_post) {
4770
5117
  VALUE ret;
4771
- struct easy_form_perform_args perform_args = { self, curl, argc, argv, NULL, NULL, 1, 0 };
5118
+ struct easy_form_perform_args perform_args = { self, curl, argc, argv, NULL, NULL, 1, 0, NULL, NULL };
4772
5119
  ret = rb_ensure(build_and_perform_multipart_form, (VALUE)&perform_args, ensure_free_form_post, (VALUE)&perform_args);
4773
5120
  return ret;
4774
5121
  }
@@ -4815,7 +5162,7 @@ static VALUE ruby_curl_easy_max_body_bytes_set(VALUE self, VALUE val) {
4815
5162
  ruby_curl_easy *rbce;
4816
5163
  long long limit;
4817
5164
 
4818
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5165
+ rbce = ruby_curl_easy_get_handle(self);
4819
5166
 
4820
5167
  if (NIL_P(val)) {
4821
5168
  rb_hash_delete(rbce->opts, rb_easy_hkey("max_body_bytes"));
@@ -4870,10 +5217,10 @@ static VALUE ruby_curl_easy_header_str_get(VALUE self) {
4870
5217
  */
4871
5218
  static VALUE ruby_curl_easy_last_effective_url_get(VALUE self) {
4872
5219
  ruby_curl_easy *rbce;
4873
- char* url;
5220
+ char* url = NULL;
4874
5221
 
4875
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
4876
- curl_easy_getinfo(rbce->curl, CURLINFO_EFFECTIVE_URL, &url);
5222
+ rbce = ruby_curl_easy_get_handle(self);
5223
+ CURB_GETINFO(rbce->curl, CURLINFO_EFFECTIVE_URL, &url);
4877
5224
 
4878
5225
  if (url && url[0]) { // curl returns empty string if none
4879
5226
  return rb_str_new2(url);
@@ -4893,14 +5240,14 @@ static VALUE ruby_curl_easy_last_effective_url_get(VALUE self) {
4893
5240
  */
4894
5241
  static VALUE ruby_curl_easy_response_code_get(VALUE self) {
4895
5242
  ruby_curl_easy *rbce;
4896
- long code;
5243
+ long code = 0;
4897
5244
 
4898
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5245
+ rbce = ruby_curl_easy_get_handle(self);
4899
5246
  #ifdef HAVE_CURLINFO_RESPONSE_CODE
4900
- curl_easy_getinfo(rbce->curl, CURLINFO_RESPONSE_CODE, &code);
5247
+ CURB_GETINFO(rbce->curl, CURLINFO_RESPONSE_CODE, &code);
4901
5248
  #else
4902
5249
  // old libcurl
4903
- curl_easy_getinfo(rbce->curl, CURLINFO_HTTP_CODE, &code);
5250
+ CURB_GETINFO(rbce->curl, CURLINFO_HTTP_CODE, &code);
4904
5251
  #endif
4905
5252
 
4906
5253
  return LONG2NUM(code);
@@ -4917,10 +5264,10 @@ static VALUE ruby_curl_easy_response_code_get(VALUE self) {
4917
5264
  */
4918
5265
  static VALUE ruby_curl_easy_primary_ip_get(VALUE self) {
4919
5266
  ruby_curl_easy *rbce;
4920
- char* ip;
5267
+ char* ip = NULL;
4921
5268
 
4922
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
4923
- curl_easy_getinfo(rbce->curl, CURLINFO_PRIMARY_IP, &ip);
5269
+ rbce = ruby_curl_easy_get_handle(self);
5270
+ CURB_GETINFO(rbce->curl, CURLINFO_PRIMARY_IP, &ip);
4924
5271
 
4925
5272
  if (ip && ip[0]) { // curl returns empty string if none
4926
5273
  return rb_str_new2(ip);
@@ -4938,10 +5285,10 @@ static VALUE ruby_curl_easy_primary_ip_get(VALUE self) {
4938
5285
  */
4939
5286
  static VALUE ruby_curl_easy_http_connect_code_get(VALUE self) {
4940
5287
  ruby_curl_easy *rbce;
4941
- long code;
5288
+ long code = 0;
4942
5289
 
4943
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
4944
- curl_easy_getinfo(rbce->curl, CURLINFO_HTTP_CONNECTCODE, &code);
5290
+ rbce = ruby_curl_easy_get_handle(self);
5291
+ CURB_GETINFO(rbce->curl, CURLINFO_HTTP_CONNECTCODE, &code);
4945
5292
 
4946
5293
  return LONG2NUM(code);
4947
5294
  }
@@ -4966,10 +5313,10 @@ static VALUE ruby_curl_easy_http_connect_code_get(VALUE self) {
4966
5313
  static VALUE ruby_curl_easy_file_time_get(VALUE self) {
4967
5314
  #ifdef HAVE_CURLINFO_FILETIME
4968
5315
  ruby_curl_easy *rbce;
4969
- long time;
5316
+ long time = 0;
4970
5317
 
4971
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
4972
- curl_easy_getinfo(rbce->curl, CURLINFO_FILETIME, &time);
5318
+ rbce = ruby_curl_easy_get_handle(self);
5319
+ CURB_GETINFO(rbce->curl, CURLINFO_FILETIME, &time);
4973
5320
 
4974
5321
  return LONG2NUM(time);
4975
5322
  #else
@@ -4987,10 +5334,10 @@ static VALUE ruby_curl_easy_file_time_get(VALUE self) {
4987
5334
  */
4988
5335
  static VALUE ruby_curl_easy_total_time_get(VALUE self) {
4989
5336
  ruby_curl_easy *rbce;
4990
- double time;
5337
+ double time = 0;
4991
5338
 
4992
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
4993
- curl_easy_getinfo(rbce->curl, CURLINFO_TOTAL_TIME, &time);
5339
+ rbce = ruby_curl_easy_get_handle(self);
5340
+ CURB_GETINFO(rbce->curl, CURLINFO_TOTAL_TIME, &time);
4994
5341
 
4995
5342
  return rb_float_new(time);
4996
5343
  }
@@ -5004,10 +5351,10 @@ static VALUE ruby_curl_easy_total_time_get(VALUE self) {
5004
5351
  */
5005
5352
  static VALUE ruby_curl_easy_name_lookup_time_get(VALUE self) {
5006
5353
  ruby_curl_easy *rbce;
5007
- double time;
5354
+ double time = 0;
5008
5355
 
5009
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5010
- curl_easy_getinfo(rbce->curl, CURLINFO_NAMELOOKUP_TIME, &time);
5356
+ rbce = ruby_curl_easy_get_handle(self);
5357
+ CURB_GETINFO(rbce->curl, CURLINFO_NAMELOOKUP_TIME, &time);
5011
5358
 
5012
5359
  return rb_float_new(time);
5013
5360
  }
@@ -5021,10 +5368,10 @@ static VALUE ruby_curl_easy_name_lookup_time_get(VALUE self) {
5021
5368
  */
5022
5369
  static VALUE ruby_curl_easy_connect_time_get(VALUE self) {
5023
5370
  ruby_curl_easy *rbce;
5024
- double time;
5371
+ double time = 0;
5025
5372
 
5026
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5027
- curl_easy_getinfo(rbce->curl, CURLINFO_CONNECT_TIME, &time);
5373
+ rbce = ruby_curl_easy_get_handle(self);
5374
+ CURB_GETINFO(rbce->curl, CURLINFO_CONNECT_TIME, &time);
5028
5375
 
5029
5376
  return rb_float_new(time);
5030
5377
  }
@@ -5042,10 +5389,10 @@ static VALUE ruby_curl_easy_connect_time_get(VALUE self) {
5042
5389
  #if defined(HAVE_CURLINFO_APPCONNECT_TIME)
5043
5390
  static VALUE ruby_curl_easy_app_connect_time_get(VALUE self) {
5044
5391
  ruby_curl_easy *rbce;
5045
- double time;
5392
+ double time = 0;
5046
5393
 
5047
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5048
- curl_easy_getinfo(rbce->curl, CURLINFO_APPCONNECT_TIME, &time);
5394
+ rbce = ruby_curl_easy_get_handle(self);
5395
+ CURB_GETINFO(rbce->curl, CURLINFO_APPCONNECT_TIME, &time);
5049
5396
 
5050
5397
  return rb_float_new(time);
5051
5398
  }
@@ -5063,10 +5410,10 @@ static VALUE ruby_curl_easy_app_connect_time_get(VALUE self) {
5063
5410
  */
5064
5411
  static VALUE ruby_curl_easy_pre_transfer_time_get(VALUE self) {
5065
5412
  ruby_curl_easy *rbce;
5066
- double time;
5413
+ double time = 0;
5067
5414
 
5068
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5069
- curl_easy_getinfo(rbce->curl, CURLINFO_PRETRANSFER_TIME, &time);
5415
+ rbce = ruby_curl_easy_get_handle(self);
5416
+ CURB_GETINFO(rbce->curl, CURLINFO_PRETRANSFER_TIME, &time);
5070
5417
 
5071
5418
  return rb_float_new(time);
5072
5419
  }
@@ -5081,10 +5428,10 @@ static VALUE ruby_curl_easy_pre_transfer_time_get(VALUE self) {
5081
5428
  */
5082
5429
  static VALUE ruby_curl_easy_start_transfer_time_get(VALUE self) {
5083
5430
  ruby_curl_easy *rbce;
5084
- double time;
5431
+ double time = 0;
5085
5432
 
5086
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5087
- curl_easy_getinfo(rbce->curl, CURLINFO_STARTTRANSFER_TIME, &time);
5433
+ rbce = ruby_curl_easy_get_handle(self);
5434
+ CURB_GETINFO(rbce->curl, CURLINFO_STARTTRANSFER_TIME, &time);
5088
5435
 
5089
5436
  return rb_float_new(time);
5090
5437
  }
@@ -5103,10 +5450,10 @@ static VALUE ruby_curl_easy_start_transfer_time_get(VALUE self) {
5103
5450
  static VALUE ruby_curl_easy_redirect_time_get(VALUE self) {
5104
5451
  #ifdef HAVE_CURLINFO_REDIRECT_TIME
5105
5452
  ruby_curl_easy *rbce;
5106
- double time;
5453
+ double time = 0;
5107
5454
 
5108
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5109
- curl_easy_getinfo(rbce->curl, CURLINFO_REDIRECT_TIME, &time);
5455
+ rbce = ruby_curl_easy_get_handle(self);
5456
+ CURB_GETINFO(rbce->curl, CURLINFO_REDIRECT_TIME, &time);
5110
5457
 
5111
5458
  return rb_float_new(time);
5112
5459
  #else
@@ -5126,10 +5473,10 @@ static VALUE ruby_curl_easy_redirect_time_get(VALUE self) {
5126
5473
  static VALUE ruby_curl_easy_redirect_count_get(VALUE self) {
5127
5474
  #ifdef HAVE_CURLINFO_REDIRECT_COUNT
5128
5475
  ruby_curl_easy *rbce;
5129
- long count;
5476
+ long count = 0;
5130
5477
 
5131
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5132
- curl_easy_getinfo(rbce->curl, CURLINFO_REDIRECT_COUNT, &count);
5478
+ rbce = ruby_curl_easy_get_handle(self);
5479
+ CURB_GETINFO(rbce->curl, CURLINFO_REDIRECT_COUNT, &count);
5133
5480
 
5134
5481
  return LONG2NUM(count);
5135
5482
  #else
@@ -5151,10 +5498,10 @@ static VALUE ruby_curl_easy_redirect_count_get(VALUE self) {
5151
5498
  static VALUE ruby_curl_easy_redirect_url_get(VALUE self) {
5152
5499
  #ifdef HAVE_CURLINFO_REDIRECT_URL
5153
5500
  ruby_curl_easy *rbce;
5154
- char* url;
5501
+ char* url = NULL;
5155
5502
 
5156
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5157
- curl_easy_getinfo(rbce->curl, CURLINFO_REDIRECT_URL, &url);
5503
+ rbce = ruby_curl_easy_get_handle(self);
5504
+ CURB_GETINFO(rbce->curl, CURLINFO_REDIRECT_URL, &url);
5158
5505
 
5159
5506
  if (url && url[0]) { // curl returns empty string if none
5160
5507
  return rb_str_new2(url);
@@ -5178,15 +5525,15 @@ static VALUE ruby_curl_easy_redirect_url_get(VALUE self) {
5178
5525
  */
5179
5526
  static VALUE ruby_curl_easy_uploaded_bytes_get(VALUE self) {
5180
5527
  ruby_curl_easy *rbce;
5181
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5528
+ rbce = ruby_curl_easy_get_handle(self);
5182
5529
 
5183
5530
  #ifdef HAVE_CURLINFO_SIZE_UPLOAD_T
5184
- curl_off_t bytes;
5185
- curl_easy_getinfo(rbce->curl, CURLINFO_SIZE_UPLOAD_T, &bytes);
5531
+ curl_off_t bytes = 0;
5532
+ CURB_GETINFO(rbce->curl, CURLINFO_SIZE_UPLOAD_T, &bytes);
5186
5533
  return LL2NUM(bytes);
5187
5534
  #else
5188
- double bytes;
5189
- curl_easy_getinfo(rbce->curl, CURLINFO_SIZE_UPLOAD, &bytes);
5535
+ double bytes = 0;
5536
+ CURB_GETINFO(rbce->curl, CURLINFO_SIZE_UPLOAD, &bytes);
5190
5537
  return rb_float_new(bytes);
5191
5538
  #endif
5192
5539
  }
@@ -5200,15 +5547,15 @@ static VALUE ruby_curl_easy_uploaded_bytes_get(VALUE self) {
5200
5547
  */
5201
5548
  static VALUE ruby_curl_easy_downloaded_bytes_get(VALUE self) {
5202
5549
  ruby_curl_easy *rbce;
5203
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5550
+ rbce = ruby_curl_easy_get_handle(self);
5204
5551
 
5205
5552
  #ifdef HAVE_CURLINFO_SIZE_DOWNLOAD_T
5206
- curl_off_t bytes;
5207
- curl_easy_getinfo(rbce->curl, CURLINFO_SIZE_DOWNLOAD_T, &bytes);
5553
+ curl_off_t bytes = 0;
5554
+ CURB_GETINFO(rbce->curl, CURLINFO_SIZE_DOWNLOAD_T, &bytes);
5208
5555
  return LL2NUM(bytes);
5209
5556
  #else
5210
- double bytes;
5211
- curl_easy_getinfo(rbce->curl, CURLINFO_SIZE_DOWNLOAD, &bytes);
5557
+ double bytes = 0;
5558
+ CURB_GETINFO(rbce->curl, CURLINFO_SIZE_DOWNLOAD, &bytes);
5212
5559
  return rb_float_new(bytes);
5213
5560
  #endif
5214
5561
  }
@@ -5222,15 +5569,15 @@ static VALUE ruby_curl_easy_downloaded_bytes_get(VALUE self) {
5222
5569
  */
5223
5570
  static VALUE ruby_curl_easy_upload_speed_get(VALUE self) {
5224
5571
  ruby_curl_easy *rbce;
5225
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5572
+ rbce = ruby_curl_easy_get_handle(self);
5226
5573
 
5227
5574
  #ifdef HAVE_CURLINFO_SPEED_UPLOAD_T
5228
- curl_off_t bytes;
5229
- curl_easy_getinfo(rbce->curl, CURLINFO_SPEED_UPLOAD_T, &bytes);
5575
+ curl_off_t bytes = 0;
5576
+ CURB_GETINFO(rbce->curl, CURLINFO_SPEED_UPLOAD_T, &bytes);
5230
5577
  return LL2NUM(bytes);
5231
5578
  #else
5232
- double bytes;
5233
- curl_easy_getinfo(rbce->curl, CURLINFO_SPEED_UPLOAD, &bytes);
5579
+ double bytes = 0;
5580
+ CURB_GETINFO(rbce->curl, CURLINFO_SPEED_UPLOAD, &bytes);
5234
5581
  return rb_float_new(bytes);
5235
5582
  #endif
5236
5583
  }
@@ -5244,15 +5591,15 @@ static VALUE ruby_curl_easy_upload_speed_get(VALUE self) {
5244
5591
  */
5245
5592
  static VALUE ruby_curl_easy_download_speed_get(VALUE self) {
5246
5593
  ruby_curl_easy *rbce;
5247
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5594
+ rbce = ruby_curl_easy_get_handle(self);
5248
5595
 
5249
5596
  #ifdef HAVE_CURLINFO_SPEED_DOWNLOAD_T
5250
- curl_off_t bytes;
5251
- curl_easy_getinfo(rbce->curl, CURLINFO_SPEED_DOWNLOAD_T, &bytes);
5597
+ curl_off_t bytes = 0;
5598
+ CURB_GETINFO(rbce->curl, CURLINFO_SPEED_DOWNLOAD_T, &bytes);
5252
5599
  return LL2NUM(bytes);
5253
5600
  #else
5254
- double bytes;
5255
- curl_easy_getinfo(rbce->curl, CURLINFO_SPEED_DOWNLOAD, &bytes);
5601
+ double bytes = 0;
5602
+ CURB_GETINFO(rbce->curl, CURLINFO_SPEED_DOWNLOAD, &bytes);
5256
5603
  return rb_float_new(bytes);
5257
5604
  #endif
5258
5605
  }
@@ -5266,10 +5613,10 @@ static VALUE ruby_curl_easy_download_speed_get(VALUE self) {
5266
5613
  */
5267
5614
  static VALUE ruby_curl_easy_header_size_get(VALUE self) {
5268
5615
  ruby_curl_easy *rbce;
5269
- long size;
5616
+ long size = 0;
5270
5617
 
5271
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5272
- curl_easy_getinfo(rbce->curl, CURLINFO_HEADER_SIZE, &size);
5618
+ rbce = ruby_curl_easy_get_handle(self);
5619
+ CURB_GETINFO(rbce->curl, CURLINFO_HEADER_SIZE, &size);
5273
5620
 
5274
5621
  return LONG2NUM(size);
5275
5622
  }
@@ -5284,10 +5631,10 @@ static VALUE ruby_curl_easy_header_size_get(VALUE self) {
5284
5631
  */
5285
5632
  static VALUE ruby_curl_easy_request_size_get(VALUE self) {
5286
5633
  ruby_curl_easy *rbce;
5287
- long size;
5634
+ long size = 0;
5288
5635
 
5289
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5290
- curl_easy_getinfo(rbce->curl, CURLINFO_REQUEST_SIZE, &size);
5636
+ rbce = ruby_curl_easy_get_handle(self);
5637
+ CURB_GETINFO(rbce->curl, CURLINFO_REQUEST_SIZE, &size);
5291
5638
 
5292
5639
  return LONG2NUM(size);
5293
5640
  }
@@ -5301,10 +5648,10 @@ static VALUE ruby_curl_easy_request_size_get(VALUE self) {
5301
5648
  */
5302
5649
  static VALUE ruby_curl_easy_ssl_verify_result_get(VALUE self) {
5303
5650
  ruby_curl_easy *rbce;
5304
- long result;
5651
+ long result = 0;
5305
5652
 
5306
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5307
- curl_easy_getinfo(rbce->curl, CURLINFO_SSL_VERIFYRESULT, &result);
5653
+ rbce = ruby_curl_easy_get_handle(self);
5654
+ CURB_GETINFO(rbce->curl, CURLINFO_SSL_VERIFYRESULT, &result);
5308
5655
 
5309
5656
  return LONG2NUM(result);
5310
5657
  }
@@ -5326,15 +5673,15 @@ NOTE: you must call curl_slist_free_all(3) on the list pointer once you're done
5326
5673
  */
5327
5674
  static VALUE ruby_curl_easy_downloaded_content_length_get(VALUE self) {
5328
5675
  ruby_curl_easy *rbce;
5329
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5676
+ rbce = ruby_curl_easy_get_handle(self);
5330
5677
 
5331
5678
  #ifdef HAVE_CURLINFO_CONTENT_LENGTH_DOWNLOAD_T
5332
- curl_off_t bytes;
5333
- curl_easy_getinfo(rbce->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &bytes);
5679
+ curl_off_t bytes = 0;
5680
+ CURB_GETINFO(rbce->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &bytes);
5334
5681
  return LL2NUM(bytes);
5335
5682
  #else
5336
- double bytes;
5337
- curl_easy_getinfo(rbce->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &bytes);
5683
+ double bytes = 0;
5684
+ CURB_GETINFO(rbce->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &bytes);
5338
5685
  return rb_float_new(bytes);
5339
5686
  #endif
5340
5687
  }
@@ -5347,15 +5694,15 @@ static VALUE ruby_curl_easy_downloaded_content_length_get(VALUE self) {
5347
5694
  */
5348
5695
  static VALUE ruby_curl_easy_uploaded_content_length_get(VALUE self) {
5349
5696
  ruby_curl_easy *rbce;
5350
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5697
+ rbce = ruby_curl_easy_get_handle(self);
5351
5698
 
5352
5699
  #ifdef HAVE_CURLINFO_CONTENT_LENGTH_UPLOAD_T
5353
- curl_off_t bytes;
5354
- curl_easy_getinfo(rbce->curl, CURLINFO_CONTENT_LENGTH_UPLOAD_T, &bytes);
5700
+ curl_off_t bytes = 0;
5701
+ CURB_GETINFO(rbce->curl, CURLINFO_CONTENT_LENGTH_UPLOAD_T, &bytes);
5355
5702
  return LL2NUM(bytes);
5356
5703
  #else
5357
- double bytes;
5358
- curl_easy_getinfo(rbce->curl, CURLINFO_CONTENT_LENGTH_UPLOAD, &bytes);
5704
+ double bytes = 0;
5705
+ CURB_GETINFO(rbce->curl, CURLINFO_CONTENT_LENGTH_UPLOAD, &bytes);
5359
5706
  return rb_float_new(bytes);
5360
5707
  #endif
5361
5708
  }
@@ -5371,10 +5718,10 @@ static VALUE ruby_curl_easy_uploaded_content_length_get(VALUE self) {
5371
5718
  */
5372
5719
  static VALUE ruby_curl_easy_content_type_get(VALUE self) {
5373
5720
  ruby_curl_easy *rbce;
5374
- char* type;
5721
+ char* type = NULL;
5375
5722
 
5376
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5377
- curl_easy_getinfo(rbce->curl, CURLINFO_CONTENT_TYPE, &type);
5723
+ rbce = ruby_curl_easy_get_handle(self);
5724
+ CURB_GETINFO(rbce->curl, CURLINFO_CONTENT_TYPE, &type);
5378
5725
 
5379
5726
  if (type && type[0]) { // curl returns empty string if none
5380
5727
  return rb_str_new2(type);
@@ -5414,10 +5761,10 @@ Pass a pointer to a long to receive a bitmask indicating the authentication meth
5414
5761
  static VALUE ruby_curl_easy_os_errno_get(VALUE self) {
5415
5762
  #ifdef HAVE_CURLINFO_OS_ERRNO
5416
5763
  ruby_curl_easy *rbce;
5417
- long result;
5764
+ long result = 0;
5418
5765
 
5419
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5420
- curl_easy_getinfo(rbce->curl, CURLINFO_OS_ERRNO, &result);
5766
+ rbce = ruby_curl_easy_get_handle(self);
5767
+ CURB_GETINFO(rbce->curl, CURLINFO_OS_ERRNO, &result);
5421
5768
 
5422
5769
  return LONG2NUM(result);
5423
5770
  #else
@@ -5443,10 +5790,10 @@ static VALUE ruby_curl_easy_os_errno_get(VALUE self) {
5443
5790
  static VALUE ruby_curl_easy_num_connects_get(VALUE self) {
5444
5791
  #ifdef HAVE_CURLINFO_NUM_CONNECTS
5445
5792
  ruby_curl_easy *rbce;
5446
- long result;
5793
+ long result = 0;
5447
5794
 
5448
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5449
- curl_easy_getinfo(rbce->curl, CURLINFO_NUM_CONNECTS, &result);
5795
+ rbce = ruby_curl_easy_get_handle(self);
5796
+ CURB_GETINFO(rbce->curl, CURLINFO_NUM_CONNECTS, &result);
5450
5797
 
5451
5798
  return LONG2NUM(result);
5452
5799
  #else
@@ -5478,12 +5825,12 @@ static VALUE ruby_curl_easy_num_connects_get(VALUE self) {
5478
5825
  static VALUE ruby_curl_easy_cookielist_get(VALUE self) {
5479
5826
  #ifdef HAVE_CURLINFO_COOKIELIST
5480
5827
  ruby_curl_easy *rbce;
5481
- struct curl_slist *cookies;
5828
+ struct curl_slist *cookies = NULL;
5482
5829
  struct curl_slist *cookie;
5483
5830
  VALUE rb_cookies;
5484
5831
 
5485
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5486
- curl_easy_getinfo(rbce->curl, CURLINFO_COOKIELIST, &cookies);
5832
+ rbce = ruby_curl_easy_get_handle(self);
5833
+ CURB_GETINFO(rbce->curl, CURLINFO_COOKIELIST, &cookies);
5487
5834
  if (!cookies)
5488
5835
  return Qnil;
5489
5836
  rb_cookies = rb_ary_new();
@@ -5522,8 +5869,8 @@ static VALUE ruby_curl_easy_ftp_entry_path_get(VALUE self) {
5522
5869
  ruby_curl_easy *rbce;
5523
5870
  char* path = NULL;
5524
5871
 
5525
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5526
- curl_easy_getinfo(rbce->curl, CURLINFO_FTP_ENTRY_PATH, &path);
5872
+ rbce = ruby_curl_easy_get_handle(self);
5873
+ CURB_GETINFO(rbce->curl, CURLINFO_FTP_ENTRY_PATH, &path);
5527
5874
 
5528
5875
  if (path && path[0]) { // curl returns NULL or empty string if none
5529
5876
  return rb_str_new2(path);
@@ -5542,7 +5889,7 @@ static VALUE ruby_curl_easy_ftp_entry_path_get(VALUE self) {
5542
5889
  */
5543
5890
  static VALUE ruby_curl_easy_multi_get(VALUE self) {
5544
5891
  ruby_curl_easy *rbce;
5545
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5892
+ rbce = ruby_curl_easy_get_handle(self);
5546
5893
  return rbce->multi;
5547
5894
  }
5548
5895
 
@@ -5555,7 +5902,7 @@ static VALUE ruby_curl_easy_multi_set(VALUE self, VALUE multi) {
5555
5902
  VALUE old_multi;
5556
5903
  ruby_curl_multi *old_rbcm;
5557
5904
  CURLMcode result;
5558
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5905
+ rbce = ruby_curl_easy_get_handle(self);
5559
5906
 
5560
5907
  if (!NIL_P(multi) && rb_obj_is_kind_of(multi, cCurlMulti) != Qtrue) {
5561
5908
  rb_raise(rb_eTypeError, "expected Curl::Multi or nil");
@@ -5584,7 +5931,7 @@ static VALUE ruby_curl_easy_multi_set(VALUE self, VALUE multi) {
5584
5931
  */
5585
5932
  static VALUE ruby_curl_easy_last_result(VALUE self) {
5586
5933
  ruby_curl_easy *rbce;
5587
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5934
+ rbce = ruby_curl_easy_get_handle(self);
5588
5935
  return LONG2NUM(rbce->last_result);
5589
5936
  }
5590
5937
 
@@ -5594,7 +5941,7 @@ static VALUE ruby_curl_easy_last_result(VALUE self) {
5594
5941
  */
5595
5942
  static VALUE ruby_curl_easy_last_error(VALUE self) {
5596
5943
  ruby_curl_easy *rbce;
5597
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5944
+ rbce = ruby_curl_easy_get_handle(self);
5598
5945
 
5599
5946
  if (rbce->err_buf[0]) { // curl returns NULL or empty string if none
5600
5947
  return rb_str_new2(rbce->err_buf);
@@ -5632,7 +5979,8 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
5632
5979
  long option = NUM2LONG(opt);
5633
5980
  rb_io_t *open_f_ptr;
5634
5981
 
5635
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
5982
+ rbce = ruby_curl_easy_get_handle(self);
5983
+ ruby_curl_easy_check_idle(rbce);
5636
5984
 
5637
5985
  switch (option) {
5638
5986
  /* BEHAVIOR OPTIONS */
@@ -5652,6 +6000,7 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
5652
6000
  CURB_OBJECT_HSETTER(ruby_curl_easy, url);
5653
6001
  } break;
5654
6002
  case CURLOPT_CUSTOMREQUEST:
6003
+ if (!NIL_P(val)) curb_check_header_line(StringValue(val), "custom request method");
5655
6004
  curl_easy_setopt(rbce->curl, CURLOPT_CUSTOMREQUEST, NIL_P(val) ? NULL : StringValueCStr(val));
5656
6005
  break;
5657
6006
  case CURLOPT_HTTP_VERSION: {
@@ -5727,6 +6076,7 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
5727
6076
  #ifdef HAVE_CURLOPT_REQUEST_TARGET
5728
6077
  case CURLOPT_REQUEST_TARGET: {
5729
6078
  /* Forward request-target directly to libcurl as a string. */
6079
+ if (!NIL_P(val)) curb_check_header_line(StringValue(val), "request target");
5730
6080
  curl_easy_setopt(rbce->curl, CURLOPT_REQUEST_TARGET, NIL_P(val) ? NULL : StringValueCStr(val));
5731
6081
  } break;
5732
6082
  #endif
@@ -5883,23 +6233,36 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
5883
6233
  curl_easy_setopt(rbce->curl, CURLOPT_HAPROXYPROTOCOL, NUM2LONG(val));
5884
6234
  break;
5885
6235
  #endif
5886
- case CURLOPT_STDERR:
6236
+ case CURLOPT_STDERR: {
5887
6237
  // libcurl requires raw FILE pointer and this should be IO object in Ruby.
5888
6238
  // Tempfile or StringIO won't work.
6239
+ FILE *stderr_file;
5889
6240
  Check_Type(val, T_FILE);
5890
6241
  GetOpenFile(val, open_f_ptr);
5891
- curl_easy_setopt(rbce->curl, CURLOPT_STDERR, rb_io_stdio_file(open_f_ptr));
5892
- /* Retain a Ruby reference to the IO to prevent GC/finalization
5893
- * while libcurl still holds and writes to the underlying FILE*. */
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. */
5894
6255
  rb_easy_set("stderr_io", val);
5895
- break;
6256
+ } break;
5896
6257
  case CURLOPT_PROTOCOLS:
5897
6258
  case CURLOPT_REDIR_PROTOCOLS:
5898
6259
  curl_easy_setopt(rbce->curl, option, NUM2LONG(val));
6260
+ if (option == CURLOPT_PROTOCOLS) rbce->protocols_set = 1;
5899
6261
  break;
5900
6262
  #ifdef HAVE_CURLOPT_PROTOCOLS_STR
5901
6263
  case CURLOPT_PROTOCOLS_STR:
5902
6264
  curl_easy_setopt(rbce->curl, CURLOPT_PROTOCOLS_STR, NIL_P(val) ? NULL : StringValueCStr(val));
6265
+ rbce->protocols_set = !NIL_P(val);
5903
6266
  break;
5904
6267
  #endif
5905
6268
  #ifdef HAVE_CURLOPT_REDIR_PROTOCOLS_STR
@@ -6084,6 +6447,28 @@ static VALUE ruby_curl_easy_inspect(VALUE self) {
6084
6447
 
6085
6448
  /* ================== ESCAPING FUNCS ==============*/
6086
6449
 
6450
+ struct curl_string_result {
6451
+ char *data;
6452
+ long length;
6453
+ };
6454
+
6455
+ static VALUE curl_string_result_body(VALUE argp) {
6456
+ struct curl_string_result *args = (struct curl_string_result *)argp;
6457
+ return rb_str_new(args->data, args->length);
6458
+ }
6459
+
6460
+ static VALUE curl_string_result_free(VALUE argp) {
6461
+ struct curl_string_result *args = (struct curl_string_result *)argp;
6462
+ curl_free(args->data);
6463
+ return Qnil;
6464
+ }
6465
+
6466
+ static VALUE curl_string_result_to_ruby(char *data, long length) {
6467
+ struct curl_string_result args = { data, length };
6468
+ return rb_ensure(curl_string_result_body, (VALUE)&args,
6469
+ curl_string_result_free, (VALUE)&args);
6470
+ }
6471
+
6087
6472
  /*
6088
6473
  * call-seq:
6089
6474
  * easy.escape("some text") => "some%20text"
@@ -6096,10 +6481,9 @@ static VALUE ruby_curl_easy_inspect(VALUE self) {
6096
6481
  static VALUE ruby_curl_easy_escape(VALUE self, VALUE svalue) {
6097
6482
  ruby_curl_easy *rbce;
6098
6483
  char *result;
6099
- VALUE rresult;
6100
6484
  VALUE str = svalue;
6101
6485
 
6102
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
6486
+ rbce = ruby_curl_easy_get_handle(self);
6103
6487
 
6104
6488
  /* NOTE: make sure the value is a string; rb_obj_as_string always yields a
6105
6489
  * String, even if the object's to_s misbehaves and returns a non-String. */
@@ -6111,10 +6495,8 @@ static VALUE ruby_curl_easy_escape(VALUE self, VALUE svalue) {
6111
6495
  result = (char*)curl_escape(StringValuePtr(str), (int)RSTRING_LEN(str));
6112
6496
  #endif
6113
6497
 
6114
- rresult = rb_str_new2(result);
6115
- curl_free(result);
6116
-
6117
- return rresult;
6498
+ if (!result) rb_raise(eCurlErrError, "Failed to URL-encode string");
6499
+ return curl_string_result_to_ruby(result, (long)strlen(result));
6118
6500
  }
6119
6501
 
6120
6502
  /*
@@ -6129,9 +6511,8 @@ static VALUE ruby_curl_easy_unescape(VALUE self, VALUE str) {
6129
6511
  ruby_curl_easy *rbce;
6130
6512
  int rlen;
6131
6513
  char *result;
6132
- VALUE rresult;
6133
6514
 
6134
- TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
6515
+ rbce = ruby_curl_easy_get_handle(self);
6135
6516
 
6136
6517
  /* Coerce as its own statement before use below - StringValuePtr and
6137
6518
  * RSTRING_LEN are sibling args to the same call and C does not guarantee
@@ -6142,13 +6523,13 @@ static VALUE ruby_curl_easy_unescape(VALUE self, VALUE str) {
6142
6523
  result = (char*)curl_easy_unescape(rbce->curl, StringValuePtr(str), (int)RSTRING_LEN(str), &rlen);
6143
6524
  #else
6144
6525
  result = (char*)curl_unescape(StringValuePtr(str), (int)RSTRING_LEN(str));
6145
- rlen = strlen(result);
6146
6526
  #endif
6147
6527
 
6148
- rresult = rb_str_new(result, rlen);
6149
- curl_free(result);
6150
-
6151
- return rresult;
6528
+ if (!result) rb_raise(eCurlErrError, "Failed to URL-decode string");
6529
+ #if (LIBCURL_VERSION_NUM < 0x070f04)
6530
+ rlen = (int)strlen(result);
6531
+ #endif
6532
+ return curl_string_result_to_ruby(result, rlen);
6152
6533
  }
6153
6534
 
6154
6535