httpx 0.19.8 → 0.20.0
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/0_20_0.md +36 -0
- data/lib/httpx/adapters/sentry.rb +102 -0
- data/lib/httpx/connection.rb +10 -2
- data/lib/httpx/io/ssl.rb +8 -3
- data/lib/httpx/options.rb +5 -0
- data/lib/httpx/plugins/authentication/basic.rb +24 -0
- data/lib/httpx/plugins/authentication/digest.rb +102 -0
- data/lib/httpx/plugins/authentication/ntlm.rb +37 -0
- data/lib/httpx/plugins/authentication/socks5.rb +24 -0
- data/lib/httpx/plugins/basic_authentication.rb +6 -6
- data/lib/httpx/plugins/digest_authentication.rb +15 -111
- data/lib/httpx/plugins/follow_redirects.rb +17 -5
- data/lib/httpx/plugins/ntlm_authentication.rb +8 -18
- data/lib/httpx/plugins/proxy/http.rb +76 -13
- data/lib/httpx/plugins/proxy/socks5.rb +6 -4
- data/lib/httpx/plugins/proxy.rb +30 -8
- data/lib/httpx/plugins/response_cache/store.rb +1 -0
- data/lib/httpx/request.rb +3 -1
- data/lib/httpx/session.rb +7 -2
- data/lib/httpx/version.rb +1 -1
- data/sig/chainable.rbs +4 -4
- data/sig/plugins/authentication/basic.rbs +19 -0
- data/sig/plugins/authentication/digest.rbs +24 -0
- data/sig/plugins/authentication/ntlm.rbs +20 -0
- data/sig/plugins/authentication/socks5.rbs +18 -0
- data/sig/plugins/basic_authentication.rbs +2 -2
- data/sig/plugins/digest_authentication.rbs +3 -13
- data/sig/plugins/ntlm_authentication.rbs +3 -8
- data/sig/plugins/proxy/http.rbs +13 -3
- data/sig/plugins/proxy.rbs +5 -3
- data/sig/session.rbs +1 -1
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5a31aeffa012bd1eea19c1e892c8669624de0d21c9cd8911ae025375f0b93fb
|
4
|
+
data.tar.gz: 285707682eb2d1451106d28bc2d4a8ceb247d4237139c9e7d83bd5a0163b5a5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f2c50aa8895c6f07ed0acbca1b1171343da2b55bc838ae7f2a0f7a44300c101ea0020938946bf630b80ad6b81bed3cb54910b0734576b6e7c000e058497128c
|
7
|
+
data.tar.gz: 83e19a86d841056821d9198c9a8e3e7b06ec354445289a1fd7d612d070b02b0a01aa9f45f5acdb0b35e4da413c2c3955646e5f55c5421db95879092a0a3aa377
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# 0.19.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,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
|
data/lib/httpx/connection.rb
CHANGED
@@ -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
|
-
)
|
106
|
-
) &&
|
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
|
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
|
10
|
+
module BasicAuth
|
11
11
|
class << self
|
12
12
|
def load_dependencies(_klass)
|
13
|
-
|
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
|
23
|
-
authentication(
|
22
|
+
def basic_auth(user, password)
|
23
|
+
authentication(Authentication::Basic.new(user, password).authenticate)
|
24
24
|
end
|
25
|
-
alias_method :
|
25
|
+
alias_method :basic_authentication, :basic_auth
|
26
26
|
end
|
27
27
|
end
|
28
|
-
register_plugin :basic_authentication,
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
93
|
-
qop = params["qop"]
|
47
|
+
probe_response = wrap { super(request).first }
|
94
48
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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,
|
61
|
+
register_plugin :digest_authentication, DigestAuth
|
158
62
|
end
|
159
63
|
end
|
@@ -44,7 +44,7 @@ module HTTPX
|
|
44
44
|
|
45
45
|
max_redirects = redirect_request.max_redirects
|
46
46
|
|
47
|
-
return response unless REDIRECT_STATUS.include?(response.status)
|
47
|
+
return response unless REDIRECT_STATUS.include?(response.status) && response.headers.key?("location")
|
48
48
|
return response unless max_redirects.positive?
|
49
49
|
|
50
50
|
retry_request = build_redirect_request(redirect_request, response, options)
|
@@ -86,10 +86,22 @@ module HTTPX
|
|
86
86
|
redirect_uri = __get_location_from_response(response)
|
87
87
|
max_redirects = request.max_redirects
|
88
88
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
89
|
+
if response.status == 305 && options.respond_to?(:proxy)
|
90
|
+
# The requested resource MUST be accessed through the proxy given by
|
91
|
+
# the Location field. The Location field gives the URI of the proxy.
|
92
|
+
retry_options = options.merge(headers: request.headers,
|
93
|
+
proxy: { uri: redirect_uri },
|
94
|
+
body: request.body,
|
95
|
+
max_redirects: max_redirects - 1)
|
96
|
+
redirect_uri = request.url
|
97
|
+
else
|
98
|
+
|
99
|
+
# redirects are **ALWAYS** GET
|
100
|
+
retry_options = options.merge(headers: request.headers,
|
101
|
+
body: request.body,
|
102
|
+
max_redirects: max_redirects - 1)
|
103
|
+
end
|
104
|
+
|
93
105
|
build_request(:get, redirect_uri, retry_options)
|
94
106
|
end
|
95
107
|
|