faraday 2.14.4 → 2.15.1
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/Rakefile +11 -5
- data/lib/faraday/connection.rb +23 -16
- data/lib/faraday/encoders/nested_params_encoder.rb +1 -1
- data/lib/faraday/error.rb +29 -2
- data/lib/faraday/logging/formatter.rb +1 -1
- data/lib/faraday/options/proxy_options.rb +2 -2
- data/lib/faraday/options.rb +4 -3
- data/lib/faraday/rack_builder.rb +4 -4
- data/lib/faraday/response/raise_error.rb +7 -1
- data/lib/faraday/response.rb +5 -2
- data/lib/faraday/utils/headers.rb +41 -7
- data/lib/faraday/utils.rb +15 -3
- data/lib/faraday/version.rb +1 -1
- data/spec/external_adapters/faraday_specs_setup.rb +2 -0
- data/spec/support/integration/test_pki.rb +77 -0
- data/spec/support/integration/tls_server.rb +92 -0
- data/spec/support/integration_helper.rb +71 -0
- metadata +7 -31
- data/spec/faraday/adapter/test_spec.rb +0 -460
- data/spec/faraday/adapter_registry_spec.rb +0 -28
- data/spec/faraday/adapter_spec.rb +0 -55
- data/spec/faraday/connection_spec.rb +0 -860
- data/spec/faraday/error_spec.rb +0 -175
- data/spec/faraday/middleware_registry_spec.rb +0 -31
- data/spec/faraday/middleware_spec.rb +0 -213
- data/spec/faraday/options/env_spec.rb +0 -76
- data/spec/faraday/options/options_spec.rb +0 -297
- data/spec/faraday/options/proxy_options_spec.rb +0 -79
- data/spec/faraday/options/request_options_spec.rb +0 -19
- data/spec/faraday/params_encoders/flat_spec.rb +0 -42
- data/spec/faraday/params_encoders/nested_spec.rb +0 -179
- data/spec/faraday/rack_builder_spec.rb +0 -317
- data/spec/faraday/request/authorization_spec.rb +0 -118
- data/spec/faraday/request/instrumentation_spec.rb +0 -74
- data/spec/faraday/request/json_spec.rb +0 -199
- data/spec/faraday/request/url_encoded_spec.rb +0 -93
- data/spec/faraday/request_spec.rb +0 -119
- data/spec/faraday/response/json_spec.rb +0 -206
- data/spec/faraday/response/logger_spec.rb +0 -299
- data/spec/faraday/response/raise_error_spec.rb +0 -286
- data/spec/faraday/response_spec.rb +0 -84
- data/spec/faraday/utils/headers_spec.rb +0 -109
- data/spec/faraday/utils_spec.rb +0 -120
- data/spec/faraday_spec.rb +0 -69
- data/spec/spec_helper.rb +0 -142
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a700341d7d0844ede1248c479375ec0b3cb7fd51526c7c4abbaf9ce3db6c8a44
|
|
4
|
+
data.tar.gz: 76de6bace7c8995002b54ce7f19610a311418ef253c19a2fb354dee8ea986866
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5bc51dff2b4420115e15f36532fa2f9c2a31aa60d28d9acd864dcd4a50185e28fe5ac4dbeab648915d6a605e3606d06e207c083b3387a3253040bc9c108d6264
|
|
7
|
+
data.tar.gz: 82893f3f9d2152459d2a7b9b271601926e723a0723b7c3d20dc615795ce3d210473f2b6f359ec68fb4cd0f0a590ecae3121c325a257040c33701094cf56966fb
|
data/Rakefile
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'rspec/core/rake_task'
|
|
4
3
|
require 'bundler'
|
|
5
4
|
|
|
6
5
|
Bundler::GemHelper.install_tasks
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
# The publish workflow sets FARADAY_RELEASE because its bundle (release/Gemfile) doesn't include RSpec.
|
|
8
|
+
unless ENV['FARADAY_RELEASE']
|
|
9
|
+
require 'rspec/core/rake_task'
|
|
10
|
+
|
|
11
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
|
12
|
+
task.ruby_opts = %w[-W]
|
|
13
|
+
# Integration specs need service containers, see .github/workflows/integration-tests.yml
|
|
14
|
+
task.exclude_pattern = 'spec/integration/**/*_spec.rb'
|
|
15
|
+
end
|
|
11
16
|
|
|
12
|
-
task default: :spec
|
|
17
|
+
task default: :spec
|
|
18
|
+
end
|
data/lib/faraday/connection.rb
CHANGED
|
@@ -210,7 +210,7 @@ module Faraday
|
|
|
210
210
|
#
|
|
211
211
|
# @overload options(url, params = nil, headers = nil)
|
|
212
212
|
# Makes an OPTIONS HTTP request to the given URL.
|
|
213
|
-
# @param url [String, URI, nil] String base URL to
|
|
213
|
+
# @param url [String, URI, nil] String base URL to use as a prefix for all requests.
|
|
214
214
|
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
|
|
215
215
|
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
|
|
216
216
|
#
|
|
@@ -300,7 +300,10 @@ module Faraday
|
|
|
300
300
|
end
|
|
301
301
|
end
|
|
302
302
|
|
|
303
|
-
# Determine if this Faraday::Connection
|
|
303
|
+
# Determine if this Faraday::Connection is currently making parallel
|
|
304
|
+
# requests, i.e. if it is called inside an {#in_parallel} block.
|
|
305
|
+
# Use {#support_parallel?} to check if the adapter can make parallel
|
|
306
|
+
# requests at all.
|
|
304
307
|
#
|
|
305
308
|
# @return [Boolean]
|
|
306
309
|
def in_parallel?
|
|
@@ -315,13 +318,13 @@ module Faraday
|
|
|
315
318
|
# @yield a block to execute multiple requests.
|
|
316
319
|
# @return [void]
|
|
317
320
|
def in_parallel(manager = nil, &block)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
321
|
+
previous_manager = @parallel_manager
|
|
322
|
+
@parallel_manager = manager || previous_manager || default_parallel_manager do
|
|
323
|
+
warn('Warning: `in_parallel` called but no parallel-capable adapter ' \
|
|
324
|
+
'on Faraday stack', caller[2, 10].join("\n"))
|
|
322
325
|
nil
|
|
323
326
|
end
|
|
324
|
-
return yield unless @parallel_manager
|
|
327
|
+
return yield unless @parallel_manager && !@parallel_manager.equal?(previous_manager)
|
|
325
328
|
|
|
326
329
|
if @parallel_manager.respond_to?(:execute)
|
|
327
330
|
# Execute is the new method that is responsible for executing the block.
|
|
@@ -332,7 +335,7 @@ module Faraday
|
|
|
332
335
|
@parallel_manager.run
|
|
333
336
|
end
|
|
334
337
|
ensure
|
|
335
|
-
@parallel_manager =
|
|
338
|
+
@parallel_manager = previous_manager
|
|
336
339
|
end
|
|
337
340
|
|
|
338
341
|
# Sets the Hash proxy options.
|
|
@@ -362,7 +365,7 @@ module Faraday
|
|
|
362
365
|
#
|
|
363
366
|
# conn.get("nigiri?page=2") # accesses https://httpbingo.org/api/nigiri
|
|
364
367
|
def url_prefix=(url, encoder = nil)
|
|
365
|
-
uri = @url_prefix = Utils.URI(url)
|
|
368
|
+
uri = @url_prefix = Utils.URI(url.to_s)
|
|
366
369
|
self.path_prefix = uri.path
|
|
367
370
|
|
|
368
371
|
params.merge_query(uri.query, encoder)
|
|
@@ -445,7 +448,7 @@ module Faraday
|
|
|
445
448
|
req.options.proxy = proxy_for_request(url)
|
|
446
449
|
req.url(url) if url
|
|
447
450
|
req.headers.update(headers) if headers
|
|
448
|
-
req.body = body
|
|
451
|
+
req.body = body unless body.nil?
|
|
449
452
|
yield(req) if block_given?
|
|
450
453
|
end
|
|
451
454
|
|
|
@@ -496,8 +499,6 @@ module Faraday
|
|
|
496
499
|
|
|
497
500
|
# Creates a duplicate of this Faraday::Connection.
|
|
498
501
|
#
|
|
499
|
-
# @api private
|
|
500
|
-
#
|
|
501
502
|
# @return [Faraday::Connection]
|
|
502
503
|
def dup
|
|
503
504
|
self.class.new(build_exclusive_url,
|
|
@@ -505,7 +506,7 @@ module Faraday
|
|
|
505
506
|
params: params.dup,
|
|
506
507
|
builder: builder.dup,
|
|
507
508
|
ssl: ssl.dup,
|
|
508
|
-
request: options.dup)
|
|
509
|
+
request: options.dup).tap { |copy| copy.proxy = proxy if @manual_proxy }
|
|
509
510
|
end
|
|
510
511
|
|
|
511
512
|
# Yields username and password extracted from a URI if they both exist.
|
|
@@ -519,7 +520,7 @@ module Faraday
|
|
|
519
520
|
def with_uri_credentials(uri)
|
|
520
521
|
return unless uri.user && uri.password
|
|
521
522
|
|
|
522
|
-
yield(
|
|
523
|
+
yield(URI::DEFAULT_PARSER.unescape(uri.user), URI::DEFAULT_PARSER.unescape(uri.password))
|
|
523
524
|
end
|
|
524
525
|
|
|
525
526
|
def proxy_from_env(url)
|
|
@@ -532,7 +533,7 @@ module Faraday
|
|
|
532
533
|
uri = if uri.host.nil?
|
|
533
534
|
find_default_proxy
|
|
534
535
|
else
|
|
535
|
-
URI.parse(
|
|
536
|
+
URI.parse(uri.to_s).find_proxy
|
|
536
537
|
end
|
|
537
538
|
when URI
|
|
538
539
|
uri = url.find_proxy
|
|
@@ -560,7 +561,13 @@ module Faraday
|
|
|
560
561
|
end
|
|
561
562
|
end
|
|
562
563
|
|
|
563
|
-
|
|
564
|
+
# Determine if an adapter can make parallel requests.
|
|
565
|
+
#
|
|
566
|
+
# @param adapter [Class, nil] the adapter class to check. Defaults to the
|
|
567
|
+
# adapter of this Faraday::Connection.
|
|
568
|
+
#
|
|
569
|
+
# @return [Boolean]
|
|
570
|
+
def support_parallel?(adapter = @builder.adapter&.klass)
|
|
564
571
|
adapter.respond_to?(:supports_parallel?) && adapter&.supports_parallel?
|
|
565
572
|
end
|
|
566
573
|
end
|
data/lib/faraday/error.rb
CHANGED
|
@@ -70,7 +70,7 @@ module Faraday
|
|
|
70
70
|
# to `super`. See NilStatusError.
|
|
71
71
|
def exc_msg_and_response!(exc, response = nil)
|
|
72
72
|
if @response.nil? && @wrapped_exception.nil?
|
|
73
|
-
@wrapped_exception, msg, @response = exc_msg_and_response(exc, response)
|
|
73
|
+
@wrapped_exception, msg, @response = exc_msg_and_response(exc, omit_request_data(response))
|
|
74
74
|
return msg
|
|
75
75
|
end
|
|
76
76
|
|
|
@@ -85,7 +85,14 @@ module Faraday
|
|
|
85
85
|
when Hash
|
|
86
86
|
[nil, build_error_message_from_hash(exc), exc]
|
|
87
87
|
when Faraday::Env
|
|
88
|
-
|
|
88
|
+
sanitized = omit_request_data(exc)
|
|
89
|
+
message =
|
|
90
|
+
if sanitized.equal?(exc)
|
|
91
|
+
build_error_message_from_env(exc)
|
|
92
|
+
else
|
|
93
|
+
build_error_message_from_hash(sanitized)
|
|
94
|
+
end
|
|
95
|
+
[nil, message, sanitized]
|
|
89
96
|
else
|
|
90
97
|
[nil, exc.to_s, response]
|
|
91
98
|
end
|
|
@@ -93,6 +100,26 @@ module Faraday
|
|
|
93
100
|
|
|
94
101
|
private
|
|
95
102
|
|
|
103
|
+
# When the RaiseError middleware opted this request out of request data
|
|
104
|
+
# (`include_request: false`), it marks the Env. Replace env-carrying
|
|
105
|
+
# responses (the Response object raised by the Json middleware, or an Env
|
|
106
|
+
# passed directly) with a request-less hash so that errors raised by other
|
|
107
|
+
# middlewares (e.g. ParsingError) cannot expose request headers and body.
|
|
108
|
+
# The body reads the explicit `response_body` member because `Env#body`
|
|
109
|
+
# aliases to the request body while the status is nil.
|
|
110
|
+
def omit_request_data(response)
|
|
111
|
+
env =
|
|
112
|
+
case response
|
|
113
|
+
when Faraday::Response
|
|
114
|
+
response.env if response.finished?
|
|
115
|
+
when Faraday::Env
|
|
116
|
+
response
|
|
117
|
+
end
|
|
118
|
+
return response unless env.is_a?(Faraday::Env) && env[:include_request] == false
|
|
119
|
+
|
|
120
|
+
{ status: env.status, headers: env.response_headers, body: env.response_body }
|
|
121
|
+
end
|
|
122
|
+
|
|
96
123
|
def build_error_message_from_hash(hash)
|
|
97
124
|
# Be defensive with external Hash objects - they might be missing keys
|
|
98
125
|
status = hash.fetch(:status, nil)
|
|
@@ -41,7 +41,7 @@ module Faraday
|
|
|
41
41
|
def exception(exc)
|
|
42
42
|
return unless log_errors?
|
|
43
43
|
|
|
44
|
-
public_send(log_level) { "error: #{exc.full_message}" }
|
|
44
|
+
public_send(log_level) { "error: #{apply_filters(exc.full_message)}" }
|
|
45
45
|
|
|
46
46
|
log_headers('error', exc.response_headers) if exc.respond_to?(:response_headers) && log_headers?(:error)
|
|
47
47
|
return unless exc.respond_to?(:response_body) && exc.response_body && log_body?(:error)
|
|
@@ -42,7 +42,7 @@ module Faraday
|
|
|
42
42
|
super
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
memoized(:user) { uri&.user &&
|
|
46
|
-
memoized(:password) { uri&.password &&
|
|
45
|
+
memoized(:user) { uri&.user && URI::DEFAULT_PARSER.unescape(uri.user) }
|
|
46
|
+
memoized(:password) { uri&.password && URI::DEFAULT_PARSER.unescape(uri.password) }
|
|
47
47
|
end
|
|
48
48
|
end
|
data/lib/faraday/options.rb
CHANGED
|
@@ -74,7 +74,7 @@ module Faraday
|
|
|
74
74
|
|
|
75
75
|
# Public
|
|
76
76
|
def fetch(key, *args)
|
|
77
|
-
unless
|
|
77
|
+
unless members.include?(key.to_sym) && !send(key).nil?
|
|
78
78
|
key_setter = "#{key}="
|
|
79
79
|
if !args.empty?
|
|
80
80
|
send(key_setter, args.first)
|
|
@@ -186,14 +186,15 @@ module Faraday
|
|
|
186
186
|
def [](key)
|
|
187
187
|
key = key.to_sym
|
|
188
188
|
if (method = self.class.memoized_attributes[key])
|
|
189
|
-
|
|
189
|
+
value = super
|
|
190
|
+
value.nil? ? (self[key] = instance_eval(&method)) : value
|
|
190
191
|
else
|
|
191
192
|
super
|
|
192
193
|
end
|
|
193
194
|
end
|
|
194
195
|
|
|
195
196
|
def symbolized_key_set
|
|
196
|
-
|
|
197
|
+
Set.new(keys.map(&:to_sym))
|
|
197
198
|
end
|
|
198
199
|
|
|
199
200
|
def self.inherited(subclass)
|
data/lib/faraday/rack_builder.rb
CHANGED
|
@@ -73,6 +73,9 @@ module Faraday
|
|
|
73
73
|
super
|
|
74
74
|
@adapter = original.adapter
|
|
75
75
|
@handlers = original.handlers.dup
|
|
76
|
+
# @app is memoized on the first request. Drop it so the duplicate
|
|
77
|
+
# rebuilds from the copied handlers and middleware added after dup runs.
|
|
78
|
+
@app = nil
|
|
76
79
|
end
|
|
77
80
|
|
|
78
81
|
def build
|
|
@@ -115,6 +118,7 @@ module Faraday
|
|
|
115
118
|
def adapter(klass = NO_ARGUMENT, *args, **kwargs, &block)
|
|
116
119
|
return @adapter if klass == NO_ARGUMENT || klass.nil?
|
|
117
120
|
|
|
121
|
+
raise_if_locked
|
|
118
122
|
klass = Faraday::Adapter.lookup_middleware(klass) if klass.is_a?(Symbol)
|
|
119
123
|
@adapter = self.class::Handler.new(klass, *args, **kwargs, &block)
|
|
120
124
|
end
|
|
@@ -230,10 +234,6 @@ module Faraday
|
|
|
230
234
|
raise MISSING_ADAPTER_ERROR unless @adapter
|
|
231
235
|
end
|
|
232
236
|
|
|
233
|
-
def adapter_set?
|
|
234
|
-
!@adapter.nil?
|
|
235
|
-
end
|
|
236
|
-
|
|
237
237
|
def use_symbol(mod, key, ...)
|
|
238
238
|
use(mod.lookup_middleware(key), ...)
|
|
239
239
|
end
|
|
@@ -20,7 +20,13 @@ module Faraday
|
|
|
20
20
|
}.freeze
|
|
21
21
|
# rubocop:enable Naming/ConstantName
|
|
22
22
|
|
|
23
|
-
DEFAULT_OPTIONS = { include_request: true, allowed_statuses: [] }.freeze
|
|
23
|
+
DEFAULT_OPTIONS = { include_request: true, allowed_statuses: [].freeze }.freeze
|
|
24
|
+
|
|
25
|
+
def on_request(env)
|
|
26
|
+
# Announce the opt-out on the shared Env so that errors raised by other
|
|
27
|
+
# middlewares (e.g. ParsingError from the Json middleware) honor it too.
|
|
28
|
+
env[:include_request] = false unless options[:include_request]
|
|
29
|
+
end
|
|
24
30
|
|
|
25
31
|
def on_complete(env)
|
|
26
32
|
return if Array(options[:allowed_statuses]).include?(env[:status])
|
data/lib/faraday/response.rb
CHANGED
|
@@ -54,7 +54,9 @@ module Faraday
|
|
|
54
54
|
raise 'response already finished' if finished?
|
|
55
55
|
|
|
56
56
|
@env = env.is_a?(Env) ? env : Env.from(env)
|
|
57
|
-
|
|
57
|
+
callbacks = @on_complete_callbacks
|
|
58
|
+
@on_complete_callbacks = []
|
|
59
|
+
callbacks.each { |callback| callback.call(@env) }
|
|
58
60
|
self
|
|
59
61
|
end
|
|
60
62
|
|
|
@@ -76,7 +78,8 @@ module Faraday
|
|
|
76
78
|
end
|
|
77
79
|
|
|
78
80
|
def marshal_load(env)
|
|
79
|
-
@env = Env.from(env)
|
|
81
|
+
@env = Env.from(env) if env
|
|
82
|
+
@on_complete_callbacks = []
|
|
80
83
|
end
|
|
81
84
|
|
|
82
85
|
# Expand the env with more properties, without overriding existing ones.
|
|
@@ -38,13 +38,11 @@ module Faraday
|
|
|
38
38
|
|
|
39
39
|
# symbol -> string mapper + cache
|
|
40
40
|
KeyMap = Hash.new do |map, key|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.join('-') # => "User-Agent"
|
|
47
|
-
end
|
|
41
|
+
next key if key.respond_to?(:to_str)
|
|
42
|
+
|
|
43
|
+
value = key.to_s.split('_') # user_agent: %w(user agent)
|
|
44
|
+
.each(&:capitalize!) # => %w(User Agent)
|
|
45
|
+
.join('-') # => "User-Agent"
|
|
48
46
|
keymap_mutex.synchronize { map[key] = value }
|
|
49
47
|
end
|
|
50
48
|
KeyMap[:etag] = 'ETag'
|
|
@@ -62,6 +60,8 @@ module Faraday
|
|
|
62
60
|
super
|
|
63
61
|
end
|
|
64
62
|
|
|
63
|
+
alias store []=
|
|
64
|
+
|
|
65
65
|
def fetch(key, ...)
|
|
66
66
|
key = KeyMap[key]
|
|
67
67
|
key = @names.fetch(key.downcase, key)
|
|
@@ -77,6 +77,28 @@ module Faraday
|
|
|
77
77
|
super
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
def clear
|
|
81
|
+
@names.clear
|
|
82
|
+
super
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def shift = rebuilding_names { super }
|
|
86
|
+
def compact! = rebuilding_names { super }
|
|
87
|
+
def rehash = rebuilding_names { super }
|
|
88
|
+
|
|
89
|
+
# Without a block these return an enumerator that calls back into the
|
|
90
|
+
# method once iterated, so the index only needs rebuilding in that call.
|
|
91
|
+
def delete_if = block_given? ? rebuilding_names { super } : super
|
|
92
|
+
def keep_if = block_given? ? rebuilding_names { super } : super
|
|
93
|
+
def reject! = block_given? ? rebuilding_names { super } : super
|
|
94
|
+
def select! = block_given? ? rebuilding_names { super } : super
|
|
95
|
+
alias filter! select!
|
|
96
|
+
|
|
97
|
+
# A mapping hash is a valid alternative to a block.
|
|
98
|
+
def transform_keys!(*args, &block)
|
|
99
|
+
args.empty? && !block ? super : rebuilding_names { super }
|
|
100
|
+
end
|
|
101
|
+
|
|
80
102
|
def dig(key, *rest)
|
|
81
103
|
key = KeyMap[key]
|
|
82
104
|
key = @names.fetch(key.downcase, key)
|
|
@@ -136,6 +158,18 @@ module Faraday
|
|
|
136
158
|
|
|
137
159
|
private
|
|
138
160
|
|
|
161
|
+
# Runs the block, then rebuilds the name index even if the block raised
|
|
162
|
+
# or exited early, as the underlying Hash may already be partially mutated.
|
|
163
|
+
def rebuilding_names
|
|
164
|
+
yield
|
|
165
|
+
ensure
|
|
166
|
+
rebuild_names
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def rebuild_names
|
|
170
|
+
@names = each_key.to_h { |key| [KeyMap[key].downcase, key] }
|
|
171
|
+
end
|
|
172
|
+
|
|
139
173
|
# Join multiple values with a comma.
|
|
140
174
|
def add_parsed(key, value)
|
|
141
175
|
if key?(key)
|
data/lib/faraday/utils.rb
CHANGED
|
@@ -27,10 +27,22 @@ module Faraday
|
|
|
27
27
|
|
|
28
28
|
ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/
|
|
29
29
|
|
|
30
|
+
# CGI.escape works on raw bytes: it matches ESCAPE_RE only where no multibyte character
|
|
31
|
+
# hides an ASCII byte (Shift_JIS 'ア' is "\x83A"), unlike ESCAPE_RE it doesn't raise on
|
|
32
|
+
# invalid bytes, and it returns String subclasses (SafeBuffer) as is when nothing is escaped.
|
|
33
|
+
CGI_ESCAPE_ENCODINGS = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::BINARY].freeze
|
|
34
|
+
private_constant :CGI_ESCAPE_ENCODINGS
|
|
35
|
+
|
|
30
36
|
def escape(str)
|
|
31
|
-
str.to_s
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
str = str.to_s
|
|
38
|
+
if str.instance_of?(String) && CGI_ESCAPE_ENCODINGS.include?(str.encoding) && str.valid_encoding?
|
|
39
|
+
escaped = CGI.escape(str)
|
|
40
|
+
default_space_encoding == '+' ? escaped : escaped.gsub('+', default_space_encoding)
|
|
41
|
+
else
|
|
42
|
+
str.gsub(ESCAPE_RE) do |match|
|
|
43
|
+
"%#{match.unpack('H2' * match.bytesize).join('%').upcase}"
|
|
44
|
+
end.gsub(' ', default_space_encoding)
|
|
45
|
+
end
|
|
34
46
|
end
|
|
35
47
|
|
|
36
48
|
def unescape(str)
|
data/lib/faraday/version.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require 'webmock/rspec'
|
|
4
4
|
WebMock.disable_net_connect!(allow_localhost: true)
|
|
5
5
|
|
|
6
|
+
# The gem ships only `spec/external_adapters` and `spec/support` (see `spec.files` in
|
|
7
|
+
# faraday.gemspec). Requiring a file from anywhere else means updating the gemspec too.
|
|
6
8
|
require_relative '../support/helper_methods'
|
|
7
9
|
require_relative '../support/disabling_stub'
|
|
8
10
|
require_relative '../support/streaming_response_checker'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
|
|
5
|
+
module Faraday
|
|
6
|
+
module IntegrationHelpers
|
|
7
|
+
# Throwaway certificate authority used by the SSL and proxy integration
|
|
8
|
+
# specs. Everything is generated in memory, so no certificate files need to
|
|
9
|
+
# be committed or generated by the CI workflow.
|
|
10
|
+
class TestPKI
|
|
11
|
+
Credentials = Struct.new(:cert, :key)
|
|
12
|
+
|
|
13
|
+
attr_reader :ca_cert, :ca_key
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@ca_key = OpenSSL::PKey::RSA.new(2048)
|
|
17
|
+
@ca_cert = build_certificate('Faraday Test CA', ca_key, ca_key, nil) do |cert, factory|
|
|
18
|
+
[
|
|
19
|
+
factory.create_extension('basicConstraints', 'CA:TRUE', true),
|
|
20
|
+
factory.create_extension('keyUsage', 'keyCertSign,cRLSign', true),
|
|
21
|
+
factory.create_extension('subjectKeyIdentifier', 'hash', false)
|
|
22
|
+
].each { |ext| cert.add_extension(ext) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Issues a certificate signed by this CA.
|
|
27
|
+
#
|
|
28
|
+
# @param common_name [String] certificate subject CN
|
|
29
|
+
# @param dns_names [Array<String>] DNS subjectAltNames
|
|
30
|
+
# @param ip_addresses [Array<String>] IP subjectAltNames
|
|
31
|
+
# @return [Credentials]
|
|
32
|
+
def issue(common_name, dns_names: [], ip_addresses: [])
|
|
33
|
+
key = OpenSSL::PKey::RSA.new(2048)
|
|
34
|
+
sans = dns_names.map { |name| "DNS:#{name}" } + ip_addresses.map { |ip| "IP:#{ip}" }
|
|
35
|
+
|
|
36
|
+
cert = build_certificate(common_name, key, ca_key, ca_cert) do |built, factory|
|
|
37
|
+
built.add_extension(factory.create_extension('basicConstraints', 'CA:FALSE', true))
|
|
38
|
+
built.add_extension(factory.create_extension('subjectAltName', sans.join(','), false)) if sans.any?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Credentials.new(cert, key)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Writes the CA certificate to disk, as expected by the `ca_file` option.
|
|
45
|
+
def write_ca_file(path)
|
|
46
|
+
File.write(path, ca_cert.to_pem)
|
|
47
|
+
path
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [OpenSSL::X509::Store] a store that only trusts this CA
|
|
51
|
+
def cert_store
|
|
52
|
+
OpenSSL::X509::Store.new.tap { |store| store.add_cert(ca_cert) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def build_certificate(common_name, key, signing_key, issuer_cert)
|
|
58
|
+
cert = OpenSSL::X509::Certificate.new
|
|
59
|
+
cert.version = 2
|
|
60
|
+
cert.serial = rand(1..(2**32))
|
|
61
|
+
cert.subject = OpenSSL::X509::Name.parse("/CN=#{common_name}")
|
|
62
|
+
cert.issuer = issuer_cert ? issuer_cert.subject : cert.subject
|
|
63
|
+
cert.public_key = key.public_key
|
|
64
|
+
cert.not_before = Time.now - 60
|
|
65
|
+
cert.not_after = Time.now + 3600
|
|
66
|
+
|
|
67
|
+
factory = OpenSSL::X509::ExtensionFactory.new
|
|
68
|
+
factory.subject_certificate = cert
|
|
69
|
+
factory.issuer_certificate = issuer_cert || cert
|
|
70
|
+
yield cert, factory
|
|
71
|
+
|
|
72
|
+
cert.sign(signing_key, OpenSSL::Digest.new('SHA256'))
|
|
73
|
+
cert
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
require 'socket'
|
|
6
|
+
|
|
7
|
+
module Faraday
|
|
8
|
+
module IntegrationHelpers
|
|
9
|
+
# Minimal HTTPS server running in a background thread, used to exercise
|
|
10
|
+
# SSLOptions and ProxyOptions against real TLS handshakes.
|
|
11
|
+
#
|
|
12
|
+
# Every request is answered with a JSON document describing what the server
|
|
13
|
+
# saw, e.g. `{"path":"/","client_cert_subject":null,"remote_ip":"127.0.0.1"}`.
|
|
14
|
+
class TLSServer
|
|
15
|
+
attr_reader :port
|
|
16
|
+
|
|
17
|
+
# @param credentials [TestPKI::Credentials] server certificate and key
|
|
18
|
+
# @param client_ca [TestPKI, nil] when given, clients must present a
|
|
19
|
+
# certificate signed by this CA (mutual TLS)
|
|
20
|
+
# @param host [String] address to listen on; '0.0.0.0' makes the server
|
|
21
|
+
# reachable from service containers, such as the proxies
|
|
22
|
+
def initialize(credentials, client_ca: nil, host: '127.0.0.1')
|
|
23
|
+
@tcp_server = TCPServer.new(host, 0)
|
|
24
|
+
@port = @tcp_server.addr[1]
|
|
25
|
+
@ssl_server = OpenSSL::SSL::SSLServer.new(@tcp_server, ssl_context(credentials, client_ca))
|
|
26
|
+
@ssl_server.start_immediately = false
|
|
27
|
+
@threads = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.start(...)
|
|
31
|
+
new(...).start
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def start
|
|
35
|
+
@acceptor = Thread.new do
|
|
36
|
+
loop do
|
|
37
|
+
socket = @ssl_server.accept
|
|
38
|
+
@threads << Thread.new { handle(socket) }
|
|
39
|
+
end
|
|
40
|
+
rescue IOError, Errno::EBADF
|
|
41
|
+
nil # server stopped
|
|
42
|
+
end
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def stop
|
|
47
|
+
@ssl_server.close
|
|
48
|
+
@acceptor&.join(1)
|
|
49
|
+
@threads.each { |thread| thread.join(1) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def url(host = 'localhost')
|
|
53
|
+
"https://#{host}:#{port}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def ssl_context(credentials, client_ca)
|
|
59
|
+
OpenSSL::SSL::SSLContext.new.tap do |ctx|
|
|
60
|
+
ctx.cert = credentials.cert
|
|
61
|
+
ctx.key = credentials.key
|
|
62
|
+
if client_ca
|
|
63
|
+
ctx.cert_store = client_ca.cert_store
|
|
64
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def handle(socket)
|
|
70
|
+
socket.accept
|
|
71
|
+
request_line = socket.gets
|
|
72
|
+
while (line = socket.gets)
|
|
73
|
+
break if line.strip.empty?
|
|
74
|
+
end
|
|
75
|
+
respond(socket, request_line.to_s.split[1])
|
|
76
|
+
rescue OpenSSL::SSL::SSLError, SystemCallError, IOError
|
|
77
|
+
nil # the client rejected the handshake or went away
|
|
78
|
+
ensure
|
|
79
|
+
socket.close unless socket.closed?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def respond(socket, path)
|
|
83
|
+
body = JSON.generate(path: path, client_cert_subject: socket.peer_cert&.subject&.to_s,
|
|
84
|
+
remote_ip: socket.io.remote_address.ip_address)
|
|
85
|
+
socket.write("HTTP/1.1 200 OK\r\n" \
|
|
86
|
+
"Content-Type: application/json\r\n" \
|
|
87
|
+
"Content-Length: #{body.bytesize}\r\n" \
|
|
88
|
+
"Connection: close\r\n\r\n#{body}")
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|