curb 1.2.0 → 1.2.2

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: 832660904f4d95f97b0795a4e39850f2fca2c70e521a0bb795a9c551f6fdf9f3
4
- data.tar.gz: 903cb16a9a77396e90ea9ab2f33fb94a84d6a811525d23506af43195ab49fec2
3
+ metadata.gz: 784cff29d3284c08afa7a76364d5643b5d08ecff8802bb7b17e047438074bc37
4
+ data.tar.gz: 336bff9af874b57b0099da1e9fbde7c9b256e831de819635bc57c947d245c70c
5
5
  SHA512:
6
- metadata.gz: bf99199469304444421b00adbaa2b64707c7eddcdaff9a3c662531ebf5f2220a1b6555145fa516422921ac0980c851e7ce234ff88391d608ca6783211f364ddf
7
- data.tar.gz: 41aec998852a8458ed9f5508f37374ac72fb170c4235a7279e4f9864d2af143281cd81e1dec6099c80765d97b2263d1ee34bebd2b193701b9a4175a024ca5991
6
+ metadata.gz: 02c0c968ada08138391d35a09a85ccc82ab6cf0b17cb9ef71c7a496c6008f74aff97ecacae74bfe10ae958cb6c20907096459c71ab227ad0638a6381e55dd142
7
+ data.tar.gz: cb575a3ec72ac5b8f1080c7893cd49410ac4285a02044a267335846ff4981a7d3372a087101215413ff2e994ec2f8484787a8b5621a777dd5ca70dd1e8f5c9b3
data/README.md CHANGED
@@ -74,7 +74,66 @@ puts "\n=== FTP Directory Listing Example ==="
74
74
  list = Curl::Easy.new('ftp://ftp.example.com/remote/directory/')
75
75
  list.username = 'user'
76
76
  list.password = 'password'
77
- 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
+
102
+ list.perform
103
+ puts list.body
104
+ ```
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
+
108
+ #### Full LIST directory listing
109
+ To retrieve the full `LIST` output (permissions, owner, size, timestamp, name), simply do not set `dirlistonly`:
110
+
111
+ ```ruby
112
+ list = Curl::Easy.new('ftp://ftp.example.com/remote/directory/')
113
+ list.username = 'user'
114
+ list.password = 'password'
115
+
116
+ # Explicitly ensure names+metadata (LIST) rather than NLST
117
+ # list.set(:dirlistonly, 0) # optional; default is LIST for directory URLs
118
+
119
+ list.perform
120
+ puts list.body # multi-line LIST output
121
+ ```
122
+
123
+ Through an HTTP proxy tunnel, the same considerations apply as the NLST example above — just omit `dirlistonly` and keep the optional EPSV/EPRT/PASV tweaks if needed:
124
+
125
+ ```ruby
126
+ list = Curl::Easy.new('ftp://ftp.example.com/remote/directory/')
127
+ list.username = 'user'
128
+ list.password = 'password'
129
+ list.proxy_url = 'http://proxy.example.com:80'
130
+ list.proxy_tunnel = true
131
+
132
+ # Optional tweaks if the proxy/server combination struggles
133
+ # list.set(:ftp_use_epsv, 0)
134
+ # list.set(:ftp_use_eprt, 0)
135
+ # list.set(:ftp_skip_pasv_ip, 1)
136
+
78
137
  list.perform
79
138
  puts list.body
80
139
  ```
@@ -327,6 +386,8 @@ end
327
386
 
328
387
  ### HTTP POST form:
329
388
 
389
+ 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, ...)`.
390
+
330
391
  ```ruby
331
392
  c = Curl::Easy.http_post("http://my.rails.box/thing/create",
332
393
  Curl::PostField.content('thing[name]', 'box'),
@@ -339,6 +400,19 @@ c = Curl::Easy.http_post("http://my.rails.box/thing/create",
339
400
  c = Curl::Easy.new("http://my.rails.box/files/upload")
340
401
  c.multipart_form_post = true
341
402
  c.http_post(Curl::PostField.file('thing[file]', 'myfile.rb'))
403
+
404
+ ### Custom request target
405
+
406
+ 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:
407
+
408
+ ```ruby
409
+ c = Curl::Easy.new("http://127.0.0.1:9129/methods")
410
+ c.request_target = "http://localhost:9129/methods" # absolute-form target
411
+ c.headers = { 'Host' => 'example.com' } # override Host header if needed
412
+ c.perform
413
+ ```
414
+
415
+ For HTTPS, prefer `easy.resolve = ["host:443:IP"]` to keep Host/SNI/certificates aligned.
342
416
  ```
343
417
 
344
418
  ### Using HTTP/2
@@ -418,3 +492,31 @@ end
418
492
  * `on_missing` is called when the response code is 4xx
419
493
  * `on_failure` is called when the response code is 5xx
420
494
  * `on_complete` is called in all cases.
