curb 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 25a6615cec70148cb19373a5ad0326a93b2e1b4424bd2d02734038857e261659
4
- data.tar.gz: 85f2b05e3fd7e8b400d8cd86d65aa2778e4c60e55337442e34d6cda673278c55
3
+ metadata.gz: 48135be870a425c76338a773b41dc9438883b30cab622cc8c2d81394b860047d
4
+ data.tar.gz: e7a439c010c1bdbd3ab26b92f2126a4d518a2c7808d7acc6af2e57a03ec07376
5
5
  SHA512:
6
- metadata.gz: de609adca7bcd45c867a8fa91a20caa429e0064c68587d498bf25f455a1d4c49c6d00ec851af7c80aa1cc2694b4d0dd19291ebfb9b3010793dd8fa8c28e22d4c
7
- data.tar.gz: af3d577715760e6b6ca977c5a6295d0af5d8b60858d673675d778deff7b06120876b72975ea35062c5299ac45fbab2d45a70b375cf94d07b410eb8c649a443d1
6
+ metadata.gz: b5c61d835a10a80211b625dd3ef8cf949fdcf30be9584d2a6793d69e309e3cb283097c2502a8912801ab9ee13679f1e71344034a197c54c88872157e66df74a0
7
+ data.tar.gz: 20fbde1832dc7338dba7f78c08556eceab4532247f41f0bb1c0c15ba11fc0cd2f07aabeaef62525eba24afc95b5e9854dbb3d71f6d6f493097d614ae913818f8
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Curb - Libcurl bindings for Ruby
2
2
 
