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.
- checksums.yaml +4 -4
- data/README.md +57 -0
- data/Rakefile +8 -3
- data/doc.rb +48 -8
- data/ext/curb.c +24 -0
- data/ext/curb.h +3 -3
- data/ext/curb_easy.c +1378 -55
- data/ext/curb_easy.h +26 -0
- data/ext/curb_errors.c +2 -0
- data/ext/curb_errors.h +1 -0
- data/ext/curb_multi.c +48 -2
- data/ext/curb_multi.h +1 -0
- data/ext/extconf.rb +8 -0
- data/lib/curl/download.rb +160 -0
- data/lib/curl/easy.rb +113 -13
- data/lib/curl/multi.rb +172 -39
- data/lib/curl.rb +471 -11
- data/tests/bug_poison.rb +29 -0
- data/tests/tc_curl_download.rb +86 -0
- data/tests/tc_curl_easy.rb +76 -0
- data/tests/tc_curl_maxfilesize.rb +201 -1
- data/tests/tc_curl_multi.rb +258 -0
- data/tests/tc_curl_network_policy.rb +1475 -0
- data/tests/tc_curl_protocols.rb +351 -0
- data/tests/tc_fiber_scheduler.rb +41 -0
- metadata +7 -2
data/ext/curb_easy.h
CHANGED
|
@@ -11,6 +11,18 @@
|
|
|
11
11
|
|
|
12
12
|
#include <curl/easy.h>
|
|
13
13
|
|
|
14
|
+
#define CURB_NETWORK_POLICY_NONE 0
|
|
15
|
+
#define CURB_NETWORK_POLICY_PUBLIC 1
|
|
16
|
+
|
|
17
|
+
#define CURB_CIDR_FAMILY_IPV4 4
|
|
18
|
+
#define CURB_CIDR_FAMILY_IPV6 6
|
|
19
|
+
|
|
20
|
+
typedef struct {
|
|
21
|
+
unsigned char family;
|
|
22
|
+
unsigned char prefix_bits;
|
|
23
|
+
unsigned char address[16];
|
|
24
|
+
} curb_cidr_rule;
|
|
25
|
+
|
|
14
26
|
#ifdef CURL_VERSION_SSL
|
|
15
27
|
#if LIBCURL_VERSION_NUM >= 0x070b00
|
|
16
28
|
# if LIBCURL_VERSION_NUM <= 0x071004
|
|
@@ -38,6 +50,7 @@ typedef struct {
|
|
|
38
50
|
|
|
39
51
|
/* Buffer for error details from CURLOPT_ERRORBUFFER */
|
|
40
52
|
char err_buf[CURL_ERROR_SIZE];
|
|
53
|
+
char unsafe_destination_error[CURL_ERROR_SIZE];
|
|
41
54
|
|
|
42
55
|
VALUE self; /* owning Ruby object */
|
|
43
56
|
VALUE opts; /* rather then allocate everything we might need to store, allocate a Hash and only store objects we actually use... */
|
|
@@ -67,6 +80,7 @@ typedef struct {
|
|
|
67
80
|
long ftp_filemethod;
|
|
68
81
|
long http_version;
|
|
69
82
|
unsigned short resolve_mode;
|
|
83
|
+
unsigned short network_policy;
|
|
70
84
|
|
|
71
85
|
/* bool flags */
|
|
72
86
|
char proxy_tunnel;
|
|
@@ -83,13 +97,25 @@ typedef struct {
|
|
|
83
97
|
char cookielist_engine_enabled; /* track if CURLOPT_COOKIELIST was used with a non-command to enable engine */
|
|
84
98
|
char ignore_content_length;
|
|
85
99
|
char callback_active;
|
|
100
|
+
char unsafe_destination_blocked;
|
|
101
|
+
char allow_proxy;
|
|
102
|
+
char allow_unix_socket;
|
|
103
|
+
char forbid_reuse_set;
|
|
104
|
+
unsigned int native_active;
|
|
105
|
+
long forbid_reuse;
|
|
86
106
|
|
|
87
107
|
struct curl_slist *curl_headers;
|
|
88
108
|
struct curl_slist *curl_proxy_headers;
|
|
89
109
|
struct curl_slist *curl_ftp_commands;
|
|
90
110
|
struct curl_slist *curl_resolve;
|
|
111
|
+
struct curl_slist *curl_connect_to;
|
|
112
|
+
curb_cidr_rule *network_allowed_cidr_rules;
|
|
113
|
+
char **network_allowed_hosts;
|
|
91
114
|
|
|
92
115
|
unsigned long multi_attachment_generation;
|
|
116
|
+
curl_off_t downloaded_body_bytes;
|
|
117
|
+
size_t network_allowed_cidr_rule_count;
|
|
118
|
+
size_t network_allowed_host_count;
|
|
93
119
|
int last_result; /* last result code from multi loop */
|
|
94
120
|
|
|
95
121
|
} ruby_curl_easy;
|
data/ext/curb_errors.c
CHANGED
|
@@ -110,6 +110,7 @@ VALUE eCurlErrSSLShutdownFailed;
|
|
|
110
110
|
VALUE eCurlErrAgain;
|
|
111
111
|
VALUE eCurlErrSSLCRLBadfile;
|
|
112
112
|
VALUE eCurlErrSSLIssuerError;
|
|
113
|
+
VALUE eCurlErrUnsafeDestination;
|
|
113
114
|
|
|
114
115
|
/* multi errors */
|
|
115
116
|
VALUE mCurlErrFailedInit;
|
|
@@ -699,6 +700,7 @@ void init_curb_errors() {
|
|
|
699
700
|
eCurlErrSSLCacertBadfile = rb_define_class_under(mCurlErr, "SSLCaertBadFile", eCurlErrError);
|
|
700
701
|
eCurlErrSSLCRLBadfile = rb_define_class_under(mCurlErr, "SSLCRLBadfile", eCurlErrError);
|
|
701
702
|
eCurlErrSSLIssuerError = rb_define_class_under(mCurlErr, "SSLIssuerError", eCurlErrError);
|
|
703
|
+
eCurlErrUnsafeDestination = rb_define_class_under(mCurlErr, "UnsafeDestinationError", eCurlErrError);
|
|
702
704
|
eCurlErrSSLShutdownFailed = rb_define_class_under(mCurlErr, "SSLShutdownFailed", eCurlErrError);
|
|
703
705
|
eCurlErrSSH = rb_define_class_under(mCurlErr, "SSH", eCurlErrError);
|
|
704
706
|
|
data/ext/curb_errors.h
CHANGED
|
@@ -106,6 +106,7 @@ extern VALUE eCurlErrSSLShutdownFailed;
|
|
|
106
106
|
extern VALUE eCurlErrAgain;
|
|
107
107
|
extern VALUE eCurlErrSSLCRLBadfile;
|
|
108
108
|
extern VALUE eCurlErrSSLIssuerError;
|
|
109
|
+
extern VALUE eCurlErrUnsafeDestination;
|
|
109
110
|
|
|
110
111
|
/* multi errors */
|
|
111
112
|
extern VALUE mCurlErrFailedInit;
|
data/ext/curb_multi.c
CHANGED
|
@@ -74,6 +74,7 @@ extern VALUE mCurl;
|
|
|
74
74
|
static VALUE idCall;
|
|
75
75
|
static ID id_deferred_exception_ivar;
|
|
76
76
|
static ID id_deferred_exception_source_id_ivar;
|
|
77
|
+
static ID id_native_safety_signatures_ivar;
|
|
77
78
|
static ID id_socket_io_cache_ivar;
|
|
78
79
|
|
|
79
80
|
#ifdef RDOC_NEVER_DEFINED
|
|
@@ -286,6 +287,33 @@ void rb_curl_multi_forget_easy(ruby_curl_multi *rbcm, void *rbce_ptr) {
|
|
|
286
287
|
st_delete(rbcm->attached, &key, NULL);
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
CURLMcode rb_curl_multi_detach_easy(ruby_curl_multi *rbcm, void *rbce_ptr) {
|
|
291
|
+
ruby_curl_easy *rbce = (ruby_curl_easy *)rbce_ptr;
|
|
292
|
+
st_data_t key;
|
|
293
|
+
|
|
294
|
+
if (!rbcm || !rbce || !rbcm->attached) {
|
|
295
|
+
return CURLM_OK;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
key = (st_data_t)rbce;
|
|
299
|
+
if (!st_delete(rbcm->attached, &key, NULL)) {
|
|
300
|
+
return CURLM_OK;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (rbcm->handle && rbce->curl) {
|
|
304
|
+
CURLMcode result = curl_multi_remove_handle(rbcm->handle, rbce->curl);
|
|
305
|
+
if (result != CURLM_OK) {
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (rbcm->active > 0) {
|
|
311
|
+
rbcm->active--;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return CURLM_OK;
|
|
315
|
+
}
|
|
316
|
+
|
|
289
317
|
static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm) {
|
|
290
318
|
if (!rbcm || !rbcm->attached) {
|
|
291
319
|
return;
|
|
@@ -314,6 +342,8 @@ static int rb_curl_multi_has_easy(ruby_curl_multi *rbcm, ruby_curl_easy *rbce) {
|
|
|
314
342
|
|
|
315
343
|
static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy) {
|
|
316
344
|
VALUE requests;
|
|
345
|
+
VALUE object_id;
|
|
346
|
+
VALUE safety_signatures;
|
|
317
347
|
|
|
318
348
|
if (NIL_P(self) || NIL_P(easy)) {
|
|
319
349
|
return;
|
|
@@ -324,7 +354,17 @@ static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy) {
|
|
|
324
354
|
return;
|
|
325
355
|
}
|
|
326
356
|
|
|
327
|
-
|
|
357
|
+
object_id = rb_obj_id(easy);
|
|
358
|
+
rb_hash_delete(requests, object_id);
|
|
359
|
+
|
|
360
|
+
if (!rb_ivar_defined(self, id_native_safety_signatures_ivar)) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
safety_signatures = rb_ivar_get(self, id_native_safety_signatures_ivar);
|
|
365
|
+
if (RB_TYPE_P(safety_signatures, T_HASH)) {
|
|
366
|
+
rb_hash_delete(safety_signatures, object_id);
|
|
367
|
+
}
|
|
328
368
|
}
|
|
329
369
|
|
|
330
370
|
/* TypedData-compatible free function */
|
|
@@ -1427,6 +1467,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1427
1467
|
long wait_ms = cCurlMutiDefaulttimeout;
|
|
1428
1468
|
|
|
1429
1469
|
if (multi_socket_timer_due(ctx)) {
|
|
1470
|
+
ctx->timeout_deadline_ms = -1;
|
|
1430
1471
|
mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
|
|
1431
1472
|
curb_debugf("[curb.socket] socket_action timeout(due) -> mrc=%d running=%d", mrc, rbcm->running);
|
|
1432
1473
|
if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
|
|
@@ -1688,10 +1729,14 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
|
|
|
1688
1729
|
#else
|
|
1689
1730
|
rb_thread_wait_for(tv);
|
|
1690
1731
|
#endif
|
|
1691
|
-
|
|
1732
|
+
/* libcurl can report active work without a socket callback or deadline;
|
|
1733
|
+
* drive the timeout socket after the scheduler-aware sleep so the state
|
|
1734
|
+
* machine does not stall indefinitely. */
|
|
1735
|
+
did_timeout = 1;
|
|
1692
1736
|
}
|
|
1693
1737
|
|
|
1694
1738
|
if (did_timeout) {
|
|
1739
|
+
ctx->timeout_deadline_ms = -1;
|
|
1695
1740
|
mrc = curl_multi_socket_action(rbcm->handle, CURL_SOCKET_TIMEOUT, 0, &rbcm->running);
|
|
1696
1741
|
curb_debugf("[curb.socket] socket_action timeout -> mrc=%d running=%d", mrc, rbcm->running);
|
|
1697
1742
|
if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
|
|
@@ -2190,6 +2235,7 @@ void init_curb_multi() {
|
|
|
2190
2235
|
idCall = rb_intern("call");
|
|
2191
2236
|
id_deferred_exception_ivar = rb_intern("@__curb_deferred_exception");
|
|
2192
2237
|
id_deferred_exception_source_id_ivar = rb_intern("@__curb_deferred_exception_source_id");
|
|
2238
|
+
id_native_safety_signatures_ivar = rb_intern("@__curb_native_safety_signatures");
|
|
2193
2239
|
id_socket_io_cache_ivar = rb_intern("@__curb_socket_io_cache");
|
|
2194
2240
|
cCurlMulti = rb_define_class_under(mCurl, "Multi", rb_cObject);
|
|
2195
2241
|
|
data/ext/curb_multi.h
CHANGED
data/ext/extconf.rb
CHANGED
|
@@ -298,6 +298,8 @@ have_constant "curlopt_sockoptfunction"
|
|
|
298
298
|
have_constant "curlopt_sockoptdata"
|
|
299
299
|
have_constant "curlopt_opensocketfunction"
|
|
300
300
|
have_constant "curlopt_opensocketdata"
|
|
301
|
+
have_constant "curlopt_prereqfunction"
|
|
302
|
+
have_constant "curlopt_prereqdata"
|
|
301
303
|
|
|
302
304
|
# Deprecated constants (still check for them for backward compat)
|
|
303
305
|
have_constant "curlopt_ioctlfunction"
|
|
@@ -391,6 +393,7 @@ have_constant "curlopt_socks5_gssapi_nec"
|
|
|
391
393
|
have_constant "curlopt_interface"
|
|
392
394
|
have_constant "curlopt_localport"
|
|
393
395
|
have_constant "curlopt_dns_cache_timeout"
|
|
396
|
+
have_constant "curlopt_dns_servers"
|
|
394
397
|
have_constant "curlopt_dns_use_global_cache"
|
|
395
398
|
have_constant "curlopt_buffersize"
|
|
396
399
|
have_constant "curlopt_port"
|
|
@@ -531,6 +534,7 @@ have_constant "curlusessl_none"
|
|
|
531
534
|
have_constant "curlusessl_try"
|
|
532
535
|
have_constant "curlusessl_control"
|
|
533
536
|
have_constant "curlusessl_all"
|
|
537
|
+
have_constant "curlopt_connect_to"
|
|
534
538
|
have_constant "curlopt_resolve"
|
|
535
539
|
have_constant "curlopt_request_target"
|
|
536
540
|
have_constant "curlopt_sslcert"
|
|
@@ -558,6 +562,10 @@ have_constant :CURL_SSLVERSION_TLSv1_2
|
|
|
558
562
|
have_constant :CURL_SSLVERSION_TLSv1_3
|
|
559
563
|
|
|
560
564
|
have_constant "curlopt_ssl_verifypeer"
|
|
565
|
+
have_constant "curlopt_doh_url"
|
|
566
|
+
have_constant "curlopt_doh_ssl_verifypeer"
|
|
567
|
+
have_constant "curlopt_doh_ssl_verifyhost"
|
|
568
|
+
have_constant "curlopt_doh_ssl_verifystatus"
|
|
561
569
|
have_constant "curlopt_cainfo"
|
|
562
570
|
have_constant "curlopt_issuercert"
|
|
563
571
|
have_constant "curlopt_capath"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
module Curl
|
|
7
|
+
class DownloadTargetExistsError < Errno::EEXIST; end
|
|
8
|
+
DOWNLOAD_OPTION_KEYS = [:download_dir, :overwrite].freeze
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def download_options_hash?(value)
|
|
12
|
+
value.is_a?(Hash) && value.keys.all? { |key| DOWNLOAD_OPTION_KEYS.include?(key) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def normalize_download_arguments(filename, options)
|
|
16
|
+
if download_options_hash?(filename) && options.empty?
|
|
17
|
+
options = filename
|
|
18
|
+
filename = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
[filename, options || {}]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse_download_options(options)
|
|
25
|
+
raise ArgumentError, "download options must be a Hash" unless options.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
options = options.dup
|
|
28
|
+
download_dir = options.delete(:download_dir)
|
|
29
|
+
overwrite = options.key?(:overwrite) ? !!options.delete(:overwrite) : false
|
|
30
|
+
raise ArgumentError, "unsupported download option(s): #{options.keys.join(', ')}" unless options.empty?
|
|
31
|
+
|
|
32
|
+
[download_dir, overwrite]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def resolve_download_output(url, filename = nil, options = {})
|
|
36
|
+
filename, options = normalize_download_arguments(filename, options)
|
|
37
|
+
download_dir, overwrite = parse_download_options(options)
|
|
38
|
+
|
|
39
|
+
if filename.is_a?(IO)
|
|
40
|
+
filename.binmode if filename.respond_to?(:binmode)
|
|
41
|
+
return [nil, filename, false, overwrite]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
path = if download_dir
|
|
45
|
+
safe_download_path(url, download_dir, filename: filename)
|
|
46
|
+
elsif filename.nil?
|
|
47
|
+
safe_download_path(url)
|
|
48
|
+
else
|
|
49
|
+
filename.to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
[path, nil, true, overwrite]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def prepare_download_output(url, filename = nil, options = {})
|
|
56
|
+
path, io, safe_output, overwrite = resolve_download_output(url, filename, options)
|
|
57
|
+
return [path, io, safe_output] unless safe_output
|
|
58
|
+
|
|
59
|
+
[path, open_safe_download_output(path, overwrite: overwrite), true]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def download_filename_from_url(url)
|
|
63
|
+
path = begin
|
|
64
|
+
URI.parse(url.to_s).path
|
|
65
|
+
rescue URI::InvalidURIError
|
|
66
|
+
url.to_s.split(/\?/, 2).first
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
basename = File.basename(path.to_s)
|
|
70
|
+
validate_download_filename!(basename)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def safe_download_path(url, destination_dir = Dir.pwd, filename: nil)
|
|
74
|
+
dir = File.expand_path(destination_dir.to_s)
|
|
75
|
+
raise ArgumentError, "download destination directory does not exist: #{destination_dir}" unless File.directory?(dir)
|
|
76
|
+
|
|
77
|
+
File.join(dir, filename.nil? ? download_filename_from_url(url) : validate_download_filename!(filename))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_download_filename!(filename)
|
|
81
|
+
filename = filename.to_s
|
|
82
|
+
invalid = filename.empty? ||
|
|
83
|
+
filename == '.' ||
|
|
84
|
+
filename == '..' ||
|
|
85
|
+
filename == File::SEPARATOR ||
|
|
86
|
+
filename.start_with?('.') ||
|
|
87
|
+
filename.include?("\0") ||
|
|
88
|
+
filename.include?(File::SEPARATOR) ||
|
|
89
|
+
filename.include?('\\') ||
|
|
90
|
+
filename.match?(/\A[A-Za-z]:/) ||
|
|
91
|
+
(File::ALT_SEPARATOR && filename.include?(File::ALT_SEPARATOR))
|
|
92
|
+
|
|
93
|
+
raise ArgumentError, "unsafe download filename derived from URL: #{filename.inspect}" if invalid
|
|
94
|
+
|
|
95
|
+
filename
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def open_safe_download_output(path, overwrite: false)
|
|
99
|
+
SafeDownloadOutput.new(path, overwrite: overwrite)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class SafeDownloadOutput
|
|
104
|
+
def initialize(path, overwrite: false)
|
|
105
|
+
@path = File.expand_path(path.to_s)
|
|
106
|
+
@overwrite = overwrite
|
|
107
|
+
raise DownloadTargetExistsError, @path if !@overwrite && File.exist?(@path)
|
|
108
|
+
|
|
109
|
+
@tmp = Tempfile.new(['.curb-download-', '.tmp'], File.dirname(@path))
|
|
110
|
+
@tmp.binmode
|
|
111
|
+
@closed = false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
attr_reader :path
|
|
115
|
+
|
|
116
|
+
def write(data)
|
|
117
|
+
@tmp.write(data)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def <<(data)
|
|
121
|
+
write(data)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def close(success = false)
|
|
125
|
+
return if @closed
|
|
126
|
+
|
|
127
|
+
@tmp.flush unless @tmp.closed?
|
|
128
|
+
@tmp.close unless @tmp.closed?
|
|
129
|
+
install_tmp_file if success
|
|
130
|
+
ensure
|
|
131
|
+
@tmp.close! if @tmp
|
|
132
|
+
@closed = true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def install_tmp_file
|
|
138
|
+
if @overwrite
|
|
139
|
+
File.rename(@tmp.path, @path)
|
|
140
|
+
else
|
|
141
|
+
File.link(@tmp.path, @path)
|
|
142
|
+
end
|
|
143
|
+
rescue Errno::EEXIST
|
|
144
|
+
raise DownloadTargetExistsError, @path
|
|
145
|
+
rescue NotImplementedError
|
|
146
|
+
install_tmp_file_by_copy
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def install_tmp_file_by_copy
|
|
150
|
+
flags = File::WRONLY | File::CREAT | File::EXCL
|
|
151
|
+
flags |= File::BINARY if File.const_defined?(:BINARY)
|
|
152
|
+
|
|
153
|
+
File.open(@path, flags) do |dest|
|
|
154
|
+
File.open(@tmp.path, 'rb') { |src| IO.copy_stream(src, dest) }
|
|
155
|
+
end
|
|
156
|
+
rescue Errno::EEXIST
|
|
157
|
+
raise DownloadTargetExistsError, @path
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
data/lib/curl/easy.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require 'curl/download'
|
|
2
3
|
module Curl
|
|
3
4
|
class Easy
|
|
4
5
|
class << self
|
|
@@ -83,10 +84,12 @@ module Curl
|
|
|
83
84
|
|
|
84
85
|
alias_method :_curb_native_close, :close
|
|
85
86
|
alias_method :_curb_native_multi_set, :multi=
|
|
87
|
+
alias_method :_curb_native_reset, :reset
|
|
86
88
|
|
|
87
89
|
def close
|
|
88
90
|
previous_multi = self.multi
|
|
89
91
|
result = _curb_native_close
|
|
92
|
+
__curb_clear_safety_override!
|
|
90
93
|
previous_multi.__send__(:__unregister_idle_easy_reference, self) if previous_multi
|
|
91
94
|
result
|
|
92
95
|
end
|
|
@@ -94,12 +97,22 @@ module Curl
|
|
|
94
97
|
def multi=(multi)
|
|
95
98
|
previous_multi = self.multi
|
|
96
99
|
return multi if previous_multi.equal?(multi)
|
|
97
|
-
|
|
100
|
+
|
|
101
|
+
if previous_multi && previous_multi.requests[self.object_id]
|
|
102
|
+
previous_multi.remove(self)
|
|
103
|
+
end
|
|
104
|
+
|
|
98
105
|
result = _curb_native_multi_set(multi)
|
|
99
106
|
previous_multi.__send__(:__unregister_idle_easy_reference, self) if previous_multi
|
|
100
107
|
multi.__send__(:__register_idle_easy_reference, self) if multi
|
|
101
108
|
result
|
|
102
109
|
end
|
|
110
|
+
|
|
111
|
+
def reset
|
|
112
|
+
result = _curb_native_reset
|
|
113
|
+
__curb_clear_safety_override!
|
|
114
|
+
result
|
|
115
|
+
end
|
|
103
116
|
|
|
104
117
|
alias post http_post
|
|
105
118
|
alias put http_put
|
|
@@ -154,6 +167,84 @@ module Curl
|
|
|
154
167
|
Curl.const_get("CURLOPT_#{opt.to_s.upcase}")
|
|
155
168
|
end
|
|
156
169
|
|
|
170
|
+
def allowed_protocols=(protocols)
|
|
171
|
+
set_protocol_allowlist('CURLOPT_PROTOCOLS_STR', 'CURLOPT_PROTOCOLS', protocols)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def allowed_redirect_protocols=(protocols)
|
|
175
|
+
set_protocol_allowlist('CURLOPT_REDIR_PROTOCOLS_STR', 'CURLOPT_REDIR_PROTOCOLS', protocols)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def safe_http!
|
|
179
|
+
__curb_set_safety_override!(
|
|
180
|
+
:protocols => [:http, :https],
|
|
181
|
+
:redirect_protocols => [:http, :https]
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def __curb_set_safety_override!(options)
|
|
188
|
+
@__curb_safety_override = (defined?(@__curb_safety_override) && @__curb_safety_override) ? @__curb_safety_override.dup : {}
|
|
189
|
+
@__curb_safety_override[:protocols] = Array(options[:protocols]).map { |protocol| protocol.to_s.downcase.to_sym } if options.key?(:protocols)
|
|
190
|
+
@__curb_safety_override[:redirect_protocols] = Array(options[:redirect_protocols]).map { |protocol| protocol.to_s.downcase.to_sym } if options.key?(:redirect_protocols)
|
|
191
|
+
@__curb_safety_override[:max_body_bytes] = options[:max_body_bytes] if options.key?(:max_body_bytes) && options[:max_body_bytes]
|
|
192
|
+
@__curb_safety_override_generation = __curb_safety_override_generation + 1
|
|
193
|
+
Curl.__send__(:apply_safety!, self) if Curl.respond_to?(:apply_safety!, true)
|
|
194
|
+
self
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def __curb_clear_safety_override!
|
|
198
|
+
had_override = defined?(@__curb_safety_override) && @__curb_safety_override
|
|
199
|
+
return unless had_override
|
|
200
|
+
return if frozen?
|
|
201
|
+
|
|
202
|
+
@__curb_safety_override = nil
|
|
203
|
+
@__curb_safety_override_generation = __curb_safety_override_generation + 1
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def __curb_safety_override
|
|
207
|
+
defined?(@__curb_safety_override) ? @__curb_safety_override : nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def __curb_safety_override_generation
|
|
211
|
+
defined?(@__curb_safety_override_generation) ? @__curb_safety_override_generation : 0
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def set_protocol_allowlist(string_option, bitmask_option, protocols)
|
|
215
|
+
protocol_names = Array(protocols).map { |protocol| protocol.to_s.downcase }
|
|
216
|
+
raise ArgumentError, "at least one protocol is required" if protocol_names.empty?
|
|
217
|
+
|
|
218
|
+
valid_names = %w[
|
|
219
|
+
dict file ftp ftps gopher gophers http https imap imaps ldap ldaps
|
|
220
|
+
mqtt pop3 pop3s rtmp rtmpe rtmps rtmpt rtmpte rtmpts rtsp scp sftp
|
|
221
|
+
smb smbs smtp smtps telnet tftp ws wss
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
if Curl.const_defined?(string_option)
|
|
225
|
+
protocol_names.each do |name|
|
|
226
|
+
raise ArgumentError, "unsupported protocol: #{name.inspect}" unless valid_names.include?(name)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
setopt(Curl.const_get(string_option), protocol_names.join(','))
|
|
230
|
+
elsif Curl.const_defined?(bitmask_option)
|
|
231
|
+
protocol_pairs = protocol_names.map do |name|
|
|
232
|
+
const_name = "CURLPROTO_#{name.upcase}"
|
|
233
|
+
raise ArgumentError, "unsupported protocol: #{name.inspect}" unless Curl.const_defined?(const_name)
|
|
234
|
+
|
|
235
|
+
[name, Curl.const_get(const_name)]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
setopt(Curl.const_get(bitmask_option), protocol_pairs.inject(0) { |mask, pair| mask | pair.last })
|
|
239
|
+
else
|
|
240
|
+
raise NotImplementedError, "protocol allowlists are not supported by this libcurl"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
protocols
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
public
|
|
247
|
+
|
|
157
248
|
#
|
|
158
249
|
# call-seq:
|
|
159
250
|
# easy.perform => true
|
|
@@ -163,6 +254,7 @@ module Curl
|
|
|
163
254
|
# the configured HTTP Verb.
|
|
164
255
|
#
|
|
165
256
|
def perform
|
|
257
|
+
Curl.__send__(:apply_safety!, self) if Curl.respond_to?(:apply_safety!, true)
|
|
166
258
|
self.class.flush_deferred_multi_closes
|
|
167
259
|
|
|
168
260
|
if Curl.scheduler_active? && self.multi.nil?
|
|
@@ -210,6 +302,10 @@ module Curl
|
|
|
210
302
|
raise callback_error
|
|
211
303
|
end
|
|
212
304
|
|
|
305
|
+
if respond_to?(:unsafe_destination_error) && (unsafe_destination_error = self.unsafe_destination_error)
|
|
306
|
+
raise Curl::Err::UnsafeDestinationError, unsafe_destination_error
|
|
307
|
+
end
|
|
308
|
+
|
|
213
309
|
if self.last_result != 0 && self.on_failure.nil?
|
|
214
310
|
err_class, err_summary = Curl::Easy.error(self.last_result)
|
|
215
311
|
err_detail = self.last_error
|
|
@@ -641,11 +737,15 @@ module Curl
|
|
|
641
737
|
end
|
|
642
738
|
|
|
643
739
|
# call-seq:
|
|
644
|
-
# Curl::Easy.download(url, filename =
|
|
740
|
+
# Curl::Easy.download(url, filename = nil, options = {}) { |curl| ... }
|
|
645
741
|
#
|
|
646
742
|
# Stream the specified url (via perform) and save the data directly to the
|
|
647
|
-
# supplied filename
|
|
648
|
-
#
|
|
743
|
+
# supplied filename. The destination is written through a temporary file and
|
|
744
|
+
# existing files are not overwritten unless <tt>:overwrite => true</tt> is
|
|
745
|
+
# passed. When filename is omitted, the destination is safely derived from the
|
|
746
|
+
# last URL path component in the current directory. Pass
|
|
747
|
+
# <tt>:download_dir</tt> to treat the filename as a basename inside a trusted
|
|
748
|
+
# directory and reject absolute, parent-directory, dotfile, and nested names.
|
|
649
749
|
#
|
|
650
750
|
# If a block is supplied, it will be passed the curl instance prior to the
|
|
651
751
|
# perform call.
|
|
@@ -658,16 +758,11 @@ module Curl
|
|
|
658
758
|
# returns a size that differs from the data chunk size - in this case, the
|
|
659
759
|
# offending chunk will *not* be written to the file, the file will be closed,
|
|
660
760
|
# and a Curl::Err::AbortedByCallbackError will be raised.
|
|
661
|
-
def download(url, filename =
|
|
761
|
+
def download(url, filename = nil, download_options = {}, &blk)
|
|
662
762
|
curl = Curl::Easy.new(url, &blk)
|
|
763
|
+
_download_path, output, safe_output = Curl.prepare_download_output(url, filename, download_options)
|
|
663
764
|
|
|
664
|
-
|
|
665
|
-
filename.binmode if filename.respond_to?(:binmode)
|
|
666
|
-
filename
|
|
667
|
-
else
|
|
668
|
-
File.open(filename, 'wb')
|
|
669
|
-
end
|
|
670
|
-
|
|
765
|
+
performed = false
|
|
671
766
|
begin
|
|
672
767
|
old_on_body = curl.on_body do |data|
|
|
673
768
|
result = old_on_body ? old_on_body.call(data) : data.length
|
|
@@ -675,8 +770,13 @@ module Curl
|
|
|
675
770
|
result
|
|
676
771
|
end
|
|
677
772
|
curl.perform
|
|
773
|
+
performed = true
|
|
678
774
|
ensure
|
|
679
|
-
|
|
775
|
+
if safe_output
|
|
776
|
+
output.close(performed)
|
|
777
|
+
else
|
|
778
|
+
output.close rescue IOError
|
|
779
|
+
end
|
|
680
780
|
end
|
|
681
781
|
|
|
682
782
|
return curl
|