httpx 0.19.6 → 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_19_7.md +5 -0
- data/doc/release_notes/0_19_8.md +5 -0
- data/doc/release_notes/0_20_0.md +36 -0
- data/lib/httpx/adapters/datadog.rb +112 -58
- 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 +35 -9
- 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 +17 -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.
|
@@ -1,12 +1,52 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require "
|
5
|
-
require "
|
3
|
+
if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0"
|
4
|
+
require "datadog/tracing/contrib/integration"
|
5
|
+
require "datadog/tracing/contrib/configuration/settings"
|
6
|
+
require "datadog/tracing/contrib/patcher"
|
6
7
|
|
7
|
-
|
8
|
+
TRACING_MODULE = Datadog::Tracing
|
9
|
+
else
|
10
|
+
|
11
|
+
require "ddtrace/contrib/integration"
|
12
|
+
require "ddtrace/contrib/configuration/settings"
|
13
|
+
require "ddtrace/contrib/patcher"
|
14
|
+
|
15
|
+
TRACING_MODULE = Datadog
|
16
|
+
end
|
17
|
+
|
18
|
+
module TRACING_MODULE # rubocop:disable Naming/ClassAndModuleCamelCase
|
8
19
|
module Contrib
|
9
20
|
module HTTPX
|
21
|
+
if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0"
|
22
|
+
METADATA_MODULE = TRACING_MODULE::Metadata
|
23
|
+
|
24
|
+
TYPE_OUTBOUND = TRACING_MODULE::Metadata::Ext::HTTP::TYPE_OUTBOUND
|
25
|
+
|
26
|
+
TAG_PEER_SERVICE = TRACING_MODULE::Metadata::Ext::TAG_PEER_SERVICE
|
27
|
+
|
28
|
+
TAG_URL = TRACING_MODULE::Metadata::Ext::HTTP::TAG_URL
|
29
|
+
TAG_METHOD = TRACING_MODULE::Metadata::Ext::HTTP::TAG_METHOD
|
30
|
+
TAG_TARGET_HOST = TRACING_MODULE::Metadata::Ext::NET::TAG_TARGET_HOST
|
31
|
+
TAG_TARGET_PORT = TRACING_MODULE::Metadata::Ext::NET::TAG_TARGET_PORT
|
32
|
+
|
33
|
+
TAG_STATUS_CODE = TRACING_MODULE::Metadata::Ext::HTTP::TAG_STATUS_CODE
|
34
|
+
|
35
|
+
else
|
36
|
+
|
37
|
+
METADATA_MODULE = Datadog
|
38
|
+
|
39
|
+
TYPE_OUTBOUND = TRACING_MODULE::Ext::HTTP::TYPE_OUTBOUND
|
40
|
+
TAG_PEER_SERVICE = TRACING_MODULE::Ext::Integration::TAG_PEER_SERVICE
|
41
|
+
TAG_URL = TRACING_MODULE::Ext::HTTP::URL
|
42
|
+
TAG_METHOD = TRACING_MODULE::Ext::HTTP::METHOD
|
43
|
+
TAG_TARGET_HOST = TRACING_MODULE::Ext::NET::TARGET_HOST
|
44
|
+
TAG_TARGET_PORT = TRACING_MODULE::Ext::NET::TARGET_PORT
|
45
|
+
TAG_STATUS_CODE = Datadog::Ext::HTTP::STATUS_CODE
|
46
|
+
PROPAGATOR = TRACING_MODULE::HTTPPropagator
|
47
|
+
|
48
|
+
end
|
49
|
+
|
10
50
|
# HTTPX Datadog Plugin
|
11
51
|
#
|
12
52
|
# Enables tracing for httpx requests. A span will be created for each individual requests,
|
@@ -15,6 +55,8 @@ module Datadog
|
|
15
55
|
#
|
16
56
|
module Plugin
|
17
57
|
class RequestTracer
|
58
|
+
include Contrib::HttpAnnotationHelper
|
59
|
+
|
18
60
|
SPAN_REQUEST = "httpx.request"
|
19
61
|
|
20
62
|
def initialize(request)
|
@@ -22,43 +64,37 @@ module Datadog
|
|
22
64
|
end
|
23
65
|
|
24
66
|
def call
|
25
|
-
return
|
67
|
+
return unless tracing_enabled?
|
26
68
|
|
27
69
|
@request.on(:response, &method(:finish))
|
28
70
|
|
29
71
|
verb = @request.verb.to_s.upcase
|
30
72
|
uri = @request.uri
|
31
73
|
|
32
|
-
@span =
|
33
|
-
service_name = datadog_config[:split_by_domain] ? uri.host : datadog_pin.service_name
|
74
|
+
@span = build_span
|
34
75
|
|
35
|
-
|
36
|
-
@span.service = service_name
|
37
|
-
@span.span_type = Datadog::Ext::HTTP::TYPE_OUTBOUND
|
38
|
-
@span.resource = verb
|
76
|
+
@span.resource = verb
|
39
77
|
|
40
|
-
|
78
|
+
# Add additional request specific tags to the span.
|
41
79
|
|
42
|
-
|
80
|
+
@span.set_tag(TAG_URL, @request.path)
|
81
|
+
@span.set_tag(TAG_METHOD, verb)
|
43
82
|
|
44
|
-
|
45
|
-
|
83
|
+
@span.set_tag(TAG_TARGET_HOST, uri.host)
|
84
|
+
@span.set_tag(TAG_TARGET_PORT, uri.port.to_s)
|
46
85
|
|
47
|
-
|
48
|
-
|
86
|
+
# Tag as an external peer service
|
87
|
+
@span.set_tag(TAG_PEER_SERVICE, @span.service)
|
49
88
|
|
50
|
-
|
51
|
-
@span.set_tag(Datadog::Ext::Integration::TAG_PEER_SERVICE, @span.service)
|
89
|
+
propagate_headers if @configuration[:distributed_tracing]
|
52
90
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
end
|
57
|
-
rescue StandardError => e
|
58
|
-
Datadog.logger.error("error preparing span for http request: #{e}")
|
91
|
+
# Set analytics sample rate
|
92
|
+
if Contrib::Analytics.enabled?(@configuration[:analytics_enabled])
|
93
|
+
Contrib::Analytics.set_sample_rate(@span, @configuration[:analytics_sample_rate])
|
59
94
|
end
|
60
95
|
rescue StandardError => e
|
61
|
-
Datadog.logger.
|
96
|
+
Datadog.logger.error("error preparing span for http request: #{e}")
|
97
|
+
Datadog.logger.error(e.backtrace)
|
62
98
|
end
|
63
99
|
|
64
100
|
def finish(response)
|
@@ -67,7 +103,7 @@ module Datadog
|
|
67
103
|
if response.is_a?(::HTTPX::ErrorResponse)
|
68
104
|
@span.set_error(response.error)
|
69
105
|
else
|
70
|
-
@span.set_tag(
|
106
|
+
@span.set_tag(TAG_STATUS_CODE, response.status.to_s)
|
71
107
|
|
72
108
|
@span.set_error(::HTTPX::HTTPError.new(response)) if response.status >= 400 && response.status <= 599
|
73
109
|
end
|
@@ -77,40 +113,48 @@ module Datadog
|
|
77
113
|
|
78
114
|
private
|
79
115
|
|
80
|
-
|
81
|
-
return true if @request.headers.key?(Datadog::Ext::Transport::HTTP::HEADER_META_TRACER_VERSION)
|
82
|
-
|
83
|
-
return false unless @datadog_pin
|
84
|
-
|
85
|
-
span = @datadog_pin.tracer.active_span
|
116
|
+
if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0"
|
86
117
|
|
87
|
-
|
118
|
+
def build_span
|
119
|
+
TRACING_MODULE.trace(
|
120
|
+
SPAN_REQUEST,
|
121
|
+
service: service_name(@request.uri.host, configuration, Datadog.configuration_for(self)),
|
122
|
+
span_type: TYPE_OUTBOUND
|
123
|
+
)
|
124
|
+
end
|
88
125
|
|
89
|
-
|
90
|
-
|
126
|
+
def propagate_headers
|
127
|
+
TRACING_MODULE::Propagation::HTTP.inject!(TRACING_MODULE.active_trace, @request.headers)
|
128
|
+
end
|
91
129
|
|
92
|
-
|
93
|
-
|
130
|
+
def configuration
|
131
|
+
@configuration ||= Datadog.configuration.tracing[:httpx, @request.uri.host]
|
132
|
+
end
|
94
133
|
|
95
|
-
|
96
|
-
|
134
|
+
def tracing_enabled?
|
135
|
+
TRACING_MODULE.enabled?
|
136
|
+
end
|
137
|
+
else
|
138
|
+
def build_span
|
139
|
+
service_name = configuration[:split_by_domain] ? @request.uri.host : configuration[:service_name]
|
140
|
+
configuration[:tracer].trace(
|
141
|
+
SPAN_REQUEST,
|
142
|
+
service: service_name,
|
143
|
+
span_type: TYPE_OUTBOUND
|
144
|
+
)
|
145
|
+
end
|
97
146
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
tracer = datadog_config[:tracer]
|
147
|
+
def propagate_headers
|
148
|
+
Datadog::HTTPPropagator.inject!(@span.context, @request.headers)
|
149
|
+
end
|
102
150
|
|
103
|
-
|
104
|
-
|
105
|
-
app: "httpx",
|
106
|
-
app_type: Datadog::Ext::AppTypes::WEB,
|
107
|
-
tracer: -> { tracer }
|
108
|
-
)
|
151
|
+
def configuration
|
152
|
+
@configuration ||= Datadog.configuration[:httpx, @request.uri.host]
|
109
153
|
end
|
110
|
-
end
|
111
154
|
|
112
|
-
|
113
|
-
|
155
|
+
def tracing_enabled?
|
156
|
+
configuration[:tracer].enabled
|
157
|
+
end
|
114
158
|
end
|
115
159
|
end
|
116
160
|
|
@@ -125,7 +169,11 @@ module Datadog
|
|
125
169
|
module Configuration
|
126
170
|
# Default settings for httpx
|
127
171
|
#
|
128
|
-
class Settings <
|
172
|
+
class Settings < TRACING_MODULE::Contrib::Configuration::Settings
|
173
|
+
DEFAULT_ERROR_HANDLER = lambda do |response|
|
174
|
+
Datadog::Ext::HTTP::ERROR_RANGE.cover?(response.status)
|
175
|
+
end
|
176
|
+
|
129
177
|
option :service_name, default: "httpx"
|
130
178
|
option :distributed_tracing, default: true
|
131
179
|
option :split_by_domain, default: false
|
@@ -145,14 +193,14 @@ module Datadog
|
|
145
193
|
o.lazy
|
146
194
|
end
|
147
195
|
|
148
|
-
option :error_handler, default:
|
196
|
+
option :error_handler, default: DEFAULT_ERROR_HANDLER
|
149
197
|
end
|
150
198
|
end
|
151
199
|
|
152
200
|
# Patcher enables patching of 'httpx' with datadog components.
|
153
201
|
#
|
154
202
|
module Patcher
|
155
|
-
include
|
203
|
+
include TRACING_MODULE::Contrib::Patcher
|
156
204
|
|
157
205
|
module_function
|
158
206
|
|
@@ -192,8 +240,14 @@ module Datadog
|
|
192
240
|
super && version >= MINIMUM_VERSION
|
193
241
|
end
|
194
242
|
|
195
|
-
|
196
|
-
|
243
|
+
if defined?(::DDTrace) && ::DDTrace::VERSION::STRING >= "1.0.0"
|
244
|
+
def new_configuration
|
245
|
+
Configuration::Settings.new
|
246
|
+
end
|
247
|
+
else
|
248
|
+
def default_configuration
|
249
|
+
Configuration::Settings.new
|
250
|
+
end
|
197
251
|
end
|
198
252
|
|
199
253
|
def patcher
|
@@ -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
|