httpx 0.19.8 → 0.20.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_19_8.md +1 -1
  3. data/doc/release_notes/0_20_0.md +36 -0
  4. data/doc/release_notes/0_20_1.md +5 -0
  5. data/doc/release_notes/0_20_2.md +7 -0
  6. data/lib/httpx/adapters/sentry.rb +102 -0
  7. data/lib/httpx/connection.rb +16 -2
  8. data/lib/httpx/io/ssl.rb +8 -3
  9. data/lib/httpx/options.rb +5 -0
  10. data/lib/httpx/plugins/authentication/basic.rb +24 -0
  11. data/lib/httpx/plugins/authentication/digest.rb +102 -0
  12. data/lib/httpx/plugins/authentication/ntlm.rb +37 -0
  13. data/lib/httpx/plugins/authentication/socks5.rb +24 -0
  14. data/lib/httpx/plugins/basic_authentication.rb +6 -6
  15. data/lib/httpx/plugins/digest_authentication.rb +15 -111
  16. data/lib/httpx/plugins/follow_redirects.rb +17 -5
  17. data/lib/httpx/plugins/ntlm_authentication.rb +8 -18
  18. data/lib/httpx/plugins/proxy/http.rb +76 -13
  19. data/lib/httpx/plugins/proxy/socks5.rb +6 -4
  20. data/lib/httpx/plugins/proxy.rb +30 -8
  21. data/lib/httpx/plugins/response_cache/store.rb +1 -0
  22. data/lib/httpx/pool.rb +4 -4
  23. data/lib/httpx/request.rb +3 -1
  24. data/lib/httpx/resolver/native.rb +6 -2
  25. data/lib/httpx/selector.rb +9 -2
  26. data/lib/httpx/session.rb +7 -2
  27. data/lib/httpx/version.rb +1 -1
  28. data/sig/chainable.rbs +4 -4
  29. data/sig/connection.rbs +3 -0
  30. data/sig/plugins/authentication/basic.rbs +19 -0
  31. data/sig/plugins/authentication/digest.rbs +24 -0
  32. data/sig/plugins/authentication/ntlm.rbs +20 -0
  33. data/sig/plugins/authentication/socks5.rbs +18 -0
  34. data/sig/plugins/basic_authentication.rbs +2 -2
  35. data/sig/plugins/digest_authentication.rbs +3 -13
  36. data/sig/plugins/ntlm_authentication.rbs +3 -8
  37. data/sig/plugins/proxy/http.rbs +13 -3
  38. data/sig/plugins/proxy.rbs +5 -3
  39. data/sig/pool.rbs +2 -2
  40. data/sig/resolver/native.rbs +3 -1
  41. data/sig/selector.rbs +1 -1
  42. data/sig/session.rbs +1 -1
  43. metadata +17 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05a379db82c9c86f84a847680f4a784ed62a4cbcb8ce8d09a3d3c29abeb7d5d1
4
- data.tar.gz: 5046f5d1e86f7d5c523a171b23fd5fb8126b63636a021c204a7c33a4ebf5e319
3
+ metadata.gz: b49337207a605c716ef9a1043660239ced7f5060758148f905be6c1d5546a8bc
4
+ data.tar.gz: 732ffb35912e5176827941034b74ce2777d744e828e1ed86351bdd221f010d19
5
5
  SHA512:
6
- metadata.gz: d62d606f0688c485f79a38feed6b94841005fb6a70c5ab934c7b70cb7a7958066b85a496405959a7660da3f12f194d844f10660621ce6fb98c7aa898c38d855c
7
- data.tar.gz: 42b46c44c4c864222f14235534be31db4eb5155716c542457178be010b227fdb391c095b009759e9c237d6320da7133123334cfc4b3d4dd6940ff58aa6df9f0e
6
+ metadata.gz: 5c1f12fa0d1675f8e8c478ea272456f910dc558b7836eaf0bfa6e916d29439e7dd093ffdde4903a43b474559909052df4d26872846e704c393045f10d496a53a
7
+ data.tar.gz: 9011bf6d1826ea2a44a3570f9dcbe9dc5bb319303809cf8b44247f9ac2bdb099ccc0895192155960df320dbdad4bebf009a7ec77dc1e37d1e8e27a59077efe5a
@@ -1,4 +1,4 @@
1
- # 0.19.7
1
+ # 0.19.8
2
2
 