495
+
496
+ ### Cookies
497
+
498
+ - 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.
499
+ - 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.
500
+ - Inspect engine cookies: `easy.cookielist` returns an array of strings (Netscape or Set-Cookie format).
501
+ - 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).
502
+ - Clearing manual cookies: assign an empty string (`easy.cookies = ''`). Assigning `nil` has no effect in current versions.
503
+
504
+ Examples:
505
+
506
+ ```ruby
507
+ easy = Curl::Easy.new("https://example.com")
508
+
509
+ # Use the cookie engine and persist cookies
510
+ easy.enable_cookies = true
511
+ easy.cookiejar = "/tmp/cookies.txt"
512
+ easy.perform
513
+
514
+ # Later: inspect and tweak engine cookies
515
+ p easy.cookielist
516
+ easy.cookielist = 'ALL' # clear stored cookies
517
+
518
+ # Send custom Cookie header for a single request
519
+ easy.cookies = "flag=1; session_override=abc"
520
+ easy.perform
521
+ easy.cookies = '' # clear manual Cookie header
522
+ ```
data/ext/curb.c CHANGED
@@ -616,6 +616,11 @@ void Init_curb_core() {
616
616
  #if HAVE_CURLOPT_PROXYHEADER
617
617
  CURB_DEFINE(CURLOPT_PROXYHEADER);
618
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
619
624
  #if HAVE_CURLOPT_HTTP200ALIASES
620
625
  CURB_DEFINE(CURLOPT_HTTP200ALIASES);
621
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.2.0"
32
- #define CURB_VER_NUM 1020
31
+ #define CURB_VERSION "1.2.2"
32
+ #define CURB_VER_NUM 1022
33
33
  #define CURB_VER_MAJ 1
34
34
  #define CURB_VER_MIN 2
35
- #define CURB_VER_MIC 0
35
+ #define CURB_VER_MIC 2
36
36
  #define CURB_VER_PATCH 0
37
37
 
38
38
 
data/ext/curb_easy.c CHANGED
@@ -42,13 +42,34 @@ static VALUE callback_exception(VALUE unused, VALUE exception) {
42
42
  return Qfalse;
43
43
  }
44
44
 
45
- /* These handle both body and header data */
46
- static size_t default_data_handler(char *stream,
45
+ /* Default body handler appends to easy.body_data buffer */
46
+ static size_t default_body_handler(char *stream,
47
47
  size_t size,
48
48
  size_t nmemb,
49
- VALUE out) {
50
- rb_str_buf_cat(out, stream, size * nmemb);
51
- return size * nmemb;
49
+ void *userdata) {
50
+ ruby_curl_easy *rbce = (ruby_curl_easy *)userdata;
51
+ size_t total = size * nmemb;
52
+ VALUE out = rb_easy_get("body_data");
53
+ if (NIL_P(out)) {
54
+ out = rb_easy_set("body_data", rb_str_buf_new(32768));
55
+ }
56
+ rb_str_buf_cat(out, stream, total);
57
+ return total;
58
+ }
59
+
60
+ /* Default header handler appends to easy.header_data buffer */
61
+ static size_t default_header_handler(char *stream,
62
+ size_t size,
63
+ size_t nmemb,
64
+ void *userdata) {
65
+ ruby_curl_easy *rbce = (ruby_curl_easy *)userdata;
66
+ size_t total = size * nmemb;
67
+ VALUE out = rb_easy_get("header_data");
68
+ if (NIL_P(out)) {
69
+ out = rb_easy_set("header_data", rb_str_buf_new(16384));
70
+ }
71
+ rb_str_buf_cat(out, stream, total);
72
+ return total;
52
73
  }
53
74
 
54
75
  // size_t function( void *ptr, size_t size, size_t nmemb, void *stream);
@@ -172,11 +193,16 @@ static VALUE call_progress_handler(VALUE ary) {
172
193
  rb_ary_entry(ary, 4)); // rb_float_new(ulnow));
173
194
  }
174
195
 
175
- static int proc_progress_handler(VALUE proc,
196
+ static int proc_progress_handler(void *clientp,
176
197
  double dltotal,
177
198
  double dlnow,
178
199
  double ultotal,
179
200
  double ulnow) {
201
+ ruby_curl_easy *rbce = (ruby_curl_easy *)clientp;
202
+ VALUE proc = rb_easy_get("progress_proc");
203
+ if (proc == Qnil) {
204
+ return 0;
205
+ }
180
206
  VALUE procret;
181
207
  VALUE callargs = rb_ary_new2(5);
182
208
 
@@ -205,7 +231,12 @@ static int proc_debug_handler(CURL *curl,
205
231
  curl_infotype type,
206
232
  char *data,
207
233
  size_t data_len,
208
- VALUE proc) {
234
+ void *clientp) {
235
+ ruby_curl_easy *rbce = (ruby_curl_easy *)clientp;
236
+ VALUE proc = rb_easy_get("debug_proc");
237
+ if (proc == Qnil) {
238
+ return 0;
239
+ }
209
240
  VALUE callargs = rb_ary_new2(3);
210
241
  rb_ary_store(callargs, 0, proc);
211
242
  rb_ary_store(callargs, 1, INT2NUM(type));
@@ -225,6 +256,30 @@ void curl_easy_mark(ruby_curl_easy *rbce) {
225
256
  }
226
257
 
227
258
  static void ruby_curl_easy_free(ruby_curl_easy *rbce) {
259
+ if (!rbce) {
260
+ return;
261
+ }
262
+
263
+ if (!NIL_P(rbce->multi)) {
264
+ VALUE multi_val = rbce->multi;
265
+ ruby_curl_multi *rbcm = NULL;
266
+
267
+ rbce->multi = Qnil;
268
+
269
+ if (!NIL_P(multi_val) && RB_TYPE_P(multi_val, T_DATA)) {
270
+ Data_Get_Struct(multi_val, ruby_curl_multi, rbcm);
271
+ if (rbcm) {
272
+ /* Best-effort: ensure the handle is detached from the multi to
273
+ * avoid libcurl retaining a dangling pointer to a soon-to-be
274
+ * cleaned-up easy handle. We cannot raise from GC, so ignore errors. */
275
+ if (rbcm->handle && rbce->curl) {
276
+ curl_multi_remove_handle(rbcm->handle, rbce->curl);
277
+ }
278
+ rb_curl_multi_forget_easy(rbcm, rbce);
279
+ }
280
+ }
281
+ }
282
+
228
283
  if (rbce->curl_headers) {
229
284
  curl_slist_free_all(rbce->curl_headers);
230
285
  }
@@ -255,6 +310,8 @@ static void ruby_curl_easy_free(ruby_curl_easy *rbce) {
255
310
  curl_easy_cleanup(rbce->curl);
256
311
  rbce->curl = NULL;
257
312
  }
313
+
314
+ rbce->self = Qnil;
258
315
  }
259
316
 
260
317
  void curl_easy_free(ruby_curl_easy *rbce) {
@@ -270,6 +327,7 @@ static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
270
327
 
271
328
  memset(rbce->err_buf, 0, CURL_ERROR_SIZE);
272
329
 
330
+ rbce->self = Qnil;
273
331
  rbce->curl_headers = NULL;
274
332
  rbce->curl_proxy_headers = NULL;
275
333
  rbce->curl_ftp_commands = NULL;
@@ -310,6 +368,7 @@ static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
310
368
  rbce->verbose = 0;
311
369
  rbce->multipart_form_post = 0;
312
370
  rbce->enable_cookies = 0;
371
+ rbce->cookielist_engine_enabled = 0;
313
372
  rbce->ignore_content_length = 0;
314
373
  rbce->callback_active = 0;
315
374
  rbce->last_result = 0;
@@ -360,13 +419,14 @@ static VALUE ruby_curl_easy_initialize(int argc, VALUE *argv, VALUE self) {
360
419
  rbce->opts = Qnil;
361
420
 
362
421
  ruby_curl_easy_zero(rbce);
422
+ rbce->self = self;
363
423
 
364
424
  curl_easy_setopt(rbce->curl, CURLOPT_ERRORBUFFER, &rbce->err_buf);
365
425
 
366
426
  rb_easy_set("url", url);
367
427
 
368
428
  /* set the pointer to the curl handle */
369
- ecode = curl_easy_setopt(rbce->curl, CURLOPT_PRIVATE, (void*)self);
429
+ ecode = curl_easy_setopt(rbce->curl, CURLOPT_PRIVATE, (void*)rbce);
370
430
  if (ecode != CURLE_OK) {
371
431
  raise_curl_easy_error_exception(ecode);
372
432
  }
@@ -431,7 +491,11 @@ static VALUE ruby_curl_easy_clone(VALUE self) {
431
491
  /* Set the error buffer on the new curl handle using the new err_buf */
432
492
  curl_easy_setopt(newrbce->curl, CURLOPT_ERRORBUFFER, newrbce->err_buf);
433
493
 
434
- return Data_Wrap_Struct(cCurlEasy, curl_easy_mark, curl_easy_free, newrbce);
494
+ VALUE clone = Data_Wrap_Struct(cCurlEasy, curl_easy_mark, curl_easy_free, newrbce);
495
+ newrbce->self = clone;
496
+ curl_easy_setopt(newrbce->curl, CURLOPT_PRIVATE, (void*)newrbce);
497
+
498
+ return clone;
435
499
  }
436
500
 
437
501
  /*
@@ -463,9 +527,10 @@ static VALUE ruby_curl_easy_close(VALUE self) {
463
527
  rbce->multi = Qnil;
464
528
 
465
529
  ruby_curl_easy_zero(rbce);
530
+ rbce->self = self;
466
531
 
467
532
  /* give the new curl handle a reference back to the ruby object */
468
- ecode = curl_easy_setopt(rbce->curl, CURLOPT_PRIVATE, (void*)self);
533
+ ecode = curl_easy_setopt(rbce->curl, CURLOPT_PRIVATE, (void*)rbce);
469
534
  if (ecode != CURLE_OK) {
470
535
  raise_curl_easy_error_exception(ecode);
471
536
  }
@@ -499,11 +564,12 @@ static VALUE ruby_curl_easy_reset(VALUE self) {
499
564
 
500
565
  curl_easy_reset(rbce->curl);
501
566
  ruby_curl_easy_zero(rbce);
567
+ rbce->self = self;
502
568
 
503
569
  curl_easy_setopt(rbce->curl, CURLOPT_ERRORBUFFER, &rbce->err_buf);
504
570
 
505
571
  /* reset clobbers the private setting, so reset it to self */
506
- ecode = curl_easy_setopt(rbce->curl, CURLOPT_PRIVATE, (void*)self);
572
+ ecode = curl_easy_setopt(rbce->curl, CURLOPT_PRIVATE, (void*)rbce);
507
573
  if (ecode != CURLE_OK) {
508
574
  raise_curl_easy_error_exception(ecode);
509
575
  }
@@ -664,7 +730,16 @@ static VALUE ruby_curl_easy_proxypwd_get(VALUE self) {
664
730
  * call-seq:
665
731
  * easy.cookies => "name1=content1; name2=content2;"
666
732
  *
667
- * Obtain the cookies for this Curl::Easy instance.
733
+ * Obtain the manually set Cookie header string for this Curl::Easy instance.
734
+ *
735
+ * Notes:
736
+ * - This corresponds to libcurl's CURLOPT_COOKIE and only affects the outgoing
737
+ * Cookie request header. It does NOT modify the internal libcurl cookie engine
738
+ * that stores cookies received via Set-Cookie.
739
+ * - To inspect or modify cookies stored in the cookie engine, use
740
+ * +easy.cookielist+ (getter) and +easy.cookielist=+ or +easy.set(:cookielist, ...)+ (setter).
741
+ * - To clear a previously set manual Cookie header, assign an empty string.
742
+ * Assigning +nil+ currently has no effect.
668
743
  */
669
744
  static VALUE ruby_curl_easy_cookies_get(VALUE self) {
670
745
  CURB_OBJECT_HGETTER(ruby_curl_easy, cookies);
@@ -674,7 +749,8 @@ static VALUE ruby_curl_easy_cookies_get(VALUE self) {
674
749
  * call-seq:
675
750
  * easy.cookiefile => string
676
751
  *
677
- * Obtain the cookiefile file for this Curl::Easy instance.
752
+ * Obtain the cookiefile path for this Curl::Easy instance (used to load cookies when the
753
+ * cookie engine is enabled).
678
754
  */
679
755
  static VALUE ruby_curl_easy_cookiefile_get(VALUE self) {
680
756
  CURB_OBJECT_HGETTER(ruby_curl_easy, cookiefile);
@@ -684,7 +760,8 @@ static VALUE ruby_curl_easy_cookiefile_get(VALUE self) {
684
760
  * call-seq:
685
761
  * easy.cookiejar => string
686
762
  *
687
- * Obtain the cookiejar file to use for this Curl::Easy instance.
763
+ * Obtain the cookiejar path for this Curl::Easy instance (used to persist cookies when the
764
+ * cookie engine is enabled).
688
765
  */
689
766
  static VALUE ruby_curl_easy_cookiejar_get(VALUE self) {
690
767
  CURB_OBJECT_HGETTER(ruby_curl_easy, cookiejar);
@@ -984,7 +1061,15 @@ static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
984
1061
  * call-seq:
985
1062
  * easy.ftp_commands = ["CWD /", "MKD directory"] => ["CWD /", ...]
986
1063
  *
987
- * Explicitly sets the list of commands to execute on the FTP server when calling perform
1064
+ * Explicitly sets the list of commands to execute on the FTP server when calling perform.
1065
+ *
1066
+ * NOTE:
1067
+ * - This maps to libcurl CURLOPT_QUOTE; it sends commands on the control connection.
1068
+ * - Do not include data-transfer commands like LIST/NLST/RETR/STOR here. libcurl does not
1069
+ * parse PASV/EPSV replies from QUOTE commands and will not establish the required data
1070
+ * connection. For directory listings, set CURLOPT_DIRLISTONLY (via `easy.set(:dirlistonly, true)`)
1071
+ * and request an FTP directory URL (e.g. "ftp://host/path/") so libcurl manages PASV/EPSV
1072
+ * and the data connection for you.
988
1073
  */
989
1074
  static VALUE ruby_curl_easy_ftp_commands_set(VALUE self, VALUE ftp_commands) {
990
1075
  CURB_OBJECT_HSETTER(ruby_curl_easy, ftp_commands);
@@ -1893,7 +1978,12 @@ static VALUE ruby_curl_easy_multipart_form_post_q(VALUE self) {
1893
1978
  * easy.enable_cookies = boolean => boolean
1894
1979
  *
1895
1980
  * Configure whether the libcurl cookie engine is enabled for this Curl::Easy
1896
- * instance.
1981
+ * instance. When enabled, cookies received via Set-Cookie are stored by libcurl
1982
+ * and automatically sent on subsequent matching requests. Use +easy.cookiefile+
1983
+ * to load cookies and +easy.cookiejar+ to persist them.
1984
+ *
1985
+ * This setting is independent from the manual Cookie header set via +easy.cookies+.
1986
+ * The manual header is additive and can be cleared by assigning an empty string.
1897
1987
  */
1898
1988
  static VALUE ruby_curl_easy_enable_cookies_set(VALUE self, VALUE enable_cookies)
1899
1989
  {
@@ -2346,9 +2436,9 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2346
2436
  /* clear out the body_data if it was set */
2347
2437
  rb_easy_del("body_data");
2348
2438
  } else {
2349
- VALUE body_buffer = rb_easy_set("body_data", rb_str_buf_new(32768));
2350
- curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)&default_data_handler);
2351
- curl_easy_setopt(curl, CURLOPT_WRITEDATA, body_buffer);
2439
+ rb_easy_set("body_data", rb_str_buf_new(32768));
2440
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)&default_body_handler);
2441
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, rbce);
2352
2442
  }
2353
2443
 
2354
2444
  if (!rb_easy_nil("header_proc")) {
@@ -2357,9 +2447,9 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2357
2447
  /* clear out the header_data if it was set */
2358
2448
  rb_easy_del("header_data");
2359
2449
  } else {
2360
- VALUE header_buffer = rb_easy_set("header_data", rb_str_buf_new(16384));
2361
- curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, (curl_write_callback)&default_data_handler);
2362
- curl_easy_setopt(curl, CURLOPT_HEADERDATA, header_buffer);
2450
+ rb_easy_set("header_data", rb_str_buf_new(16384));
2451
+ curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, (curl_write_callback)&default_header_handler);
2452
+ curl_easy_setopt(curl, CURLOPT_HEADERDATA, rbce);
2363
2453
  }
2364
2454
 
2365
2455
  /* encoding */
@@ -2370,20 +2460,21 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2370
2460
  // progress and debug procs
2371
2461
  if (!rb_easy_nil("progress_proc")) {
2372
2462
  curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, (curl_progress_callback)&proc_progress_handler);
2373
- curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, rb_easy_get("progress_proc"));
2463
+ curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, rbce);
2374
2464
  curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
2375
2465
  } else {
2376
2466
  curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1);
2467
+ curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, rbce);
2377
2468
  }
2378
2469
 
2379
2470
  if (!rb_easy_nil("debug_proc")) {
2380
2471
  curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, (curl_debug_callback)&proc_debug_handler);
2381
- curl_easy_setopt(curl, CURLOPT_DEBUGDATA, rb_easy_get("debug_proc"));
2472
+ curl_easy_setopt(curl, CURLOPT_DEBUGDATA, rbce);
2382
2473
  curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
2383
2474
  } else {
2384
2475
  // have to remove handler to re-enable standard verbosity
2385
2476
  curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, NULL);
2386
- curl_easy_setopt(curl, CURLOPT_DEBUGDATA, NULL);
2477
+ curl_easy_setopt(curl, CURLOPT_DEBUGDATA, rbce);
2387
2478
  curl_easy_setopt(curl, CURLOPT_VERBOSE, rbce->verbose);
2388
2479
  }
2389
2480
 
@@ -2391,7 +2482,14 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2391
2482
 
2392
2483
  curl_easy_setopt(curl, CURLOPT_HEADER, rbce->header_in_body);
2393
2484
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, rbce->follow_location);
2485
+ #if LIBCURL_VERSION_NUM == 0x081000
2486
+ /* Work around 8.16.0 regression that clamps -1 (infinite) to zero */
2487
+ if (rbce->max_redirs >= 0) {
2488
+ curl_easy_setopt(curl, CURLOPT_MAXREDIRS, rbce->max_redirs);
2489
+ }
2490
+ #else
2394
2491
  curl_easy_setopt(curl, CURLOPT_MAXREDIRS, rbce->max_redirs);
