httpx 0.19.8 → 0.20.2
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_19_8.md +1 -1
- data/doc/release_notes/0_20_0.md +36 -0
- data/doc/release_notes/0_20_1.md +5 -0
- data/doc/release_notes/0_20_2.md +7 -0
- data/lib/httpx/adapters/sentry.rb +102 -0
- data/lib/httpx/connection.rb +16 -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/pool.rb +4 -4
- data/lib/httpx/request.rb +3 -1
- data/lib/httpx/resolver/native.rb +6 -2
- data/lib/httpx/selector.rb +9 -2
- data/lib/httpx/session.rb +7 -2
- data/lib/httpx/version.rb +1 -1
- data/sig/chainable.rbs +4 -4
- data/sig/connection.rbs +3 -0
- 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/pool.rbs +2 -2
- data/sig/resolver/native.rbs +3 -1
- data/sig/selector.rbs +1 -1
- data/sig/session.rbs +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b49337207a605c716ef9a1043660239ced7f5060758148f905be6c1d5546a8bc
|
4
|
+
data.tar.gz: 732ffb35912e5176827941034b74ce2777d744e828e1ed86351bdd221f010d19
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c1f12fa0d1675f8e8c478ea272456f910dc558b7836eaf0bfa6e916d29439e7dd093ffdde4903a43b474559909052df4d26872846e704c393045f10d496a53a
|
7
|
+
data.tar.gz: 9011bf6d1826ea2a44a3570f9dcbe9dc5bb319303809cf8b44247f9ac2bdb099ccc0895192155960df320dbdad4bebf009a7ec77dc1e37d1e8e27a59077efe5a
|
data/doc/release_notes/0_19_8.md
CHANGED
@@ -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,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
|
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
|
@@ -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
|
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
|