curb 1.3.5 → 1.3.6

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
@@ -11,16 +11,36 @@
11
11
  #include "curb_multi.h"
12
12
 
13
13
  #include <errno.h>
14
+ #include <stdlib.h>
14
15
  #include <string.h>
15
16
  #ifndef _WIN32
17
+ #include <sys/types.h>
18
+ #include <sys/socket.h>
19
+ #include <netinet/in.h>
20
+ #include <arpa/inet.h>
21
+ #else
22
+ #include <winsock2.h>
23
+ #include <ws2tcpip.h>
24
+ #endif
25
+ #ifndef _WIN32
16
26
  #include <strings.h>
17
27
  #endif
18
28
 
29
+ #if defined(HAVE_CURLOPT_OPENSOCKETFUNCTION) && defined(HAVE_CURLOPT_OPENSOCKETDATA)
30
+ #define CURB_HAVE_OPENSOCKET_NETWORK_POLICY 1
31
+ #endif
32
+
33
+ #if defined(HAVE_CURLOPT_PREREQFUNCTION) && defined(HAVE_CURLOPT_PREREQDATA)
34
+ #define CURB_HAVE_PREREQ_HOST_POLICY 1
35
+ #endif
36
+
19
37
  extern VALUE mCurl;
20
38
 
21
39
  static VALUE idCall;
22
40
  static VALUE idJoin;
23
41
  static VALUE rbstrAmp;
42
+ static ID idNetworkPolicyNone;
43
+ static ID idNetworkPolicyPublic;
24
44
 
25
45
  #ifdef RDOC_NEVER_DEFINED
26
46
  mCurl = rb_define_module("Curl");