3
3
  ## Bugfixes
4
4
 
@@ -0,0 +1,36 @@
1
+ # 0.20.0
2
+
3
+ ## Features
4
+
5
+ ### Sentry integration
6
+
7
+ Documentation: https://gitlab.com/honeyryderchuck/httpx/-/wikis/Sentry-Adapter
8
+
9
+ `httpx` ships with integration for `sentry-ruby` to provide HTTP request specific breadcrumbs and tracing. It can be enabled via:
10
+
11
+ ```ruby
12
+ require "httpx/adapters/sentry"
13
+ ```
14
+
15
+ ### Proxy alternative auth schemes
16
+
17
+ Besides the already previously supported (and still default) HTTP Basic Auth, the `:proxy` plugin supports HTTP Digest and NTLM auth as well. These are made available via the following APIs:
18
+
19
+ ```ruby
20
+ http = HTTPX.plugin(:proxy)
21
+ http.with_proxy_basic_auth(username: "user", password: "pass", uri: "http://proxy-uri:8126")
22
+ http.with_proxy_digest_auth(username: "user", password: "pass", uri: "http://proxy-uri:8126")
23
+ http.with_proxy_ntlm_auth(username: "user", password: "pass", uri: "http://proxy-uri:8126")
24
+
25
+ # or alternatively
26
+ http.with_proxy(proxy: "basic", username: "user", password: "pass", uri: "http://proxy-uri:8126")
27
+ ```
28
+
29
+ ## Bugfixes
30
+
31
+ * HTTPS requests on an URL with an IP as a host, will now correctly not perform SNI during the TLS handshake, as per RFC;
32
+ * `:follow_redirects` plugin will now halt redirections on 3xx responses with no `"location"` headers; this means it won't crash on 304 responses.
33
+ * If the `httpx` session has the `:proxy` plugin enabled, HTTP 305 responses will retry the request via the proxy exposed in the `"location"` header, as the RFC mandates.
34
+ * `alt-svc` connection switch for HTTPS requests will be halted if the advertised alternative service "downgrades" to cleartext (example: `alt-svc` advertises `"h2c"`, but original connection was enabled via TLS).
35
+ * A new connection to a TLS-enabled `alt-svc` advertised for a previous request, will now use that request's hostname as the SNI hostname, when performing the TLS handshake.
36
+ * the `:response_cache` plugin will now correctly handle capitalized HTTP headers advertised in the `"vary"` header.
@@ -0,0 +1,5 @@
1
+ # 0.20.1
2
+
3
+ ## Bugfixes
4
+
5
+ * bugfix for unregistering connections when timing out on DNS resolving; this wasn't happening, leaving a few cases where requests to the same domain timing out on resolution would hang on the second request.
@@ -0,0 +1,7 @@
1
+ # 0.20.2
2
+
3
+ ## Bugfixes
4
+
5
+ * fix for selector timeout errors closing all connections and ignoring resolvers.
6
+
7
+ Timeout errors on select were being propagated to all pooled connections, although not all of them were being selected on, and not all of them having timed out. plus, resolver timeouts were doing the same, making connections fail with connection timeout error, rather than resolve timeout error. A patch was implemented, where the selector now yields an error to the selected connections, rather than plain raising exception.
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry-ruby"
4
+
5
+ module HTTPX::Plugins
6
+ module Sentry
7
+ module Tracer
8
+ module_function
9
+
10
+ def call(request)
11
+ sentry_span = start_sentry_span
12
+
13
+ return unless sentry_span
14
+
15
+ set_sentry_trace_header(request, sentry_span)
16
+
17
+ request.on(:response, &method(:finish_sentry_span).curry(3)[sentry_span, request])
18
+ end
19
+
20
+ def start_sentry_span
21
+ return unless ::Sentry.initialized? && (span = ::Sentry.get_current_scope.get_span)
22
+ return if span.sampled == false
23
+
24
+ span.start_child(op: "httpx.client", start_timestamp: ::Sentry.utc_now.to_f)
25
+ end
26
+
27
+ def set_sentry_trace_header(request, sentry_span)
28
+ return unless sentry_span
29
+
30
+ trace = ::Sentry.get_current_client.generate_sentry_trace(sentry_span)
31
+ request.headers[::Sentry::SENTRY_TRACE_HEADER_NAME] = trace if trace
32
+ end
33
+
34
+ def finish_sentry_span(span, request, response)
35
+ return unless ::Sentry.initialized?
36
+
37
+ record_sentry_breadcrumb(request, response)
38
+ record_sentry_span(request, response, span)
39
+ end
40
+
41
+ def record_sentry_breadcrumb(req, res)
42
+ return unless ::Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
43
+
44
+ request_info = extract_request_info(req)
45
+
46
+ data = if response.is_a?(HTTPX::ErrorResponse)
47
+ { error: res.message, **request_info }
48
+ else
49
+ { status: res.status, **request_info }
50
+ end
51
+
52
+ crumb = ::Sentry::Breadcrumb.new(
53
+ level: :info,
54
+ category: "httpx",
55
+ type: :info,
56
+ data: data
57
+ )
58
+ ::Sentry.add_breadcrumb(crumb)
59
+ end
60
+
61
+ def record_sentry_span(req, res, sentry_span)
62
+ return unless sentry_span
63
+
64
+ request_info = extract_request_info(req)
65
+ sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
66
+ sentry_span.set_data(:status, res.status)
67
+ sentry_span.set_timestamp(::Sentry.utc_now.to_f)
68
+ end
69
+
70
+ def extract_request_info(req)
71
+ uri = req.uri
72
+
73
+ result = {
74
+ method: req.verb.to_s.upcase,
75
+ }
76
+
77
+ if ::Sentry.configuration.send_default_pii
78
+ uri += "?#{req.query}" unless req.query.empty?
79
+ result[:body] = req.body.to_s unless req.body.empty? || req.body.unbounded_body?
80
+ end
81
+
82
+ result[:url] = uri.to_s
83
+
84
+ result
85
+ end
86
+ end
87
+
88
+ module ConnectionMethods
89
+ def send(request)
90
+ Tracer.call(request)
91
+ super
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ Sentry.register_patch do
98
+ sentry_session = ::HTTPX.plugin(HTTPX::Plugins::Sentry)
99
+
100
+ HTTPX.send(:remove_const, :Session)
101
+ HTTPX.send(:const_set, :Session, sentry_session.class)
102
+ end
@@ -102,8 +102,8 @@ module HTTPX
102
102
  # origin came from an ORIGIN frame, we're going to verify the hostname with the