2492
+ #endif
2395
2493
 
2396
2494
  curl_easy_setopt(curl, CURLOPT_HTTPPROXYTUNNEL, rbce->proxy_tunnel);
2397
2495
  curl_easy_setopt(curl, CURLOPT_FILETIME, rbce->fetch_file_time);
@@ -2486,9 +2584,8 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2486
2584
  #endif
2487
2585
  }
2488
2586
 
2489
- /* Set up HTTP cookie handling if necessary
2490
- FIXME this may not get disabled if it's enabled, the disabled again from ruby.
2491
- */
2587
+ /* Set up HTTP cookie handling if necessary */
2588
+ /* Enable/attach cookie engine if requested, or implicitly via COOKIELIST usage */
2492
2589
  if (rbce->enable_cookies) {
2493
2590
  if (!rb_easy_nil("cookiejar")) {
2494
2591
  curl_easy_setopt(curl, CURLOPT_COOKIEJAR, rb_easy_get_str("cookiejar"));
@@ -2499,6 +2596,9 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2499
2596
  } else {
2500
2597
  curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); /* "" = magic to just enable */
2501
2598
  }
2599
+ } else if (rbce->cookielist_engine_enabled) {
2600
+ /* Ensure cookie engine is enabled even if enable_cookies? is false. */
2601
+ curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");
2502
2602
  }
