httpx 1.7.8 → 1.8.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/doc/release_notes/1_7_1.md +1 -2
- data/doc/release_notes/1_8_0.md +100 -0
- data/doc/release_notes/1_8_1.md +27 -0
- data/lib/httpx/adapters/datadog.rb +12 -1
- data/lib/httpx/adapters/faraday.rb +2 -2
- data/lib/httpx/adapters/webmock.rb +6 -0
- data/lib/httpx/connection/http1.rb +15 -12
- data/lib/httpx/connection/http2.rb +69 -9
- data/lib/httpx/connection.rb +119 -21
- data/lib/httpx/errors.rb +8 -1
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/io/ssl.rb +8 -9
- data/lib/httpx/io/tcp.rb +22 -5
- data/lib/httpx/io/unix.rb +2 -2
- data/lib/httpx/loggable.rb +11 -4
- data/lib/httpx/options.rb +16 -4
- data/lib/httpx/parser/http1.rb +8 -2
- data/lib/httpx/plugins/auth/digest.rb +1 -1
- data/lib/httpx/plugins/auth.rb +61 -4
- data/lib/httpx/plugins/{response_cache → cache}/file_store.rb +1 -1
- data/lib/httpx/plugins/{response_cache → cache}/store.rb +1 -1
- data/lib/httpx/plugins/cache.rb +221 -0
- data/lib/httpx/plugins/callbacks.rb +12 -0
- data/lib/httpx/plugins/digest_auth.rb +4 -0
- data/lib/httpx/plugins/fiber_concurrency.rb +75 -4
- data/lib/httpx/plugins/ntlm_v2_auth.rb +92 -0
- data/lib/httpx/plugins/oauth.rb +66 -14
- data/lib/httpx/plugins/persistent.rb +18 -2
- data/lib/httpx/plugins/proxy.rb +5 -0
- data/lib/httpx/plugins/push_promise.rb +2 -2
- data/lib/httpx/plugins/response_cache.rb +26 -105
- data/lib/httpx/plugins/retries.rb +7 -5
- data/lib/httpx/plugins/server_sent_events.rb +158 -0
- data/lib/httpx/plugins/ssrf_filter.rb +17 -2
- data/lib/httpx/plugins/stream.rb +9 -5
- data/lib/httpx/plugins/stream_bidi.rb +2 -0
- data/lib/httpx/plugins/tracing.rb +15 -4
- data/lib/httpx/pool.rb +2 -2
- data/lib/httpx/request/body.rb +2 -2
- data/lib/httpx/request.rb +42 -10
- data/lib/httpx/resolver/cache/base.rb +1 -8
- data/lib/httpx/resolver/cache/file.rb +56 -0
- data/lib/httpx/resolver/https.rb +20 -20
- data/lib/httpx/resolver/native.rb +21 -3
- data/lib/httpx/resolver/resolver.rb +4 -0
- data/lib/httpx/resolver/system.rb +5 -2
- data/lib/httpx/response/body.rb +6 -4
- data/lib/httpx/response.rb +12 -5
- data/lib/httpx/selector.rb +12 -1
- data/lib/httpx/session.rb +6 -1
- data/lib/httpx/session_extensions.rb +2 -2
- data/lib/httpx/timers.rb +3 -0
- data/lib/httpx/version.rb +1 -1
- data/sig/chainable.rbs +3 -0
- data/sig/connection/http1.rbs +1 -1
- data/sig/connection/http2.rbs +4 -1
- data/sig/connection.rbs +16 -9
- data/sig/errors.rbs +9 -3
- data/sig/httpx.rbs +2 -0
- data/sig/io/tcp.rbs +2 -0
- data/sig/loggable.rbs +8 -0
- data/sig/options.rbs +25 -12
- data/sig/parser/http1.rbs +3 -1
- data/sig/plugins/auth/ntlm.rbs +1 -1
- data/sig/plugins/auth.rbs +4 -0
- data/sig/plugins/{response_cache → cache}/file_store.rbs +2 -2
- data/sig/plugins/{response_cache → cache}/store.rbs +2 -2
- data/sig/plugins/cache.rbs +69 -0
- data/sig/plugins/fiber_concurrency.rbs +4 -0
- data/sig/plugins/ntlm_v2_auth.rbs +36 -0
- data/sig/plugins/persistent.rbs +3 -0
- data/sig/plugins/response_cache.rbs +13 -38
- data/sig/plugins/retries.rbs +5 -5
- data/sig/plugins/server_sent_events.rbs +45 -0
- data/sig/plugins/ssrf_filter.rbs +5 -1
- data/sig/plugins/stream.rbs +1 -1
- data/sig/plugins/stream_bidi.rbs +0 -2
- data/sig/plugins/webdav.rbs +1 -1
- data/sig/pool.rbs +2 -2
- data/sig/request.rbs +9 -6
- data/sig/resolver/cache/file.rbs +13 -0
- data/sig/resolver/entry.rbs +1 -1
- data/sig/resolver/https.rbs +3 -3
- data/sig/resolver/multi.rbs +1 -1
- data/sig/resolver/native.rbs +5 -5
- data/sig/resolver/resolver.rbs +1 -3
- data/sig/resolver/system.rbs +5 -2
- data/sig/resolver.rbs +3 -0
- data/sig/response.rbs +3 -0
- data/sig/selector.rbs +13 -8
- data/sig/timers.rbs +7 -5
- data/sig/transcoder/body.rbs +1 -1
- data/sig/transcoder/gzip.rbs +3 -2
- data/sig/transcoder/multipart.rbs +4 -1
- data/sig/transcoder/utils/deflater.rbs +2 -0
- data/sig/transcoder.rbs +2 -0
- data/sig/utils.rbs +1 -1
- metadata +19 -7
data/lib/httpx/plugins/auth.rb
CHANGED
|
@@ -20,6 +20,9 @@ module HTTPX
|
|
|
20
20
|
#
|
|
21
21
|
# :auth_header_value :: the token to use as a string, or a callable which returns a string when called.
|
|
22
22
|
# :auth_header_type :: the authentication type to use in the "authorization" header value (i.e. "Bearer", "Digest"...)
|
|
23
|
+
# :auth_header_expires_at :: timestamp at which the auth header will be discarded. should be a callable (like a proc)
|
|
24
|
+
# receiving the request as an argument, and should return either a Time object, or an integer (UNIX time).
|
|
25
|
+
# :auth_header_expires_in :: time (in seconds) since the first use of an auth header after which that header will be discarded.
|
|
23
26
|
# :generate_auth_value_on_retry :: callable which returns whether the request should regenerate the auth_header_value
|
|
24
27
|
# when the request is retried (this option will only work if the session also loads the
|
|
25
28
|
# <tt>:retries</tt> plugin).
|
|
@@ -32,6 +35,22 @@ module HTTPX
|
|
|
32
35
|
value
|
|
33
36
|
end
|
|
34
37
|
|
|
38
|
+
def option_auth_header_expires_at(value)
|
|
39
|
+
unless value.respond_to?(:call)
|
|
40
|
+
value = Float(value)
|
|
41
|
+
raise TypeError, "`:auth_header_expires_at` must be positive" unless value.positive?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def option_auth_header_expires_in(value)
|
|
48
|
+
value = Float(value)
|
|
49
|
+
raise TypeError, "`:auth_header_expires_in` must be positive" unless value.positive?
|
|
50
|
+
|
|
51
|
+
value
|
|
52
|
+
end
|
|
53
|
+
|
|
35
54
|
def option_generate_auth_value_on_retry(value)
|
|
36
55
|
raise TypeError, "`:generate_auth_value_on_retry` must be a callable" unless value.respond_to?(:call)
|
|
37
56
|
|
|
@@ -43,7 +62,7 @@ module HTTPX
|
|
|
43
62
|
def initialize(*)
|
|
44
63
|
super
|
|
45
64
|
|
|
46
|
-
@auth_header_value = nil
|
|
65
|
+
@auth_header_value = @auth_header_expires_at = nil
|
|
47
66
|
@auth_header_value_mtx = Thread::Mutex.new
|
|
48
67
|
@skip_auth_header_value = false
|
|
49
68
|
end
|
|
@@ -65,7 +84,7 @@ module HTTPX
|
|
|
65
84
|
|
|
66
85
|
def reset_auth_header_value!
|
|
67
86
|
@auth_header_value_mtx.synchronize do
|
|
68
|
-
@auth_header_value = nil
|
|
87
|
+
@auth_header_value = @auth_header_expires_at = nil
|
|
69
88
|
end
|
|
70
89
|
end
|
|
71
90
|
|
|
@@ -75,7 +94,12 @@ module HTTPX
|
|
|
75
94
|
return super if @skip_auth_header_value || request.authorized?
|
|
76
95
|
|
|
77
96
|
auth_header_value = @auth_header_value_mtx.synchronize do
|
|
78
|
-
|
|
97
|
+
try_invalidate_auth_header_value
|
|
98
|
+
|
|
99
|
+
@auth_header_value ||= begin
|
|
100
|
+
set_auth_header_expires_at(request)
|
|
101
|
+
generate_auth_token
|
|
102
|
+
end
|
|
79
103
|
end
|
|
80
104
|
|
|
81
105
|
request.authorize(auth_header_value) if auth_header_value
|
|
@@ -83,6 +107,14 @@ module HTTPX
|
|
|
83
107
|
super
|
|
84
108
|
end
|
|
85
109
|
|
|
110
|
+
def try_invalidate_auth_header_value
|
|
111
|
+
return unless (expires_at = @auth_header_expires_at)
|
|
112
|
+
|
|
113
|
+
return if expires_at > Time.now.utc.to_i
|
|
114
|
+
|
|
115
|
+
@auth_header_value = @auth_header_expires_at = nil
|
|
116
|
+
end
|
|
117
|
+
|
|
86
118
|
def generate_auth_token
|
|
87
119
|
return unless (auth_value = @options.auth_header_value)
|
|
88
120
|
|
|
@@ -91,6 +123,19 @@ module HTTPX
|
|
|
91
123
|
auth_value
|
|
92
124
|
end
|
|
93
125
|
|
|
126
|
+
def set_auth_header_expires_at(request)
|
|
127
|
+
@auth_header_expires_at = if (expires_in = request.options.auth_header_expires_in)
|
|
128
|
+
Time.now.to_i + expires_in
|
|
129
|
+
elsif (expires_at = request.options.auth_header_expires_at)
|
|
130
|
+
if expires_at.respond_to?(:call) && (expires_at = expires_at.call(request))
|
|
131
|
+
expires_at = expires_at.to_f
|
|
132
|
+
raise Error, "`:auth_header_expires_at` must be positive" unless expires_at.positive?
|
|
133
|
+
|
|
134
|
+
expires_at
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
94
139
|
def dynamic_auth_token?(auth_header_value)
|
|
95
140
|
auth_header_value&.respond_to?(:call)
|
|
96
141
|
end
|
|
@@ -150,12 +195,24 @@ module HTTPX
|
|
|
150
195
|
# otherwise, it means that the first request already passed here, so this request should
|
|
151
196
|
# use whatever was generated for it.
|
|
152
197
|
@auth_header_value_mtx.synchronize do
|
|
153
|
-
|
|
198
|
+
if request.auth_token_value == @auth_header_value
|
|
199
|
+
@auth_header_value = generate_auth_token
|
|
200
|
+
set_auth_header_expires_at(request)
|
|
201
|
+
end
|
|
154
202
|
end
|
|
155
203
|
|
|
156
204
|
request.unauthorize!
|
|
157
205
|
end
|
|
158
206
|
|
|
207
|
+
def when_to_retry(request, response, *)
|
|
208
|
+
return super unless response.is_a?(Response)
|
|
209
|
+
|
|
210
|
+
# retry immediately if authentication no longer valid.
|
|
211
|
+
return if response.status == 401
|
|
212
|
+
|
|
213
|
+
super
|
|
214
|
+
end
|
|
215
|
+
|
|
159
216
|
def auth_error?(response, options)
|
|
160
217
|
response.is_a?(Response) && response.status == 401 && dynamic_auth_token?(options.auth_header_value)
|
|
161
218
|
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
#
|
|
6
|
+
# This plugin adds support for caching and reusing responses
|
|
7
|
+
#
|
|
8
|
+
# https://gitlab.com/os85/httpx/wikis/Cache
|
|
9
|
+
#
|
|
10
|
+
module Cache
|
|
11
|
+
class << self
|
|
12
|
+
def load_dependencies(*)
|
|
13
|
+
require_relative "cache/store"
|
|
14
|
+
require_relative "cache/file_store"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def extra_options(options)
|
|
18
|
+
options.merge(
|
|
19
|
+
response_cache_store: :store,
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# adds support for the following options:
|
|
25
|
+
#
|
|
26
|
+
# :cache_key :: callable which receives a request and returns the corresponding cache key as a string
|
|
27
|
+
# (to be used by the cache store when storing cached responses)
|
|
28
|
+
# :cacheable_request :: callable which receives a request and returns whether this request can use a previously cached response,
|
|
29
|
+
# or for which a freshly retrieved response can be cached.
|
|
30
|
+
# :cacheable_response :: callable which receives a request and a (freshly retrieved) response and returns whether the response
|
|
31
|
+
# can be cached.
|
|
32
|
+
# :valid_cached_response :: callable which receives a request and a (previously cached) response and returns whether the response
|
|
33
|
+
# can still be used / returned to the caller.
|
|
34
|
+
# :response_cache_store :: object where cached responses are fetch from or stored in; defaults to <tt>:store</tt> (in-memory
|
|
35
|
+
# cache), can be set to <tt>:file_store</tt> (file system cache store) as well, or any object which
|
|
36
|
+
# abides by the Cache Store Interface
|
|
37
|
+
#
|
|
38
|
+
# The Cache Store Interface requires implementation of the following methods:
|
|
39
|
+
#
|
|
40
|
+
# * +#get(request) -> response or nil+
|
|
41
|
+
# * +#set(request, response) -> void+
|
|
42
|
+
# * +#clear() -> void+)
|
|
43
|
+
#
|
|
44
|
+
module OptionsMethods
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def option_cache_key(v)
|
|
48
|
+
raise TypeError, "`:cache_key` must be a callable" unless v.respond_to?(:call)
|
|
49
|
+
|
|
50
|
+
v
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def option_cacheable_request(v)
|
|
54
|
+
raise TypeError, "`:cacheable_request` must be a callable" unless v.respond_to?(:call)
|
|
55
|
+
|
|
56
|
+
v
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def option_cacheable_response(v)
|
|
60
|
+
raise TypeError, "`:cacheable_response` must be a callable" unless v.respond_to?(:call)
|
|
61
|
+
|
|
62
|
+
v
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def option_valid_cached_response(v)
|
|
66
|
+
raise TypeError, "`:valid_cached_response` must be a callable" unless v.respond_to?(:call)
|
|
67
|
+
|
|
68
|
+
v
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def option_response_cache_store(value)
|
|
72
|
+
case value
|
|
73
|
+
when :store
|
|
74
|
+
Store.new
|
|
75
|
+
when :file_store
|
|
76
|
+
FileStore.new
|
|
77
|
+
else
|
|
78
|
+
value
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
module InstanceMethods
|
|
84
|
+
# wipes out all cached responses from the cache store.
|
|
85
|
+
def clear_response_cache
|
|
86
|
+
@options.response_cache_store.clear
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def build_request(*)
|
|
90
|
+
request = super
|
|
91
|
+
return request unless cacheable_request?(request)
|
|
92
|
+
|
|
93
|
+
prepare_cache(request)
|
|
94
|
+
|
|
95
|
+
request
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def send_request(request, *)
|
|
101
|
+
return request if request.response
|
|
102
|
+
|
|
103
|
+
super
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def fetch_response(request, *)
|
|
107
|
+
response = super
|
|
108
|
+
|
|
109
|
+
return unless response
|
|
110
|
+
|
|
111
|
+
if cacheable_request?(request) && cacheable_response?(request, response) && !response.cached?
|
|
112
|
+
log { "caching response for #{request.uri}..." }
|
|
113
|
+
request.options.response_cache_store.set(request, response)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
response
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# whether +request+ can use cached responses.
|
|
120
|
+
def cacheable_request?(request)
|
|
121
|
+
return false unless (call = request.options.cacheable_request)
|
|
122
|
+
|
|
123
|
+
call[request]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# whether the retrieved +response+ can be cached.
|
|
127
|
+
def cacheable_response?(request, response)
|
|
128
|
+
return false unless (call = request.options.cacheable_response)
|
|
129
|
+
|
|
130
|
+
call[request, response]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# whether the cached +cached_response+ is still valid for the current +request+
|
|
134
|
+
def valid_cached_response?(request, cached_response)
|
|
135
|
+
return false unless (call = request.options.valid_cached_response)
|
|
136
|
+
|
|
137
|
+
call[request, cached_response]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# will either assign a still-fresh cached response to +request+, or set up its HTTP
|
|
141
|
+
# cache invalidation headers in case it's not fresh anymore.
|
|
142
|
+
def prepare_cache(request)
|
|
143
|
+
cached_response = retrieve_cached_response(request)
|
|
144
|
+
|
|
145
|
+
return unless cached_response && valid_cached_response?(request, cached_response)
|
|
146
|
+
|
|
147
|
+
request.cached_response = nil
|
|
148
|
+
|
|
149
|
+
# if the cached response is still usable, we use it
|
|
150
|
+
cached_response.body.rewind
|
|
151
|
+
cached_response = cached_response.dup
|
|
152
|
+
cached_response.mark_as_cached!
|
|
153
|
+
request.response = cached_response
|
|
154
|
+
request.emit_response(cached_response)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# calls the cache store to retrieve the cached response for +request+. Caches it
|
|
158
|
+
# for convenience of subplugins in order to minimize overhead of retrieval (which may
|
|
159
|
+
# involve network).
|
|
160
|
+
def retrieve_cached_response(request)
|
|
161
|
+
request.cached_response ||= request.options.response_cache_store.get(request)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
module RequestMethods
|
|
166
|
+
# points to a previously cached Response corresponding to this request.
|
|
167
|
+
attr_accessor :cached_response
|
|
168
|
+
|
|
169
|
+
def initialize(*)
|
|
170
|
+
super
|
|
171
|
+
@cached_response = nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def merge_headers(*)
|
|
175
|
+
super
|
|
176
|
+
@response_cache_key = nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# returns a unique cache key as a String identifying this request
|
|
180
|
+
def response_cache_key
|
|
181
|
+
return unless (call = @options.cache_key)
|
|
182
|
+
|
|
183
|
+
call[self]
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
module ResponseMethods
|
|
188
|
+
attr_writer :original_request
|
|
189
|
+
|
|
190
|
+
def initialize(*)
|
|
191
|
+
super
|
|
192
|
+
@cached = false
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# a copy of the request this response was originally cached from
|
|
196
|
+
def original_request
|
|
197
|
+
@original_request || @request
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# whether this Response was duplicated from a previously {RequestMethods#cached_response}.
|
|
201
|
+
def cached?
|
|
202
|
+
@cached
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# sets this Response as being duplicated from a previously cached response.
|
|
206
|
+
def mark_as_cached!
|
|
207
|
+
@cached = true
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
module ResponseBodyMethods
|
|
212
|
+
def decode_chunk(chunk)
|
|
213
|
+
return chunk if @response.cached?
|
|
214
|
+
|
|
215
|
+
super
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
register_plugin :cache, Cache
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -122,6 +122,18 @@ module HTTPX
|
|
|
122
122
|
end
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
module RequestMethods
|
|
126
|
+
def drain_body
|
|
127
|
+
super.tap do |chunk|
|
|
128
|
+
emit(:body_chunk, chunk) if chunk
|
|
129
|
+
rescue StandardError => e
|
|
130
|
+
# in case an error occurs in callback code
|
|
131
|
+
@drain_error = e
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
125
137
|
module ConnectionMethods
|
|
126
138
|
private
|
|
127
139
|
|
|
@@ -51,9 +51,13 @@ module HTTPX
|
|
|
51
51
|
|
|
52
52
|
if probe_response.status == 401 && digest.can_authenticate?(probe_response.headers["www-authenticate"])
|
|
53
53
|
request.transition(:idle)
|
|
54
|
+
|
|
54
55
|
if (auth_header = digest.authenticate(request, probe_response.headers["www-authenticate"]))
|
|
55
56
|
request.authorize(auth_header)
|
|
56
57
|
end
|
|
58
|
+
|
|
59
|
+
next(request.response) if request.response
|
|
60
|
+
|
|
57
61
|
super(request)
|
|
58
62
|
else
|
|
59
63
|
probe_response
|
|
@@ -70,18 +70,56 @@ module HTTPX
|
|
|
70
70
|
)
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
def initial_call
|
|
74
|
+
return unless current_context?
|
|
75
|
+
|
|
76
|
+
super
|
|
77
|
+
end
|
|
78
|
+
|
|
73
79
|
def interests
|
|
74
80
|
return if connecting? && @pending.none?(&:current_context?)
|
|
75
81
|
|
|
76
82
|
super
|
|
77
83
|
end
|
|
78
84
|
|
|
79
|
-
def
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
def on_io_error(e)
|
|
86
|
+
return super unless e.is_a?(IOError) && e.message.include?("stream closed in another thread")
|
|
87
|
+
|
|
88
|
+
# @fiber-switch-guard
|
|
89
|
+
# sockets closed during fiber scheduler switches are raised in separate fibers than the fiber the
|
|
90
|
+
# socket may be used in. this check verifies that this is actually about this socket.
|
|
91
|
+
return unless to_io.closed?
|
|
92
|
+
|
|
93
|
+
if @state == :closing
|
|
94
|
+
# @fiber-switch-guard
|
|
95
|
+
# if the connection is reused across fibers, the socket may have been closed in the other fiber
|
|
96
|
+
# and switched here during the process, so continue what it was doing and transition to closed
|
|
97
|
+
# via #call.
|
|
98
|
+
call
|
|
99
|
+
elsif !backlog?
|
|
100
|
+
super
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def on_connect_error(e)
|
|
105
|
+
return super unless e.is_a?(IOError) && e.message.include?("stream closed in another thread")
|
|
106
|
+
|
|
107
|
+
# @fiber-switch-guard
|
|
108
|
+
# sockets closed during fiber scheduler switches are raised in separate fibers than the fiber the
|
|
109
|
+
# socket may be used in. this check verifies that this is actually about this socket.
|
|
110
|
+
return unless to_io.closed? && !backlog?
|
|
82
111
|
|
|
83
112
|
super
|
|
84
113
|
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
# checks whether the connection has any pending request (which the connection itself may
|
|
118
|
+
# have stored, or it may be somewhere in the parser).
|
|
119
|
+
def backlog?
|
|
120
|
+
@pending.any? ||
|
|
121
|
+
(@parser && (@parser.pending.any? || @parser.requests.any?))
|
|
122
|
+
end
|
|
85
123
|
end
|
|
86
124
|
|
|
87
125
|
module HTTP1Methods
|
|
@@ -161,6 +199,12 @@ module HTTPX
|
|
|
161
199
|
end
|
|
162
200
|
|
|
163
201
|
module ResolverNativeMethods
|
|
202
|
+
def initial_call
|
|
203
|
+
return unless @queries.values.any?(&:current_context?) || @connections.any?(&:current_context?)
|
|
204
|
+
|
|
205
|
+
super
|
|
206
|
+
end
|
|
207
|
+
|
|
164
208
|
def calculate_interests
|
|
165
209
|
return if @queries.empty?
|
|
166
210
|
|
|
@@ -168,14 +212,41 @@ module HTTPX
|
|
|
168
212
|
|
|
169
213
|
super
|
|
170
214
|
end
|
|
215
|
+
|
|
216
|
+
def disconnect
|
|
217
|
+
return unless @connections.all?(&:current_context?)
|
|
218
|
+
|
|
219
|
+
super
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def on_io_error(e)
|
|
223
|
+
# TODO: return super if this is not stream clsed in another thread
|
|
224
|
+
|
|
225
|
+
log { "IO Erroring: #{e.message}, current:#{@name}, queries:#{@queries.size}" }
|
|
226
|
+
return unless @name
|
|
227
|
+
|
|
228
|
+
super
|
|
229
|
+
end
|
|
171
230
|
end
|
|
172
231
|
|
|
173
232
|
module ResolverSystemMethods
|
|
233
|
+
def initial_call
|
|
234
|
+
return unless current_context?
|
|
235
|
+
|
|
236
|
+
super
|
|
237
|
+
end
|
|
238
|
+
|
|
174
239
|
def interests
|
|
175
|
-
return unless
|
|
240
|
+
return unless current_context?
|
|
176
241
|
|
|
177
242
|
super
|
|
178
243
|
end
|
|
244
|
+
|
|
245
|
+
private
|
|
246
|
+
|
|
247
|
+
def current_context?
|
|
248
|
+
@queries.any? { |_, conn| conn.current_context? }
|
|
249
|
+
end
|
|
179
250
|
end
|
|
180
251
|
|
|
181
252
|
module FiberConcurrencyH2C
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTPX
|
|
4
|
+
module Plugins
|
|
5
|
+
# https://gitlab.com/os85/httpx/wikis/Auth#ntlm-v2-auth
|
|
6
|
+
module NtlmV2Auth
|
|
7
|
+
class << self
|
|
8
|
+
def load_dependencies(klass)
|
|
9
|
+
require "rubyntlm"
|
|
10
|
+
klass.plugin(:auth)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def extra_options(options)
|
|
14
|
+
options.merge(max_concurrent_requests: 1)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Authenticator
|
|
19
|
+
def initialize(user, password, domain: nil)
|
|
20
|
+
@user = user
|
|
21
|
+
@password = password
|
|
22
|
+
@domain = domain
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def can_authenticate?(www_authenticate)
|
|
26
|
+
www_authenticate && /NTLM/i.match?(www_authenticate)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def negotiate
|
|
30
|
+
t1 = Net::NTLM::Message::Type1.new
|
|
31
|
+
t1.domain = @domain if @domain
|
|
32
|
+
"NTLM #{t1.encode64}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def authenticate(_request, www_authenticate)
|
|
36
|
+
challenge_b64 = www_authenticate[/NTLM (.+)/i, 1]
|
|
37
|
+
t2 = Net::NTLM::Message.decode64(challenge_b64)
|
|
38
|
+
t3 = t2.response(
|
|
39
|
+
{ user: @user, password: @password, domain: @domain },
|
|
40
|
+
ntlmv2: true
|
|
41
|
+
)
|
|
42
|
+
"NTLM #{t3.encode64}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
module OptionsMethods
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def option_ntlm(value)
|
|
50
|
+
raise TypeError, ":ntlm must be a #{Authenticator}" unless value.is_a?(NtlmV2Auth::Authenticator)
|
|
51
|
+
|
|
52
|
+
value
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
module InstanceMethods
|
|
57
|
+
def ntlm_auth(user, password, domain = nil)
|
|
58
|
+
with(ntlm: Authenticator.new(user, password, domain: domain))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def send_requests(*requests)
|
|
64
|
+
requests.flat_map do |request|
|
|
65
|
+
ntlm = request.options.ntlm
|
|
66
|
+
|
|
67
|
+
if ntlm
|
|
68
|
+
request.authorize(ntlm.negotiate)
|
|
69
|
+
probe_response = wrap { super(request).first }
|
|
70
|
+
|
|
71
|
+
return probe_response unless probe_response.is_a?(Response)
|
|
72
|
+
|
|
73
|
+
if probe_response.status == 401 && ntlm.can_authenticate?(probe_response.headers["www-authenticate"])
|
|
74
|
+
request.transition(:idle)
|
|
75
|
+
request.unauthorize!
|
|
76
|
+
request.authorize(ntlm.authenticate(request,
|
|
77
|
+
probe_response.headers["www-authenticate"]).encode("utf-8"))
|
|
78
|
+
super(request)
|
|
79
|
+
else
|
|
80
|
+
probe_response
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
super(request)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
register_plugin :ntlm_v2_auth, NtlmV2Auth
|
|
91
|
+
end
|
|
92
|
+
end
|