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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_19_7.md +5 -0
  3. data/doc/release_notes/0_19_8.md +5 -0
  4. data/doc/release_notes/0_20_0.md +36 -0
  5. data/lib/httpx/adapters/datadog.rb +112 -58
  6. data/lib/httpx/adapters/sentry.rb +102 -0
  7. data/lib/httpx/connection.rb +10 -2
  8. data/lib/httpx/io/ssl.rb +8 -3
  9. data/lib/httpx/options.rb +5 -0
  10. data/lib/httpx/plugins/authentication/basic.rb +24 -0
  11. data/lib/httpx/plugins/authentication/digest.rb +102 -0
  12. data/lib/httpx/plugins/authentication/ntlm.rb +37 -0
  13. data/lib/httpx/plugins/authentication/socks5.rb +24 -0
  14. data/lib/httpx/plugins/basic_authentication.rb +6 -6
  15. data/lib/httpx/plugins/digest_authentication.rb +15 -111
  16. data/lib/httpx/plugins/follow_redirects.rb +17 -5
  17. data/lib/httpx/plugins/ntlm_authentication.rb +8 -18
  18. data/lib/httpx/plugins/proxy/http.rb +76 -13
  19. data/lib/httpx/plugins/proxy/socks5.rb +6 -4
  20. data/lib/httpx/plugins/proxy.rb +35 -9
  21. data/lib/httpx/plugins/response_cache/store.rb +1 -0
  22. data/lib/httpx/request.rb +3 -1
  23. data/lib/httpx/session.rb +7 -2
  24. data/lib/httpx/version.rb +1 -1
  25. data/sig/chainable.rbs +4 -4
  26. data/sig/plugins/authentication/basic.rbs +19 -0
  27. data/sig/plugins/authentication/digest.rbs +24 -0
  28. data/sig/plugins/authentication/ntlm.rbs +20 -0
  29. data/sig/plugins/authentication/socks5.rbs +18 -0
  30. data/sig/plugins/basic_authentication.rbs +2 -2
  31. data/sig/plugins/digest_authentication.rbs +3 -13
  32. data/sig/plugins/ntlm_authentication.rbs +3 -8
  33. data/sig/plugins/proxy/http.rbs +13 -3
  34. data/sig/plugins/proxy.rbs +5 -3
  35. data/sig/session.rbs +1 -1
  36. metadata +17 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 832389a759da52da8b345ffa687a9704b3ec3465a936664d3cab2247586a51de
4
- data.tar.gz: 16e4bda290c1631c64f263daf416aec4a4858b7404a3570f229c800da1ca8be2
3
+ metadata.gz: a5a31aeffa012bd1eea19c1e892c8669624de0d21c9cd8911ae025375f0b93fb
4
+ data.tar.gz: 285707682eb2d1451106d28bc2d4a8ceb247d4237139c9e7d83bd5a0163b5a5c
5
5
  SHA512:
6
- metadata.gz: 73816d590b2990b61c4d67dbade348fe7f7ab93e4996a7a4585b4ef895f6410f785c3e85d6f73d621aa2367f0a42add8344502cafd78312f056940f339be4c34
7
- data.tar.gz: 3cb533d7d796f63bc869e8213c0c03b8d290698f8ee3c07affb9b29b79e60e802baa25ca78d7ddf9e7e530ccfc970490a69029878399522c48b8f7bc43222a99
6
+ metadata.gz: 6f2c50aa8895c6f07ed0acbca1b1171343da2b55bc838ae7f2a0f7a44300c101ea0020938946bf630b80ad6b81bed3cb54910b0734576b6e7c000e058497128c
7
+ data.tar.gz: 83e19a86d841056821d9198c9a8e3e7b06ec354445289a1fd7d612d070b02b0a01aa9f45f5acdb0b35e4da413c2c3955646e5f55c5421db95879092a0a3aa377
@@ -0,0 +1,5 @@
1
+ # 0.19.7
2
+
3
+ ## Bugfixes
4
+
5
+ * fix: in `:proxy` plugin, user/pass credentials were not being used when passed as options (only when embedded in the URL). This was a regression introduced in the 0.19.x series.
@@ -0,0 +1,5 @@
1
+ # 0.19.7
2
+
3
+ ## Bugfixes
4
+
5
+ * `datadog` went v1, which broke `httpx` adapter. Now it supports both 1.0 and 0.x versions.
@@ -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
- require "ddtrace/contrib/integration"
4
- require "ddtrace/contrib/rest_client/configuration/settings"
5
- require "ddtrace/contrib/rest_client/patcher"
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
- module Datadog
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 if skip_tracing?
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 = datadog_pin.tracer.trace(SPAN_REQUEST)
33
- service_name = datadog_config[:split_by_domain] ? uri.host : datadog_pin.service_name
74
+ @span = build_span
34
75
 