2503
2603
 
2504
2604
  if (!rb_easy_nil("cookies")) {
@@ -3595,6 +3695,12 @@ static VALUE ruby_curl_easy_num_connects_get(VALUE self) {
3595
3695
  * Returned strings are in Netscape cookiejar format or in Set-Cookie format.
3596
3696
  * Since 7.43.0 cookies in the Set-Cookie format without a domain name are not exported.
3597
3697
  *
3698
+ * To modify the cookie engine (add/replace/remove), use +easy.cookielist= string+
3699
+ * or +easy.set(:cookielist, string)+ with one of the following accepted inputs:
3700
+ * - A Set-Cookie style header string: "Set-Cookie: name=value; Domain=example.com; Path=/; Expires=..."
3701
+ * - One or more lines in Netscape cookie file format (tab-separated fields)
3702
+ * - Special commands: "ALL" (clear all), "SESS" (remove session cookies), "FLUSH" (write to jar), "RELOAD" (reload from file)
3703
+ *
3598
3704
  * @see https://curl.se/libcurl/c/CURLINFO_COOKIELIST.html option <code>CURLINFO_COOKIELIST</code> of
3599
3705
  * <code>curl_easy_getopt(3)</code> to see how libcurl behaves.
3600
3706
  * @note requires libcurl 7.14.1 or higher, otherwise +-1+ is always returned
@@ -3656,7 +3762,7 @@ static VALUE ruby_curl_easy_ftp_entry_path_get(VALUE self) {
3656
3762
  return Qnil;
3657
3763
  }
3658
3764
  #else
3659
- rb_warn("Installed libcurl is too old to support num_connects");
3765
+ rb_warn("Installed libcurl is too old to support ftp_entry_path");
3660
3766
  return Qnil;
3661
3767
  #endif
3662
3768
  }
@@ -3815,9 +3921,72 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
3815
3921
  VALUE cookiejar = val;
3816
3922
  CURB_OBJECT_HSETTER(ruby_curl_easy, cookiejar);
3817
3923
  } break;
3924
+ #if HAVE_CURLOPT_REQUEST_TARGET
3925
+ case CURLOPT_REQUEST_TARGET: {
3926
+ /* Forward request-target directly to libcurl as a string. */
3927
+ curl_easy_setopt(rbce->curl, CURLOPT_REQUEST_TARGET, NIL_P(val) ? NULL : StringValueCStr(val));
3928
+ } break;
3929
+ #endif
3818
3930
  case CURLOPT_TCP_NODELAY: {
3819
3931
  curl_easy_setopt(rbce->curl, CURLOPT_TCP_NODELAY, NUM2LONG(val));
3820
3932
  } break;
3933
+ /* FTP-specific toggles */
3934
+ #if HAVE_CURLOPT_DIRLISTONLY
3935
+ case CURLOPT_DIRLISTONLY: {
3936
+ int type = rb_type(val);
3937
+ VALUE value;
3938
+ if (type == T_TRUE) {
3939
+ value = rb_int_new(1);
3940
+ } else if (type == T_FALSE) {
3941
+ value = rb_int_new(0);
3942
+ } else {
3943
+ value = rb_funcall(val, rb_intern("to_i"), 0);
3944
+ }
3945
+ curl_easy_setopt(rbce->curl, CURLOPT_DIRLISTONLY, NUM2LONG(value));
3946
+ } break;
3947
+ #endif
3948
+ #if HAVE_CURLOPT_FTP_USE_EPSV
3949
+ case CURLOPT_FTP_USE_EPSV: {
3950
+ int type = rb_type(val);
3951
+ VALUE value;
3952
+ if (type == T_TRUE) {
3953
+ value = rb_int_new(1);
3954
+ } else if (type == T_FALSE) {
3955
+ value = rb_int_new(0);
3956
+ } else {
3957
+ value = rb_funcall(val, rb_intern("to_i"), 0);
3958
+ }
3959
+ curl_easy_setopt(rbce->curl, CURLOPT_FTP_USE_EPSV, NUM2LONG(value));
3960
+ } break;
3961
+ #endif
3962
+ #if HAVE_CURLOPT_FTP_USE_EPRT
3963
+ case CURLOPT_FTP_USE_EPRT: {
3964
+ int type = rb_type(val);
3965
+ VALUE value;
3966
+ if (type == T_TRUE) {
3967
+ value = rb_int_new(1);
3968
+ } else if (type == T_FALSE) {
3969
+ value = rb_int_new(0);
3970
+ } else {
3971
+ value = rb_funcall(val, rb_intern("to_i"), 0);
3972
+ }
3973
+ curl_easy_setopt(rbce->curl, CURLOPT_FTP_USE_EPRT, NUM2LONG(value));
3974
+ } break;
3975
+ #endif
3976
+ #if HAVE_CURLOPT_FTP_SKIP_PASV_IP
3977
+ case CURLOPT_FTP_SKIP_PASV_IP: {
3978
+ int type = rb_type(val);
3979
+ VALUE value;
3980
+ if (type == T_TRUE) {
3981
+ value = rb_int_new(1);
3982
+ } else if (type == T_FALSE) {
3983
+ value = rb_int_new(0);
3984
+ } else {
3985
+ value = rb_funcall(val, rb_intern("to_i"), 0);
3986
+ }
3987
+ curl_easy_setopt(rbce->curl, CURLOPT_FTP_SKIP_PASV_IP, NUM2LONG(value));
3988
+ } break;
3989
+ #endif
3821
3990
  case CURLOPT_RANGE: {
3822
3991
  curl_easy_setopt(rbce->curl, CURLOPT_RANGE, StringValueCStr(val));
3823
3992
  } break;