103
103
  # SSL certificate
104
104
  (@origins.size == 1 || @origin == uri.origin || (@io && @io.verify_hostname(uri.host)))
105
- ) || match_altsvcs?(uri)
106
- ) && @options == options
105
+ ) && @options == options
106
+ ) || (match_altsvcs?(uri) && match_altsvc_options?(uri, options))
107
107
  end
108
108
 
109
109
  def mergeable?(connection)
@@ -162,6 +162,14 @@ module HTTPX
162
162
  end
163
163
  end
164
164
 
165
+ def match_altsvc_options?(uri, options)
166
+ return @options == options unless @options.ssl[:hostname] == uri.host
167
+
168
+ dup_options = @options.merge(ssl: { hostname: nil })
169
+ dup_options.ssl.delete(:hostname)
170
+ dup_options == options
171
+ end
172
+
165
173
  def connecting?
166
174
  @state == :idle
167
175
  end
@@ -268,6 +276,12 @@ module HTTPX
268
276
  @state == :open || @state == :inactive
269
277
  end
270
278
 
279
+ def raise_timeout_error(interval)
280
+ error = HTTPX::TimeoutError.new(interval, "timed out while waiting on select")
281
+ error.set_backtrace(caller)
282
+ on_error(error)
283
+ end
284
+
271
285
  private
