httpx 0.19.7 → 0.20.1
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 +5 -0
- data/doc/release_notes/0_20_0.md +36 -0
- data/doc/release_notes/0_20_1.md +5 -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 +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/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/pool.rbs +2 -2
- 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: 2d7bcf0e6c97e369caf28c209d551d4f9667745b6a6a530ae2efd328c611e0e5
|
4
|
+
data.tar.gz: 822c0437afcefb35e7d4319f91e71c5b4c81720bd0d7a7763db1d1f8bfbc4b1a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 747e7d2ca2a4aee92f1774473e9b38565fec3879ee6b6d0ab3c4eeb1313740c6bbd3648c8c6f21b40903f4bf9bc842647b531968debcda747652afb483577b11
|
7
|
+
data.tar.gz: 88d3bafe38437bebb42960d980dddc85e9e12a917c492203744ded3f93c79736feedf839604cd1a4b080a59c64c96c26c545808cc29d19bcdaa93fa55154bbef
|
@@ -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.
|
@@ -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
|