httpx 0.19.8 → 0.20.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|