272
286
 
273
287
  def connect
data/lib/httpx/io/ssl.rb CHANGED
@@ -4,8 +4,11 @@ require "openssl"
4
4
 
5
5
  module HTTPX
6
6
  TLSError = OpenSSL::SSL::SSLError
7
+ IPRegex = Regexp.union(Resolv::IPv4::Regex, Resolv::IPv6::Regex)
7
8
 
8
9
  class SSL < TCP
10
+ using RegexpExtensions unless Regexp.method_defined?(:match?)
11
+
9
12
  TLS_OPTIONS = if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
10
13
  { alpn_protocols: %w[h2 http/1.1].freeze }.freeze
11
14
  else
@@ -19,6 +22,8 @@ module HTTPX
19
22
  @sni_hostname = ctx_options.delete(:hostname) || @hostname
20
23
  @ctx.set_params(ctx_options) unless ctx_options.empty?
21
24
  @state = :negotiated if @keep_open
25
+
26
+ @hostname_is_ip = IPRegex.match?(@sni_hostname)
22
27
  end
23
28
 
24
29
  def protocol
@@ -56,7 +61,7 @@ module HTTPX
56
61
 
57
62
  unless @io.is_a?(OpenSSL::SSL::SSLSocket)
58
63
  @io = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
59
- @io.hostname = @sni_hostname
64
+ @io.hostname = @sni_hostname unless @hostname_is_ip
60
65
  @io.sync_close = true
61
66
  end
62
67
  try_ssl_connect
@@ -66,7 +71,7 @@ module HTTPX
66
71
  # :nocov:
67
72
  def try_ssl_connect
68
73
  @io.connect_nonblock
69
- @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
74
+ @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && !@hostname_is_ip
70
75
  transition(:negotiated)
71
76
  @interests = :w
72
77
  rescue ::IO::WaitReadable
@@ -98,7 +103,7 @@ module HTTPX
98
103
  @interests = :w
99
104
  return
100
105
  end
101
- @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
106
+ @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && !@hostname_is_ip
102
107
  transition(:negotiated)
103
108
  @interests = :w
104
109
  end
data/lib/httpx/options.rb CHANGED
@@ -132,6 +132,7 @@ module HTTPX
132
132
  def freeze
133
133
  super
134
134
  @origin.freeze
135
+ @base_path.freeze
135
136
  @timeout.freeze
136
137
  @headers.freeze
137
138
  @addresses.freeze
@@ -141,6 +142,10 @@ module HTTPX
141
142
  URI(value)
142
143
  end
143
144
 
145
+ def option_base_path(value)
146
+ String(value)
147
+ end
148
+
144
149
  def option_headers(value)
145
150
  Headers.new(value)