@@ -3895,8 +4064,23 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
3895
4064
  #endif
3896
4065
  #if HAVE_CURLOPT_COOKIELIST
3897
4066
  case CURLOPT_COOKIELIST: {
4067
+ /* Forward to libcurl */
3898
4068
  curl_easy_setopt(rbce->curl, CURLOPT_COOKIELIST, StringValueCStr(val));
3899
- } break;
4069
+ /* Track whether the cookie engine should be enabled for requests.
4070
+ * According to libcurl docs, CURLOPT_COOKIELIST also enables the cookie engine
4071
+ * when provided with a non-command string. Some environments may still require
4072
+ * an explicit "enable" via CURLOPT_COOKIEFILE="" to send cookies on requests.
4073
+ * We do that in the perform setup when this flag is set.
4074
+ */
4075
+ if (RB_TYPE_P(val, T_STRING)) {
4076
+ const char *s = StringValueCStr(val);
4077
+ if (!(strcmp(s, "ALL") == 0 || strcmp(s, "SESS") == 0 || strcmp(s, "FLUSH") == 0 || strcmp(s, "RELOAD") == 0)) {
4078
+ rbce->cookielist_engine_enabled = 1;
4079
+ }
4080
+ } else {
4081
+ /* Non-string values are unexpected; be conservative and do not enable. */
4082
+ }
4083
+ } break;
3900
4084
  #endif
3901
4085
  #if HAVE_CURLOPT_PROXY_SSL_VERIFYHOST
3902
4086
  case CURLOPT_PROXY_SSL_VERIFYHOST:
data/ext/curb_easy.h CHANGED
@@ -39,6 +39,7 @@ typedef struct {
39
39
  /* Buffer for error details from CURLOPT_ERRORBUFFER */
40
40
  char err_buf[CURL_ERROR_SIZE];
41
41
 
42
+ VALUE self; /* owning Ruby object */
42
43
  VALUE opts; /* rather then allocate everything we might need to store, allocate a Hash and only store objects we actually use... */
43
44
  VALUE multi; /* keep a multi handle alive for each easy handle not being used by a multi handle. This improves easy performance when not within a multi context */
44
45
 
@@ -77,6 +78,7 @@ typedef struct {
77
78
  char verbose;
78
79
  char multipart_form_post;
79
80
  char enable_cookies;
81
+ char cookielist_engine_enabled; /* track if CURLOPT_COOKIELIST was used with a non-command to enable engine */
80
82
  char ignore_content_length;
81
83
  char callback_active;
82
84
 
data/ext/curb_multi.c CHANGED
@@ -76,6 +76,10 @@ static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy);
76
76
  static void rb_curl_multi_read_info(VALUE self, CURLM *mptr);
77
77
  static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_running);
78
78
 
79
+ static int detach_easy_entry(st_data_t key, st_data_t val, st_data_t arg);
80
+ static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm);
81
+ static void curl_multi_mark(void *ptr);
82
+
79
83
  static VALUE callback_exception(VALUE did_raise, VALUE exception) {
80
84
  // TODO: we could have an option to enable exception reporting
81
85
  /* VALUE ret = rb_funcall(exception, rb_intern("message"), 0);
@@ -97,8 +101,67 @@ static VALUE callback_exception(VALUE did_raise, VALUE exception) {
97
101
  return exception;
98
102
  }
99
103
 
104
+ static int detach_easy_entry(st_data_t key, st_data_t val, st_data_t arg) {
105
+ ruby_curl_multi *rbcm = (ruby_curl_multi *)arg;
106
+ VALUE easy = (VALUE)val;
107
+ ruby_curl_easy *rbce = NULL;
108
+
109
+ if (RB_TYPE_P(easy, T_DATA)) {
110
+ Data_Get_Struct(easy, ruby_curl_easy, rbce);
111
+ }
112
+
113
+ if (!rbce) {
114
+ return ST_CONTINUE;
115
+ }
116
+
117
+ if (rbcm && rbcm->handle && rbce->curl) {
118
+ curl_multi_remove_handle(rbcm->handle, rbce->curl);
119
+ }
120
+
121
+ rbce->multi = Qnil;
122
+
123
+ return ST_CONTINUE;
124
+ }
125
+
126
+ void rb_curl_multi_forget_easy(ruby_curl_multi *rbcm, void *rbce_ptr) {
127
+ ruby_curl_easy *rbce = (ruby_curl_easy *)rbce_ptr;
128
+
129
+ if (!rbcm || !rbce || !rbcm->attached) {
130
+ return;
131
+ }
132
+
133
+ st_data_t key = (st_data_t)rbce;
134
+ st_delete(rbcm->attached, &key, NULL);
135
+ }
136
+
137
+ static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm) {
138
+ if (!rbcm || !rbcm->attached) {
139
+ return;
140
+ }
141
+
142
+ st_table *attached = rbcm->attached;
143
+ rbcm->attached = NULL;
144
+
145
+ st_foreach(attached, detach_easy_entry, (st_data_t)rbcm);
146
+
147
+ st_free_table(attached);
148
+
149
+ rbcm->active = 0;
150
+ rbcm->running = 0;
151
+ }
152
+
100
153
  void curl_multi_free(ruby_curl_multi *rbcm) {
101
- curl_multi_cleanup(rbcm->handle);
154
+ if (!rbcm) {
155
+ return;
156
+ }
157
+
158
+ rb_curl_multi_detach_all(rbcm);
159
+
160
+ if (rbcm->handle) {
161
+ curl_multi_cleanup(rbcm->handle);
162
+ rbcm->handle = NULL;
163
+ }
164
+
102
165
  free(rbcm);
103
166
  }
104
167
 
@@ -110,6 +173,18 @@ static void ruby_curl_multi_init(ruby_curl_multi *rbcm) {
110
173
 
111
174
  rbcm->active = 0;
112
175
  rbcm->running = 0;
176
+
177
+ if (rbcm->attached) {
178
+ st_free_table(rbcm->attached);
179
+ rbcm->attached = NULL;
180
+ }
181
+
182
+ rbcm->attached = st_init_numtable();
183
+ if (!rbcm->attached) {
184
+ curl_multi_cleanup(rbcm->handle);
185
+ rbcm->handle = NULL;
186
+ rb_raise(rb_eNoMemError, "Failed to allocate multi attachment table");
187
+ }
113
188
  }
114
189
 
115
190
  /*
@@ -124,6 +199,8 @@ VALUE ruby_curl_multi_new(VALUE klass) {
124
199
  rb_raise(rb_eNoMemError, "Failed to allocate memory for Curl::Multi");
125
200
  }
126
201
 
202
+ MEMZERO(rbcm, ruby_curl_multi, 1);
203
+
127
204
  ruby_curl_multi_init(rbcm);
128
205
 
129
206
  /*
@@ -131,8 +208,8 @@ VALUE ruby_curl_multi_new(VALUE klass) {
131
208
  * If your structure references other Ruby objects, then your mark function needs to
132
209
  * identify these objects using rb_gc_mark(value). If the structure doesn't reference
133
210
  * other Ruby objects, you can simply pass 0 as a function pointer.
134
- */
135
- return Data_Wrap_Struct(klass, 0, curl_multi_free, rbcm);
211
+ */
212
+ return Data_Wrap_Struct(klass, curl_multi_mark, curl_multi_free, rbcm);
136
213
  }