@@ -44,9 +64,649 @@ static FILE * rb_io_stdio_file(rb_io_t *fptr) {
44
64
  #endif
45
65
  static struct curl_slist *duplicate_curl_slist(struct curl_slist *list);
46
66
  static size_t proc_data_handler(char *stream, size_t size, size_t nmemb, VALUE proc);
67
+ static int curb_array_includes_string(VALUE list, VALUE value);
47
68
 
48
69
  /* ================== CURL HANDLER FUNCS ==============*/
49
70
 
71
+ static int curb_ipv4_is_unsafe_destination(const unsigned char *ip) {
72
+ if (ip[0] == 0) return 1; /* 0.0.0.0/8 */
73
+ if (ip[0] == 10) return 1; /* RFC1918 */
74
+ if (ip[0] == 100 && (ip[1] & 0xc0) == 0x40) return 1; /* 100.64.0.0/10 */
75
+ if (ip[0] == 100 && ip[1] == 100 && ip[2] == 100 && ip[3] == 200) return 1;
76
+ if (ip[0] == 127) return 1; /* loopback */
77
+ if (ip[0] == 169 && ip[1] == 254) return 1; /* link-local / metadata */
78
+ if (ip[0] == 172 && ip[1] >= 16 && ip[1] <= 31) return 1;/* RFC1918 */
79
+ if (ip[0] == 192 && ip[1] == 168) return 1; /* RFC1918 */
80
+ if (ip[0] == 192 && ip[1] == 0 && ip[2] == 0) return 1; /* IETF protocol assignments */
81
+ if (ip[0] == 192 && ip[1] == 0 && ip[2] == 2) return 1; /* TEST-NET-1 */
82
+ if (ip[0] == 198 && (ip[1] == 18 || ip[1] == 19)) return 1;
83
+ if (ip[0] == 198 && ip[1] == 51 && ip[2] == 100) return 1;
84
+ if (ip[0] == 203 && ip[1] == 0 && ip[2] == 113) return 1;
85
+ if (ip[0] >= 224) return 1; /* multicast/reserved */
86
+
87
+ return 0;
88
+ }
89
+
90
+ static int curb_ip_prefix_matches(const unsigned char *ip, const unsigned char *prefix, int bits) {
91
+ int full_bytes = bits / 8;
92
+ int remaining_bits = bits % 8;
93
+ int i;
94
+
95
+ for (i = 0; i < full_bytes; i++) {
96
+ if (ip[i] != prefix[i]) return 0;
97
+ }
98
+
99
+ if (remaining_bits) {
100
+ unsigned char mask = (unsigned char)(0xff << (8 - remaining_bits));
101
+ if ((ip[full_bytes] & mask) != (prefix[full_bytes] & mask)) return 0;
102
+ }
103
+
104
+ return 1;
105
+ }
106
+
107
+ static int curb_ipv6_prefix_matches(const unsigned char *ip, const unsigned char *prefix, int bits) {
108
+ return curb_ip_prefix_matches(ip, prefix, bits);
109
+ }
110
+
111
+ static int curb_ipv6_is_all_zero(const unsigned char *ip) {
112
+ int i;
113
+
114
+ for (i = 0; i < 16; i++) {
115
+ if (ip[i] != 0) return 0;
116
+ }
117
+
118
+ return 1;
119
+ }
120
+
121
+ static int curb_ipv6_is_ipv4_mapped(const unsigned char *ip) {
122
+ static const unsigned char prefix[12] = {
123
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff
124
+ };
125
+
126
+ return curb_ipv6_prefix_matches(ip, prefix, 96);
127
+ }
128
+
129
+ static int curb_ipv6_is_ipv4_compatible(const unsigned char *ip) {
130
+ static const unsigned char prefix[12] = {
131
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
132
+ };
133
+
134
+ return curb_ipv6_prefix_matches(ip, prefix, 96);
135
+ }
136
+
137
+ static int curb_ipv6_is_nat64_well_known(const unsigned char *ip) {
138
+ static const unsigned char prefix[12] = {
139
+ 0x00, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0
140
+ };
141
+
142
+ return curb_ipv6_prefix_matches(ip, prefix, 96);
143
+ }
144
+
145
+ static int curb_ipv6_is_nat64_local_use(const unsigned char *ip) {
146
+ static const unsigned char prefix[6] = {
147
+ 0x00, 0x64, 0xff, 0x9b, 0x00, 0x01
148
+ };
149
+
150
+ return curb_ipv6_prefix_matches(ip, prefix, 48);
151
+ }
152
+
153
+ static int curb_ipv6_is_unsafe_destination(const unsigned char *ip) {
154
+ static const unsigned char documentation_prefix[4] = { 0x20, 0x01, 0x0d, 0xb8 };
155
+ static const unsigned char benchmarking_prefix[6] = { 0x20, 0x01, 0x00, 0x02, 0, 0 };
156
+
157
+ if (curb_ipv6_is_all_zero(ip)) return 1; /* ::/128 */
158
+ if (curb_ipv6_is_ipv4_mapped(ip)) return curb_ipv4_is_unsafe_destination(ip + 12);
159
+ if (curb_ipv6_is_nat64_well_known(ip)) return curb_ipv4_is_unsafe_destination(ip + 12);
160
+ if (curb_ipv6_is_nat64_local_use(ip)) return 1;
161
+ if (curb_ipv6_is_ipv4_compatible(ip)) return 1; /* deprecated non-public space */
162
+ if (ip[0] == 0 && ip[1] == 0 && ip[14] == 0 && ip[15] == 1) return 1;
163
+ if ((ip[0] & 0xfe) == 0xfc) return 1; /* fc00::/7 */
164
+ if (ip[0] == 0xfe && (ip[1] & 0xc0) == 0x80) return 1; /* fe80::/10 */
165
+ if (ip[0] == 0xfe && (ip[1] & 0xc0) == 0xc0) return 1; /* fec0::/10 */
166
+ if (ip[0] == 0xff) return 1; /* multicast */
167
+ if (curb_ipv6_prefix_matches(ip, documentation_prefix, 32)) return 1;
168
+ if (curb_ipv6_prefix_matches(ip, benchmarking_prefix, 48)) return 1;
169
+ if (ip[0] == 0x20 && ip[1] == 0x02) return 1; /* 2002::/16 */
170
+
171
+ return 0;
172
+ }
173
+
174
+ static void curb_clear_network_allowed_cidr_rules(ruby_curl_easy *rbce) {
175
+ if (!rbce || !rbce->network_allowed_cidr_rules) return;
176
+
177
+ xfree(rbce->network_allowed_cidr_rules);
178
+ rbce->network_allowed_cidr_rules = NULL;
179
+ rbce->network_allowed_cidr_rule_count = 0;
180
+ }
181
+
182
+ static void curb_raise_invalid_cidr(char *tmp, const char *cidr) {
183
+ if (tmp) xfree(tmp);
184
+ rb_raise(rb_eArgError, "invalid CIDR range: %s", cidr);
185
+ }
186
+
187
+ static void curb_parse_cidr_rule(const char *cidr, curb_cidr_rule *rule) {
188
+ size_t len;
189
+ char *tmp;
190
+ char *address;
191
+ char *prefix_str = NULL;
192
+ char *slash;
193
+ long prefix = -1;
194
+ int max_prefix = 0;
195
+ unsigned char parsed[16];
196
+
197
+ if (!cidr || !rule) {
198
+ rb_raise(rb_eArgError, "invalid CIDR range");
199
+ }
200
+
201
+ len = strlen(cidr);
202
+ if (len == 0) {
203
+ rb_raise(rb_eArgError, "invalid CIDR range: %s", cidr);
204
+ }
205
+
206
+ tmp = ALLOC_N(char, len + 1);
207
+ memcpy(tmp, cidr, len + 1);
208
+ address = tmp;
209
+
210
+ if (address[0] == '[') {
211
+ char *closing = strchr(address, ']');
212
+ if (!closing) curb_raise_invalid_cidr(tmp, cidr);
213
+
214
+ *closing = '\0';
215
+ address++;
216
+
217
+ if (closing[1] == '/') {
218
+ prefix_str = closing + 2;
219
+ } else if (closing[1] != '\0') {
220
+ curb_raise_invalid_cidr(tmp, cidr);
221
+ }
222
+ } else {
223
+ slash = strchr(address, '/');
224
+ if (slash) {
225
+ *slash = '\0';
226
+ prefix_str = slash + 1;
227
+ }
228
+ }
229
+
230
+ memset(rule, 0, sizeof(curb_cidr_rule));
231
+
232
+ if (inet_pton(AF_INET, address, parsed) == 1) {
233
+ rule->family = CURB_CIDR_FAMILY_IPV4;
234
+ max_prefix = 32;
235
+ memcpy(rule->address, parsed, 4);
236
+ } else if (inet_pton(AF_INET6, address, parsed) == 1) {
237
+ rule->family = CURB_CIDR_FAMILY_IPV6;
238
+ max_prefix = 128;
239
+ memcpy(rule->address, parsed, 16);
240
+ } else {
241
+ curb_raise_invalid_cidr(tmp, cidr);
242
+ }
243
+
244
+ if (prefix_str) {
245
+ char *endptr = NULL;
246
+
247
+ if (prefix_str[0] == '\0') curb_raise_invalid_cidr(tmp, cidr);
248
+
249
+ errno = 0;
250
+ prefix = strtol(prefix_str, &endptr, 10);
251
+ if (errno != 0 || !endptr || *endptr != '\0' || prefix < 0 || prefix > max_prefix) {
252
+ curb_raise_invalid_cidr(tmp, cidr);
253
+ }
254
+ } else {
255
+ prefix = max_prefix;
256
+ }
257
+
258
+ rule->prefix_bits = (unsigned char)prefix;
259
+ xfree(tmp);
260
+ }
261
+
262
+ static VALUE curb_normalize_cidr_list(VALUE cidrs) {
263
+ VALUE list;
264
+ VALUE normalized;
265
+ long i;
266
+
267
+ if (NIL_P(cidrs)) return Qnil;
268
+
269
+ list = rb_check_array_type(cidrs);
270
+ if (NIL_P(list)) {
271
+ list = rb_ary_new_from_args(1, cidrs);
272
+ }
273
+
274
+ normalized = rb_ary_new_capa(RARRAY_LEN(list));
275
+ for (i = 0; i < RARRAY_LEN(list); i++) {
276
+ VALUE item = rb_ary_entry(list, i);
277
+ VALUE cidr = rb_obj_as_string(item);
278
+ curb_cidr_rule unused_rule;
279
+
280
+ curb_parse_cidr_rule(StringValueCStr(cidr), &unused_rule);
281
+ if (!curb_array_includes_string(normalized, cidr)) {
282
+ rb_ary_push(normalized, rb_str_dup(cidr));
283
+ }
284
+ }
285
+
286
+ return normalized;
287
+ }
288
+
289
+ static VALUE curb_dup_string_array(VALUE list) {
290
+ VALUE copy;
291
+ long i;
292
+
293
+ if (NIL_P(list)) return Qnil;
294
+
295
+ copy = rb_ary_new_capa(RARRAY_LEN(list));
296
+ for (i = 0; i < RARRAY_LEN(list); i++) {
297
+ rb_ary_push(copy, rb_str_dup(rb_ary_entry(list, i)));
298
+ }
299
+
300
+ return copy;
301
+ }
302
+
303
+ static int curb_array_includes_string(VALUE list, VALUE value) {
304
+ long i;
305
+
306
+ for (i = 0; i < RARRAY_LEN(list); i++) {
307
+ if (rb_str_cmp(rb_ary_entry(list, i), value) == 0) return 1;
308
+ }
309
+
310
+ return 0;
311
+ }
312
+
313
+ static char curb_ascii_downcase(char c) {
314
+ if (c >= 'A' && c <= 'Z') return (char)(c - 'A' + 'a');
315
+ return c;
316
+ }
317
+
318
+ static char *curb_normalized_host_from_range(const char *start, const char *end, int raise_errors) {
319
+ char *host;
320
+ size_t len;
321
+ size_t i;
322
+
323
+ while (start < end && (*start == ' ' || *start == '\t' || *start == '\r' || *start == '\n')) start++;
324
+ while (end > start && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\r' || end[-1] == '\n')) end--;
325
+ while (end > start && end[-1] == '.') end--;
326
+
327
+ if (end <= start) {
328
+ if (!raise_errors) return NULL;
329
+ rb_raise(rb_eArgError, "allowed_hosts cannot include blank entries");
330
+ }
331
+
332
+ len = (size_t)(end - start);
333
+ host = ALLOC_N(char, len + 1);
334
+ for (i = 0; i < len; i++) {
335
+ host[i] = curb_ascii_downcase(start[i]);
336
+ }
337
+ host[len] = '\0';
338
+
339
+ return host;
340
+ }
341
+
342
+ static char *curb_normalize_host_value_impl(const char *value, int raise_errors) {
343
+ const char *start;
344
+ const char *end;
345
+ const char *scheme;
346
+ const char *authority_end;
347
+ const char *scan;
348
+ const char *last_at = NULL;
349
+ int colon_count = 0;
350
+
351
+ if (!value) {
352
+ if (!raise_errors) return NULL;
353
+ rb_raise(rb_eArgError, "allowed_hosts cannot include blank entries");
354
+ }
355
+
356
+ start = value;
357
+ end = value + strlen(value);
358
+ while (start < end && (*start == ' ' || *start == '\t' || *start == '\r' || *start == '\n')) start++;
359
+ while (end > start && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\r' || end[-1] == '\n')) end--;
360
+ if (end <= start) {
361
+ if (!raise_errors) return NULL;
362
+ rb_raise(rb_eArgError, "allowed_hosts cannot include blank entries");
363
+ }
364
+
365
+ scheme = strstr(start, "://");
366
+ if (scheme && scheme < end) {
367
+ start = scheme + 3;
368
+ }
369
+
370
+ authority_end = end;
371
+ for (scan = start; scan < end; scan++) {
372
+ if (*scan == '/' || *scan == '?' || *scan == '#') {
373
+ authority_end = scan;
374
+ break;
375
+ }
376
+ }
377
+
378
+ for (scan = start; scan < authority_end; scan++) {
379
+ if (*scan == '@') last_at = scan;
380
+ }
381
+ if (last_at) start = last_at + 1;
382
+
383
+ if (start < authority_end && *start == '[') {
384
+ const char *closing = start + 1;
385
+ while (closing < authority_end && *closing != ']') closing++;
386
+ if (closing >= authority_end) {
387
+ if (!raise_errors) return NULL;
388
+ rb_raise(rb_eArgError, "invalid allowed host: %s", value);
389
+ }
390
+ return curb_normalized_host_from_range(start + 1, closing, raise_errors);
391
+ }
392
+
393
+ for (scan = start; scan < authority_end; scan++) {
394
+ if (*scan == ':') colon_count++;
395
+ }
396
+
397
+ if (colon_count <= 1) {
398
+ for (scan = start; scan < authority_end; scan++) {
399
+ if (*scan == ':') {
400
+ authority_end = scan;
401
+ break;
402
+ }
403
+ }
404
+ }
405
+
406
+ return curb_normalized_host_from_range(start, authority_end, raise_errors);
407
+ }
408
+
409
+ static char *curb_normalize_host_value(const char *value) {
410
+ return curb_normalize_host_value_impl(value, 1);
411
+ }
412
+
413
+ static char *curb_try_normalize_host_value(const char *value) {
414
+ return curb_normalize_host_value_impl(value, 0);
415
+ }
416
+
417
+ static VALUE curb_normalize_host_list(VALUE hosts) {
418
+ VALUE list;
419
+ VALUE normalized;
420
+ long i;
421
+
422
+ if (NIL_P(hosts)) return Qnil;
423
+
424
+ list = rb_check_array_type(hosts);
425
+ if (NIL_P(list)) {
426
+ list = rb_ary_new_from_args(1, hosts);
427
+ }
428
+
429
+ normalized = rb_ary_new_capa(RARRAY_LEN(list));
430
+ for (i = 0; i < RARRAY_LEN(list); i++) {
431
+ VALUE item = rb_ary_entry(list, i);
432
+ VALUE host_value = rb_obj_as_string(item);
433
+ char *host = curb_normalize_host_value(StringValueCStr(host_value));
434
+ VALUE normalized_host = rb_str_new_cstr(host);
435
+
436
+ if (!curb_array_includes_string(normalized, normalized_host)) {
437
+ rb_ary_push(normalized, normalized_host);
438
+ }
439
+ xfree(host);
440
+ }
441
+
442
+ return normalized;
443
+ }
444
+
445
+ static void curb_clear_network_allowed_hosts(ruby_curl_easy *rbce) {
446
+ size_t i;
447
+
448
+ if (!rbce || !rbce->network_allowed_hosts) return;
449
+
450
+ for (i = 0; i < rbce->network_allowed_host_count; i++) {
451
+ if (rbce->network_allowed_hosts[i]) xfree(rbce->network_allowed_hosts[i]);
452
+ }
453
+ xfree(rbce->network_allowed_hosts);
454
+ rbce->network_allowed_hosts = NULL;
455
+ rbce->network_allowed_host_count = 0;
456
+ }
457
+
458
+ static char *curb_strdup_cstr(const char *value) {
459
+ size_t len = strlen(value);
460
+ char *copy = ALLOC_N(char, len + 1);
461
+ memcpy(copy, value, len + 1);
462
+ return copy;
463
+ }
464
+
465
+ static void curb_prepare_network_allowed_hosts(ruby_curl_easy *rbce) {
466
+ VALUE hosts;
467
+ long count;
468
+ long i;
469
+ char **rules;
470
+
471
+ if (!rbce) return;
472
+
473
+ curb_clear_network_allowed_hosts(rbce);
474
+
475
+ hosts = rb_hash_aref(rbce->opts, rb_easy_hkey("allowed_hosts"));
476
+ if (NIL_P(hosts)) return;
477
+
478
+ count = RARRAY_LEN(hosts);
479
+ if (count <= 0) return;
480
+
481
+ rules = ALLOC_N(char *, count);
482
+ for (i = 0; i < count; i++) {
483
+ VALUE host = rb_ary_entry(hosts, i);
484
+ rules[i] = curb_strdup_cstr(StringValueCStr(host));
485
+ }
486
+
487
+ rbce->network_allowed_hosts = rules;
488
+ rbce->network_allowed_host_count = (size_t)count;
489
+ }
490
+
491
+ static int curb_host_rules_match(const ruby_curl_easy *rbce, const char *host) {
492
+ size_t i;
493
+
494
+ if (!rbce || rbce->network_allowed_host_count == 0) return 1;
495
+
496
+ for (i = 0; i < rbce->network_allowed_host_count; i++) {
497
+ if (strcmp(host, rbce->network_allowed_hosts[i]) == 0) return 1;
498
+ }
499
+
500
+ return 0;
501
+ }
502
+
503
+ static void curb_prepare_network_allowed_cidr_rules(ruby_curl_easy *rbce) {
504
+ VALUE cidrs;
505
+ long count;
506
+ long i;
507
+ curb_cidr_rule *rules;
508
+
509
+ if (!rbce) return;
510
+
511
+ curb_clear_network_allowed_cidr_rules(rbce);
512
+
513
+ cidrs = rb_hash_aref(rbce->opts, rb_easy_hkey("allowed_cidrs"));
514
+ if (NIL_P(cidrs)) return;
515
+
516
+ count = RARRAY_LEN(cidrs);
517
+ if (count <= 0) return;
518
+
519
+ rules = ALLOC_N(curb_cidr_rule, count);
520
+ for (i = 0; i < count; i++) {
521
+ VALUE cidr = rb_ary_entry(cidrs, i);
522
+ curb_parse_cidr_rule(StringValueCStr(cidr), &rules[i]);
523
+ }
524
+
525
+ rbce->network_allowed_cidr_rules = rules;
526
+ rbce->network_allowed_cidr_rule_count = (size_t)count;
527
+ }
528
+
529
+ static int curb_cidr_rules_match(const ruby_curl_easy *rbce, unsigned char family, const unsigned char *ip) {
530
+ size_t i;
531
+
532
+ if (!rbce || rbce->network_allowed_cidr_rule_count == 0) return 1;
533
+
534
+ for (i = 0; i < rbce->network_allowed_cidr_rule_count; i++) {
535
+ const curb_cidr_rule *rule = &rbce->network_allowed_cidr_rules[i];
536
+ if (rule->family != family) continue;
537
+ if (curb_ip_prefix_matches(ip, rule->address, rule->prefix_bits)) return 1;
538
+ }
539
+
540
+ return 0;
541
+ }
542
+
543
+ static void curb_format_ipv4(char *buf, size_t len, const unsigned char *ip) {
544
+ snprintf(buf, len, "%u.%u.%u.%u",
545
+ (unsigned int)ip[0], (unsigned int)ip[1],
546
+ (unsigned int)ip[2], (unsigned int)ip[3]);
547
+ }
548
+
549
+ static void curb_format_ipv6(char *buf, size_t len, const unsigned char *ip) {
550
+ #ifdef AF_INET6
551
+ #ifdef _WIN32
552
+ if (inet_ntop(AF_INET6, (void *)ip, buf, len)) return;
553
+ #else
554
+ if (inet_ntop(AF_INET6, (const void *)ip, buf, (socklen_t)len)) return;
555
+ #endif
556
+ #endif
557
+
558
+ snprintf(buf, len,
559
+ "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
560
+ (unsigned int)ip[0], (unsigned int)ip[1],
561
+ (unsigned int)ip[2], (unsigned int)ip[3],
562
+ (unsigned int)ip[4], (unsigned int)ip[5],
563
+ (unsigned int)ip[6], (unsigned int)ip[7],
564
+ (unsigned int)ip[8], (unsigned int)ip[9],
565
+ (unsigned int)ip[10], (unsigned int)ip[11],
566
+ (unsigned int)ip[12], (unsigned int)ip[13],
567
+ (unsigned int)ip[14], (unsigned int)ip[15]);
568
+ }
569
+
570
+ static void curb_store_destination_error(ruby_curl_easy *rbce, const char *address, const char *reason) {
571
+ if (!rbce) return;
572
+
573
+ rbce->unsafe_destination_blocked = 1;
574
+
575
+ if (reason && reason[0]) {
576
+ snprintf(rbce->unsafe_destination_error, CURL_ERROR_SIZE,
577
+ "blocked destination address %s %s by public network policy", address, reason);
578
+ } else {
579
+ snprintf(rbce->unsafe_destination_error, CURL_ERROR_SIZE,
580
+ "blocked unsafe destination address %s by public network policy", address);
581
+ }
582
+
583
+ snprintf(rbce->err_buf, CURL_ERROR_SIZE, "%s", rbce->unsafe_destination_error);
584
+ }
585
+
586
+ static void curb_store_unsafe_destination_error(ruby_curl_easy *rbce, const char *address) {
587
+ curb_store_destination_error(rbce, address, NULL);
588
+ }
589
+
590
+ static void curb_store_host_allowlist_error(ruby_curl_easy *rbce, const char *host) {
591
+ if (!rbce) return;
592
+
593
+ rbce->unsafe_destination_blocked = 1;
594
+ snprintf(rbce->unsafe_destination_error, CURL_ERROR_SIZE,
595
+ "blocked URL host %s by safe mode host allowlist", host ? host : "unknown");
596
+ snprintf(rbce->err_buf, CURL_ERROR_SIZE, "%s", rbce->unsafe_destination_error);
597
+ }
598
+
599
+ #ifdef CURB_HAVE_PREREQ_HOST_POLICY
600
+ static int curb_host_allowlist_prereq(void *clientp,
601
+ char *conn_primary_ip,
602
+ char *conn_local_ip,
603
+ int conn_primary_port,
604
+ int conn_local_port) {
605
+ ruby_curl_easy *rbce = (ruby_curl_easy *)clientp;
606
+ char *effective_url = NULL;
607
+ char *host = NULL;
608
+ CURLcode rc;
609
+
610
+ (void)conn_primary_ip;
611
+ (void)conn_local_ip;
612
+ (void)conn_primary_port;
613
+ (void)conn_local_port;
614
+
615
+ if (!rbce || rbce->network_allowed_host_count == 0) {
616
+ return CURL_PREREQFUNC_OK;
617
+ }
618
+
619
+ rc = curl_easy_getinfo(rbce->curl, CURLINFO_EFFECTIVE_URL, &effective_url);
620
+ if (rc != CURLE_OK || !effective_url) {
621
+ curb_store_host_allowlist_error(rbce, "unknown");
622
+ return CURL_PREREQFUNC_ABORT;
623
+ }
624
+
625
+ host = curb_try_normalize_host_value(effective_url);
626
+ if (!host) {
627
+ curb_store_host_allowlist_error(rbce, "unknown");
628
+ return CURL_PREREQFUNC_ABORT;
629
+ }
630
+
631
+ if (!curb_host_rules_match(rbce, host)) {
632
+ curb_store_host_allowlist_error(rbce, host);
633
+ xfree(host);
634
+ return CURL_PREREQFUNC_ABORT;
635
+ }
636
+
637
+ xfree(host);
638
+ return CURL_PREREQFUNC_OK;
639
+ }
640
+ #endif
641
+
642
+ #ifdef CURB_HAVE_OPENSOCKET_NETWORK_POLICY
643
+ static curl_socket_t curb_public_network_opensocket(void *clientp, curlsocktype purpose, struct curl_sockaddr *address) {
644
+ ruby_curl_easy *rbce = (ruby_curl_easy *)clientp;
645
+ curl_socket_t sockfd;
646
+ char address_string[80];
647
+ int checked_destination_address = 0;
648
+
649
+ (void)purpose;
650
+
651
+ if (!address) {
652
+ curb_store_unsafe_destination_error(rbce, "unknown");
653
+ return CURL_SOCKET_BAD;
654
+ }
655
+
656
+ #ifdef AF_INET
657
+ if (address->family == AF_INET && address->addrlen >= sizeof(struct sockaddr_in)) {
658
+ const struct sockaddr_in *sin = (const struct sockaddr_in *)&address->addr;
659
+ const unsigned char *ip = (const unsigned char *)&sin->sin_addr;
660
+ checked_destination_address = 1;
661
+
662
+ if (curb_ipv4_is_unsafe_destination(ip)) {
663
+ curb_format_ipv4(address_string, sizeof(address_string), ip);
664
+ curb_store_unsafe_destination_error(rbce, address_string);
665
+ return CURL_SOCKET_BAD;
666
+ }
667
+
668
+ if (!curb_cidr_rules_match(rbce, CURB_CIDR_FAMILY_IPV4, ip)) {
669
+ curb_format_ipv4(address_string, sizeof(address_string), ip);
670
+ curb_store_destination_error(rbce, address_string, "outside allowed CIDR ranges");
671
+ return CURL_SOCKET_BAD;
672
+ }
673
+ }
674
+ #endif
675
+
676
+ #ifdef AF_INET6
677
+ if (address->family == AF_INET6 && address->addrlen >= sizeof(struct sockaddr_in6)) {
678
+ const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)&address->addr;
679
+ const unsigned char *ip = (const unsigned char *)&sin6->sin6_addr;
680
+ checked_destination_address = 1;
681
+
682
+ if (curb_ipv6_is_unsafe_destination(ip)) {
683
+ curb_format_ipv6(address_string, sizeof(address_string), ip);
684
+ curb_store_unsafe_destination_error(rbce, address_string);
685
+ return CURL_SOCKET_BAD;
686
+ }
687
+
688
+ if (!curb_cidr_rules_match(rbce, CURB_CIDR_FAMILY_IPV6, ip)) {
689
+ curb_format_ipv6(address_string, sizeof(address_string), ip);
690
+ curb_store_destination_error(rbce, address_string, "outside allowed CIDR ranges");
691
+ return CURL_SOCKET_BAD;
692
+ }
693
+ }
694
+ #endif
695
+
696
+ if (!checked_destination_address && rbce && rbce->network_allowed_cidr_rule_count > 0) {
697
+ curb_store_destination_error(rbce, "unknown", "outside allowed CIDR ranges");
698
+ return CURL_SOCKET_BAD;
699
+ }
700
+
701
+ sockfd = socket(address->family, address->socktype, address->protocol);
702
+ if (sockfd == CURL_SOCKET_BAD) {
703
+ return CURL_SOCKET_BAD;
704
+ }
705
+
706
+ return sockfd;
707
+ }
708
+ #endif
709
+
50
710
  static VALUE callback_exception(VALUE unused, VALUE exception) {
51
711
  return Qfalse;
52
712
  }
@@ -93,6 +753,20 @@ static VALUE with_easy_callback_active(ruby_curl_easy *rbce, VALUE (*func)(VALUE
93
753
  return rb_ensure(func, arg, ensure_clear_easy_callback_active, (VALUE)rbce);
94
754
  }
95
755
 
756
+ static void ruby_curl_easy_enter_native(ruby_curl_easy *rbce) {
757
+ if (rbce) {
758
+ rbce->native_active++;
759
+ }
760
+ }
761
+
762
+ static VALUE ruby_curl_easy_leave_native(VALUE arg) {
763
+ ruby_curl_easy *rbce = (ruby_curl_easy *)arg;
764
+ if (rbce && rbce->native_active > 0) {
765
+ rbce->native_active--;
766
+ }
767
+ return Qnil;
768
+ }
769
+
96
770
  struct stream_read_call_args {
97
771
  VALUE stream;
98
772
  size_t read_bytes;
@@ -170,6 +844,30 @@ static int curl_seek_fail_result(void) {
170
844
  #endif
171
845
  }
172
846
 
847
+ static int ruby_curl_easy_body_limit_exceeded(ruby_curl_easy *rbce, size_t total) {
848
+ VALUE max_body_bytes = rb_easy_get("max_body_bytes");
849
+ curl_off_t limit;
850
+
851
+ if (NIL_P(max_body_bytes)) {
852
+ return 0;
853
+ }
854
+
855
+ limit = (curl_off_t)NUM2LL(max_body_bytes);
856
+ if (limit <= 0) {
857
+ return 0;
858
+ }
859
+
860
+ if ((curl_off_t)total > limit - rbce->downloaded_body_bytes) {
861
+ if (NIL_P(rbce->callback_error)) {
862
+ rbce->callback_error = rb_exc_new_cstr(eCurlErrFileSizeExceeded, "Maximum body size exceeded");
863
+ }
864
+ return 1;
865
+ }
866
+
867
+ rbce->downloaded_body_bytes += (curl_off_t)total;
868
+ return 0;
869
+ }
870
+
173
871
  /* Default body handler appends to easy.body_data buffer */
174
872
  static size_t default_body_handler(char *stream,
175
873
  size_t size,
@@ -178,6 +876,11 @@ static size_t default_body_handler(char *stream,
178
876
  ruby_curl_easy *rbce = (ruby_curl_easy *)userdata;
179
877
  size_t total = size * nmemb;
180
878
  VALUE out = rb_easy_get("body_data");
879
+
880
+ if (ruby_curl_easy_body_limit_exceeded(rbce, total)) {
881
+ return 0;
882
+ }
883
+
181
884
  if (NIL_P(out)) {
182
885
  out = rb_easy_set("body_data", rb_str_buf_new(32768));
183
886
  }
@@ -346,6 +1049,10 @@ static size_t proc_data_handler_body(char *stream,
346
1049
  args.nmemb = nmemb;
347
1050
  args.proc = rb_easy_get("body_proc");
348
1051
 
1052
+ if (ruby_curl_easy_body_limit_exceeded(rbce, size * nmemb)) {
1053
+ return 0;
1054
+ }
1055
+
349
1056
  dispatch_args.rbce = rbce;
350
1057
  dispatch_args.func = call_proc_data_handler_wrapped;
351
1058
  dispatch_args.arg = (VALUE)&args;
@@ -534,6 +1241,27 @@ static void ruby_curl_easy_clear_resolve_list(ruby_curl_easy *rbce) {
534
1241
  rbce->curl_resolve = NULL;
535
1242
  }
536
1243
 
1244
+ static void ruby_curl_easy_clear_connect_to_list(ruby_curl_easy *rbce) {
1245
+ if (!rbce || !rbce->curl_connect_to) {
1246
+ return;
1247
+ }
1248
+ #ifdef HAVE_CURLOPT_CONNECT_TO
1249
+ if (rbce->curl) {
1250
+ curl_easy_setopt(rbce->curl, CURLOPT_CONNECT_TO, NULL);
1251
+ }
1252
+ #endif
1253
+ curl_slist_free_all(rbce->curl_connect_to);
1254
+ rbce->curl_connect_to = NULL;
1255
+ }
1256
+
1257
+ static void ruby_curl_easy_clear_setup_lists(ruby_curl_easy *rbce) {
1258
+ ruby_curl_easy_clear_headers_list(rbce);
1259
+ ruby_curl_easy_clear_proxy_headers_list(rbce);
1260
+ ruby_curl_easy_clear_ftp_commands_list(rbce);
1261
+ ruby_curl_easy_clear_resolve_list(rbce);
1262
+ ruby_curl_easy_clear_connect_to_list(rbce);
1263
+ }
1264
+
537
1265
  /* Legacy wrapper for external callers */
538
1266
  void ruby_curl_easy_mark(ruby_curl_easy *rbce) {
539
1267
  curl_easy_mark((void *)rbce);
@@ -590,6 +1318,9 @@ static void ruby_curl_easy_free(ruby_curl_easy *rbce) {
590
1318
  ruby_curl_easy_clear_proxy_headers_list(rbce);
591
1319
  ruby_curl_easy_clear_ftp_commands_list(rbce);
592
1320
  ruby_curl_easy_clear_resolve_list(rbce);
1321
+ ruby_curl_easy_clear_connect_to_list(rbce);
1322
+ curb_clear_network_allowed_cidr_rules(rbce);
1323
+ curb_clear_network_allowed_hosts(rbce);
593
1324
 
594
1325
  if (rbce->curl) {
595
1326
  /* disable any progress or debug events */
@@ -659,12 +1390,18 @@ static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
659
1390
  rbce->opts = rb_hash_new();
660
1391
 
661
1392
  memset(rbce->err_buf, 0, CURL_ERROR_SIZE);
1393
+ memset(rbce->unsafe_destination_error, 0, CURL_ERROR_SIZE);
662
1394
 
663
1395
  rbce->self = Qnil;
664
1396
  rbce->curl_headers = NULL;
665
1397
  rbce->curl_proxy_headers = NULL;
666
1398
  rbce->curl_ftp_commands = NULL;
667
1399
  rbce->curl_resolve = NULL;
1400
+ rbce->curl_connect_to = NULL;
1401
+ rbce->network_allowed_cidr_rules = NULL;
1402
+ rbce->network_allowed_hosts = NULL;
1403
+ rbce->network_allowed_cidr_rule_count = 0;
1404
+ rbce->network_allowed_host_count = 0;
668
1405
 
669
1406
  /* various-typed opts */
670
1407
  rbce->local_port = 0;
@@ -689,6 +1426,7 @@ static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
689
1426
  rbce->ftp_filemethod = -1;
690
1427
  rbce->http_version = CURL_HTTP_VERSION_NONE;
691
1428
  rbce->resolve_mode = CURL_IPRESOLVE_WHATEVER;
1429
+ rbce->network_policy = CURB_NETWORK_POLICY_NONE;
692
1430
 
693
1431
  /* bool opts */
694
1432
  rbce->proxy_tunnel = 0;
@@ -705,6 +1443,12 @@ static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
705
1443
  rbce->cookielist_engine_enabled = 0;
706
1444
  rbce->ignore_content_length = 0;
707
1445
  rbce->callback_active = 0;
1446
+ rbce->unsafe_destination_blocked = 0;
1447
+ rbce->allow_proxy = 0;
1448
+ rbce->allow_unix_socket = 0;
1449
+ rbce->forbid_reuse_set = 0;
1450
+ rbce->native_active = 0;
1451
+ rbce->forbid_reuse = 0;
708
1452
  rbce->callback_error = Qnil;
709
1453
  rbce->last_result = 0;
710
1454
  }
@@ -837,10 +1581,18 @@ static VALUE ruby_curl_easy_clone(VALUE self) {
837
1581
  newrbce->curl_proxy_headers = (rbce->curl_proxy_headers) ? duplicate_curl_slist(rbce->curl_proxy_headers) : NULL;
838
1582
  newrbce->curl_ftp_commands = (rbce->curl_ftp_commands) ? duplicate_curl_slist(rbce->curl_ftp_commands) : NULL;
839
1583
  newrbce->curl_resolve = (rbce->curl_resolve) ? duplicate_curl_slist(rbce->curl_resolve) : NULL;
1584
+ newrbce->curl_connect_to = (rbce->curl_connect_to) ? duplicate_curl_slist(rbce->curl_connect_to) : NULL;
1585
+ newrbce->network_allowed_cidr_rules = NULL;
1586
+ newrbce->network_allowed_cidr_rule_count = 0;
1587
+ newrbce->network_allowed_hosts = NULL;
1588
+ newrbce->network_allowed_host_count = 0;
840
1589
 
841
1590
  /* A cloned easy should not retain ownership reference to the original multi. */
842
1591
  newrbce->multi = Qnil;
843
1592
  newrbce->callback_error = Qnil;
1593
+ newrbce->unsafe_destination_blocked = 0;
1594
+ memset(newrbce->unsafe_destination_error, 0, CURL_ERROR_SIZE);
1595
+ newrbce->native_active = 0;
844
1596
 
845
1597
  if (rbce->opts != Qnil) {
846
1598
  newrbce->opts = rb_funcall(rbce->opts, rb_intern("dup"), 0);
@@ -889,6 +1641,10 @@ static VALUE ruby_curl_easy_close(VALUE self) {
889
1641
  rb_raise(rb_eRuntimeError, "Cannot close an active curl handle within a callback");
890
1642
  }
891
1643
 
1644
+ if (rbce->native_active) {
1645
+ rb_raise(rb_eRuntimeError, "Cannot close an active curl handle during native operation");
1646
+ }
1647
+
892
1648
  ruby_curl_easy_free(rbce);
893
1649
 
894
1650
  /* reinit the handle */
@@ -935,6 +1691,10 @@ static VALUE ruby_curl_easy_reset(VALUE self) {
935
1691
  rb_raise(rb_eRuntimeError, "Cannot close an active curl handle within a callback");
936
1692
  }
937
1693
 
1694
+ if (rbce->native_active) {
1695
+ rb_raise(rb_eRuntimeError, "Cannot reset an active curl handle during native operation");
1696
+ }
1697
+
938
1698
  opts_dup = rb_funcall(rbce->opts, rb_intern("dup"), 0);
939
1699
 
940
1700
  ruby_curl_easy_cleanup(self, rbce);
@@ -1281,7 +2041,17 @@ static VALUE ruby_curl_easy_useragent_get(VALUE self) {
1281
2041
  *
1282
2042
  * This is handy if you want to perform a POST against a Curl::Multi instance.
1283
2043
  */
1284
- static VALUE ruby_curl_easy_post_body_set_with_mode(VALUE self, VALUE post_body, int force_http_get_on_nil) {
2044
+ struct post_body_set_args {
2045
+ VALUE self;
2046
+ VALUE post_body;
2047
+ int force_http_get_on_nil;
2048
+ };
2049
+
2050
+ static VALUE ruby_curl_easy_post_body_set_with_mode_body(VALUE argp) {
2051
+ struct post_body_set_args *args = (struct post_body_set_args *)argp;
2052
+ VALUE self = args->self;
2053
+ VALUE post_body = args->post_body;
2054
+ int force_http_get_on_nil = args->force_http_get_on_nil;
1285
2055
  ruby_curl_easy *rbce;
1286
2056
  CURL *curl;
1287
2057
 
@@ -1345,6 +2115,17 @@ static VALUE ruby_curl_easy_post_body_set_with_mode(VALUE self, VALUE post_body,
1345
2115
  return Qnil;
1346
2116
  }
1347
2117
 
2118
+ static VALUE ruby_curl_easy_post_body_set_with_mode(VALUE self, VALUE post_body, int force_http_get_on_nil) {
2119
+ ruby_curl_easy *rbce;
2120
+ struct post_body_set_args args = { self, post_body, force_http_get_on_nil };
2121
+
2122
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2123
+ ruby_curl_easy_enter_native(rbce);
2124
+
2125
+ return rb_ensure(ruby_curl_easy_post_body_set_with_mode_body, (VALUE)&args,
2126
+ ruby_curl_easy_leave_native, (VALUE)rbce);
2127
+ }
2128
+
1348
2129
  static VALUE ruby_curl_easy_post_body_set(VALUE self, VALUE post_body) {
1349
2130
  return ruby_curl_easy_post_body_set_with_mode(self, post_body, 1);
1350
2131
  }
@@ -1438,76 +2219,217 @@ static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
1438
2219
  upload = ruby_curl_upload_new(cCurlUpload);
1439
2220
  ruby_curl_upload_stream_set(upload, upload_stream);
1440
2221
 
1441
- curl = rbce->curl;
1442
- rb_easy_set("upload", upload); /* keep the upload object alive as long as
1443
- the easy handle is active or until the upload
1444
- is complete or terminated... */
2222
+ curl = rbce->curl;
2223
+ rb_easy_set("upload", upload); /* keep the upload object alive as long as
2224
+ the easy handle is active or until the upload
2225
+ is complete or terminated... */
2226
+
2227
+ curl_easy_setopt(curl, CURLOPT_NOBODY, 0);
2228
+ curl_easy_setopt(curl, CURLOPT_POST, 0);
2229
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, NULL);
2230
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0);
2231
+ curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
2232
+ curl_easy_setopt(curl, CURLOPT_READFUNCTION, (curl_read_callback)read_data_handler);
2233
+ #ifdef HAVE_CURLOPT_SEEKFUNCTION
2234
+ curl_easy_setopt(curl, CURLOPT_SEEKFUNCTION, (curl_seek_callback)seek_data_handler);
2235
+ #endif
2236
+ curl_easy_setopt(curl, CURLOPT_READDATA, rbce);
2237
+ #ifdef HAVE_CURLOPT_SEEKDATA
2238
+ curl_easy_setopt(curl, CURLOPT_SEEKDATA, rbce);
2239
+ #endif
2240
+
2241
+ if (!NIL_P(infile_size)) {
2242
+ curl_easy_setopt(curl, CURLOPT_INFILESIZE, NUM2LONG(infile_size));
2243
+ }
2244
+
2245
+ // if we made it this far, all should be well.
2246
+ return data;
2247
+ }
2248
+
2249
+ /*
2250
+ * call-seq:
2251
+ * easy.ftp_commands = ["CWD /", "MKD directory"] => ["CWD /", ...]
2252
+ *
2253
+ * Explicitly sets the list of commands to execute on the FTP server when calling perform.
2254
+ *
2255
+ * NOTE:
2256
+ * - This maps to libcurl CURLOPT_QUOTE; it sends commands on the control connection.
2257
+ * - Do not include data-transfer commands like LIST/NLST/RETR/STOR here. libcurl does not
2258
+ * parse PASV/EPSV replies from QUOTE commands and will not establish the required data
2259
+ * connection. For directory listings, set CURLOPT_DIRLISTONLY (via `easy.set(:dirlistonly, true)`)
2260
+ * and request an FTP directory URL (e.g. "ftp://host/path/") so libcurl manages PASV/EPSV
2261
+ * and the data connection for you.
2262
+ */
2263
+ static VALUE ruby_curl_easy_ftp_commands_set(VALUE self, VALUE ftp_commands) {
2264
+ CURB_OBJECT_HSETTER(ruby_curl_easy, ftp_commands);
2265
+ }
2266
+
2267
+ /*
2268
+ * call-seq:
2269
+ * easy.ftp_commands => array or nil
2270
+ */
2271
+ static VALUE ruby_curl_easy_ftp_commands_get(VALUE self) {
2272
+ CURB_OBJECT_HGETTER(ruby_curl_easy, ftp_commands);
2273
+ }
2274
+
2275
+ /*
2276
+ * call-seq:
2277
+ * easy.resolve = [ "example.com:80:127.0.0.1" ] => [ "example.com:80:127.0.0.1" ]
2278
+ *
2279
+ * Set the resolve list to statically resolve hostnames to IP addresses,
2280
+ * bypassing DNS for matching hostname/port combinations.
2281
+ */
2282
+ static VALUE ruby_curl_easy_resolve_set(VALUE self, VALUE resolve) {
2283
+ CURB_OBJECT_HSETTER(ruby_curl_easy, resolve);
2284
+ }
2285
+
2286
+ /*
2287
+ * call-seq:
2288
+ * easy.resolve => array or nil
2289
+ */
2290
+ static VALUE ruby_curl_easy_resolve_get(VALUE self) {
2291
+ CURB_OBJECT_HGETTER(ruby_curl_easy, resolve);
2292
+ }
2293
+
2294
+ /*
2295
+ * call-seq:
2296
+ * easy.connect_to = [ "example.com:80:127.0.0.1:80" ] => [ "example.com:80:127.0.0.1:80" ]
2297
+ *
2298
+ * Set the connect-to list to redirect matching request host/port pairs to
2299
+ * alternate connection host/port pairs.
2300
+ */
2301
+ static VALUE ruby_curl_easy_connect_to_set(VALUE self, VALUE connect_to) {
2302
+ CURB_OBJECT_HSETTER(ruby_curl_easy, connect_to);
2303
+ }
2304
+
2305
+ /*
2306
+ * call-seq:
2307
+ * easy.connect_to => array or nil
2308
+ */
2309
+ static VALUE ruby_curl_easy_connect_to_get(VALUE self) {
2310
+ CURB_OBJECT_HGETTER(ruby_curl_easy, connect_to);
2311
+ }
2312
+
2313
+ /*
2314
+ * call-seq:
2315
+ * easy.doh_url = "https://dns.example/dns-query" => "https://dns.example/dns-query"
2316
+ *
2317
+ * Set the DNS-over-HTTPS URL to use for resolving hostnames.
2318
+ */
2319
+ static VALUE ruby_curl_easy_doh_url_set(VALUE self, VALUE doh_url) {
2320
+ CURB_OBJECT_HSETTER(ruby_curl_easy, doh_url);
2321
+ }
2322
+
2323
+ /*
2324
+ * call-seq:
2325
+ * easy.doh_url => string or nil
2326
+ */
2327
+ static VALUE ruby_curl_easy_doh_url_get(VALUE self) {
2328
+ CURB_OBJECT_HGETTER(ruby_curl_easy, doh_url);
2329
+ }
2330
+
2331
+ #ifdef HAVE_CURLOPT_DNS_SERVERS
2332
+ /*
2333
+ * call-seq:
2334
+ * easy.dns_servers => string or nil
2335
+ */
2336
+ static VALUE ruby_curl_easy_dns_servers_get(VALUE self) {
2337
+ CURB_OBJECT_HGETTER(ruby_curl_easy, dns_servers);
2338
+ }
2339
+ #endif
2340
+
2341
+ static VALUE ruby_curl_easy_allow_unix_socket_set(VALUE self, VALUE allow) {
2342
+ ruby_curl_easy *rbce;
2343
+
2344
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2345
+ rbce->allow_unix_socket = RTEST(allow) ? 1 : 0;
1445
2346
 
1446
- curl_easy_setopt(curl, CURLOPT_NOBODY, 0);
1447
- curl_easy_setopt(curl, CURLOPT_POST, 0);
1448
- curl_easy_setopt(curl, CURLOPT_POSTFIELDS, NULL);
1449
- curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0);
1450
- curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
1451
- curl_easy_setopt(curl, CURLOPT_READFUNCTION, (curl_read_callback)read_data_handler);
1452
- #ifdef HAVE_CURLOPT_SEEKFUNCTION
1453
- curl_easy_setopt(curl, CURLOPT_SEEKFUNCTION, (curl_seek_callback)seek_data_handler);
1454
- #endif
1455
- curl_easy_setopt(curl, CURLOPT_READDATA, rbce);
1456
- #ifdef HAVE_CURLOPT_SEEKDATA
1457
- curl_easy_setopt(curl, CURLOPT_SEEKDATA, rbce);
1458
- #endif
2347
+ return allow;
2348
+ }
1459
2349
 
1460
- if (!NIL_P(infile_size)) {
1461
- curl_easy_setopt(curl, CURLOPT_INFILESIZE, NUM2LONG(infile_size));
1462
- }
2350
+ static VALUE ruby_curl_easy_allow_proxy_set(VALUE self, VALUE allow) {
2351
+ ruby_curl_easy *rbce;
1463
2352
 
1464
- // if we made it this far, all should be well.
1465
- return data;
2353
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2354
+ rbce->allow_proxy = RTEST(allow) ? 1 : 0;
2355
+
2356
+ return allow;
1466
2357
  }
1467
2358
 
1468
2359
  /*
1469
2360
  * call-seq:
1470
- * easy.ftp_commands = ["CWD /", "MKD directory"] => ["CWD /", ...]
2361
+ * easy.allowed_cidrs = ["203.0.113.0/24"] => ["203.0.113.0/24"]
1471
2362
  *
1472
- * Explicitly sets the list of commands to execute on the FTP server when calling perform.
1473
- *
1474
- * NOTE:
1475
- * - This maps to libcurl CURLOPT_QUOTE; it sends commands on the control connection.
1476
- * - Do not include data-transfer commands like LIST/NLST/RETR/STOR here. libcurl does not
1477
- * parse PASV/EPSV replies from QUOTE commands and will not establish the required data
1478
- * connection. For directory listings, set CURLOPT_DIRLISTONLY (via `easy.set(:dirlistonly, true)`)
1479
- * and request an FTP directory URL (e.g. "ftp://host/path/") so libcurl manages PASV/EPSV
1480
- * and the data connection for you.
2363
+ * Set resolved-peer CIDR ranges that are allowed when network_policy is :public.
2364
+ * Private/local unsafe ranges are still blocked before this allowlist is
2365
+ * evaluated.
1481
2366
  */
1482
- static VALUE ruby_curl_easy_ftp_commands_set(VALUE self, VALUE ftp_commands) {
1483
- CURB_OBJECT_HSETTER(ruby_curl_easy, ftp_commands);
2367
+ static VALUE ruby_curl_easy_allowed_cidrs_set(VALUE self, VALUE cidrs) {
2368
+ ruby_curl_easy *rbce;
2369
+ VALUE normalized;
2370
+ VALUE stored;
2371
+
2372
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2373
+ normalized = curb_normalize_cidr_list(cidrs);
2374
+ stored = curb_dup_string_array(normalized);
2375
+ rb_hash_aset(rbce->opts, rb_easy_hkey("allowed_cidrs"), stored);
2376
+ if (rbce->network_policy == CURB_NETWORK_POLICY_PUBLIC) {
2377
+ curb_prepare_network_allowed_cidr_rules(rbce);
2378
+ } else {
2379
+ curb_clear_network_allowed_cidr_rules(rbce);
2380
+ }
2381
+
2382
+ return curb_dup_string_array(stored);
1484
2383
  }
1485
2384
 
1486
2385
  /*
1487
2386
  * call-seq:
1488
- * easy.ftp_commands => array or nil
2387
+ * easy.allowed_cidrs => array or nil
1489
2388
  */
1490
- static VALUE ruby_curl_easy_ftp_commands_get(VALUE self) {
1491
- CURB_OBJECT_HGETTER(ruby_curl_easy, ftp_commands);
2389
+ static VALUE ruby_curl_easy_allowed_cidrs_get(VALUE self) {
2390
+ ruby_curl_easy *rbce;
2391
+ VALUE cidrs;
2392
+
2393
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2394
+ cidrs = rb_hash_aref(rbce->opts, rb_easy_hkey("allowed_cidrs"));
2395
+
2396
+ return curb_dup_string_array(cidrs);
1492
2397
  }
1493
2398
 
1494
2399
  /*
1495
2400
  * call-seq:
1496
- * easy.resolve = [ "example.com:80:127.0.0.1" ] => [ "example.com:80:127.0.0.1" ]
2401
+ * easy.allowed_hosts = ["api.example.com"] => ["api.example.com"]
1497
2402
  *
1498
- * Set the resolve list to statically resolve hostnames to IP addresses,
1499
- * bypassing DNS for matching hostname/port combinations.
2403
+ * Set URL hosts allowed for this handle. When libcurl supports
2404
+ * CURLOPT_PREREQFUNCTION, this is checked before each request, including
2405
+ * followed redirects.
1500
2406
  */
1501
- static VALUE ruby_curl_easy_resolve_set(VALUE self, VALUE resolve) {
1502
- CURB_OBJECT_HSETTER(ruby_curl_easy, resolve);
2407
+ static VALUE ruby_curl_easy_allowed_hosts_set(VALUE self, VALUE hosts) {
2408
+ ruby_curl_easy *rbce;
2409
+ VALUE normalized;
2410
+ VALUE stored;
2411
+
2412
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2413
+ normalized = curb_normalize_host_list(hosts);
2414
+ stored = curb_dup_string_array(normalized);
2415
+ rb_hash_aset(rbce->opts, rb_easy_hkey("allowed_hosts"), stored);
2416
+ curb_prepare_network_allowed_hosts(rbce);
2417
+
2418
+ return curb_dup_string_array(stored);
1503
2419
  }
1504
2420
 
1505
2421
  /*
1506
2422
  * call-seq:
1507
- * easy.resolve => array or nil
2423
+ * easy.allowed_hosts => array or nil
1508
2424
  */
1509
- static VALUE ruby_curl_easy_resolve_get(VALUE self) {
1510
- CURB_OBJECT_HGETTER(ruby_curl_easy, resolve);
2425
+ static VALUE ruby_curl_easy_allowed_hosts_get(VALUE self) {
2426
+ ruby_curl_easy *rbce;
2427
+ VALUE hosts;
2428
+
2429
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
2430
+ hosts = rb_hash_aref(rbce->opts, rb_easy_hkey("allowed_hosts"));
2431
+
2432
+ return curb_dup_string_array(hosts);
1511
2433
  }
1512
2434
 
1513
2435
  /* ================== IMMED ATTRS ==================*/
@@ -2494,6 +3416,101 @@ static VALUE ruby_curl_easy_resolve_mode_set(VALUE self, VALUE resolve_mode) {
2494
3416
  }
2495
3417
  }
2496
3418
 
3419
+ /*
3420
+ * call-seq:
3421
+ * easy.network_policy => symbol
3422
+ *
3423
+ * Determine which native network policy will be enforced when libcurl opens
3424
+ * sockets for this handle.
3425
+ */
3426
+ static VALUE ruby_curl_easy_network_policy_get(VALUE self) {
3427
+ ruby_curl_easy *rbce;
3428
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3429
+
3430
+ switch (rbce->network_policy) {
3431
+ case CURB_NETWORK_POLICY_PUBLIC:
3432
+ return ID2SYM(idNetworkPolicyPublic);
3433
+ case CURB_NETWORK_POLICY_NONE:
3434
+ default:
3435
+ return ID2SYM(idNetworkPolicyNone);
3436
+ }
3437
+ }
3438
+
3439
+ /*
3440
+ * call-seq:
3441
+ * easy.network_policy = symbol => symbol
3442
+ *
3443
+ * Supported values:
3444
+ * [:none] disables native destination checks.
3445
+ * [:public] blocks loopback, private, link-local, multicast,
3446
+ * unspecified, metadata, and other non-public addresses.
3447
+ */
3448
+ static VALUE ruby_curl_easy_network_policy_set(VALUE self, VALUE network_policy) {
3449
+ ruby_curl_easy *rbce;
3450
+ ID network_policy_id;
3451
+
3452
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3453
+
3454
+ if (NIL_P(network_policy)) {
3455
+ rbce->network_policy = CURB_NETWORK_POLICY_NONE;
3456
+ return ID2SYM(idNetworkPolicyNone);
3457
+ }
3458
+
3459
+ if (TYPE(network_policy) != T_SYMBOL) {
3460
+ rb_raise(rb_eTypeError, "network_policy must be a Symbol");
3461
+ }
3462
+
3463
+ network_policy_id = rb_to_id(network_policy);
3464
+
3465
+ if (network_policy_id == idNetworkPolicyNone) {
3466
+ rbce->network_policy = CURB_NETWORK_POLICY_NONE;
3467
+ rbce->allow_proxy = 0;
3468
+ return network_policy;
3469
+ } else if (network_policy_id == idNetworkPolicyPublic) {
3470
+ #ifndef CURB_HAVE_OPENSOCKET_NETWORK_POLICY
3471
+ rb_raise(rb_eNotImpError, "network_policy=:public requires CURLOPT_OPENSOCKETFUNCTION support");
3472
+ #else
3473
+ rbce->network_policy = CURB_NETWORK_POLICY_PUBLIC;
3474
+ rbce->allow_proxy = 0;
3475
+ return network_policy;
3476
+ #endif
3477
+ }
3478
+
3479
+ rb_raise(rb_eArgError, "network_policy must be one of :none, :public");
3480
+ }
3481
+
3482
+ /*
3483
+ * call-seq:
3484
+ * easy.unsafe_destination_error => string or nil
3485
+ *
3486
+ * Return the native network policy block reason from the most recent transfer.
3487
+ */
3488
+ static VALUE ruby_curl_easy_unsafe_destination_error_get(VALUE self) {
3489
+ ruby_curl_easy *rbce;
3490
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3491
+
3492
+ if (rbce->unsafe_destination_blocked && rbce->unsafe_destination_error[0]) {
3493
+ return rb_str_new2(rbce->unsafe_destination_error);
3494
+ }
3495
+
3496
+ return Qnil;
3497
+ }
3498
+
3499
+ #ifdef HAVE_CURLOPT_UNIX_SOCKET_PATH
3500
+ /*
3501
+ * call-seq:
3502
+ * easy.unix_socket_path => string or nil
3503
+ *
3504
+ * Return the configured Unix socket path, if set through CURLOPT_UNIX_SOCKET_PATH.
3505
+ */
3506
+ static VALUE ruby_curl_easy_unix_socket_path_get(VALUE self) {
3507
+ ruby_curl_easy *rbce;
3508
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
3509
+
3510
+ return rb_easy_get("unix_socket_path");
3511
+ }
3512
+ #endif
3513
+
2497
3514
  /*
2498
3515
  * call-seq:
2499
3516
  * easy.http_version = Curl::HTTP_1_1 => Curl::HTTP_1_1
@@ -2802,13 +3819,32 @@ static VALUE cb_each_resolve(VALUE resolve, VALUE wrap, int _c, const VALUE *_pt
2802
3819
  return resolve_string;
2803
3820
  }
2804
3821
 
3822
+ /***********************************************
3823
+ * This is an rb_iterate callback used to set up the connect-to list.
3824
+ */
3825
+ static VALUE cb_each_connect_to(VALUE connect_to, VALUE wrap, int _c, const VALUE *_ptr, VALUE unused) {
3826
+ struct curl_slist **list;
3827
+ VALUE connect_to_string;
3828
+ TypedData_Get_Struct(wrap, struct curl_slist *, &curl_slist_ptr_type, list);
3829
+
3830
+ connect_to_string = rb_obj_as_string(connect_to);
3831
+ struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(connect_to_string));
3832
+ if (!new_list) {
3833
+ rb_raise(rb_eNoMemError, "Failed to append to connect-to list");
3834
+ }
3835
+ *list = new_list;
3836
+
3837
+ return connect_to_string;
3838
+ }
3839
+
2805
3840
  /***********************************************
2806
3841
  *
2807
3842
  * Setup a connection
2808
3843
  *
2809
3844
  * Always returns Qtrue, rb_raise on error.
2810
3845
  */
2811
- VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
3846
+ static VALUE ruby_curl_easy_setup_body(VALUE arg) {
3847
+ ruby_curl_easy *rbce = (ruby_curl_easy *)arg;
2812
3848
  // TODO this could do with a bit of refactoring...
2813
3849
  CURL *curl;
2814
3850
  VALUE url, _url = rb_easy_get("url");
@@ -2816,9 +3852,14 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2816
3852
  struct curl_slist **phdrs = &(rbce->curl_proxy_headers);
2817
3853
  struct curl_slist **cmds = &(rbce->curl_ftp_commands);
2818
3854
  struct curl_slist **rslv = &(rbce->curl_resolve);
3855
+ struct curl_slist **cnto = &(rbce->curl_connect_to);
3856
+ int public_policy_disables_proxy;
2819
3857
 
2820
3858
  curl = rbce->curl;
3859
+ public_policy_disables_proxy = rbce->network_policy == CURB_NETWORK_POLICY_PUBLIC && !rbce->allow_proxy;
2821
3860
  rbce->callback_error = Qnil;
3861
+ rbce->unsafe_destination_blocked = 0;
3862
+ memset(rbce->unsafe_destination_error, 0, CURL_ERROR_SIZE);
2822
3863
 
2823
3864
  if (_url == Qnil) {
2824
3865
  rb_raise(eCurlErrError, "No URL supplied");
@@ -2827,6 +3868,60 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2827
3868
  url = rb_check_string_type(_url);
2828
3869
  curl_easy_setopt(curl, CURLOPT_URL, StringValuePtr(url));
2829
3870
 
3871
+ #ifdef HAVE_CURLOPT_DOH_URL
3872
+ curl_easy_setopt(curl, CURLOPT_DOH_URL, rb_easy_nil("doh_url") ? NULL : rb_easy_get_str("doh_url"));
3873
+ #endif
3874
+
3875
+ #ifdef HAVE_CURLOPT_DNS_SERVERS
3876
+ if (rbce->network_policy == CURB_NETWORK_POLICY_PUBLIC && !rb_easy_nil("dns_servers")) {
3877
+ rb_raise(eCurlErrUnsafeDestination, "DNS server overrides are disabled by public network policy");
3878
+ }
3879
+ curl_easy_setopt(curl, CURLOPT_DNS_SERVERS, rb_easy_nil("dns_servers") ? NULL : rb_easy_get_str("dns_servers"));
3880
+ #endif
3881
+
3882
+ #ifdef CURB_HAVE_PREREQ_HOST_POLICY
3883
+ if (!rb_easy_nil("allowed_hosts")) {
3884
+ curb_prepare_network_allowed_hosts(rbce);
3885
+ curl_easy_setopt(curl, CURLOPT_PREREQFUNCTION, curb_host_allowlist_prereq);
3886
+ curl_easy_setopt(curl, CURLOPT_PREREQDATA, rbce);
3887
+ } else {
3888
+ curb_clear_network_allowed_hosts(rbce);
3889
+ curl_easy_setopt(curl, CURLOPT_PREREQFUNCTION, NULL);
3890
+ curl_easy_setopt(curl, CURLOPT_PREREQDATA, NULL);
3891
+ }
3892
+ #else
3893
+ curb_clear_network_allowed_hosts(rbce);
3894
+ #endif
3895
+
3896
+ #ifdef CURB_HAVE_OPENSOCKET_NETWORK_POLICY
3897
+ if (rbce->network_policy == CURB_NETWORK_POLICY_PUBLIC) {
3898
+ curb_prepare_network_allowed_cidr_rules(rbce);
3899
+ curl_easy_setopt(curl, CURLOPT_OPENSOCKETFUNCTION, curb_public_network_opensocket);
3900
+ curl_easy_setopt(curl, CURLOPT_OPENSOCKETDATA, rbce);
3901
+ #ifdef HAVE_CURLOPT_FRESH_CONNECT
3902
+ curl_easy_setopt(curl, CURLOPT_FRESH_CONNECT, 1L);
3903
+ #endif
3904
+ #ifdef HAVE_CURLOPT_FORBID_REUSE
3905
+ curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, 1L);
3906
+ #endif
3907
+ #ifdef HAVE_CURLOPT_UNIX_SOCKET_PATH
3908
+ if (!rbce->allow_unix_socket) {
3909
+ curl_easy_setopt(curl, CURLOPT_UNIX_SOCKET_PATH, NULL);
3910
+ }
3911
+ #endif
3912
+ } else {
3913
+ curb_clear_network_allowed_cidr_rules(rbce);
3914
+ curl_easy_setopt(curl, CURLOPT_OPENSOCKETFUNCTION, NULL);
3915
+ curl_easy_setopt(curl, CURLOPT_OPENSOCKETDATA, NULL);
3916
+ #ifdef HAVE_CURLOPT_FRESH_CONNECT
3917
+ curl_easy_setopt(curl, CURLOPT_FRESH_CONNECT, 0L);
3918
+ #endif
3919
+ #ifdef HAVE_CURLOPT_FORBID_REUSE
3920
+ curl_easy_setopt(curl, CURLOPT_FORBID_REUSE, rbce->forbid_reuse_set ? rbce->forbid_reuse : 0L);
3921
+ #endif
3922
+ }
3923
+ #endif
3924
+
2830
3925
  // network stuff and auth
2831
3926
  if (!rb_easy_nil("interface_hm")) {
2832
3927
  curl_easy_setopt(curl, CURLOPT_INTERFACE, rb_easy_get_str("interface_hm"));
@@ -2858,13 +3953,17 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2858
3953
  curl_easy_setopt(curl, CURLOPT_USERPWD, NULL);
2859
3954
  }
2860
3955
 
2861
- if (rb_easy_nil("proxy_url")) {
3956
+ if (public_policy_disables_proxy) {
3957
+ curl_easy_setopt(curl, CURLOPT_PROXY, "");
3958
+ } else if (rb_easy_nil("proxy_url")) {
2862
3959
  curl_easy_setopt(curl, CURLOPT_PROXY, NULL);
2863
3960
  } else {
2864
3961
  curl_easy_setopt(curl, CURLOPT_PROXY, rb_easy_get_str("proxy_url"));
2865
3962
  }
2866
3963
 
2867
- if (rb_easy_nil("proxypwd")) {
3964
+ if (public_policy_disables_proxy) {
3965
+ curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, NULL);
3966
+ } else if (rb_easy_nil("proxypwd")) {
2868
3967
  curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, NULL);
2869
3968
  } else {
2870
3969
  curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, rb_easy_get_str("proxypwd"));
@@ -2879,6 +3978,8 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2879
3978
  #endif
2880
3979
 
2881
3980
  // body/header procs
3981
+ rbce->downloaded_body_bytes = 0;
3982
+
2882
3983
  if (!rb_easy_nil("body_proc")) {
2883
3984
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)&proc_data_handler_body);
2884
3985
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, rbce);
@@ -2949,7 +4050,8 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2949
4050
  curl_easy_setopt(curl, CURLOPT_MAXREDIRS, rbce->max_redirs);
2950
4051
  #endif
2951
4052
 
2952
- curl_easy_setopt(curl, CURLOPT_HTTPPROXYTUNNEL, rbce->proxy_tunnel);
4053
+ curl_easy_setopt(curl, CURLOPT_HTTPPROXYTUNNEL,
4054
+ public_policy_disables_proxy ? 0L : rbce->proxy_tunnel);
2953
4055
  curl_easy_setopt(curl, CURLOPT_FILETIME, rbce->fetch_file_time);
2954
4056
  curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, rbce->ssl_verify_peer);
2955
4057
  curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, rbce->ssl_verify_host);
@@ -3112,6 +4214,11 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
3112
4214
  curl_easy_setopt(curl, CURLOPT_USERAGENT, rb_easy_get_str("useragent"));
3113
4215
  }
3114
4216
 
4217
+ /* Setup can be rerun for safety-policy changes while a handle is attached.
4218
+ * Rebuild slist-backed options from Ruby opts instead of appending to stale
4219
+ * native lists. */
4220
+ ruby_curl_easy_clear_setup_lists(rbce);
4221
+
3115
4222
  /* Setup HTTP headers if necessary */
3116
4223
  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, NULL); // XXX: maybe we shouldn't be clearing this?
3117
4224
 
@@ -3189,8 +4296,44 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
3189
4296
  }
3190
4297
  #endif
3191
4298
 
4299
+ #ifdef HAVE_CURLOPT_CONNECT_TO
4300
+ /* Setup connect-to list if necessary */
4301
+ if (!rb_easy_nil("connect_to")) {
4302
+ if (rb_easy_type_check("connect_to", T_ARRAY)) {
4303
+ VALUE wrap = TypedData_Wrap_Struct(rb_cObject, &curl_slist_ptr_type, cnto);
4304
+ rb_block_call(rb_easy_get("connect_to"), rb_intern("each"), 0, NULL, cb_each_connect_to, wrap);
4305
+ } else {
4306
+ VALUE connect_to_str = rb_obj_as_string(rb_easy_get("connect_to"));
4307
+ struct curl_slist *new_list = curl_slist_append(*cnto, StringValuePtr(connect_to_str));
4308
+ if (!new_list) {
4309
+ rb_raise(rb_eNoMemError, "Failed to append to connect-to list");
4310
+ }
4311
+ *cnto = new_list;
4312
+ }
4313
+
4314
+ if (*cnto) {
4315
+ curl_easy_setopt(curl, CURLOPT_CONNECT_TO, *cnto);
4316
+ }
4317
+ }
4318
+ #endif
4319
+
3192
4320
  return Qnil;
3193
4321
  }
4322
+
4323
+ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
4324
+ ruby_curl_easy_enter_native(rbce);
4325
+ return rb_ensure(ruby_curl_easy_setup_body, (VALUE)rbce,
4326
+ ruby_curl_easy_leave_native, (VALUE)rbce);
4327
+ }
4328
+
4329
+ static VALUE ruby_curl_easy_setup_self(VALUE self) {
4330
+ ruby_curl_easy *rbce;
4331
+
4332
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
4333
+ ruby_curl_easy_setup(rbce);
4334
+
4335
+ return self;
4336
+ }
3194
4337
  /***********************************************
3195
4338
  *
3196
4339
  * Clean up a connection
@@ -3202,6 +4345,7 @@ VALUE ruby_curl_easy_cleanup( VALUE self, ruby_curl_easy *rbce ) {
3202
4345
  CURL *curl = rbce->curl;
3203
4346
  struct curl_slist *ftp_commands;
3204
4347
  struct curl_slist *resolve;
4348
+ struct curl_slist *connect_to;
3205
4349
 
3206
4350
  ruby_curl_easy_clear_headers_list(rbce);
3207
4351
  ruby_curl_easy_clear_proxy_headers_list(rbce);
@@ -3216,6 +4360,14 @@ VALUE ruby_curl_easy_cleanup( VALUE self, ruby_curl_easy *rbce ) {
3216
4360
  ruby_curl_easy_clear_resolve_list(rbce);
3217
4361
  }
3218
4362
 
4363
+ connect_to = rbce->curl_connect_to;
4364
+ if (connect_to) {
4365
+ ruby_curl_easy_clear_connect_to_list(rbce);
4366
+ }
4367
+
4368
+ curb_clear_network_allowed_cidr_rules(rbce);
4369
+ curb_clear_network_allowed_hosts(rbce);
4370
+
3219
4371
  /* clean up a PUT request's curl options. */
3220
4372
  if (!rb_easy_nil("upload")) {
3221
4373
  rb_easy_del("upload"); // set the upload object to Qnil to let the GC clean up
@@ -3350,6 +4502,23 @@ struct easy_form_perform_args {
3350
4502
  int form_set_on_curl;
3351
4503
  };
3352
4504
 
4505
+ struct easy_join_args {
4506
+ VALUE args_ary;
4507
+ };
4508
+
4509
+ static VALUE join_easy_arguments_body(VALUE argp) {
4510
+ struct easy_join_args *args = (struct easy_join_args *)argp;
4511
+ return rb_funcall(args->args_ary, idJoin, 1, rbstrAmp);
4512
+ }
4513
+
4514
+ static VALUE join_easy_arguments(ruby_curl_easy *rbce, VALUE args_ary) {
4515
+ struct easy_join_args args = { args_ary };
4516
+
4517
+ ruby_curl_easy_enter_native(rbce);
4518
+ return rb_ensure(join_easy_arguments_body, (VALUE)&args,
4519
+ ruby_curl_easy_leave_native, (VALUE)rbce);
4520
+ }
4521
+
3353
4522
  static void append_multipart_form_argument(VALUE arg,
3354
4523
  struct curl_httppost **first,
3355
4524
  struct curl_httppost **last) {
@@ -3452,7 +4621,7 @@ static VALUE ruby_curl_easy_perform_post(int argc, VALUE *argv, VALUE self) {
3452
4621
  } else {
3453
4622
  VALUE post_body = Qnil;
3454
4623
  /* TODO: check for PostField.file and raise error before to_s fails */
3455
- if ((post_body = rb_funcall(args_ary, idJoin, 1, rbstrAmp)) == Qnil) {
4624
+ if ((post_body = join_easy_arguments(rbce, args_ary)) == Qnil) {
3456
4625
  rb_raise(eCurlErrError, "Failed to join arguments");
3457
4626
  return Qnil;
3458
4627
  } else {
@@ -3511,7 +4680,7 @@ static VALUE ruby_curl_easy_perform_patch(int argc, VALUE *argv, VALUE self) {
3511
4680
  return ret;
3512
4681
  } else {
3513
4682
  /* Join arguments into a raw PATCH body */
3514
- VALUE patch_body = rb_funcall(args_ary, idJoin, 1, rbstrAmp);
4683
+ VALUE patch_body = join_easy_arguments(rbce, args_ary);
3515
4684
  if (patch_body == Qnil) {
3516
4685
  rb_raise(eCurlErrError, "Failed to join arguments");
3517
4686
  return Qnil;
@@ -3570,7 +4739,7 @@ static VALUE ruby_curl_easy_perform_put(int argc, VALUE *argv, VALUE self) {
3570
4739
  }
3571
4740
  /* Fallback: join all arguments */
3572
4741
  else {
3573
- VALUE post_body = rb_funcall(args_ary, idJoin, 1, rbstrAmp);
4742
+ VALUE post_body = join_easy_arguments(rbce, args_ary);
3574
4743
  if (post_body != Qnil && rb_type(post_body) == T_STRING &&
3575
4744
  RSTRING_LEN(post_body) > 0) {
3576
4745
  ruby_curl_easy_put_data_set(self, post_body);
@@ -3600,6 +4769,47 @@ static VALUE ruby_curl_easy_body_str_get(VALUE self) {
3600
4769
  CURB_OBJECT_HGETTER(ruby_curl_easy, body_data);
3601
4770
  }
3602
4771
 
4772
+ /*
4773
+ * call-seq:
4774
+ * easy.max_body_bytes = bytes_or_nil => bytes_or_nil
4775
+ *
4776
+ * Set an application-level cap for bytes accepted by the body write callback.
4777
+ * +nil+ or 0 clears the cap.
4778
+ */
4779
+ static VALUE ruby_curl_easy_max_body_bytes_set(VALUE self, VALUE val) {
4780
+ ruby_curl_easy *rbce;
4781
+ long long limit;
4782
+
4783
+ TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
4784
+
4785
+ if (NIL_P(val)) {
4786
+ rb_hash_delete(rbce->opts, rb_easy_hkey("max_body_bytes"));
4787
+ return Qnil;
4788
+ }
4789
+
4790
+ limit = NUM2LL(val);
4791
+ if (limit < 0) {
4792
+ rb_raise(rb_eArgError, "max_body_bytes must be greater than or equal to zero");
4793
+ }
4794
+
4795
+ if (limit == 0) {
4796
+ rb_hash_delete(rbce->opts, rb_easy_hkey("max_body_bytes"));
4797
+ } else {
4798
+ val = LL2NUM(limit);
4799
+ rb_hash_aset(rbce->opts, rb_easy_hkey("max_body_bytes"), val);
4800
+ }
4801
+
4802
+ return val;
4803
+ }
4804
+
4805
+ /*
4806
+ * call-seq:
4807
+ * easy.max_body_bytes => bytes_or_nil
4808
+ */
4809
+ static VALUE ruby_curl_easy_max_body_bytes_get(VALUE self) {
4810
+ CURB_OBJECT_HGETTER(ruby_curl_easy, max_body_bytes);
4811
+ }
4812
+
3603
4813
  /*
3604
4814
  * call-seq:
3605
4815
  * easy.header_str => "response header"
@@ -4307,12 +5517,28 @@ static VALUE ruby_curl_easy_multi_get(VALUE self) {
4307
5517
  */
4308
5518
  static VALUE ruby_curl_easy_multi_set(VALUE self, VALUE multi) {
4309
5519
  ruby_curl_easy *rbce;
5520
+ VALUE old_multi;
5521
+ ruby_curl_multi *old_rbcm;
5522
+ CURLMcode result;
4310
5523
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
4311
5524
 
4312
5525
  if (!NIL_P(multi) && rb_obj_is_kind_of(multi, cCurlMulti) != Qtrue) {
4313
5526
  rb_raise(rb_eTypeError, "expected Curl::Multi or nil");
4314
5527
  }
4315
5528
 
5529
+ old_multi = rbce->multi;
5530
+ if (old_multi == multi) {
5531
+ return rbce->multi;
5532
+ }
5533
+
5534
+ old_rbcm = ruby_curl_multi_pointer_if_compatible(old_multi);
5535
+ if (old_rbcm) {
5536
+ result = rb_curl_multi_detach_easy(old_rbcm, rbce);
5537
+ if (result != CURLM_OK) {
5538
+ raise_curl_multi_error_exception(result);
5539
+ }
5540
+ }
5541
+
4316
5542
  rbce->multi = multi;
4317
5543
  return rbce->multi;
4318
5544
  }
@@ -4457,6 +5683,12 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
4457
5683
  /* Forward request-target directly to libcurl as a string. */
4458
5684
  curl_easy_setopt(rbce->curl, CURLOPT_REQUEST_TARGET, NIL_P(val) ? NULL : StringValueCStr(val));
4459
5685
  } break;
5686
+ #endif
5687
+ #ifdef HAVE_CURLOPT_DOH_URL
5688
+ case CURLOPT_DOH_URL: {
5689
+ rb_hash_aset(rbce->opts, rb_easy_hkey("doh_url"), val);
5690
+ curl_easy_setopt(rbce->curl, CURLOPT_DOH_URL, NIL_P(val) ? NULL : StringValueCStr(val));
5691
+ } break;
4460
5692
  #endif
4461
5693
  case CURLOPT_TCP_NODELAY: {
4462
5694
  curl_easy_setopt(rbce->curl, CURLOPT_TCP_NODELAY, NUM2LONG(val));
@@ -4531,7 +5763,9 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
4531
5763
  curl_easy_setopt(rbce->curl, CURLOPT_SSL_CIPHER_LIST, StringValueCStr(val));
4532
5764
  } break;
4533
5765
  case CURLOPT_FORBID_REUSE: {
4534
- curl_easy_setopt(rbce->curl, CURLOPT_FORBID_REUSE, NUM2LONG(val));
5766
+ rbce->forbid_reuse = NUM2LONG(val);
5767
+ rbce->forbid_reuse_set = 1;
5768
+ curl_easy_setopt(rbce->curl, CURLOPT_FORBID_REUSE, rbce->forbid_reuse);
4535
5769
  } break;
4536
5770
  #ifdef HAVE_CURLOPT_GSSAPI_DELEGATION
4537
5771
  case CURLOPT_GSSAPI_DELEGATION: {
@@ -4540,7 +5774,14 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
4540
5774
  #endif
4541
5775
  #ifdef HAVE_CURLOPT_UNIX_SOCKET_PATH
4542
5776
  case CURLOPT_UNIX_SOCKET_PATH: {
4543
- curl_easy_setopt(rbce->curl, CURLOPT_UNIX_SOCKET_PATH, StringValueCStr(val));
5777
+ rb_hash_aset(rbce->opts, rb_easy_hkey("unix_socket_path"), val);
5778
+ curl_easy_setopt(rbce->curl, CURLOPT_UNIX_SOCKET_PATH, NIL_P(val) ? NULL : StringValueCStr(val));
5779
+ } break;
5780
+ #endif
5781
+ #ifdef HAVE_CURLOPT_DNS_SERVERS
5782
+ case CURLOPT_DNS_SERVERS: {
5783
+ rb_hash_aset(rbce->opts, rb_easy_hkey("dns_servers"), val);
5784
+ curl_easy_setopt(rbce->curl, CURLOPT_DNS_SERVERS, NIL_P(val) ? NULL : StringValueCStr(val));
4544
5785
  } break;
4545
5786
  #endif
4546
5787
  #ifdef HAVE_CURLOPT_MAX_SEND_SPEED_LARGE
@@ -4558,6 +5799,11 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
4558
5799
  curl_easy_setopt(rbce->curl, CURLOPT_MAXFILESIZE, NUM2LONG(val));
4559
5800
  break;
4560
5801
  #endif
5802
+ #ifdef HAVE_CURLOPT_MAXFILESIZE_LARGE
5803
+ case CURLOPT_MAXFILESIZE_LARGE:
5804
+ curl_easy_setopt(rbce->curl, CURLOPT_MAXFILESIZE_LARGE, (curl_off_t)NUM2LL(val));
5805
+ break;
5806
+ #endif
4561
5807
  #ifdef HAVE_CURLOPT_TCP_KEEPALIVE
4562
5808
  case CURLOPT_TCP_KEEPALIVE:
4563
5809
  curl_easy_setopt(rbce->curl, CURLOPT_TCP_KEEPALIVE, NUM2LONG(val));
@@ -4588,6 +5834,16 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
4588
5834
  case CURLOPT_REDIR_PROTOCOLS:
4589
5835
  curl_easy_setopt(rbce->curl, option, NUM2LONG(val));
4590
5836
  break;
5837
+ #ifdef HAVE_CURLOPT_PROTOCOLS_STR
5838
+ case CURLOPT_PROTOCOLS_STR:
5839
+ curl_easy_setopt(rbce->curl, CURLOPT_PROTOCOLS_STR, NIL_P(val) ? NULL : StringValueCStr(val));
5840
+ break;
5841
+ #endif
5842
+ #ifdef HAVE_CURLOPT_REDIR_PROTOCOLS_STR
5843
+ case CURLOPT_REDIR_PROTOCOLS_STR:
5844
+ curl_easy_setopt(rbce->curl, CURLOPT_REDIR_PROTOCOLS_STR, NIL_P(val) ? NULL : StringValueCStr(val));
5845
+ break;
5846
+ #endif
4591
5847
  #ifdef HAVE_CURLOPT_SSL_SESSIONID_CACHE
4592
5848
  case CURLOPT_SSL_SESSIONID_CACHE:
4593
5849
  curl_easy_setopt(rbce->curl, CURLOPT_SSL_SESSIONID_CACHE, NUM2LONG(val));
@@ -4618,6 +5874,21 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
4618
5874
  curl_easy_setopt(rbce->curl, CURLOPT_PROXY_SSL_VERIFYHOST, NUM2LONG(val));
4619
5875
  break;
4620
5876
  #endif
5877
+ #ifdef HAVE_CURLOPT_DOH_SSL_VERIFYPEER
5878
+ case CURLOPT_DOH_SSL_VERIFYPEER:
5879
+ curl_easy_setopt(rbce->curl, CURLOPT_DOH_SSL_VERIFYPEER, NUM2LONG(val));
5880
+ break;
5881
+ #endif
5882
+ #ifdef HAVE_CURLOPT_DOH_SSL_VERIFYHOST
5883
+ case CURLOPT_DOH_SSL_VERIFYHOST:
5884
+ curl_easy_setopt(rbce->curl, CURLOPT_DOH_SSL_VERIFYHOST, NUM2LONG(val));
5885
+ break;
5886
+ #endif
5887
+ #ifdef HAVE_CURLOPT_DOH_SSL_VERIFYSTATUS
5888
+ case CURLOPT_DOH_SSL_VERIFYSTATUS:
5889
+ curl_easy_setopt(rbce->curl, CURLOPT_DOH_SSL_VERIFYSTATUS, NUM2LONG(val));
5890
+ break;
5891
+ #endif
4621
5892
  #ifdef HAVE_CURLOPT_RESOLVE
4622
5893
  case CURLOPT_RESOLVE: {
4623
5894
  struct curl_slist *list = NULL;
@@ -4648,6 +5919,34 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
4648
5919
  rb_hash_aset(rbce->opts, rb_easy_hkey("resolve"), val);
4649
5920
  curl_easy_setopt(rbce->curl, CURLOPT_RESOLVE, list);
4650
5921
  } break;
5922
+ #endif
5923
+ #ifdef HAVE_CURLOPT_CONNECT_TO
5924
+ case CURLOPT_CONNECT_TO: {
5925
+ struct curl_slist *list = NULL;
5926
+ ruby_curl_easy_clear_connect_to_list(rbce);
5927
+ if (NIL_P(val)) {
5928
+ list = NULL;
5929
+ } else if (TYPE(val) == T_ARRAY) {
5930
+ long i, len = RARRAY_LEN(val);
5931
+ for (i = 0; i < len; i++) {
5932
+ VALUE item = rb_ary_entry(val, i);
5933
+ struct curl_slist *new_list = curl_slist_append(list, StringValueCStr(item));
5934
+ if (!new_list) {
5935
+ curl_slist_free_all(list);
5936
+ rb_raise(rb_eNoMemError, "Failed to append to connect-to list");
5937
+ }
5938
+ list = new_list;
5939
+ }
5940
+ } else {
5941
+ list = curl_slist_append(NULL, StringValueCStr(val));
5942
+ if (!list) {
5943
+ rb_raise(rb_eNoMemError, "Failed to create connect-to list");
5944
+ }
5945
+ }
5946
+ rbce->curl_connect_to = list;
5947
+ rb_hash_aset(rbce->opts, rb_easy_hkey("connect_to"), val);
5948
+ curl_easy_setopt(rbce->curl, CURLOPT_CONNECT_TO, list);
5949
+ } break;
4651
5950
  #endif
4652
5951
  default:
4653
5952
  rb_raise(rb_eTypeError, "Curb unsupported option");
@@ -4773,6 +6072,8 @@ static VALUE ruby_curl_easy_error_message(VALUE klass, VALUE code) {
4773
6072
  void init_curb_easy() {
4774
6073
  idCall = rb_intern("call");
4775
6074
  idJoin = rb_intern("join");
6075
+ idNetworkPolicyNone = rb_intern("none");
6076
+ idNetworkPolicyPublic = rb_intern("public");
4776
6077
 
4777
6078
  rbstrAmp = rb_str_new2("&");
4778
6079
  rb_global_variable(&rbstrAmp);
@@ -4823,6 +6124,17 @@ void init_curb_easy() {
4823
6124
  rb_define_method(cCurlEasy, "ftp_commands", ruby_curl_easy_ftp_commands_get, 0);
4824
6125
  rb_define_method(cCurlEasy, "resolve=", ruby_curl_easy_resolve_set, 1);
4825
6126
  rb_define_method(cCurlEasy, "resolve", ruby_curl_easy_resolve_get, 0);
6127
+ rb_define_method(cCurlEasy, "connect_to=", ruby_curl_easy_connect_to_set, 1);
6128
+ rb_define_method(cCurlEasy, "connect_to", ruby_curl_easy_connect_to_get, 0);
6129
+ rb_define_method(cCurlEasy, "doh_url=", ruby_curl_easy_doh_url_set, 1);
6130
+ rb_define_method(cCurlEasy, "doh_url", ruby_curl_easy_doh_url_get, 0);
6131
+ #ifdef HAVE_CURLOPT_DNS_SERVERS
6132
+ rb_define_method(cCurlEasy, "dns_servers", ruby_curl_easy_dns_servers_get, 0);
6133
+ #endif
6134
+ rb_define_method(cCurlEasy, "allowed_cidrs=", ruby_curl_easy_allowed_cidrs_set, 1);
6135
+ rb_define_method(cCurlEasy, "allowed_cidrs", ruby_curl_easy_allowed_cidrs_get, 0);
6136
+ rb_define_method(cCurlEasy, "allowed_hosts=", ruby_curl_easy_allowed_hosts_set, 1);
6137
+ rb_define_method(cCurlEasy, "allowed_hosts", ruby_curl_easy_allowed_hosts_get, 0);
4826
6138
 
4827
6139
  rb_define_method(cCurlEasy, "local_port=", ruby_curl_easy_local_port_set, 1);
4828
6140
  rb_define_method(cCurlEasy, "local_port", ruby_curl_easy_local_port_get, 0);
@@ -4896,6 +6208,8 @@ void init_curb_easy() {
4896
6208
  rb_define_method(cCurlEasy, "ignore_content_length?", ruby_curl_easy_ignore_content_length_q, 0);
4897
6209
  rb_define_method(cCurlEasy, "resolve_mode", ruby_curl_easy_resolve_mode, 0);
4898
6210
  rb_define_method(cCurlEasy, "resolve_mode=", ruby_curl_easy_resolve_mode_set, 1);
6211
+ rb_define_method(cCurlEasy, "network_policy", ruby_curl_easy_network_policy_get, 0);
6212
+ rb_define_method(cCurlEasy, "network_policy=", ruby_curl_easy_network_policy_set, 1);
4899
6213
 
4900
6214
  rb_define_method(cCurlEasy, "on_body", ruby_curl_easy_on_body_set, -1);
4901
6215
  rb_define_method(cCurlEasy, "on_header", ruby_curl_easy_on_header_set, -1);
@@ -4914,6 +6228,8 @@ void init_curb_easy() {
4914
6228
 
4915
6229
  /* Post-perform info methods */
4916
6230
  rb_define_method(cCurlEasy, "body_str", ruby_curl_easy_body_str_get, 0);
6231
+ rb_define_method(cCurlEasy, "max_body_bytes=", ruby_curl_easy_max_body_bytes_set, 1);
6232
+ rb_define_method(cCurlEasy, "max_body_bytes", ruby_curl_easy_max_body_bytes_get, 0);
4917
6233
  rb_define_method(cCurlEasy, "header_str", ruby_curl_easy_header_str_get, 0);
4918
6234
 
4919
6235
  rb_define_method(cCurlEasy, "last_effective_url", ruby_curl_easy_last_effective_url_get, 0);
@@ -4964,9 +6280,16 @@ void init_curb_easy() {
4964
6280
 
4965
6281
  rb_define_method(cCurlEasy, "multi", ruby_curl_easy_multi_get, 0);
4966
6282
  rb_define_method(cCurlEasy, "multi=", ruby_curl_easy_multi_set, 1);
6283
+ rb_define_private_method(cCurlEasy, "__curb_native_setup!", ruby_curl_easy_setup_self, 0);
6284
+ rb_define_private_method(cCurlEasy, "__curb_allow_proxy=", ruby_curl_easy_allow_proxy_set, 1);
6285
+ rb_define_private_method(cCurlEasy, "__curb_allow_unix_socket=", ruby_curl_easy_allow_unix_socket_set, 1);
4967
6286
  rb_define_private_method(cCurlEasy, "_take_callback_error", ruby_curl_easy_take_callback_error, 0);
4968
6287
  rb_define_method(cCurlEasy, "last_result", ruby_curl_easy_last_result, 0);
4969
6288
  rb_define_method(cCurlEasy, "last_error", ruby_curl_easy_last_error, 0);
6289
+ rb_define_method(cCurlEasy, "unsafe_destination_error", ruby_curl_easy_unsafe_destination_error_get, 0);
6290
+ #ifdef HAVE_CURLOPT_UNIX_SOCKET_PATH
6291
+ rb_define_method(cCurlEasy, "unix_socket_path", ruby_curl_easy_unix_socket_path_get, 0);
6292
+ #endif
4970
6293
 
4971
6294
  rb_define_method(cCurlEasy, "setopt", ruby_curl_easy_set_opt, 2);
4972
6295
  rb_define_method(cCurlEasy, "getinfo", ruby_curl_easy_get_opt, 1);