3
- * [CI Build Status](https://github.com/taf2/curb/actions/workflows/CI.yml)
3
+ [![CI](https://github.com/taf2/curb/actions/workflows/ci.yml/badge.svg)](https://github.com/taf2/curb/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/taf2/curb/branch/master/graph/badge.svg)](https://codecov.io/gh/taf2/curb)
5
+ [![Gem Version](https://badge.fury.io/rb/curb.svg)](https://badge.fury.io/rb/curb)
6
+
7
+ * [CI Build Status](https://github.com/taf2/curb/actions/workflows/ci.yml)
4
8
  * [rubydoc rdoc](http://www.rubydoc.info/github/taf2/curb/)
5
9
  * [github project](http://github.com/taf2/curb/tree/master)
6
10
 
@@ -70,11 +74,37 @@ puts "\n=== FTP Directory Listing Example ==="
70
74
  list = Curl::Easy.new('ftp://ftp.example.com/remote/directory/')
71
75
  list.username = 'user'
72
76
  list.password = 'password'
73
- list.dirlistonly = true
77
+ list.set(:dirlistonly, 1)
78
+ list.perform
79
+ puts list.body
80
+ ```
81
+
82
+ ### FTP over HTTP proxy tunnel (NLST/LIST)
83
+ When listing directories through an HTTP proxy with `proxy_tunnel` (CONNECT), let libcurl manage the passive data connection. Do not send `PASV`/`EPSV` or `NLST` via `easy.ftp_commands` — QUOTE commands run on the control connection and libcurl will not open the data connection, resulting in 425 errors.
84
+
85
+ To get NLST-like output safely:
86
+
87
+ ```ruby
88
+ list = Curl::Easy.new('ftp://ftp.example.com/remote/directory/')
89
+ list.username = 'user'
90
+ list.password = 'password'
91
+ list.proxy_url = 'http://proxy.example.com:80'
92
+ list.proxy_tunnel = true
93
+
94
+ # Ask libcurl to perform a listing (names only)
95
+ list.set(:dirlistonly, 1)
96
+
97
+ # If the proxy or server has trouble with EPSV/EPRT, you can adjust:
98
+ # list.set(:ftp_use_epsv, 0) # disable EPSV
99
+ # list.set(:ftp_use_eprt, 0) # disable EPRT (stick to IPv4 PASV)
100
+ # list.set(:ftp_skip_pasv_ip, 1) # ignore PASV host, reuse control host
101
+
74
102
  list.perform
75
103
  puts list.body
76
104
  ```
77
105
 
106
+ If you need a full `LIST` output instead of just names, omit `dirlistonly` and parse the server response accordingly. The key is to let libcurl initiate the data connection (PASV/EPSV) instead of trying to drive it via `ftp_commands`.
107
+
78
108
  ### Advanced FTP Usage with Various Options
79
109
  ```
80
110
  puts "\n=== Advanced FTP Example ==="
@@ -323,6 +353,8 @@ end
323
353
 
324
354
  ### HTTP POST form:
325
355
 
356
+ Note: Instance methods like `easy.http_post(...)` do not accept a URL argument. Set the URL first (for example, `Curl::Easy.new(url)` or `easy.url = url`) and then call `easy.http_post(...)`. If you want to pass the URL directly to the call, use the class/module helpers such as `Curl::Easy.http_post(url, ...)` or `Curl.post(url, ...)`.
357
+
326
358
  ```ruby
327
359
  c = Curl::Easy.http_post("http://my.rails.box/thing/create",
328
360
  Curl::PostField.content('thing[name]', 'box'),
@@ -335,6 +367,19 @@ c = Curl::Easy.http_post("http://my.rails.box/thing/create",
335
367
  c = Curl::Easy.new("http://my.rails.box/files/upload")
336
368
  c.multipart_form_post = true
337
369
  c.http_post(Curl::PostField.file('thing[file]', 'myfile.rb'))
370
+
371
+ ### Custom request target
372
+
373
+ Some advanced scenarios need a request-target that differs from the URL host/path (for example, absolute-form targets or special values like `*`). If your libcurl supports `CURLOPT_REQUEST_TARGET` (libcurl ≥ 7.55), you can override it:
374
+
375
+ ```ruby
376
+ c = Curl::Easy.new("http://127.0.0.1:9129/methods")
377
+ c.request_target = "http://localhost:9129/methods" # absolute-form target
378
+ c.headers = { 'Host' => 'example.com' } # override Host header if needed
379
+ c.perform
380
+ ```
381
+
382
+ For HTTPS, prefer `easy.resolve = ["host:443:IP"]` to keep Host/SNI/certificates aligned.
338
383
  ```
339
384
 
340
385
  ### Using HTTP/2
@@ -414,3 +459,31 @@ end
414
459
  * `on_missing` is called when the response code is 4xx
415
460
  * `on_failure` is called when the response code is 5xx
416
461
  * `on_complete` is called in all cases.
462
+
463
+ ### Cookies
464
+
465
+ - Manual cookies: Set the outgoing `Cookie` header via `easy.cookies = "name=value; other=val"`. This only affects the request header and does not modify libcurl's internal cookie engine.
466
+ - Cookie engine: Enable with `easy.enable_cookies = true`. Optionally set `easy.cookiefile` (to load) and/or `easy.cookiejar` (to persist). Cookies received via `Set-Cookie` go into this engine.
467
+ - Inspect engine cookies: `easy.cookielist` returns an array of strings (Netscape or Set-Cookie format).
468
+ - Modify engine cookies: use `easy.cookielist = ...` or `easy.set(:cookielist, ...)` with either a `Set-Cookie` style string, Netscape cookie lines, or special commands: `"ALL"` (clear), `"SESS"` (remove session cookies), `"FLUSH"` (write to jar), `"RELOAD"` (reload from file).
469
+ - Clearing manual cookies: assign an empty string (`easy.cookies = ''`). Assigning `nil` has no effect in current versions.
470
+
471
+ Examples:
472
+
473
+ ```ruby
474
+ easy = Curl::Easy.new("https://example.com")
475
+
476
+ # Use the cookie engine and persist cookies
477
+ easy.enable_cookies = true
478
+ easy.cookiejar = "/tmp/cookies.txt"
479
+ easy.perform
480
+
481
+ # Later: inspect and tweak engine cookies
482
+ p easy.cookielist
483
+ easy.cookielist = 'ALL' # clear stored cookies
484
+
485
+ # Send custom Cookie header for a single request
486
+ easy.cookies = "flag=1; session_override=abc"
487
+ easy.perform
488
+ easy.cookies = '' # clear manual Cookie header
489
+ ```
data/ext/curb.c CHANGED
@@ -143,12 +143,14 @@ static VALUE ruby_curl_asyncdns_q(VALUE mod) {
143
143
  * in RFC 2478). For libcurl versions < 7.10.8, always returns false.
144
144
  */
145
145
  static VALUE ruby_curl_spnego_q(VALUE mod) {
146
- #ifdef HAVE_CURL_VERSION_SPNEGO
147
146
  curl_version_info_data *ver = curl_version_info(CURLVERSION_NOW);
148
- return((ver->features & CURL_VERSION_SPNEGO) ? Qtrue : Qfalse);
149
- #else
150
- return Qfalse;
147
+ #ifdef HAVE_CURL_VERSION_SPNEGO
148
+ if (ver->features & CURL_VERSION_SPNEGO) return Qtrue;
151
149
  #endif
150
+ #ifdef HAVE_CURL_VERSION_GSSNEGOTIATE
151
+ if (ver->features & CURL_VERSION_GSSNEGOTIATE) return Qtrue;
152
+ #endif
153
+ return Qfalse;
152
154
  }
153
155
 
154
156
  /*
@@ -614,6 +616,11 @@ void Init_curb_core() {
614
616
  #if HAVE_CURLOPT_PROXYHEADER
615
617
  CURB_DEFINE(CURLOPT_PROXYHEADER);
616
618
  #endif
619
+ #if HAVE_CURLOPT_REQUEST_TARGET
620
+ /* Allows overriding the Request-URI target used in the request line.
621
+ * Useful for absolute-form requests or special targets like "*". */
622
+ CURB_DEFINE(CURLOPT_REQUEST_TARGET);
623
+ #endif
617
624
  #if HAVE_CURLOPT_HTTP200ALIASES
618
625
  CURB_DEFINE(CURLOPT_HTTP200ALIASES);
619
626
  #endif
data/ext/curb.h CHANGED
@@ -28,11 +28,11 @@
28
28
  #include "curb_macros.h"
29
29
 
30
30
  // These should be managed from the Rake 'release' task.
31
- #define CURB_VERSION "1.1.0"
32
- #define CURB_VER_NUM 1010
31
+ #define CURB_VERSION "1.2.1"
32
+ #define CURB_VER_NUM 1021
33
33
  #define CURB_VER_MAJ 1
34
- #define CURB_VER_MIN 1
35
- #define CURB_VER_MIC 0
34
+ #define CURB_VER_MIN 2
35
+ #define CURB_VER_MIC 1
36
36
  #define CURB_VER_PATCH 0
37
37
 
38
38
 
data/ext/curb_easy.c CHANGED
@@ -12,6 +12,9 @@
12
12
 
13
13
  #include <errno.h>
14
14
  #include <string.h>
15
+ #ifndef _WIN32
16
+ #include <strings.h>
17
+ #endif
15
18
 
16
19
  extern VALUE mCurl;
17
20
 
@@ -222,6 +225,24 @@ void curl_easy_mark(ruby_curl_easy *rbce) {
222
225
  }
223
226
 
224
227
  static void ruby_curl_easy_free(ruby_curl_easy *rbce) {
228
+ if (!rbce) {
229
+ return;
230
+ }
231
+
232
+ if (!NIL_P(rbce->multi)) {
233
+ VALUE multi_val = rbce->multi;
234
+ ruby_curl_multi *rbcm = NULL;
235
+
236
+ rbce->multi = Qnil;
237
+
238
+ if (!NIL_P(multi_val) && RB_TYPE_P(multi_val, T_DATA)) {
239
+ Data_Get_Struct(multi_val, ruby_curl_multi, rbcm);
240
+ if (rbcm) {
241
+ rb_curl_multi_forget_easy(rbcm, rbce);
242
+ }
243
+ }
244
+ }
245
+
225
246
  if (rbce->curl_headers) {
226
247
  curl_slist_free_all(rbce->curl_headers);
227
248
  }
@@ -307,8 +328,10 @@ static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
307
328
  rbce->verbose = 0;
308
329
  rbce->multipart_form_post = 0;
309
330
  rbce->enable_cookies = 0;
331
+ rbce->cookielist_engine_enabled = 0;
310
332
  rbce->ignore_content_length = 0;
311
333
  rbce->callback_active = 0;
334
+ rbce->last_result = 0;
312
335
  }
313
336
 
314
337
  /*
@@ -317,6 +340,9 @@ static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
317
340
  static VALUE ruby_curl_easy_allocate(VALUE klass) {
318
341
  ruby_curl_easy *rbce;
319
342
  rbce = ALLOC(ruby_curl_easy);
343
+ if (!rbce) {
344
+ rb_raise(rb_eNoMemError, "Failed to allocate memory for Curl::Easy");
345
+ }
320
346
  rbce->curl = NULL;
321
347
  rbce->opts = Qnil;
322
348
  rbce->multi = Qnil;
@@ -375,8 +401,14 @@ static VALUE ruby_curl_easy_initialize(int argc, VALUE *argv, VALUE self) {
375
401
  static struct curl_slist *duplicate_curl_slist(struct curl_slist *list) {
376
402
  struct curl_slist *dup = NULL;
377
403
  struct curl_slist *tmp;
404
+ struct curl_slist *new_list;
378
405
  for (tmp = list; tmp; tmp = tmp->next) {
379
- dup = curl_slist_append(dup, tmp->data);
406
+ new_list = curl_slist_append(dup, tmp->data);
407
+ if (!new_list) {
408
+ if (dup) { curl_slist_free_all(dup); }
409
+ rb_raise(rb_eNoMemError, "Failed to duplicate curl_slist");
410
+ }
411
+ dup = new_list;
380
412
  }
381
413
  return dup;
382
414
  }
@@ -395,6 +427,9 @@ static VALUE ruby_curl_easy_clone(VALUE self) {
395
427
  Data_Get_Struct(self, ruby_curl_easy, rbce);
396
428
 
397
429
  newrbce = ALLOC(ruby_curl_easy);
430
+ if (!newrbce) {
431
+ rb_raise(rb_eNoMemError, "Failed to allocate memory for Curl::Easy clone");
432
+ }
398
433
  /* shallow copy */
399
434
  memcpy(newrbce, rbce, sizeof(ruby_curl_easy));
400
435
 
@@ -405,6 +440,9 @@ static VALUE ruby_curl_easy_clone(VALUE self) {
405
440
  newrbce->curl_ftp_commands = (rbce->curl_ftp_commands) ? duplicate_curl_slist(rbce->curl_ftp_commands) : NULL;
406
441
  newrbce->curl_resolve = (rbce->curl_resolve) ? duplicate_curl_slist(rbce->curl_resolve) : NULL;
407
442
 
443
+ /* A cloned easy should not retain ownership reference to the original multi. */
444
+ newrbce->multi = Qnil;
445
+
408
446
  if (rbce->opts != Qnil) {
409
447
  newrbce->opts = rb_funcall(rbce->opts, rb_intern("dup"), 0);
410
448
  }
@@ -645,7 +683,16 @@ static VALUE ruby_curl_easy_proxypwd_get(VALUE self) {
645
683
  * call-seq:
646
684
  * easy.cookies => "name1=content1; name2=content2;"
647
685
  *
648
- * Obtain the cookies for this Curl::Easy instance.
686
+ * Obtain the manually set Cookie header string for this Curl::Easy instance.
687
+ *
688
+ * Notes:
689
+ * - This corresponds to libcurl's CURLOPT_COOKIE and only affects the outgoing
690
+ * Cookie request header. It does NOT modify the internal libcurl cookie engine
691
+ * that stores cookies received via Set-Cookie.
692
+ * - To inspect or modify cookies stored in the cookie engine, use
693
+ * +easy.cookielist+ (getter) and +easy.cookielist=+ or +easy.set(:cookielist, ...)+ (setter).
694
+ * - To clear a previously set manual Cookie header, assign an empty string.
695
+ * Assigning +nil+ currently has no effect.
649
696
  */
650
697
  static VALUE ruby_curl_easy_cookies_get(VALUE self) {
651
698
  CURB_OBJECT_HGETTER(ruby_curl_easy, cookies);
@@ -655,7 +702,8 @@ static VALUE ruby_curl_easy_cookies_get(VALUE self) {
655
702
  * call-seq:
656
703
  * easy.cookiefile => string
657
704
  *
658
- * Obtain the cookiefile file for this Curl::Easy instance.
705
+ * Obtain the cookiefile path for this Curl::Easy instance (used to load cookies when the
706
+ * cookie engine is enabled).
659
707
  */
660
708
  static VALUE ruby_curl_easy_cookiefile_get(VALUE self) {
661
709
  CURB_OBJECT_HGETTER(ruby_curl_easy, cookiefile);
@@ -665,7 +713,8 @@ static VALUE ruby_curl_easy_cookiefile_get(VALUE self) {
665
713
  * call-seq:
666
714
  * easy.cookiejar => string
667
715
  *
668
- * Obtain the cookiejar file to use for this Curl::Easy instance.
716
+ * Obtain the cookiejar path for this Curl::Easy instance (used to persist cookies when the
717
+ * cookie engine is enabled).
669
718
  */
670
719
  static VALUE ruby_curl_easy_cookiejar_get(VALUE self) {
671
720
  CURB_OBJECT_HGETTER(ruby_curl_easy, cookiejar);
@@ -965,7 +1014,15 @@ static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
965
1014
  * call-seq:
966
1015
  * easy.ftp_commands = ["CWD /", "MKD directory"] => ["CWD /", ...]
967
1016
  *
968
- * Explicitly sets the list of commands to execute on the FTP server when calling perform
1017
+ * Explicitly sets the list of commands to execute on the FTP server when calling perform.
1018
+ *
1019
+ * NOTE:
1020
+ * - This maps to libcurl CURLOPT_QUOTE; it sends commands on the control connection.
1021
+ * - Do not include data-transfer commands like LIST/NLST/RETR/STOR here. libcurl does not
1022
+ * parse PASV/EPSV replies from QUOTE commands and will not establish the required data
1023
+ * connection. For directory listings, set CURLOPT_DIRLISTONLY (via `easy.set(:dirlistonly, true)`)
1024
+ * and request an FTP directory URL (e.g. "ftp://host/path/") so libcurl manages PASV/EPSV
1025
+ * and the data connection for you.
969
1026
  */
970
1027
  static VALUE ruby_curl_easy_ftp_commands_set(VALUE self, VALUE ftp_commands) {
971
1028
  CURB_OBJECT_HSETTER(ruby_curl_easy, ftp_commands);
@@ -1874,7 +1931,12 @@ static VALUE ruby_curl_easy_multipart_form_post_q(VALUE self) {
1874
1931
  * easy.enable_cookies = boolean => boolean
1875
1932
  *
1876
1933
  * Configure whether the libcurl cookie engine is enabled for this Curl::Easy
1877
- * instance.
1934
+ * instance. When enabled, cookies received via Set-Cookie are stored by libcurl
1935
+ * and automatically sent on subsequent matching requests. Use +easy.cookiefile+
1936
+ * to load cookies and +easy.cookiejar+ to persist them.
1937
+ *
1938
+ * This setting is independent from the manual Cookie header set via +easy.cookies+.
1939
+ * The manual header is additive and can be cleared by assigning an empty string.
1878
1940
  */
1879
1941
  static VALUE ruby_curl_easy_enable_cookies_set(VALUE self, VALUE enable_cookies)
1880
1942
  {
@@ -2165,7 +2227,11 @@ static VALUE cb_each_http_header(VALUE header, VALUE wrap, int _c, const VALUE *
2165
2227
 
2166
2228
  //rb_p(header_str);
2167
2229
 
2168
- *list = curl_slist_append(*list, StringValuePtr(header_str));
2230
+ struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(header_str));
2231
+ if (!new_list) {
2232
+ rb_raise(rb_eNoMemError, "Failed to append to header list");
2233
+ }
2234
+ *list = new_list;
2169
2235
  return header_str;
2170
2236
  }
2171
2237
 
@@ -2197,7 +2263,11 @@ static VALUE cb_each_http_proxy_header(VALUE proxy_header, VALUE wrap, int _c, c
2197
2263
 
2198
2264
  //rb_p(header_str);
2199
2265
 
2200
- *list = curl_slist_append(*list, StringValuePtr(proxy_header_str));
2266
+ struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(proxy_header_str));
2267
+ if (!new_list) {
2268
+ rb_raise(rb_eNoMemError, "Failed to append to proxy header list");
2269
+ }
2270
+ *list = new_list;
2201
2271
  return proxy_header_str;
2202
2272
  }
2203
2273
 
@@ -2210,7 +2280,11 @@ static VALUE cb_each_ftp_command(VALUE ftp_command, VALUE wrap, int _c, const VA
2210
2280
  Data_Get_Struct(wrap, struct curl_slist *, list);
2211
2281
 
2212
2282
  ftp_command_string = rb_obj_as_string(ftp_command);
2213
- *list = curl_slist_append(*list, StringValuePtr(ftp_command));
2283
+ struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(ftp_command));
2284
+ if (!new_list) {
2285
+ rb_raise(rb_eNoMemError, "Failed to append to FTP command list");
2286
+ }
2287
+ *list = new_list;
2214
2288
 
2215
2289
  return ftp_command_string;
2216
2290
  }
@@ -2224,7 +2298,11 @@ static VALUE cb_each_resolve(VALUE resolve, VALUE wrap, int _c, const VALUE *_pt
2224
2298
  Data_Get_Struct(wrap, struct curl_slist *, list);
2225
2299
 
2226
2300
  resolve_string = rb_obj_as_string(resolve);
2227
- *list = curl_slist_append(*list, StringValuePtr(resolve));
2301
+ struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(resolve));
2302
+ if (!new_list) {
2303
+ rb_raise(rb_eNoMemError, "Failed to append to resolve list");
2304
+ }
2305
+ *list = new_list;
2228
2306
 
2229
2307
  return resolve_string;
2230
2308
  }
@@ -2296,6 +2374,14 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2296
2374
  curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, rb_easy_get_str("proxypwd"));
2297
2375
  }
2298
2376
 
2377
+ #if HAVE_CURLOPT_NOPROXY
2378
+ if (rb_easy_nil("noproxy")) {
2379
+ curl_easy_setopt(curl, CURLOPT_NOPROXY, NULL);
2380
+ } else {
2381
+ curl_easy_setopt(curl, CURLOPT_NOPROXY, rb_easy_get_str("noproxy"));
2382
+ }
2383
+ #endif
2384
+
2299
2385
  // body/header procs
2300
2386
  if (!rb_easy_nil("body_proc")) {
2301
2387
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)&proc_data_handler_body);
@@ -2443,9 +2529,8 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2443
2529
  #endif
2444
2530
  }
2445
2531
 
2446
- /* Set up HTTP cookie handling if necessary
2447
- FIXME this may not get disabled if it's enabled, the disabled again from ruby.
2448
- */
2532
+ /* Set up HTTP cookie handling if necessary */
2533
+ /* Enable/attach cookie engine if requested, or implicitly via COOKIELIST usage */
2449
2534
  if (rbce->enable_cookies) {
2450
2535
  if (!rb_easy_nil("cookiejar")) {
2451
2536
  curl_easy_setopt(curl, CURLOPT_COOKIEJAR, rb_easy_get_str("cookiejar"));
@@ -2456,6 +2541,9 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2456
2541
  } else {
2457
2542
  curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); /* "" = magic to just enable */
2458
2543
  }
2544
+ } else if (rbce->cookielist_engine_enabled) {
2545
+ /* Ensure cookie engine is enabled even if enable_cookies? is false. */
2546
+ curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");
2459
2547
  }
2460
2548
 
2461
2549
  if (!rb_easy_nil("cookies")) {
@@ -2517,7 +2605,11 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2517
2605
  rb_iterate(rb_each, rb_easy_get("headers"), cb_each_http_header, wrap);
2518
2606
  } else {
2519
2607
  VALUE headers_str = rb_obj_as_string(rb_easy_get("headers"));
2520
- *hdrs = curl_slist_append(*hdrs, StringValuePtr(headers_str));
2608
+ struct curl_slist *new_list = curl_slist_append(*hdrs, StringValuePtr(headers_str));
2609
+ if (!new_list) {
2610
+ rb_raise(rb_eNoMemError, "Failed to append to headers list");
2611
+ }
2612
+ *hdrs = new_list;
2521
2613
  }
2522
2614
 
2523
2615
  if (*hdrs) {
@@ -2535,7 +2627,11 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2535
2627
  rb_iterate(rb_each, rb_easy_get("proxy_headers"), cb_each_http_proxy_header, wrap);
2536
2628
  } else {
2537
2629
  VALUE proxy_headers_str = rb_obj_as_string(rb_easy_get("proxy_headers"));
2538
- *phdrs = curl_slist_append(*phdrs, StringValuePtr(proxy_headers_str));
2630
+ struct curl_slist *new_list = curl_slist_append(*phdrs, StringValuePtr(proxy_headers_str));
2631
+ if (!new_list) {
2632
+ rb_raise(rb_eNoMemError, "Failed to append to proxy headers list");
2633
+ }
2634
+ *phdrs = new_list;
2539
2635
  }
2540
2636
 
2541
2637
  if (*phdrs) {
@@ -2635,10 +2731,31 @@ static VALUE ruby_curl_easy_perform_verb_str(VALUE self, const char *verb) {
2635
2731
 
2636
2732
  memset(rbce->err_buf, 0, CURL_ERROR_SIZE);
2637
2733
 
2734
+ /* Use method override and adjust related options for special verbs. */
2638
2735
  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, verb);
2639
2736
 
2737
+ /* For HEAD, ensure no body is requested/downloaded, as some servers
2738
+ * include a Content-Length header which should not cause libcurl to
2739
+ * wait for a body that will never arrive. */
2740
+ int is_head = (verb && (
2741
+ #ifdef _WIN32
2742
+ _stricmp(verb, "HEAD") == 0
2743
+ #else
2744
+ strcasecmp(verb, "HEAD") == 0
2745
+ #endif
2746
+ ));
2747
+ if (is_head) {
2748
+ curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
2749
+ curl_easy_setopt(curl, CURLOPT_HTTPGET, 0L);
2750
+ curl_easy_setopt(curl, CURLOPT_POST, 0L);
2751
+ }
2752
+
2640
2753
  retval = rb_funcall(self, rb_intern("perform"), 0);
2641
2754
 
2755
+ /* Restore state after request. */
2756
+ if (is_head) {
2757
+ curl_easy_setopt(curl, CURLOPT_NOBODY, 0L);
2758
+ }
2642
2759
  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, NULL);
2643
2760
 
2644
2761
  return retval;
@@ -3523,6 +3640,12 @@ static VALUE ruby_curl_easy_num_connects_get(VALUE self) {
3523
3640
  * Returned strings are in Netscape cookiejar format or in Set-Cookie format.
3524
3641
  * Since 7.43.0 cookies in the Set-Cookie format without a domain name are not exported.
3525
3642
  *
3643
+ * To modify the cookie engine (add/replace/remove), use +easy.cookielist= string+
3644
+ * or +easy.set(:cookielist, string)+ with one of the following accepted inputs:
3645
+ * - A Set-Cookie style header string: "Set-Cookie: name=value; Domain=example.com; Path=/; Expires=..."
3646
+ * - One or more lines in Netscape cookie file format (tab-separated fields)
3647
+ * - Special commands: "ALL" (clear all), "SESS" (remove session cookies), "FLUSH" (write to jar), "RELOAD" (reload from file)
3648
+ *
3526
3649
  * @see https://curl.se/libcurl/c/CURLINFO_COOKIELIST.html option <code>CURLINFO_COOKIELIST</code> of
3527
3650
  * <code>curl_easy_getopt(3)</code> to see how libcurl behaves.
3528
3651
  * @note requires libcurl 7.14.1 or higher, otherwise +-1+ is always returned
@@ -3584,7 +3707,7 @@ static VALUE ruby_curl_easy_ftp_entry_path_get(VALUE self) {
3584
3707
  return Qnil;
3585
3708
  }
3586
3709
  #else
3587
- rb_warn("Installed libcurl is too old to support num_connects");
3710
+ rb_warn("Installed libcurl is too old to support ftp_entry_path");
3588
3711
  return Qnil;
3589
3712
  #endif
3590
3713
  }
@@ -3725,6 +3848,12 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
3725
3848
  VALUE proxypwd = val;
3726
3849
  CURB_OBJECT_HSETTER(ruby_curl_easy, proxypwd);
3727
3850
  } break;
3851
+ #if HAVE_CURLOPT_NOPROXY
3852
+ case CURLOPT_NOPROXY: {
3853
+ VALUE noproxy = val;
3854
+ CURB_OBJECT_HSETTER(ruby_curl_easy, noproxy);
3855
+ } break;
3856
+ #endif
3728
3857
  case CURLOPT_COOKIE: {
3729
3858
  VALUE cookies = val;
3730
3859
  CURB_OBJECT_HSETTER(ruby_curl_easy, cookies);
@@ -3737,9 +3866,72 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
3737
3866
  VALUE cookiejar = val;
3738
3867
  CURB_OBJECT_HSETTER(ruby_curl_easy, cookiejar);
3739
3868
  } break;
3869
+ #if HAVE_CURLOPT_REQUEST_TARGET
3870
+ case CURLOPT_REQUEST_TARGET: {
3871
+ /* Forward request-target directly to libcurl as a string. */
3872
+ curl_easy_setopt(rbce->curl, CURLOPT_REQUEST_TARGET, NIL_P(val) ? NULL : StringValueCStr(val));
3873
+ } break;
3874
+ #endif
3740
3875
  case CURLOPT_TCP_NODELAY: {
3741
3876
  curl_easy_setopt(rbce->curl, CURLOPT_TCP_NODELAY, NUM2LONG(val));
3742
3877
  } break;
3878
+ /* FTP-specific toggles */
3879
+ #if HAVE_CURLOPT_DIRLISTONLY
3880
+ case CURLOPT_DIRLISTONLY: {
3881
+ int type = rb_type(val);
3882
+ VALUE value;
3883
+ if (type == T_TRUE) {
3884
+ value = rb_int_new(1);
3885
+ } else if (type == T_FALSE) {
3886
+ value = rb_int_new(0);
3887
+ } else {
3888
+ value = rb_funcall(val, rb_intern("to_i"), 0);
3889
+ }
3890
+ curl_easy_setopt(rbce->curl, CURLOPT_DIRLISTONLY, NUM2LONG(value));
3891
+ } break;
3892
+ #endif
3893
+ #if HAVE_CURLOPT_FTP_USE_EPSV
3894
+ case CURLOPT_FTP_USE_EPSV: {
3895
+ int type = rb_type(val);
3896
+ VALUE value;
3897
+ if (type == T_TRUE) {
3898
+ value = rb_int_new(1);
3899
+ } else if (type == T_FALSE) {
3900
+ value = rb_int_new(0);
3901
+ } else {
3902
+ value = rb_funcall(val, rb_intern("to_i"), 0);
3903
+ }
3904
+ curl_easy_setopt(rbce->curl, CURLOPT_FTP_USE_EPSV, NUM2LONG(value));
3905
+ } break;
3906
+ #endif
3907
+ #if HAVE_CURLOPT_FTP_USE_EPRT
3908
+ case CURLOPT_FTP_USE_EPRT: {
3909
+ int type = rb_type(val);
3910
+ VALUE value;
3911
+ if (type == T_TRUE) {
3912
+ value = rb_int_new(1);
3913
+ } else if (type == T_FALSE) {
3914
+ value = rb_int_new(0);
3915
+ } else {
3916
+ value = rb_funcall(val, rb_intern("to_i"), 0);
3917
+ }
3918
+ curl_easy_setopt(rbce->curl, CURLOPT_FTP_USE_EPRT, NUM2LONG(value));
3919
+ } break;
3920
+ #endif
3921
+ #if HAVE_CURLOPT_FTP_SKIP_PASV_IP
3922
+ case CURLOPT_FTP_SKIP_PASV_IP: {
3923
+ int type = rb_type(val);
3924
+ VALUE value;
3925
+ if (type == T_TRUE) {
3926
+ value = rb_int_new(1);
3927
+ } else if (type == T_FALSE) {
3928
+ value = rb_int_new(0);
3929
+ } else {
3930
+ value = rb_funcall(val, rb_intern("to_i"), 0);
3931
+ }
3932
+ curl_easy_setopt(rbce->curl, CURLOPT_FTP_SKIP_PASV_IP, NUM2LONG(value));
3933
+ } break;
3934
+ #endif
3743
3935
  case CURLOPT_RANGE: {
3744
3936
  curl_easy_setopt(rbce->curl, CURLOPT_RANGE, StringValueCStr(val));
3745
3937
  } break;
@@ -3802,6 +3994,9 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
3802
3994
  Check_Type(val, T_FILE);
3803
3995
  GetOpenFile(val, open_f_ptr);
3804
3996
  curl_easy_setopt(rbce->curl, CURLOPT_STDERR, rb_io_stdio_file(open_f_ptr));
3997
+ /* Retain a Ruby reference to the IO to prevent GC/finalization
3998
+ * while libcurl still holds and writes to the underlying FILE*. */
3999
+ rb_easy_set("stderr_io", val);
3805
4000
  break;
3806
4001
  case CURLOPT_PROTOCOLS:
3807
4002
  case CURLOPT_REDIR_PROTOCOLS:
@@ -3814,8 +4009,23 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
3814
4009
  #endif
3815
4010
  #if HAVE_CURLOPT_COOKIELIST
3816
4011
  case CURLOPT_COOKIELIST: {
4012
+ /* Forward to libcurl */
3817
4013
  curl_easy_setopt(rbce->curl, CURLOPT_COOKIELIST, StringValueCStr(val));
3818
- } break;
4014
+ /* Track whether the cookie engine should be enabled for requests.
4015
+ * According to libcurl docs, CURLOPT_COOKIELIST also enables the cookie engine
4016
+ * when provided with a non-command string. Some environments may still require
4017
+ * an explicit "enable" via CURLOPT_COOKIEFILE="" to send cookies on requests.
4018
+ * We do that in the perform setup when this flag is set.
4019
+ */
4020
+ if (RB_TYPE_P(val, T_STRING)) {
4021
+ const char *s = StringValueCStr(val);
4022
+ if (!(strcmp(s, "ALL") == 0 || strcmp(s, "SESS") == 0 || strcmp(s, "FLUSH") == 0 || strcmp(s, "RELOAD") == 0)) {
4023
+ rbce->cookielist_engine_enabled = 1;
4024
+ }
4025
+ } else {
4026
+ /* Non-string values are unexpected; be conservative and do not enable. */
4027
+ }
4028
+ } break;
3819
4029
  #endif
3820
4030
  #if HAVE_CURLOPT_PROXY_SSL_VERIFYHOST
3821
4031
  case CURLOPT_PROXY_SSL_VERIFYHOST:
@@ -3832,11 +4042,19 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
3832
4042
  long i, len = RARRAY_LEN(val);
3833
4043
  for (i = 0; i < len; i++) {
3834
4044
  VALUE item = rb_ary_entry(val, i);
3835
- list = curl_slist_append(list, StringValueCStr(item));
4045
+ struct curl_slist *new_list = curl_slist_append(list, StringValueCStr(item));
4046
+ if (!new_list) {
4047
+ curl_slist_free_all(list);
4048
+ rb_raise(rb_eNoMemError, "Failed to append to resolve list");
4049
+ }
4050
+ list = new_list;
3836
4051
  }
3837
4052
  } else {
3838
4053
  /* If a single string is passed, use it directly */
3839
4054
  list = curl_slist_append(NULL, StringValueCStr(val));
4055
+ if (!list) {
4056
+ rb_raise(rb_eNoMemError, "Failed to create resolve list");
4057
+ }
3840
4058
  }
3841
4059
  /* Save the list pointer in the ruby_curl_easy structure for cleanup later */
3842
4060
  rbce->curl_resolve = list;
data/ext/curb_easy.h CHANGED
@@ -77,6 +77,7 @@ typedef struct {
77
77
  char verbose;
78
78
  char multipart_form_post;
79
79
  char enable_cookies;
80
+ char cookielist_engine_enabled; /* track if CURLOPT_COOKIELIST was used with a non-command to enable engine */
80
81
  char ignore_content_length;
81
82
  char callback_active;
82
83