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.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_7_1.md +1 -2
  3. data/doc/release_notes/1_8_0.md +100 -0
  4. data/doc/release_notes/1_8_1.md +27 -0
  5. data/lib/httpx/adapters/datadog.rb +12 -1
  6. data/lib/httpx/adapters/faraday.rb +2 -2
  7. data/lib/httpx/adapters/webmock.rb +6 -0
  8. data/lib/httpx/connection/http1.rb +15 -12
  9. data/lib/httpx/connection/http2.rb +69 -9
  10. data/lib/httpx/connection.rb +119 -21
  11. data/lib/httpx/errors.rb +8 -1
  12. data/lib/httpx/headers.rb +2 -2
  13. data/lib/httpx/io/ssl.rb +8 -9
  14. data/lib/httpx/io/tcp.rb +22 -5
  15. data/lib/httpx/io/unix.rb +2 -2
  16. data/lib/httpx/loggable.rb +11 -4
  17. data/lib/httpx/options.rb +16 -4
  18. data/lib/httpx/parser/http1.rb +8 -2
  19. data/lib/httpx/plugins/auth/digest.rb +1 -1
  20. data/lib/httpx/plugins/auth.rb +61 -4
  21. data/lib/httpx/plugins/{response_cache → cache}/file_store.rb +1 -1
  22. data/lib/httpx/plugins/{response_cache → cache}/store.rb +1 -1
  23. data/lib/httpx/plugins/cache.rb +221 -0
  24. data/lib/httpx/plugins/callbacks.rb +12 -0
  25. data/lib/httpx/plugins/digest_auth.rb +4 -0
  26. data/lib/httpx/plugins/fiber_concurrency.rb +75 -4
  27. data/lib/httpx/plugins/ntlm_v2_auth.rb +92 -0
  28. data/lib/httpx/plugins/oauth.rb +66 -14
  29. data/lib/httpx/plugins/persistent.rb +18 -2
  30. data/lib/httpx/plugins/proxy.rb +5 -0
  31. data/lib/httpx/plugins/push_promise.rb +2 -2
  32. data/lib/httpx/plugins/response_cache.rb +26 -105
  33. data/lib/httpx/plugins/retries.rb +7 -5
  34. data/lib/httpx/plugins/server_sent_events.rb +158 -0
  35. data/lib/httpx/plugins/ssrf_filter.rb +17 -2
  36. data/lib/httpx/plugins/stream.rb +9 -5
  37. data/lib/httpx/plugins/stream_bidi.rb +2 -0
  38. data/lib/httpx/plugins/tracing.rb +15 -4
  39. data/lib/httpx/pool.rb +2 -2
  40. data/lib/httpx/request/body.rb +2 -2
  41. data/lib/httpx/request.rb +42 -10
  42. data/lib/httpx/resolver/cache/base.rb +1 -8
  43. data/lib/httpx/resolver/cache/file.rb +56 -0
  44. data/lib/httpx/resolver/https.rb +20 -20
  45. data/lib/httpx/resolver/native.rb +21 -3
  46. data/lib/httpx/resolver/resolver.rb +4 -0
  47. data/lib/httpx/resolver/system.rb +5 -2
  48. data/lib/httpx/response/body.rb +6 -4
  49. data/lib/httpx/response.rb +12 -5
  50. data/lib/httpx/selector.rb +12 -1
  51. data/lib/httpx/session.rb +6 -1
  52. data/lib/httpx/session_extensions.rb +2 -2
  53. data/lib/httpx/timers.rb +3 -0
  54. data/lib/httpx/version.rb +1 -1
  55. data/sig/chainable.rbs +3 -0
  56. data/sig/connection/http1.rbs +1 -1
  57. data/sig/connection/http2.rbs +4 -1
  58. data/sig/connection.rbs +16 -9
  59. data/sig/errors.rbs +9 -3
  60. data/sig/httpx.rbs +2 -0
  61. data/sig/io/tcp.rbs +2 -0
  62. data/sig/loggable.rbs +8 -0
  63. data/sig/options.rbs +25 -12
  64. data/sig/parser/http1.rbs +3 -1
  65. data/sig/plugins/auth/ntlm.rbs +1 -1
  66. data/sig/plugins/auth.rbs +4 -0
  67. data/sig/plugins/{response_cache → cache}/file_store.rbs +2 -2
  68. data/sig/plugins/{response_cache → cache}/store.rbs +2 -2
  69. data/sig/plugins/cache.rbs +69 -0
  70. data/sig/plugins/fiber_concurrency.rbs +4 -0
  71. data/sig/plugins/ntlm_v2_auth.rbs +36 -0
  72. data/sig/plugins/persistent.rbs +3 -0
  73. data/sig/plugins/response_cache.rbs +13 -38
  74. data/sig/plugins/retries.rbs +5 -5
  75. data/sig/plugins/server_sent_events.rbs +45 -0
  76. data/sig/plugins/ssrf_filter.rbs +5 -1
  77. data/sig/plugins/stream.rbs +1 -1
  78. data/sig/plugins/stream_bidi.rbs +0 -2
  79. data/sig/plugins/webdav.rbs +1 -1
  80. data/sig/pool.rbs +2 -2
  81. data/sig/request.rbs +9 -6
  82. data/sig/resolver/cache/file.rbs +13 -0
  83. data/sig/resolver/entry.rbs +1 -1
  84. data/sig/resolver/https.rbs +3 -3
  85. data/sig/resolver/multi.rbs +1 -1
  86. data/sig/resolver/native.rbs +5 -5
  87. data/sig/resolver/resolver.rbs +1 -3
  88. data/sig/resolver/system.rbs +5 -2
  89. data/sig/resolver.rbs +3 -0
  90. data/sig/response.rbs +3 -0
  91. data/sig/selector.rbs +13 -8
  92. data/sig/timers.rbs +7 -5
  93. data/sig/transcoder/body.rbs +1 -1
  94. data/sig/transcoder/gzip.rbs +3 -2
  95. data/sig/transcoder/multipart.rbs +4 -1
  96. data/sig/transcoder/utils/deflater.rbs +2 -0
  97. data/sig/transcoder.rbs +2 -0
  98. data/sig/utils.rbs +1 -1
  99. metadata +19 -7
@@ -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
- @auth_header_value ||= generate_auth_token
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
- @auth_header_value = generate_auth_token if request.auth_token_value == @auth_header_value
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
@@ -3,7 +3,7 @@
3
3
  require "pathname"
4
4
 
5
5
  module HTTPX::Plugins
6
- module ResponseCache
6
+ module Cache
7
7
  # Implementation of a file system based cache store.
8
8
  #
9
9
  # It stores cached responses in a file under a directory pointed by the +dir+
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX::Plugins
4
- module ResponseCache
4
+ module Cache
5
5
  # Implementation of a thread-safe in-memory cache store.
6
6
  class Store
7
7
  def initialize
@@ -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 send(request)
80
- # DoH requests bypass the session, so context needs to be set here.
81
- request.set_context!
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 @queries.any? { |_, conn| conn.current_context? }
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