137
214
 
138
215
  /*
@@ -292,6 +369,17 @@ VALUE ruby_curl_multi_add(VALUE self, VALUE easy) {
292
369
  * If this number is not correct, the next call to curl_multi_perform will correct it. */
293
370
  rbcm->running++;
294
371
 
372
+ if (!rbcm->attached) {
373
+ rbcm->attached = st_init_numtable();
374
+ if (!rbcm->attached) {
375
+ curl_multi_remove_handle(rbcm->handle, rbce->curl);
376
+ ruby_curl_easy_cleanup(easy, rbce);
377
+ rb_raise(rb_eNoMemError, "Failed to allocate multi attachment table");
378
+ }
379
+ }
380
+
381
+ st_insert(rbcm->attached, (st_data_t)rbce, (st_data_t)easy);
382
+
295
383
  /* track a reference to associated multi handle */
296
384
  rbce->multi = self;
297
385
 
@@ -332,9 +420,13 @@ static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy) {
332
420
  raise_curl_multi_error_exception(result);
333
421
  }
334
422
 
335
- rbcm->active--;
423
+ if (rbcm->active > 0) {
424
+ rbcm->active--;
425
+ }
336
426
 
337
427
  ruby_curl_easy_cleanup( easy, rbce );
428
+
429
+ rb_curl_multi_forget_easy(rbcm, rbce);
338
430
  }
339
431
 
340
432
  // on_success, on_failure, on_complete
@@ -362,15 +454,56 @@ static void flush_stderr_if_any(ruby_curl_easy *rbce) {
362
454
  }
363
455
  }
364
456
 
457
+ /* Helper to locate the Ruby Easy VALUE from the attached table using the
458
+ * underlying CURL* handle when CURLINFO_PRIVATE is unavailable or stale. */
459
+ struct find_easy_ctx { CURL *handle; VALUE easy; };
460
+ static int find_easy_by_handle_i(st_data_t key, st_data_t val, st_data_t arg) {
461
+ ruby_curl_easy *rbce = (ruby_curl_easy *)key;
462
+ struct find_easy_ctx *ctx = (struct find_easy_ctx *)arg;
463
+ if (rbce && rbce->curl == ctx->handle) {
464
+ ctx->easy = (VALUE)val;
465
+ return ST_STOP;
466
+ }
467
+ return ST_CONTINUE;
468
+ }
469
+
470
+ static VALUE find_easy_by_handle(ruby_curl_multi *rbcm, CURL *easy_handle) {
471
+ if (!rbcm || !rbcm->attached) return Qnil;
472
+ struct find_easy_ctx ctx; ctx.handle = easy_handle; ctx.easy = Qnil;
473
+ st_foreach(rbcm->attached, find_easy_by_handle_i, (st_data_t)&ctx);
474
+ return ctx.easy;
475
+ }
476
+
365
477
  static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int result) {
366
478
  long response_code = -1;
367
- VALUE easy;
479
+ VALUE easy = Qnil;
368
480
  ruby_curl_easy *rbce = NULL;
369
481
  VALUE callargs;
482
+ ruby_curl_multi *rbcm = NULL;
370
483
 
371
- CURLcode ecode = curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, (char**)&easy);
484
+ Data_Get_Struct(self, ruby_curl_multi, rbcm);
372
485
 
373
- Data_Get_Struct(easy, ruby_curl_easy, rbce);
486
+ /* Try to recover the ruby_curl_easy pointer stored via CURLOPT_PRIVATE. */
487
+ CURLcode private_rc = curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, (char**)&rbce);
488
+ if (private_rc == CURLE_OK && rbce) {
489
+ easy = rbce->self;
490
+ }
491
+
492
+ /* If PRIVATE is unavailable or invalid, fall back to scanning attachments. */
493
+ if (NIL_P(easy) || !RB_TYPE_P(easy, T_DATA)) {
494
+ easy = find_easy_by_handle(rbcm, easy_handle);
495
+ if (!NIL_P(easy) && RB_TYPE_P(easy, T_DATA)) {
496
+ Data_Get_Struct(easy, ruby_curl_easy, rbce);
497
+ }
498
+ }
499
+
500
+ /* If we still cannot identify the easy handle, remove it and bail. */
501
+ if (NIL_P(easy) || !RB_TYPE_P(easy, T_DATA) || !rbce) {
502
+ if (rbcm && rbcm->handle && easy_handle) {
503
+ curl_multi_remove_handle(rbcm->handle, easy_handle);
504
+ }
505
+ return;
506
+ }
374
507
 
375
508
  rbce->last_result = result; /* save the last easy result code */
376
509
 