35
- begin
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
- Datadog::HTTPPropagator.inject!(@span.context, @request.headers) if datadog_pin.tracer.enabled && !skip_distributed_tracing?
78
+ # Add additional request specific tags to the span.
41
79
 
42
- # Add additional request specific tags to the span.
80
+ @span.set_tag(TAG_URL, @request.path)
81
+ @span.set_tag(TAG_METHOD, verb)
43
82
 
44
- @span.set_tag(Datadog::Ext::HTTP::URL, @request.path)
45
- @span.set_tag(Datadog::Ext::HTTP::METHOD, verb)
83
+ @span.set_tag(TAG_TARGET_HOST, uri.host)
84
+ @span.set_tag(TAG_TARGET_PORT, uri.port.to_s)
46
85
 
47
- @span.set_tag(Datadog::Ext::NET::TARGET_HOST, uri.host)
48
- @span.set_tag(Datadog::Ext::NET::TARGET_PORT, uri.port.to_s)
86
+ # Tag as an external peer service
87
+ @span.set_tag(TAG_PEER_SERVICE, @span.service)
49
88
 
50
- # Tag as an external peer service
51
- @span.set_tag(Datadog::Ext::Integration::TAG_PEER_SERVICE, @span.service)
89
+ propagate_headers if @configuration[:distributed_tracing]
52
90
 
53
- # Set analytics sample rate
54
- if Contrib::Analytics.enabled?(datadog_config[:analytics_enabled])
55
- Contrib::Analytics.set_sample_rate(@span, datadog_config[:analytics_sample_rate])
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.debug("Failed to start span: #{e}")
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(Datadog::Ext::HTTP::STATUS_CODE, response.status.to_s)
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
- def skip_tracing?
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
- return true if span && (span.name == SPAN_REQUEST)
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
- false
90
- end
126
+ def propagate_headers
127
+ TRACING_MODULE::Propagation::HTTP.inject!(TRACING_MODULE.active_trace, @request.headers)
128
+ end
91
129
 
92
- def skip_distributed_tracing?
93
- return !datadog_pin.config[:distributed_tracing] if datadog_pin.config && datadog_pin.config.key?(:distributed_tracing)
130
+ def configuration
131
+ @configuration ||= Datadog.configuration.tracing[:httpx, @request.uri.host]
132
+ end
94
133
 
95
- !Datadog.configuration[:httpx][:distributed_tracing]
96
- end
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
- def datadog_pin
99
- @datadog_pin ||= begin
100
- service = datadog_config[:service_name]
101
- tracer = datadog_config[:tracer]
147
+ def propagate_headers
148
+ Datadog::HTTPPropagator.inject!(@span.context, @request.headers)
149
+ end
102
150
 
103
- Datadog::Pin.new(
104
- service,
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
- def datadog_config
113
- @datadog_config ||= Datadog.configuration[:httpx, @request.uri.host]
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 < Datadog::Contrib::Configuration::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: Datadog::Tracer::DEFAULT_ON_ERROR
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 Datadog::Contrib::Patcher
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
- def default_configuration
196
- Configuration::Settings.new
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
@@ -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
- ) || match_altsvcs?(uri)
106
- ) && @options == options
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