curb 0.8.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 +7 -0
- data/README.md +579 -0
- data/Rakefile +85 -27
- data/doc.rb +48 -8
- data/ext/banned.h +32 -0
- data/ext/curb.c +563 -233
- data/ext/curb.h +19 -10
- data/ext/curb_easy.c +3229 -368
- data/ext/curb_easy.h +42 -0
- data/ext/curb_errors.c +117 -18
- data/ext/curb_errors.h +9 -5
- data/ext/curb_macros.h +33 -21
- data/ext/curb_multi.c +1904 -272
- data/ext/curb_multi.h +11 -3
- data/ext/curb_postfield.c +149 -77
- data/ext/curb_postfield.h +1 -0
- data/ext/curb_upload.c +38 -11
- data/ext/curb_upload.h +2 -0
- data/ext/extconf.rb +355 -35
- data/lib/curb.rb +1 -0
- data/lib/curl/download.rb +160 -0
- data/lib/curl/easy.rb +422 -88
- data/lib/curl/multi.rb +295 -56
- data/lib/curl.rb +676 -11
- data/tests/bug_crash_on_debug.rb +14 -28
- data/tests/bug_crash_on_progress.rb +32 -16
- data/tests/bug_curb_easy_blocks_ruby_threads.rb +10 -15
- data/tests/bug_curb_easy_post_with_string_no_content_length_header.rb +6 -30
- data/tests/bug_follow_redirect_288.rb +83 -0
- data/tests/bug_instance_post_differs_from_class_post.rb +3 -5
- data/tests/bug_issue102.rb +17 -0
- data/tests/bug_issue_noproxy.rb +56 -0
- data/tests/bug_issue_post_redirect.rb +93 -0
- data/tests/bug_issue_spnego.rb +41 -0
- data/tests/bug_multi_segfault.rb +1 -0
- data/tests/bug_poison.rb +29 -0
- data/tests/bug_raise_on_callback.rb +30 -0
- data/tests/helper.rb +400 -44
- data/tests/leak_trace.rb +237 -0
- data/tests/mem_check.rb +3 -0
- data/tests/tc_curl.rb +31 -1
- data/tests/tc_curl_download.rb +98 -7
- data/tests/tc_curl_easy.rb +985 -66
- data/tests/tc_curl_easy_cookielist.rb +277 -0
- data/tests/tc_curl_easy_request_target.rb +41 -0
- data/tests/tc_curl_easy_resolve.rb +48 -0
- data/tests/tc_curl_maxfilesize.rb +212 -0
- data/tests/tc_curl_multi.rb +1111 -51
- data/tests/tc_curl_native_coverage.rb +145 -0
- data/tests/tc_curl_network_policy.rb +1475 -0
- data/tests/tc_curl_postfield.rb +207 -30
- data/tests/tc_curl_protocols.rb +388 -0
- data/tests/tc_fiber_scheduler.rb +584 -0
- data/tests/tc_ftp_options.rb +39 -0
- data/tests/tc_gc_compact.rb +223 -0
- data/tests/tc_test_server_methods.rb +110 -0
- data/tests/timeout.rb +30 -6
- metadata +66 -31
- data/README +0 -194
data/lib/curl.rb
CHANGED
|
@@ -1,14 +1,608 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
require 'curb_core'
|
|
3
|
+
require 'curl/download'
|
|
2
4
|
require 'curl/easy'
|
|
3
5
|
require 'curl/multi'
|
|
6
|
+
require 'ipaddr'
|
|
4
7
|
require 'uri'
|
|
5
8
|
|
|
6
9
|
# expose shortcut methods
|
|
7
10
|
module Curl
|
|
8
|
-
|
|
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
|
+
|
|
410
|
+
def self.scheduler_active?
|
|
411
|
+
Fiber.respond_to?(:scheduler) && !Fiber.scheduler.nil?
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def self.deferred_exception_source_id(state)
|
|
415
|
+
return unless state[:multi].instance_variable_defined?(:@__curb_deferred_exception_source_id)
|
|
416
|
+
|
|
417
|
+
state[:multi].instance_variable_get(:@__curb_deferred_exception_source_id)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def self.scheduler_waiter_blocking_supported?
|
|
421
|
+
scheduler = Fiber.scheduler
|
|
422
|
+
scheduler && scheduler.respond_to?(:block) && scheduler.respond_to?(:unblock)
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def self.wake_scheduler_waiter(waiter)
|
|
426
|
+
fiber = waiter[:fiber]
|
|
427
|
+
scheduler = waiter[:scheduler]
|
|
428
|
+
return unless fiber&.alive? && scheduler&.respond_to?(:unblock)
|
|
429
|
+
|
|
430
|
+
scheduler.unblock(waiter, fiber)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def self.complete_scheduler_waiter(waiter)
|
|
434
|
+
return if waiter[:done]
|
|
435
|
+
|
|
436
|
+
waiter[:done] = true
|
|
437
|
+
wake_scheduler_waiter(waiter)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def self.fail_scheduler_waiter(waiter, error)
|
|
441
|
+
return if waiter[:error]
|
|
442
|
+
|
|
443
|
+
waiter[:error] = error
|
|
444
|
+
wake_scheduler_waiter(waiter)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def self.release_scheduler_error(state, error)
|
|
448
|
+
source_waiter = state[:waiters][deferred_exception_source_id(state)]
|
|
449
|
+
|
|
450
|
+
if source_waiter
|
|
451
|
+
fail_scheduler_waiter(source_waiter, error)
|
|
452
|
+
else
|
|
453
|
+
state[:error] = error
|
|
454
|
+
state[:waiters].each_value { |waiter| wake_scheduler_waiter(waiter) }
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def self.block_scheduler_waiter(waiter)
|
|
459
|
+
unless scheduler_waiter_blocking_supported?
|
|
460
|
+
sleep 0
|
|
461
|
+
return
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
waiter[:fiber] = Fiber.current
|
|
465
|
+
waiter[:scheduler] ||= Fiber.scheduler
|
|
466
|
+
return if waiter[:done] || waiter[:error]
|
|
467
|
+
|
|
468
|
+
waiter[:scheduler].block(waiter, nil)
|
|
469
|
+
ensure
|
|
470
|
+
waiter[:fiber] = nil if waiter[:fiber].equal?(Fiber.current)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def self.scheduler_yield
|
|
474
|
+
scheduler = Fiber.scheduler
|
|
475
|
+
|
|
476
|
+
if scheduler&.respond_to?(:kernel_sleep)
|
|
477
|
+
scheduler.kernel_sleep(0)
|
|
478
|
+
else
|
|
479
|
+
sleep 0
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def self.release_scheduler_waiters(state)
|
|
484
|
+
source_id = deferred_exception_source_id(state)
|
|
485
|
+
|
|
486
|
+
state[:waiters].each do |easy_id, waiter|
|
|
487
|
+
next if source_id == easy_id
|
|
488
|
+
|
|
489
|
+
complete_scheduler_waiter(waiter) if waiter[:completed]
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def self.perform_with_scheduler(easy)
|
|
494
|
+
state = scheduler_state
|
|
495
|
+
waiter = {completed: false, done: false, error: nil, fiber: nil, scheduler: Fiber.scheduler}
|
|
496
|
+
state[:waiters][easy.object_id] = waiter
|
|
497
|
+
previous_complete = easy.on_complete do |completed_easy|
|
|
498
|
+
previous_complete.call(completed_easy) if previous_complete
|
|
499
|
+
waiter[:completed] = true
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
state[:pending] << easy
|
|
503
|
+
ensure_scheduler_driver(state)
|
|
504
|
+
|
|
505
|
+
until waiter[:done]
|
|
506
|
+
raise waiter[:error] if waiter[:error]
|
|
507
|
+
raise state[:error] if state[:error]
|
|
508
|
+
block_scheduler_waiter(waiter)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
while state[:driver_running] && state[:pending].empty? &&
|
|
512
|
+
state[:waiters].length == 1 && state[:waiters].key?(easy.object_id)
|
|
513
|
+
scheduler_yield
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
true
|
|
517
|
+
ensure
|
|
518
|
+
state[:waiters].delete(easy.object_id) if defined?(state) && state[:waiters]
|
|
519
|
+
if defined?(previous_complete)
|
|
520
|
+
if previous_complete
|
|
521
|
+
easy.on_complete(&previous_complete)
|
|
522
|
+
else
|
|
523
|
+
easy.on_complete
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def self.scheduler_state
|
|
529
|
+
Thread.current.thread_variable_get(:curb_scheduler_state) || begin
|
|
530
|
+
state = {
|
|
531
|
+
multi: Curl::Multi.new,
|
|
532
|
+
pending: [],
|
|
533
|
+
driver_running: false,
|
|
534
|
+
error: nil,
|
|
535
|
+
waiters: {},
|
|
536
|
+
}
|
|
537
|
+
Thread.current.thread_variable_set(:curb_scheduler_state, state)
|
|
538
|
+
state
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def self.ensure_scheduler_driver(state)
|
|
543
|
+
return if state[:driver_running]
|
|
544
|
+
|
|
545
|
+
state[:driver_running] = true
|
|
546
|
+
state[:error] = nil
|
|
547
|
+
|
|
548
|
+
runner = proc do
|
|
549
|
+
begin
|
|
550
|
+
# Give sibling fibers a chance to enqueue work so the shared multi can
|
|
551
|
+
# batch scheduler-driven Easy#perform calls together.
|
|
552
|
+
pending_count = -1
|
|
553
|
+
until pending_count == state[:pending].size
|
|
554
|
+
pending_count = state[:pending].size
|
|
555
|
+
scheduler_yield
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
loop do
|
|
559
|
+
drain_scheduler_pending(state)
|
|
560
|
+
break if state[:multi].idle?
|
|
561
|
+
|
|
562
|
+
begin
|
|
563
|
+
state[:multi].perform do
|
|
564
|
+
drain_scheduler_pending(state)
|
|
565
|
+
release_scheduler_waiters(state)
|
|
566
|
+
scheduler_yield
|
|
567
|
+
end
|
|
568
|
+
ensure
|
|
569
|
+
# Release any siblings that completed just before a deferred
|
|
570
|
+
# callback exception is re-raised.
|
|
571
|
+
release_scheduler_waiters(state)
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
rescue => e
|
|
575
|
+
release_scheduler_waiters(state)
|
|
576
|
+
release_scheduler_error(state, e)
|
|
577
|
+
ensure
|
|
578
|
+
state[:driver_running] = false
|
|
579
|
+
ensure_scheduler_driver(state) if state[:error].nil? && !state[:pending].empty?
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
if Fiber.respond_to?(:schedule)
|
|
584
|
+
Fiber.schedule(&runner)
|
|
585
|
+
else
|
|
586
|
+
Fiber.new(blocking: false, &runner).resume
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def self.drain_scheduler_pending(state)
|
|
591
|
+
pending = state[:pending]
|
|
592
|
+
until pending.empty?
|
|
593
|
+
easy = pending.first
|
|
594
|
+
|
|
595
|
+
break if state[:multi].instance_variable_defined?(:@__curb_deferred_exception)
|
|
596
|
+
|
|
597
|
+
state[:multi].add(easy)
|
|
598
|
+
break unless state[:multi].requests.key?(easy.object_id)
|
|
599
|
+
|
|
600
|
+
pending.shift
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
9
604
|
def self.http(verb, url, post_body=nil, put_data=nil, &block)
|
|
10
|
-
handle =
|
|
11
|
-
handle.reset
|
|
605
|
+
handle = Curl::Easy.new
|
|
12
606
|
handle.url = url
|
|
13
607
|
handle.post_body = post_body if post_body
|
|
14
608
|
handle.put_data = put_data if put_data
|
|
@@ -17,47 +611,118 @@ module Curl
|
|
|
17
611
|
handle
|
|
18
612
|
end
|
|
19
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
|
+
|
|
20
624
|
def self.get(url, params={}, &block)
|
|
21
625
|
http :GET, urlalize(url, params), nil, nil, &block
|
|
22
626
|
end
|
|
23
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
|
+
|
|
24
633
|
def self.post(url, params={}, &block)
|
|
25
634
|
http :POST, url, postalize(params), nil, &block
|
|
26
635
|
end
|
|
27
636
|
|
|
637
|
+
def self.safe_post(url, params={}, options={}, &block)
|
|
638
|
+
safe_http :POST, url, postalize(params), nil, options, &block
|
|
639
|
+
end
|
|
640
|
+
|
|
28
641
|
def self.put(url, params={}, &block)
|
|
29
642
|
http :PUT, url, nil, postalize(params), &block
|
|
30
643
|
end
|
|
31
644
|
|
|
645
|
+
def self.safe_put(url, params={}, options={}, &block)
|
|
646
|
+
safe_http :PUT, url, nil, postalize(params), options, &block
|
|
647
|
+
end
|
|
648
|
+
|
|
32
649
|
def self.delete(url, params={}, &block)
|
|
33
650
|
http :DELETE, url, postalize(params), nil, &block
|
|
34
651
|
end
|
|
35
652
|
|
|
653
|
+
def self.safe_delete(url, params={}, options={}, &block)
|
|
654
|
+
safe_http :DELETE, url, postalize(params), nil, options, &block
|
|
655
|
+
end
|
|
656
|
+
|
|
36
657
|
def self.patch(url, params={}, &block)
|
|
37
658
|
http :PATCH, url, postalize(params), nil, &block
|
|
38
659
|
end
|
|
39
660
|
|
|
661
|
+
def self.safe_patch(url, params={}, options={}, &block)
|
|
662
|
+
safe_http :PATCH, url, postalize(params), nil, options, &block
|
|
663
|
+
end
|
|
664
|
+
|
|
40
665
|
def self.head(url, params={}, &block)
|
|
41
666
|
http :HEAD, urlalize(url, params), nil, nil, &block
|
|
42
667
|
end
|
|
43
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
|
+
|
|
44
674
|
def self.options(url, params={}, &block)
|
|
45
675
|
http :OPTIONS, urlalize(url, params), nil, nil, &block
|
|
46
676
|
end
|
|
47
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
|
+
|
|
48
683
|
def self.urlalize(url, params={})
|
|
49
|
-
|
|
50
|
-
if
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
end
|
|
684
|
+
uri = URI(url)
|
|
685
|
+
# early return if we didn't specify any extra params
|
|
686
|
+
return uri.to_s if (params || {}).empty?
|
|
687
|
+
|
|
688
|
+
params_query = URI.encode_www_form(params || {})
|
|
689
|
+
uri.query = [uri.query.to_s, params_query].reject(&:empty?).join('&')
|
|
690
|
+
uri.to_s
|
|
57
691
|
end
|
|
58
692
|
|
|
59
693
|
def self.postalize(params={})
|
|
60
694
|
params.respond_to?(:map) ? URI.encode_www_form(params) : (params.respond_to?(:to_s) ? params.to_s : params)
|
|
61
695
|
end
|
|
62
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
|
+
|
|
724
|
+
def self.reset
|
|
725
|
+
Thread.current[:curb_curl] = Curl::Easy.new
|
|
726
|
+
end
|
|
727
|
+
|
|
63
728
|
end
|