curb 1.3.5 → 1.3.7

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_multi.h CHANGED
@@ -28,6 +28,7 @@ extern const rb_data_type_t ruby_curl_multi_data_type;
28
28
 
29
29
  void init_curb_multi();
30
30
  void rb_curl_multi_forget_easy(ruby_curl_multi *rbcm, void *rbce_ptr);
31
+ CURLMcode rb_curl_multi_detach_easy(ruby_curl_multi *rbcm, void *rbce_ptr);
31
32
 
32
33
 
33
34
  #endif
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"
@@ -459,6 +462,9 @@ have_constant "curlopt_ftp_use_eprt"
459
462
  have_constant "curlopt_ftp_use_epsv"
460
463
  have_constant "curlopt_ftp_use_pret"
461
464
  have_constant "curlopt_ftp_create_missing_dirs"
465
+ have_constant "curlftp_create_dir_none"
466
+ have_constant "curlftp_create_dir"
467
+ have_constant "curlftp_create_dir_retry"
462
468
  have_constant "curlopt_ftp_response_timeout"
463
469
  have_constant "curlopt_ftp_alternative_to_user"
464
470
  have_constant "curlopt_ftp_skip_pasv_ip"
@@ -531,6 +537,7 @@ have_constant "curlusessl_none"
531
537
  have_constant "curlusessl_try"
532
538
  have_constant "curlusessl_control"
533
539
  have_constant "curlusessl_all"
540
+ have_constant "curlopt_connect_to"
534
541
  have_constant "curlopt_resolve"
535
542
  have_constant "curlopt_request_target"
536
543
  have_constant "curlopt_sslcert"
@@ -558,6 +565,10 @@ have_constant :CURL_SSLVERSION_TLSv1_2
558
565
  have_constant :CURL_SSLVERSION_TLSv1_3
559
566
 
560
567
  have_constant "curlopt_ssl_verifypeer"
568
+ have_constant "curlopt_doh_url"
569
+ have_constant "curlopt_doh_ssl_verifypeer"
570
+ have_constant "curlopt_doh_ssl_verifyhost"
571
+ have_constant "curlopt_doh_ssl_verifystatus"
561
572
  have_constant "curlopt_cainfo"
562
573
  have_constant "curlopt_issuercert"
563
574
  have_constant "curlopt_capath"
@@ -692,6 +703,7 @@ have_header('ruby/fiber/scheduler.h')
692
703
  have_func('rb_fiber_scheduler_current', 'ruby/fiber/scheduler.h')
693
704
  have_func('rb_fiber_scheduler_io_wait', 'ruby/fiber/scheduler.h')
694
705
  have_func('rb_fiber_scheduler_io_select', 'ruby/fiber/scheduler.h')
706
+ have_func('rb_fiber_scheduler_kernel_sleep', 'ruby/fiber/scheduler.h')
695
707
  have_func('rb_io_stdio_file')
696
708
  have_func('curl_multi_wait')
697
709
  have_func('curl_multi_socket_action')
@@ -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 = url.split(/\?/).first.split(/\//).last) { |curl| ... }
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 (defaults to the last component of the URL path, which will
648
- # usually be the filename most simple urls).
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 = url.split(/\?/).first.split(/\//).last, &blk)
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
- output = if filename.is_a? IO
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
- output.close rescue IOError
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