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/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
|
-
|
|
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
|
data/tests/bug_poison.rb
ADDED
|
@@ -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
|
data/tests/tc_curl_download.rb
CHANGED
|
@@ -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")
|