146
151
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module HTTPX
6
+ module Plugins
7
+ module Authentication
8
+ class Basic
9
+ def initialize(user, password, **)
10
+ @user = user
11
+ @password = password
12
+ end
13
+
14
+ def can_authenticate?(authenticate)
15
+ authenticate && /Basic .*/.match?(authenticate)
16
+ end
17
+
18
+ def authenticate(*)
19
+ "Basic #{Base64.strict_encode64("#{@user}:#{@password}")}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "securerandom"
5
+ require "digest"
6
+
7
+ module HTTPX
8
+ module Plugins
9
+ module Authentication
10
+ class Digest
11
+ using RegexpExtensions unless Regexp.method_defined?(:match?)
12
+
13
+ def initialize(user, password, **)
14
+ @user = user
15
+ @password = password
16
+ @nonce = 0
17
+ end
18
+
19
+ def can_authenticate?(authenticate)
20
+ authenticate && /Digest .*/.match?(authenticate)
21
+ end
22
+
23
+ def authenticate(request, authenticate)
24
+ "Digest #{generate_header(request.verb.to_s.upcase, request.path, authenticate)}"
25
+ end
26
+
27
+ private
28
+
29
+ def generate_header(meth, uri, authenticate)
30
+ # discard first token, it's Digest
31
+ auth_info = authenticate[/^(\w+) (.*)/, 2]
32
+
33
+ params = Hash[auth_info.split(/ *, */)
34
+ .map { |val| val.split("=") }
35
+ .map { |k, v| [k, v.delete("\"")] }]
36
+ nonce = params["nonce"]
37
+ nc = next_nonce
38
+
39
+ # verify qop
40
+ qop = params["qop"]
41
+
42
+ if params["algorithm"] =~ /(.*?)(-sess)?$/
43
+ alg = Regexp.last_match(1)
44
+ algorithm = ::Digest.const_get(alg)
45
+ raise DigestError, "unknown algorithm \"#{alg}\"" unless algorithm
46
+
47
+ sess = Regexp.last_match(2)
48
+ params.delete("algorithm")
49
+ else
50
+ algorithm = ::Digest::MD5
51
+ end
52
+
53
+ if qop || sess
54
+ cnonce = make_cnonce
55
+ nc = format("%<nonce>08x", nonce: nc)
56
+ end
57
+
58
+ a1 = if sess
59
+ [algorithm.hexdigest("#{@user}:#{params["realm"]}:#{@password}"),
60
+ nonce,
61
+ cnonce].join ":"
62
+ else
63
+ "#{@user}:#{params["realm"]}:#{@password}"
64
+ end
65
+
66
+ ha1 = algorithm.hexdigest(a1)
67
+ ha2 = algorithm.hexdigest("#{meth}:#{uri}")
68
+ request_digest = [ha1, nonce]
69
+ request_digest.push(nc, cnonce, qop) if qop
70
+ request_digest << ha2
71
+ request_digest = request_digest.join(":")
72
+
73
+ header = [
74
+ %(username="#{@user}"),
75
+ %(nonce="#{nonce}"),
76
+ %(uri="#{uri}"),
77
+ %(response="#{algorithm.hexdigest(request_digest)}"),
78
+ ]
79
+ header << %(realm="#{params["realm"]}") if params.key?("realm")
80
+ header << %(algorithm=#{params["algorithm"]}") if params.key?("algorithm")
81
+ header << %(opaque="#{params["opaque"]}") if params.key?("opaque")
82
+ header << %(cnonce="#{cnonce}") if cnonce
83
+ header << %(nc=#{nc})
84
+ header << %(qop=#{qop}) if qop
85
+ header.join ", "
86
+ end
87
+
88
+ def make_cnonce
89
+ ::Digest::MD5.hexdigest [
90
+ Time.now.to_i,
91
+ Process.pid,
92
+ SecureRandom.random_number(2**32),
93
+ ].join ":"
94
+ end
95
+
96
+ def next_nonce
97
+ @nonce += 1
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "ntlm"
5
+
6
+ module HTTPX
7
+ module Plugins
8
+ module Authentication
9
+ class Ntlm
10
+ using RegexpExtensions unless Regexp.method_defined?(:match?)
11
+
12
+ def initialize(user, password, domain: nil)
13
+ @user = user
14
+ @password = password
15
+ @domain = domain
16
+ end
17
+
18
+ def can_authenticate?(authenticate)
19
+ authenticate && /NTLM .*/.match?(authenticate)
20
+ end
21
+
22
+ def negotiate
23
+ "NTLM #{NTLM.negotiate(domain: @domain).to_base64}"
24
+ end
25
+
26
+ def authenticate(_req, www)
27
+ challenge = www[/NTLM (.*)/, 1]
28
+
29
+ challenge = Base64.decode64(challenge)
30
+ ntlm_challenge = NTLM.authenticate(challenge, @user, @domain, @password).to_base64
31
+
32
+ "NTLM #{ntlm_challenge}"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module HTTPX
6
+ module Plugins
7
+ module Authentication
8
+ class Socks5
9
+ def initialize(user, password, **)
10
+ @user = user
11
+ @password = password
12
+ end
13
+
14
+ def can_authenticate?(*)
15
+ @user && @password
16
+ end
17
+
18
+ def authenticate(*)
19
+ [0x01, @user.bytesize, @user, @password.bytesize, @password].pack("CCA*CA*")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -7,10 +7,10 @@ module HTTPX
7
7
  #
8
8
  # https://gitlab.com/honeyryderchuck/httpx/wikis/Authentication#basic-authentication
9
9
  #
10
- module BasicAuthentication
10
+ module BasicAuth
11
11
  class << self
12
12
  def load_dependencies(_klass)
13
- require "base64"
13
+ require_relative "authentication/basic"
14
14
  end
15
15
 
16
16
  def configure(klass)
@@ -19,12 +19,12 @@ module HTTPX
19
19
  end
20
20
 
21
21
  module InstanceMethods
22
- def basic_authentication(user, password)
23
- authentication("Basic #{Base64.strict_encode64("#{user}:#{password}")}")
22
+ def basic_auth(user, password)
23
+ authentication(Authentication::Basic.new(user, password).authenticate)
24
24
  end
25
- alias_method :basic_auth, :basic_authentication
25
+ alias_method :basic_authentication, :basic_auth
26
26
  end
27
27
  end
28
- register_plugin :basic_authentication, BasicAuthentication
28
+ register_plugin :basic_authentication, BasicAuth
29
29
  end
30
30
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
4
-
5
3
  module HTTPX
6
4
  module Plugins
7
5
  #
@@ -9,9 +7,7 @@ module HTTPX
9
7
  #
10
8
  # https://gitlab.com/honeyryderchuck/httpx/wikis/Authentication#authentication
11
9
  #
12
- module DigestAuthentication
13
- using RegexpExtensions unless Regexp.method_defined?(:match?)
14
-
10
+ module DigestAuth
15
11
  DigestError = Class.new(Error)
16
12
 
17
13
  class << self
@@ -20,14 +16,13 @@ module HTTPX
20
16
  end
21
17
 
22
18
  def load_dependencies(*)
23
- require "securerandom"
24
- require "digest"
19
+ require_relative "authentication/digest"
25
20
  end
26
21
  end
27
22
 
28
23
  module OptionsMethods
29
24
  def option_digest(value)
30
- raise TypeError, ":digest must be a Digest" unless value.is_a?(Digest)
25
+ raise TypeError, ":digest must be a Digest" unless value.is_a?(Authentication::Digest)
31
26
 
32
27
  value
33
28
  end
@@ -35,7 +30,7 @@ module HTTPX
35
30
 
36
31
  module InstanceMethods
37
32
  def digest_authentication(user, password)
38
- with(digest: Digest.new(user, password))
33
+ with(digest: Authentication::Digest.new(user, password))
39
34
  end
40
35
 
41
36
  alias_method :digest_auth, :digest_authentication
@@ -44,116 +39,25 @@ module HTTPX
44
39
  requests.flat_map do |request|
45
40
  digest = request.options.digest
46
41
 
47
- if digest
48
- probe_response = wrap { super(request).first }
49
-
50
- if digest && !probe_response.is_a?(ErrorResponse) &&
51
- probe_response.status == 401 && probe_response.headers.key?("www-authenticate") &&
52
- /Digest .*/.match?(probe_response.headers["www-authenticate"])
53
-
54
- request.transition(:idle)
55
-
56
- token = digest.generate_header(request, probe_response)
57
- request.headers["authorization"] = "Digest #{token}"
58
-
59
- super(request)
60
- else
61
- probe_response
62
- end
63
- else
42
+ unless digest
64
43
  super(request)
44
+ next
65
45
  end
66
- end
67
- end
68
- end
69
-
70
- class Digest
71
- def initialize(user, password)
72
- @user = user
73
- @password = password
74
- @nonce = 0
75
- end
76
-
77
- def generate_header(request, response, _iis = false)
78
- meth = request.verb.to_s.upcase
79
- www = response.headers["www-authenticate"]
80
-
81
- # discard first token, it's Digest
82
- auth_info = www[/^(\w+) (.*)/, 2]
83
-
84
- uri = request.path
85
-
86
- params = Hash[auth_info.split(/ *, */)
87
- .map { |val| val.split("=") }
88
- .map { |k, v| [k, v.delete("\"")] }]
89
- nonce = params["nonce"]
90
- nc = next_nonce
91
46
 
92
- # verify qop
93
- qop = params["qop"]
47
+ probe_response = wrap { super(request).first }
94
48
 
95
- if params["algorithm"] =~ /(.*?)(-sess)?$/
96
- alg = Regexp.last_match(1)
97
- algorithm = ::Digest.const_get(alg)
98
- raise DigestError, "unknown algorithm \"#{alg}\"" unless algorithm
99
-
100
- sess = Regexp.last_match(2)
101
- params.delete("algorithm")
102
- else
103
- algorithm = ::Digest::MD5
104
- end
105
-
106
- if qop || sess
107
- cnonce = make_cnonce
108
- nc = format("%<nonce>08x", nonce: nc)
109
- end
110
-
111
- a1 = if sess
112
- [algorithm.hexdigest("#{@user}:#{params["realm"]}:#{@password}"),
113
- nonce,
114
- cnonce].join ":"
115
- else
116
- "#{@user}:#{params["realm"]}:#{@password}"
49
+ if probe_response.status == 401 && digest.can_authenticate?(probe_response.headers["www-authenticate"])
50
+ request.transition(:idle)
51
+ request.headers["authorization"] = digest.authenticate(request, probe_response.headers["www-authenticate"])
52
+ super(request)
53
+ else
54
+ probe_response
55
+ end
117
56
  end
118
-
119
- ha1 = algorithm.hexdigest(a1)
120
- ha2 = algorithm.hexdigest("#{meth}:#{uri}")
121
- request_digest = [ha1, nonce]
122
- request_digest.push(nc, cnonce, qop) if qop
123
- request_digest << ha2
124
- request_digest = request_digest.join(":")
125
-
126
- header = [
127
- %(username="#{@user}"),
128
- %(nonce="#{nonce}"),
129
- %(uri="#{uri}"),
130
- %(response="#{algorithm.hexdigest(request_digest)}"),
131
- ]
132
- header << %(realm="#{params["realm"]}") if params.key?("realm")
133
- header << %(algorithm=#{params["algorithm"]}") if params.key?("algorithm")
134
- header << %(opaque="#{params["opaque"]}") if params.key?("opaque")
135
- header << %(cnonce="#{cnonce}") if cnonce
136
- header << %(nc=#{nc})
137
- header << %(qop=#{qop}) if qop
138
- header.join ", "
139
- end
140
-
141
- private
142
-
143
- def make_cnonce
144
- ::Digest::MD5.hexdigest [
145
- Time.now.to_i,
146
- Process.pid,
147
- SecureRandom.random_number(2**32),
148
- ].join ":"
149
- end
150
-
151
- def next_nonce
152
- @nonce += 1
153
57
  end
154
58
  end
155
59
  end
156
60
 
157
- register_plugin :digest_authentication, DigestAuthentication
61
+ register_plugin :digest_authentication, DigestAuth
158
62
  end
159
63
  end