httpx 0.19.6 → 0.20.0

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