@@ -390,10 +523,6 @@ static void rb_curl_mutli_handle_complete(VALUE self, CURL *easy_handle, int res
390
523
  /* Flush again after removal to cover any last buffered data. */
391
524
  flush_stderr_if_any(rbce);
392
525
 
393
- if (ecode != 0) {
394
- raise_curl_easy_error_exception(ecode);
395
- }
396
-
397
526
  VALUE did_raise = rb_hash_new();
398
527
 
399
528
  if (!rb_easy_nil("complete_proc")) {
@@ -1158,11 +1287,32 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1158
1287
  VALUE ruby_curl_multi_close(VALUE self) {
1159
1288
  ruby_curl_multi *rbcm;
1160
1289
  Data_Get_Struct(self, ruby_curl_multi, rbcm);
1161
- curl_multi_cleanup(rbcm->handle);
1290
+ rb_curl_multi_detach_all(rbcm);
1291
+
1292
+ if (rbcm->handle) {
1293
+ curl_multi_cleanup(rbcm->handle);
1294
+ rbcm->handle = NULL;
1295
+ }
1296
+
1162
1297
  ruby_curl_multi_init(rbcm);
1163
1298
  return self;
1164
1299
  }
1165
1300
 
1301
+ /* GC mark: keep attached easy VALUEs alive while associated. */
1302
+ static int mark_attached_i(st_data_t key, st_data_t val, st_data_t arg) {
1303
+ VALUE easy = (VALUE)val;
1304
+ if (!NIL_P(easy)) rb_gc_mark(easy);
1305
+ return ST_CONTINUE;
1306
+ }
1307
+
1308
+ static void curl_multi_mark(void *ptr) {
1309
+ ruby_curl_multi *rbcm = (ruby_curl_multi *)ptr;
1310
+ if (!rbcm) return;
1311
+ if (rbcm->attached) {
1312
+ st_foreach(rbcm->attached, mark_attached_i, (st_data_t)0);
1313
+ }
1314
+ }
1315
+
1166
1316
 
1167
1317
  /* =================== INIT LIB =====================*/
1168
1318
  void init_curb_multi() {
data/ext/curb_multi.h CHANGED
@@ -8,18 +8,21 @@
8
8
  #define __CURB_MULTI_H
9
9
 
10
10
  #include "curb.h"
11
- #include "curb_easy.h"
12
11
  #include <curl/multi.h>
13
12
 
13
+ struct st_table;
14
+
14
15
  typedef struct {
15
16
  int active;
16
17
  int running;
17
18
  CURLM *handle;
19
+ struct st_table *attached;
18
20
  } ruby_curl_multi;
19
21
 
20
22
  extern VALUE cCurlMulti;
21
23
  void init_curb_multi();
22
24
  VALUE ruby_curl_multi_new(VALUE klass);
25
+ void rb_curl_multi_forget_easy(ruby_curl_multi *rbcm, void *rbce_ptr);
23
26
 
24
27
 
25
28
  #endif
data/ext/extconf.rb CHANGED
@@ -500,6 +500,7 @@ have_constant "curlusessl_try"
500
500
  have_constant "curlusessl_control"
501
501
  have_constant "curlusessl_all"
502
502
  have_constant "curlopt_resolve"
503
+ have_constant "curlopt_request_target"
503
504
  have_constant "curlopt_sslcert"
504
505
  have_constant "curlopt_sslcerttype"
505
506
  have_constant "curlopt_sslkey"
data/lib/curl/easy.rb CHANGED
@@ -158,6 +158,22 @@ module Curl
158
158
  set :proxy, url
159
159
  end
160
160
 
161
+ #
162
+ # call-seq:
163
+ # easy.request_target = string => string
164
+ #
165
+ # Set the request-target used in the HTTP request line (libcurl CURLOPT_REQUEST_TARGET).
166
+ # Useful for absolute-form request targets (e.g., when speaking to proxies) or
167
+ # special targets like "*" (OPTIONS *). Requires libcurl with CURLOPT_REQUEST_TARGET support.
168
+ #
169
+ def request_target=(value)
170
+ if Curl.const_defined?(:CURLOPT_REQUEST_TARGET)
171
+ set :request_target, value
172
+ else
173
+ raise NotImplementedError, "CURLOPT_REQUEST_TARGET is not supported by this libcurl"
174
+ end
175
+ end
176
+
161
177
  def ssl_verify_host=(value)
162
178
  value = 1 if value.class == TrueClass
163
179
  value = 0 if value.class == FalseClass
@@ -216,9 +232,18 @@ module Curl
216
232
  # call-seq:
217
233
  # easy.cookies = "name1=content1; name2=content2;" => string
218
234
  #
219
- # Set cookies to be sent by this Curl::Easy instance. The format of the string should
220
- # be NAME=CONTENTS, where NAME is the cookie name and CONTENTS is what the cookie should contain.
221
- # Set multiple cookies in one string like this: "name1=content1; name2=content2;" etc.
235
+ # Set the manual Cookie request header for this Curl::Easy instance.
236
+ # The format of the string should be NAME=CONTENTS, where NAME is the cookie name and
237
+ # CONTENTS is what the cookie should contain. Set multiple cookies in one string like this:
238
+ # "name1=content1; name2=content2;".
239
+ #
240
+ # Notes:
241
+ # - This only affects the outgoing Cookie header (libcurl CURLOPT_COOKIE) and does NOT
242
+ # alter the internal libcurl cookie engine (which stores cookies from Set-Cookie).
243
+ # - To change cookies stored in the engine, use {#cookielist} / {#cookielist=} or
244
+ # {#set} with :cookielist.
245
+ # - To clear a previously set manual Cookie header, assign an empty string ('').
246
+ # Assigning +nil+ has no effect in current curb versions.
222
247
  #
223
248
  def cookies=(value)
224
249
  set :cookie, value
@@ -233,6 +258,8 @@ module Curl
233
258
  # *Note* that you must set enable_cookies true to enable the cookie
234
259
  # engine, or this option will be ignored.
235
260
  #
261
+ # Note: assigning +nil+ has no effect; pass a path string to use a cookie file.
262
+ #
236
263
  def cookiefile=(value)
237
264
  set :cookiefile, value
238
265
  end
@@ -241,16 +268,40 @@ module Curl
241
268
  # call-seq:
242
269
  # easy.cookiejar = string => string
243
270
  #
244
- # Set a cookiejar file to use for this Curl::Easy instance.
245
- # Cookies from the response will be written into this file.
271
+ # Set a cookiejar file to use for this Curl::Easy instance. Cookies from the response
272
+ # will be written into this file.
246
273
  #
247
274
  # *Note* that you must set enable_cookies true to enable the cookie
248
275
  # engine, or this option will be ignored.
249
276
  #
277
+ # Note: assigning +nil+ has no effect; pass a path string to persist cookies to a file.
278
+ #
250
279
  def cookiejar=(value)
251
280
  set :cookiejar, value
252
281
  end
253
282
 
283
+ #
284
+ # call-seq:
285
+ # easy.cookielist = string => string
286
+ #
287
+ # Modify cookies in libcurl's internal cookie engine (CURLOPT_COOKIELIST).
288
+ # Accepts a Set-Cookie style string, one or more lines in Netscape cookie file format,
289
+ # or one of the special commands: "ALL" (clear), "SESS" (remove session cookies),
290
+ # "FLUSH" (write to jar), "RELOAD" (reload from file).
291
+ #
292
+ # Examples:
293
+ # easy.cookielist = "Set-Cookie: session=42; Domain=example.com; Path=/;"
294
+ # easy.cookielist = [
295
+ # ['.example.com', 'TRUE', '/', 'FALSE', 0, 'c1', 'v1'].join("\t"),
296
+ # ['.example.com', 'TRUE', '/', 'FALSE', 0, 'c2', 'v2'].join("\t"),
297
+ # ''
298
+ # ].join("\n")
299
+ # easy.cookielist = 'ALL' # clear all cookies in the engine
300
+ #
301
+ def cookielist=(value)
302
+ set :cookielist, value
303
+ end
304
+
254
305
  #
255
306
  # call-seq:
256
307
  # easy = Curl::Easy.new("url") do|c|
@@ -0,0 +1,41 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+
3
+ class TestCurbCurlEasyRequestTarget < Test::Unit::TestCase
4
+ include TestServerMethods
5
+
6
+ def setup
7
+ server_setup
8
+ end
9
+
10
+ def test_request_target_absolute_form
11
+ unless Curl.const_defined?(:CURLOPT_REQUEST_TARGET)
12
+ omit('libcurl lacks CURLOPT_REQUEST_TARGET support')
13
+ end
14
+
15
+ tmp = Tempfile.new('curb_test_request_target')
16
+ path = tmp.path
17
+ fd = IO.sysopen(path, 'w')
18
+ io = IO.new(fd, 'w')
19
+ io.sync = true
20
+
21
+ easy = Curl::Easy.new(TestServlet.url)
22
+ easy.verbose = true
23
+ easy.setopt(Curl::CURLOPT_STDERR, io)
24
+
25
+ # Force absolute-form request target, different from the URL host
26
+ easy.request_target = "http://localhost:#{TestServlet.port}#{TestServlet.path}"
27
+ easy.headers = { 'Host' => "example.com" }
28
+
29
+ easy.perform
30
+
31
+ io.flush
32
+ io.close
33
+ output = File.read(path)
34
+
35
+ assert_match(/GET\s+http:\/\/localhost:#{TestServlet.port}#{Regexp.escape(TestServlet.path)}\s+HTTP\/1\.1/, output)
36
+ assert_match(/Host:\s+example\.com/, output)
37
+ ensure
38
+ tmp.close! if defined?(tmp) && tmp
39
+ end
40
+ end
41
+
@@ -573,6 +573,35 @@ class TestCurbCurlMulti < Test::Unit::TestCase
573
573
  end
574
574
  end
575
575
 
576
+ # Regression test for issue #267 (2015): ensure that when reusing
577
+ # easy handles with constrained concurrency, the callback receives
578
+ # the correct URL for each completed request rather than repeating
579
+ # the first URL.
580
+ def test_multi_easy_http_urls_unique_across_max_connects
581
+ urls = [
582
+ { :url => TestServlet.url + '?q=1', :method => :get },
583
+ { :url => TestServlet.url + '?q=2', :method => :get },
584
+ { :url => TestServlet.url + '?q=3', :method => :get }
585
+ ]
586
+
587
+ [1, 2, 3].each do |max|
588
+ results = []
589
+ Curl::Multi.http(urls.dup, {:pipeline => true, :max_connects => max}) do |easy, code, method|
590
+ assert_equal 200, code
591
+ assert_equal :get, method
592
+ results << easy.last_effective_url
593
+ end
594
+
595
+ # Ensure we saw one completion per input URL
596
+ assert_equal urls.size, results.size, "expected #{urls.size} results with max_connects=#{max}"
597
+
598
+ # And that each URL completed exactly once (no accidental reuse/mis-reporting)
599
+ expected_urls = urls.map { |u| u[:url] }
600
+ assert_equal expected_urls.to_set, results.to_set, "unexpected URLs for max_connects=#{max}: #{results.inspect}"
601
+ assert_equal expected_urls.size, results.uniq.size, "duplicate URLs seen for max_connects=#{max}: #{results.inspect}"
602
+ end
603
+ end
604
+
576
605
  def test_multi_recieves_500
577
606
  m = Curl::Multi.new
578
607
  e = Curl::Easy.new("http://127.0.0.1:9129/methods")
@@ -0,0 +1,26 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+
3
+ class TestCurbFtpOptions < Test::Unit::TestCase
4
+ # Ensure FTP-related set(:option, ...) mappings are accepted and do not raise
5
+ # a TypeError (they used to be unsupported in setopt dispatch).
6
+ def test_can_set_ftp_listing_related_flags
7
+ c = Curl::Easy.new('ftp://example.com/')
8
+
9
+ assert_nothing_raised do
10
+ c.set(:dirlistonly, true) if Curl.const_defined?(:CURLOPT_DIRLISTONLY)
11
+ c.set(:ftp_use_epsv, 0) if Curl.const_defined?(:CURLOPT_FTP_USE_EPSV)
12
+ # These may not be present on all libcurl builds; guard by constant
13
+ c.set(:ftp_use_eprt, 0) if Curl.const_defined?(:CURLOPT_FTP_USE_EPRT)
14
+ c.set(:ftp_skip_pasv_ip, 1) if Curl.const_defined?(:CURLOPT_FTP_SKIP_PASV_IP)
15
+ end
16
+ end
17
+
18
+ # Setting ftp_commands remains supported for control-connection commands.
19
+ def test_can_assign_ftp_commands
20
+ c = Curl::Easy.new('ftp://example.com/')
21
+ c.ftp_commands = ["PWD", "CWD /"]
22
+ assert_kind_of(Array, c.ftp_commands)
23
+ assert_equal ["PWD", "CWD /"], c.ftp_commands
24
+ end
25
+ end
26
+
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('helper', __dir__)
4
+
5
+ class TestGcCompact < Test::Unit::TestCase
6
+ ITERATIONS = (ENV['CURB_GC_COMPACT_ITERATIONS'] || 5).to_i
7
+ EASY_PER_MULTI = 3
8
+
9
+ def setup
10
+ omit('GC.compact unavailable on this Ruby') unless defined?(GC.compact)
11
+ end
12
+
13
+ def test_multi_perform_with_gc_compact
14
+ ITERATIONS.times do
15
+ multi = Curl::Multi.new
16
+ add_easy_handles(multi)
17
+
18
+ compact
19
+ assert_nothing_raised { multi.perform }
20
+ compact
21
+ end
22
+ end
23
+
24
+ def test_gc_compact_during_multi_cleanup
25
+ ITERATIONS.times do
26
+ multi = Curl::Multi.new
27
+ add_easy_handles(multi)
28
+
29
+ compact
30
+ multi = nil
31
+ compact
32
+ end
33
+ end
34
+
35
+ def test_gc_compact_after_detach
36
+ multi = Curl::Multi.new
37
+ handles = add_easy_handles(multi)
38
+
39
+ compact
40
+ assert_nothing_raised { multi.perform }
41
+
42
+ handles.each { |easy| multi.remove(easy) }
43
+ compact
44
+ end
45
+
46
+ private
47
+
48
+ def add_easy_handles(multi)
49
+ Array.new(EASY_PER_MULTI) do
50
+ Curl::Easy.new($TEST_URL) do |easy|
51
+ easy.timeout = 5
52
+ easy.on_complete { |_e| }
53
+ easy.on_failure { |_e, _code| }
54
+ end.tap { |easy| multi.add(easy) }
55
+ end
56
+ end
57
+
58
+ def compact
59
+ GC.compact
60
+ end
61
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: curb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ross Bamford
8
8
  - Todd A. Fisher
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-31 00:00:00.000000000 Z
11
+ date: 2025-09-18 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Curb (probably CUrl-RuBy or something) provides Ruby-language bindings
14
14
  for the libcurl(3), a fully-featured client-side URL transfer library. cURL and
@@ -69,6 +69,7 @@ files:
69
69
  - tests/tc_curl_download.rb
70
70
  - tests/tc_curl_easy.rb
71
71
  - tests/tc_curl_easy_cookielist.rb
72
+ - tests/tc_curl_easy_request_target.rb
72
73
  - tests/tc_curl_easy_resolve.rb
73
74
  - tests/tc_curl_easy_setopt.rb
74
75
  - tests/tc_curl_maxfilesize.rb
@@ -76,6 +77,8 @@ files:
76
77
  - tests/tc_curl_postfield.rb
77
78
  - tests/tc_curl_protocols.rb
78
79
  - tests/tc_fiber_scheduler.rb
80
+ - tests/tc_ftp_options.rb
81
+ - tests/tc_gc_compact.rb
79
82
  - tests/test_basic.rb
80
83
  - tests/test_fiber_debug.rb
81
84
  - tests/test_fiber_simple.rb
@@ -135,6 +138,7 @@ test_files:
135
138
  - tests/tc_curl_download.rb
136
139
  - tests/tc_curl_easy.rb
137
140
  - tests/tc_curl_easy_cookielist.rb
141
+ - tests/tc_curl_easy_request_target.rb
138
142
  - tests/tc_curl_easy_resolve.rb
139
143
  - tests/tc_curl_easy_setopt.rb
140
144
  - tests/tc_curl_maxfilesize.rb
@@ -142,6 +146,8 @@ test_files:
142
146
  - tests/tc_curl_postfield.rb
143
147
  - tests/tc_curl_protocols.rb
144
148
  - tests/tc_fiber_scheduler.rb
149
+ - tests/tc_ftp_options.rb
150
+ - tests/tc_gc_compact.rb
145
151
  - tests/test_basic.rb
146
152
  - tests/test_fiber_debug.rb
147
153
  - tests/test_fiber_simple.rb