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/lib/curl.rb CHANGED
@@ -1,11 +1,412 @@
1
1
  # frozen_string_literal: true
2
2
  require 'curb_core'
3
+ require 'curl/download'
3
4
  require 'curl/easy'
4
5
  require 'curl/multi'
6
+ require 'ipaddr'
5
7
  require 'uri'
6
8
 
7
9
  # expose shortcut methods
8
10
  module Curl
11
+ class SafetyConfig
12
+ DEFAULT_PROTOCOLS = [:http, :https].freeze
13
+
14
+ attr_reader :max_body_bytes, :network_policy
15
+ attr_accessor :allow_proxies, :allow_resolve, :allow_connect_to, :allow_doh, :allow_unix_socket
16
+
17
+ def initialize
18
+ @protocols = DEFAULT_PROTOCOLS
19
+ @redirect_protocols = nil
20
+ @max_body_bytes = nil
21
+ @network_policy = nil
22
+ @allowed_hosts = nil
23
+ @allowed_proxy_hosts = nil
24
+ @allowed_cidrs = nil
25
+ @allow_proxies = false
26
+ @allow_resolve = false
27
+ @allow_connect_to = false
28
+ @allow_doh = false
29
+ @allow_unix_socket = false
30
+ end
31
+
32
+ def protocols
33
+ @protocols.dup
34
+ end
35
+
36
+ def protocols=(protocols)
37
+ @protocols = normalize_protocols(protocols)
38
+ end
39
+
40
+ def redirect_protocols
41
+ @redirect_protocols&.dup
42
+ end
43
+
44
+ def redirect_protocols=(protocols)
45
+ @redirect_protocols = protocols.nil? ? nil : normalize_protocols(protocols)
46
+ end
47
+
48
+ def allowed_hosts
49
+ @allowed_hosts&.map(&:dup)
50
+ end
51
+
52
+ def allowed_proxy_hosts
53
+ @allowed_proxy_hosts&.map(&:dup)
54
+ end
55
+
56
+ def allowed_cidrs
57
+ @allowed_cidrs&.map(&:dup)
58
+ end
59
+
60
+ def max_body_bytes=(bytes)
61
+ if bytes.nil?
62
+ @max_body_bytes = nil
63
+ return
64
+ end
65
+
66
+ bytes = Integer(bytes)
67
+ raise ArgumentError, "max_body_bytes must be greater than or equal to zero" if bytes < 0
68
+
69
+ @max_body_bytes = bytes.zero? ? nil : bytes
70
+ end
71
+
72
+ def network_policy=(policy)
73
+ @network_policy = normalize_network_policy(policy)
74
+ end
75
+
76
+ def allowed_hosts=(hosts)
77
+ @allowed_hosts = normalize_allowed_hosts(hosts)
78
+ end
79
+
80
+ def allowed_proxy_hosts=(hosts)
81
+ @allowed_proxy_hosts = normalize_allowed_hosts(hosts)
82
+ end
83
+
84
+ def allowed_cidrs=(cidrs)
85
+ @allowed_cidrs = normalize_allowed_cidrs(cidrs)
86
+ end
87
+
88
+ private
89
+
90
+ def normalize_protocols(protocols)
91
+ protocol_names = Array(protocols).map { |protocol| protocol.to_s.downcase.to_sym }
92
+ raise ArgumentError, "at least one protocol is required" if protocol_names.empty?
93
+
94
+ protocol_names
95
+ end
96
+
97
+ def normalize_network_policy(policy)
98
+ return nil if policy.nil?
99
+
100
+ policy_name = policy.to_s.downcase.to_sym
101
+ return policy_name if [:none, :public].include?(policy_name)
102
+
103
+ raise ArgumentError, "network_policy must be one of :none, :public"
104
+ end
105
+
106
+ def normalize_allowed_hosts(hosts)
107
+ list = normalize_optional_list(hosts)
108
+ return nil unless list
109
+
110
+ list.map { |host| normalize_allowed_host(host) }.uniq
111
+ end
112
+
113
+ def normalize_allowed_cidrs(cidrs)
114
+ list = normalize_optional_list(cidrs)
115
+ return nil unless list
116
+
117
+ list.map do |cidr|
118
+ cidr = cidr.to_s.strip
119
+ raise ArgumentError, "allowed_cidrs cannot include blank entries" if cidr.empty?
120
+
121
+ IPAddr.new(cidr)
122
+ cidr
123
+ end.uniq
124
+ rescue IPAddr::InvalidAddressError => e
125
+ raise ArgumentError, "invalid CIDR range: #{e.message}"
126
+ end
127
+
128
+ def normalize_optional_list(values)
129
+ return nil if values.nil?
130
+
131
+ list = Array(values)
132
+ return nil if list.empty?
133
+
134
+ list
135
+ end
136
+
137
+ def normalize_allowed_host(host)
138
+ host = host.to_s.strip.downcase
139
+ raise ArgumentError, "allowed_hosts cannot include blank entries" if host.empty?
140
+
141
+ parsed_host = begin
142
+ URI.parse(host).host if host.include?("://")
143
+ rescue URI::InvalidURIError
144
+ nil
145
+ end
146
+
147
+ host = parsed_host.downcase if parsed_host
148
+ host = normalize_host_authority(host) unless parsed_host
149
+ host = host[1...-1] if host.start_with?("[") && host.end_with?("]")
150
+ host = host.chomp(".")
151
+
152
+ raise ArgumentError, "allowed_hosts cannot include blank entries" if host.empty?
153
+
154
+ host
155
+ end
156
+
157
+ def normalize_host_authority(host)
158
+ authority = host.split(/[\/?#]/, 2).first
159
+ authority = authority[(authority.rindex("@") + 1)..-1] if authority.include?("@")
160
+
161
+ if authority.start_with?("[")
162
+ closing = authority.index("]")
163
+ raise ArgumentError, "invalid allowed host: #{host}" unless closing
164
+
165
+ return authority[1...closing]
166
+ end
167
+
168
+ if authority.count(":") <= 1
169
+ authority = authority.split(":", 2).first
170
+ end
171
+
172
+ authority
173
+ end
174
+ end
175
+
176
+ def self.safe!
177
+ config = SafetyConfig.new
178
+ yield config if block_given?
179
+ @safety_config = config
180
+ bump_safety_generation!
181
+ end
182
+
183
+ def self.apply_safety!(easy)
184
+ config = @safety_config
185
+ override = safety_override_for(easy)
186
+ return easy unless config || override
187
+
188
+ protocols = config&.protocols
189
+ redirect_protocols = config && (config.redirect_protocols || protocols)
190
+
191
+ if override
192
+ override_protocols = override[:protocols]
193
+ protocols = safety_protocol_intersection(protocols, override_protocols) if override_protocols
194
+
195
+ override_redirect_protocols = override[:redirect_protocols] || override_protocols
196
+ redirect_protocols = safety_protocol_intersection(redirect_protocols, override_redirect_protocols) if override_redirect_protocols
197
+ end
198
+
199
+ if protocols
200
+ easy.allowed_protocols = protocols
201
+ easy.allowed_redirect_protocols = redirect_protocols || protocols
202
+ end
203
+
204
+ apply_allowed_hosts!(easy, config.allowed_hosts) if config&.allowed_hosts
205
+ apply_allowed_cidrs!(easy, config) if config&.allowed_cidrs
206
+
207
+ if config&.network_policy
208
+ easy.network_policy = config.network_policy
209
+ apply_public_network_policy_controls!(easy, config) if config.network_policy == :public
210
+ end
211
+
212
+ apply_max_body_bytes!(easy, config.max_body_bytes) if config&.max_body_bytes
213
+ apply_max_body_bytes!(easy, override[:max_body_bytes]) if override && override.key?(:max_body_bytes)
214
+ easy
215
+ end
216
+
217
+ def self.clear_safe!
218
+ @safety_config = nil
219
+ bump_safety_generation!
220
+ end
221
+
222
+ def self.safety_active_for?(easy)
223
+ !!(@safety_config || safety_override_for(easy))
224
+ end
225
+
226
+ def self.safety_signature_for(easy)
227
+ return nil unless safety_active_for?(easy)
228
+
229
+ override_generation = if easy.respond_to?(:__curb_safety_override_generation, true)
230
+ easy.__send__(:__curb_safety_override_generation)
231
+ end
232
+
233
+ [safety_generation, override_generation.to_i]
234
+ end
235
+
236
+ def self.safety_override_for(easy)
237
+ if easy.respond_to?(:__curb_safety_override, true)
238
+ easy.__send__(:__curb_safety_override)
239
+ end
240
+ end
241
+
242
+ def self.safety_generation
243
+ @safety_generation ||= 0
244
+ end
245
+
246
+ def self.bump_safety_generation!
247
+ @safety_generation = safety_generation + 1
248
+ end
249
+
250
+ def self.safety_protocol_intersection(base_protocols, override_protocols)
251
+ return override_protocols unless base_protocols
252
+
253
+ protocols = override_protocols & base_protocols
254
+ raise ArgumentError, "safety policies allow no protocols" if protocols.empty?
255
+
256
+ protocols
257
+ end
258
+
259
+ def self.apply_max_body_bytes!(easy, max_body_bytes)
260
+ return unless max_body_bytes
261
+
262
+ current_max_body_bytes = easy.max_body_bytes
263
+ easy.max_body_bytes = max_body_bytes if current_max_body_bytes.nil? || current_max_body_bytes > max_body_bytes
264
+ end
265
+
266
+ def self.apply_public_network_policy_controls!(easy, config)
267
+ reject_resolve_override!(easy) unless config.allow_resolve
268
+ reject_connect_to_override!(easy) unless config.allow_connect_to
269
+ reject_doh_override!(easy) unless config.allow_doh
270
+ reject_dns_servers_override!(easy)
271
+ allow_proxy = config.allow_proxies || !!config.allowed_proxy_hosts
272
+ easy.__send__(:__curb_allow_proxy=, allow_proxy) if easy.respond_to?(:__curb_allow_proxy=, true)
273
+ easy.__send__(:__curb_allow_unix_socket=, config.allow_unix_socket) if easy.respond_to?(:__curb_allow_unix_socket=, true)
274
+ reject_unix_socket_override!(easy) unless config.allow_unix_socket
275
+ if config.allowed_proxy_hosts
276
+ apply_allowed_proxy!(easy, config.allowed_proxy_hosts)
277
+ elsif !config.allow_proxies
278
+ disable_proxy!(easy)
279
+ end
280
+ end
281
+
282
+ def self.apply_allowed_hosts!(easy, allowed_hosts)
283
+ if easy.respond_to?(:follow_location?) && easy.follow_location? &&
284
+ !Curl.const_defined?(:CURLOPT_PREREQFUNCTION)
285
+ raise NotImplementedError, "redirect-aware host allowlists require CURLOPT_PREREQFUNCTION support"
286
+ end
287
+
288
+ host = begin
289
+ URI.parse(easy.url.to_s).host
290
+ rescue URI::InvalidURIError
291
+ nil
292
+ end
293
+
294
+ normalized_host = host.to_s.downcase.chomp(".")
295
+ normalized_host = normalized_host[1...-1] if normalized_host.start_with?("[") && normalized_host.end_with?("]")
296
+
297
+ unless allowed_hosts.include?(normalized_host)
298
+ raise Curl::Err::UnsafeDestinationError,
299
+ "URL host #{host.inspect} is not allowed by safe mode host allowlist"
300
+ end
301
+
302
+ easy.allowed_hosts = allowed_hosts if easy.respond_to?(:allowed_hosts=)
303
+ end
304
+
305
+ def self.apply_allowed_cidrs!(easy, config)
306
+ unless config.network_policy == :public
307
+ raise ArgumentError, "allowed_cidrs require network_policy = :public"
308
+ end
309
+
310
+ easy.allowed_cidrs = config.allowed_cidrs if easy.respond_to?(:allowed_cidrs=)
311
+ end
312
+
313
+ def self.apply_allowed_proxy!(easy, allowed_proxy_hosts)
314
+ proxy_url = easy.proxy_url if easy.respond_to?(:proxy_url)
315
+ if proxy_url.nil? || proxy_url.to_s.empty?
316
+ disable_proxy!(easy)
317
+ return
318
+ end
319
+
320
+ proxy_host = normalize_proxy_host(proxy_url)
321
+ unless allowed_proxy_hosts.include?(proxy_host)
322
+ raise Curl::Err::UnsafeDestinationError,
323
+ "proxy host #{proxy_host.inspect} is not allowed by safe mode proxy allowlist"
324
+ end
325
+
326
+ easy.set(Curl::CURLOPT_NOPROXY, "") if Curl.const_defined?(:CURLOPT_NOPROXY)
327
+ end
328
+
329
+ def self.normalize_proxy_host(proxy_url)
330
+ value = proxy_url.to_s.strip
331
+ raise Curl::Err::UnsafeDestinationError, "proxy URL is empty" if value.empty?
332
+
333
+ uri = begin
334
+ URI.parse(value.include?("://") ? value : "http://#{value}")
335
+ rescue URI::InvalidURIError
336
+ nil
337
+ end
338
+
339
+ host = uri&.host
340
+ raise Curl::Err::UnsafeDestinationError, "proxy URL host is invalid" if host.nil? || host.empty?
341
+
342
+ host = host.downcase.chomp(".")
343
+ host = host[1...-1] if host.start_with?("[") && host.end_with?("]")
344
+ host
345
+ end
346
+
347
+ def self.reject_resolve_override!(easy)
348
+ return unless easy.respond_to?(:resolve)
349
+
350
+ resolve = easy.resolve
351
+ return if resolve.nil? || (resolve.respond_to?(:empty?) && resolve.empty?)
352
+
353
+ raise Curl::Err::UnsafeDestinationError, "resolve overrides are disabled by public network policy"
354
+ end
355
+
356
+ def self.reject_connect_to_override!(easy)
357
+ return unless easy.respond_to?(:connect_to)
358
+
359
+ connect_to = easy.connect_to
360
+ return if connect_to.nil? || (connect_to.respond_to?(:empty?) && connect_to.empty?)
361
+
362
+ raise Curl::Err::UnsafeDestinationError, "connect_to overrides are disabled by public network policy"
363
+ end
364
+
365
+ def self.reject_doh_override!(easy)
366
+ return unless easy.respond_to?(:doh_url)
367
+
368
+ doh_url = easy.doh_url
369
+ return if doh_url.nil? || (doh_url.respond_to?(:empty?) && doh_url.empty?)
370
+
371
+ raise Curl::Err::UnsafeDestinationError, "DoH URL overrides are disabled by public network policy"
372
+ end
373
+
374
+ def self.reject_dns_servers_override!(easy)
375
+ return unless easy.respond_to?(:dns_servers)
376
+
377
+ dns_servers = easy.dns_servers
378
+ return if dns_servers.nil? || (dns_servers.respond_to?(:empty?) && dns_servers.empty?)
379
+
380
+ raise Curl::Err::UnsafeDestinationError, "DNS server overrides are disabled by public network policy"
381
+ end
382
+
383
+ def self.reject_unix_socket_override!(easy)
384
+ return unless easy.respond_to?(:unix_socket_path)
385
+
386
+ unix_socket_path = easy.unix_socket_path
387
+ return if unix_socket_path.nil? || (unix_socket_path.respond_to?(:empty?) && unix_socket_path.empty?)
388
+
389
+ raise Curl::Err::UnsafeDestinationError, "Unix socket paths are disabled by public network policy"
390
+ end
391
+
392
+ def self.disable_proxy!(easy)
393
+ easy.proxy_url = "" if easy.respond_to?(:proxy_url=)
394
+ easy.proxy_tunnel = false if easy.respond_to?(:proxy_tunnel=)
395
+ easy.set(Curl::CURLOPT_NOPROXY, "*") if Curl.const_defined?(:CURLOPT_NOPROXY)
396
+ end
397
+
398
+ private_class_method :apply_safety!, :clear_safe!, :safety_active_for?,
399
+ :safety_signature_for, :safety_override_for,
400
+ :safety_generation, :bump_safety_generation!,
401
+ :safety_protocol_intersection,
402
+ :apply_max_body_bytes!, :apply_public_network_policy_controls!,
403
+ :apply_allowed_hosts!, :apply_allowed_cidrs!,
404
+ :apply_allowed_proxy!, :normalize_proxy_host,
405
+ :reject_resolve_override!, :reject_connect_to_override!,
406
+ :reject_doh_override!, :reject_dns_servers_override!,
407
+ :reject_unix_socket_override!,
408
+ :disable_proxy!
409
+
9
410
  def self.scheduler_active?
10
411
  Fiber.respond_to?(:scheduler) && !Fiber.scheduler.nil?
11
412
  end
@@ -201,52 +602,84 @@ module Curl
201
602
  end
202
603
 
203
604
  def self.http(verb, url, post_body=nil, put_data=nil, &block)
204
- if Thread.current[:curb_curl_yielding]
205
- handle = Curl::Easy.new # we can't reuse this
206
- else
207
- handle = Thread.current[:curb_curl] ||= Curl::Easy.new
208
- handle.reset
209
- end
605
+ handle = Curl::Easy.new
210
606
  handle.url = url
211
607
  handle.post_body = post_body if post_body
212
608
  handle.put_data = put_data if put_data
213
- if block_given?
214
- Thread.current[:curb_curl_yielding] = true
215
- yield handle
216
- Thread.current[:curb_curl_yielding] = false
217
- end
609
+ yield handle if block_given?
218
610
  handle.http(verb)
219
611
  handle
220
612
  end
221
613
 
614
+ def self.safe_http(verb, url, post_body=nil, put_data=nil, options={}, &block)
615
+ options = safe_http_options(options)
616
+
617
+ http(verb, url, post_body, put_data) do |handle|
618
+ yield handle if block
619
+ handle.safe_http!
620
+ handle.max_body_bytes = options[:max_body_bytes] if options.key?(:max_body_bytes)
621
+ end
622
+ end
623
+
222
624
  def self.get(url, params={}, &block)
223
625
  http :GET, urlalize(url, params), nil, nil, &block
224
626
  end
225
627
 
628
+ def self.safe_get(url, params={}, options={}, &block)
629
+ params, options = split_safe_http_params_options(params, options)
630
+ safe_http :GET, urlalize(url, params), nil, nil, options, &block
631
+ end
632
+
226
633
  def self.post(url, params={}, &block)
227
634
  http :POST, url, postalize(params), nil, &block
228
635
  end
229
636
 
637
+ def self.safe_post(url, params={}, options={}, &block)
638
+ safe_http :POST, url, postalize(params), nil, options, &block
639
+ end
640
+
230
641
  def self.put(url, params={}, &block)
231
642
  http :PUT, url, nil, postalize(params), &block
232
643
  end
233
644
 
645
+ def self.safe_put(url, params={}, options={}, &block)
646
+ safe_http :PUT, url, nil, postalize(params), options, &block
647
+ end
648
+
234
649
  def self.delete(url, params={}, &block)
235
650
  http :DELETE, url, postalize(params), nil, &block
236
651
  end
237
652
 
653
+ def self.safe_delete(url, params={}, options={}, &block)
654
+ safe_http :DELETE, url, postalize(params), nil, options, &block
655
+ end
656
+
238
657
  def self.patch(url, params={}, &block)
239
658
  http :PATCH, url, postalize(params), nil, &block
240
659
  end
241
660
 
661
+ def self.safe_patch(url, params={}, options={}, &block)
662
+ safe_http :PATCH, url, postalize(params), nil, options, &block
663
+ end
664
+
242
665
  def self.head(url, params={}, &block)
243
666
  http :HEAD, urlalize(url, params), nil, nil, &block
244
667
  end
245
668
 
669
+ def self.safe_head(url, params={}, options={}, &block)
670
+ params, options = split_safe_http_params_options(params, options)
671
+ safe_http :HEAD, urlalize(url, params), nil, nil, options, &block
672
+ end
673
+
246
674
  def self.options(url, params={}, &block)
247
675
  http :OPTIONS, urlalize(url, params), nil, nil, &block
248
676
  end
249
677
 
678
+ def self.safe_options(url, params={}, options={}, &block)
679
+ params, options = split_safe_http_params_options(params, options)
680
+ safe_http :OPTIONS, urlalize(url, params), nil, nil, options, &block
681
+ end
682
+
250
683
  def self.urlalize(url, params={})
251
684
  uri = URI(url)
252
685
  # early return if we didn't specify any extra params
@@ -261,6 +694,33 @@ module Curl
261
694
  params.respond_to?(:map) ? URI.encode_www_form(params) : (params.respond_to?(:to_s) ? params.to_s : params)
262
695
  end
263
696
 
697
+ def self.safe_http_options(options)
698
+ options ||= {}
699
+ raise ArgumentError, "safe HTTP options must be a Hash" unless options.is_a?(Hash)
700
+
701
+ options = options.dup
702
+ unsupported = options.keys - safe_http_option_keys
703
+ raise ArgumentError, "unsupported safe HTTP option(s): #{unsupported.join(', ')}" unless unsupported.empty?
704
+
705
+ options
706
+ end
707
+
708
+ def self.split_safe_http_params_options(params, options)
709
+ if options == {} && safe_http_option_hash?(params)
710
+ [{}, params]
711
+ else
712
+ [params, options]
713
+ end
714
+ end
715
+
716
+ def self.safe_http_option_hash?(value)
717
+ value.is_a?(Hash) && !value.empty? && (value.keys - safe_http_option_keys).empty?
718
+ end
719
+
720
+ def self.safe_http_option_keys
721
+ [:max_body_bytes]
722
+ end
723
+
264
724
  def self.reset
265
725
  Thread.current[:curb_curl] = Curl::Easy.new
266
726
  end
@@ -0,0 +1,29 @@
1
+ require 'digest'
2
+ require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
3
+
4
+ class BugTestPoisonResponse < Test::Unit::TestCase
5
+ include BugTestServerSetupTeardown
6
+
7
+ def setup
8
+ @port = unused_local_port
9
+ @response_proc = lambda do|res|
10
+ sleep 0.5
11
+ res.body = "hi"
12
+ res['Content-Type'] = "text/html"
13
+ end
14
+ super
15
+ end
16
+
17
+ def test_bug
18
+ res_a = Curl.get("https://google.com/")
19
+
20
+ first_a_body = Digest::MD5.hexdigest(res_a.body)
21
+
22
+ res_b = Curl.get("https://unclanked.com/")
23
+
24
+ b_body = Digest::MD5.hexdigest(res_b.body)
25
+ a_body = Digest::MD5.hexdigest(res_a.body)
26
+ assert_not_equal a_body, b_body, "we expected #{a_body} to be #{first_a_body}"
27
+ end
28
+
29
+ end
@@ -1,4 +1,5 @@
1
1
  require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
+ require 'tmpdir'
2
3
 
3
4
  class TestCurbCurlDownload < Test::Unit::TestCase
4
5
  include TestServerMethods
@@ -18,6 +19,91 @@ class TestCurbCurlDownload < Test::Unit::TestCase
18
19
  File.unlink(dl_path) if File.exist?(dl_path)
19
20
  end
20
21
 
22
+ def test_download_default_path_does_not_overwrite_existing_file
23
+ dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
24
+
25
+ Dir.mktmpdir('curb-download-') do |dir|
26
+ Dir.chdir(dir) do
27
+ File.write('curb_easy.c', 'sentinel')
28
+
29
+ assert_raise(Curl::DownloadTargetExistsError) do
30
+ Curl::Easy.download(dl_url)
31
+ end
32
+
33
+ assert_equal 'sentinel', File.read('curb_easy.c')
34
+ end
35
+ end
36
+ end
37
+
38
+ def test_download_default_path_strips_query_from_url_basename
39
+ dl_url = "http://127.0.0.1:9129/ext/curb_easy.c?cache=1"
40
+ source_path = File.expand_path(File.join(File.dirname(__FILE__), '..','ext','curb_easy.c'))
41
+
42
+ Dir.mktmpdir('curb-download-') do |dir|
43
+ Dir.chdir(dir) do
44
+ Curl::Easy.download(dl_url)
45
+
46
+ assert File.exist?('curb_easy.c')
47
+ assert !File.exist?('curb_easy.c?cache=1')
48
+ assert_equal File.read(source_path), File.read('curb_easy.c')
49
+ end
50
+ end
51
+ end
52
+
53
+ def test_download_safe_directory_rejects_unsafe_names
54
+ dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
55
+
56
+ Dir.mktmpdir('curb-download-') do |dir|
57
+ ['../escape.c', '.env', 'nested/file', '/tmp/escape.c', "bad\0name"].each do |name|
58
+ assert_raise(ArgumentError, "expected #{name.inspect} to be rejected") do
59
+ Curl::Easy.download(dl_url, name, :download_dir => dir)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def test_download_safe_directory_rejects_dotfile_from_url
66
+ dl_url = "http://127.0.0.1:9129/ext/.env"
67
+
68
+ Dir.mktmpdir('curb-download-') do |dir|
69
+ assert_raise(ArgumentError) do
70
+ Curl::Easy.download(dl_url, :download_dir => dir)
71
+ end
72
+
73
+ assert !File.exist?(File.join(dir, '.env'))
74
+ end
75
+ end
76
+
77
+ def test_download_overwrite_true_replaces_existing_file
78
+ dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
79
+
80
+ Dir.mktmpdir('curb-download-') do |dir|
81
+ dl_path = File.join(dir, 'curb_easy.c')
82
+ File.write(dl_path, 'sentinel')
83
+
84
+ Curl::Easy.download(dl_url, dl_path, :overwrite => true)
85
+
86
+ assert_equal File.read(File.join(File.dirname(__FILE__), '..','ext','curb_easy.c')), File.read(dl_path)
87
+ end
88
+ end
89
+
90
+ def test_download_callback_abort_does_not_replace_existing_file
91
+ dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
92
+
93
+ Dir.mktmpdir('curb-download-') do |dir|
94
+ dl_path = File.join(dir, 'curb_easy.c')
95
+ File.write(dl_path, 'sentinel')
96
+
97
+ assert_raise(Curl::Err::WriteError, Curl::Err::RecvError) do
98
+ Curl::Easy.download(dl_url, dl_path, :overwrite => true) do |curl|
99
+ curl.on_body { |_data| 0 }
100
+ end
101
+ end
102
+
103
+ assert_equal 'sentinel', File.read(dl_path)
104
+ end
105
+ end
106
+
21
107
  def test_download_url_to_file_via_file_io
22
108
  dl_url = "http://127.0.0.1:9129/ext/curb_easy.c"
23
109
  dl_path = File.join(Dir::tmpdir, "dl_url_test.file")