httpx 0.9.0 → 0.11.1

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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +48 -0
  3. data/README.md +13 -3
  4. data/doc/release_notes/0_10_0.md +66 -0
  5. data/doc/release_notes/0_10_1.md +37 -0
  6. data/doc/release_notes/0_10_2.md +5 -0
  7. data/doc/release_notes/0_11_0.md +76 -0
  8. data/doc/release_notes/0_11_1.md +1 -0
  9. data/lib/httpx.rb +2 -0
  10. data/lib/httpx/adapters/datadog.rb +205 -0
  11. data/lib/httpx/adapters/faraday.rb +1 -3
  12. data/lib/httpx/adapters/webmock.rb +123 -0
  13. data/lib/httpx/chainable.rb +10 -9
  14. data/lib/httpx/connection.rb +7 -24
  15. data/lib/httpx/connection/http1.rb +15 -2
  16. data/lib/httpx/connection/http2.rb +15 -16
  17. data/lib/httpx/domain_name.rb +438 -0
  18. data/lib/httpx/errors.rb +4 -1
  19. data/lib/httpx/extensions.rb +21 -1
  20. data/lib/httpx/headers.rb +1 -0
  21. data/lib/httpx/io/ssl.rb +4 -9
  22. data/lib/httpx/io/tcp.rb +6 -5
  23. data/lib/httpx/io/udp.rb +8 -4
  24. data/lib/httpx/options.rb +2 -0
  25. data/lib/httpx/parser/http1.rb +14 -17
  26. data/lib/httpx/plugins/compression.rb +28 -63
  27. data/lib/httpx/plugins/compression/brotli.rb +10 -14
  28. data/lib/httpx/plugins/compression/deflate.rb +7 -6
  29. data/lib/httpx/plugins/compression/gzip.rb +23 -5
  30. data/lib/httpx/plugins/cookies.rb +21 -60
  31. data/lib/httpx/plugins/cookies/cookie.rb +173 -0
  32. data/lib/httpx/plugins/cookies/jar.rb +74 -0
  33. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +142 -0
  34. data/lib/httpx/plugins/expect.rb +34 -11
  35. data/lib/httpx/plugins/follow_redirects.rb +20 -2
  36. data/lib/httpx/plugins/h2c.rb +1 -1
  37. data/lib/httpx/plugins/multipart.rb +41 -30
  38. data/lib/httpx/plugins/multipart/encoder.rb +115 -0
  39. data/lib/httpx/plugins/multipart/mime_type_detector.rb +64 -0
  40. data/lib/httpx/plugins/multipart/part.rb +34 -0
  41. data/lib/httpx/plugins/persistent.rb +6 -1
  42. data/lib/httpx/plugins/proxy.rb +16 -2
  43. data/lib/httpx/plugins/proxy/socks4.rb +14 -14
  44. data/lib/httpx/plugins/proxy/socks5.rb +3 -2
  45. data/lib/httpx/plugins/push_promise.rb +2 -2
  46. data/lib/httpx/plugins/rate_limiter.rb +51 -0
  47. data/lib/httpx/plugins/retries.rb +3 -2
  48. data/lib/httpx/plugins/stream.rb +109 -13
  49. data/lib/httpx/pool.rb +14 -20
  50. data/lib/httpx/request.rb +29 -31
  51. data/lib/httpx/resolver.rb +7 -6
  52. data/lib/httpx/resolver/https.rb +25 -25
  53. data/lib/httpx/resolver/native.rb +29 -22
  54. data/lib/httpx/resolver/resolver_mixin.rb +4 -2
  55. data/lib/httpx/resolver/system.rb +3 -3
  56. data/lib/httpx/response.rb +16 -23
  57. data/lib/httpx/selector.rb +11 -17
  58. data/lib/httpx/session.rb +39 -30
  59. data/lib/httpx/transcoder.rb +20 -0
  60. data/lib/httpx/transcoder/chunker.rb +0 -2
  61. data/lib/httpx/transcoder/form.rb +9 -7
  62. data/lib/httpx/transcoder/json.rb +0 -4
  63. data/lib/httpx/utils.rb +45 -0
  64. data/lib/httpx/version.rb +1 -1
  65. data/sig/buffer.rbs +24 -0
  66. data/sig/callbacks.rbs +14 -0
  67. data/sig/chainable.rbs +37 -0
  68. data/sig/connection.rbs +85 -0
  69. data/sig/connection/http1.rbs +66 -0
  70. data/sig/connection/http2.rbs +77 -0
  71. data/sig/domain_name.rbs +17 -0
  72. data/sig/errors.rbs +3 -0
  73. data/sig/headers.rbs +45 -0
  74. data/sig/httpx.rbs +15 -0
  75. data/sig/loggable.rbs +11 -0
  76. data/sig/options.rbs +118 -0
  77. data/sig/parser/http1.rbs +50 -0
  78. data/sig/plugins/authentication.rbs +11 -0
  79. data/sig/plugins/basic_authentication.rbs +13 -0
  80. data/sig/plugins/compression.rbs +55 -0
  81. data/sig/plugins/compression/brotli.rbs +21 -0
  82. data/sig/plugins/compression/deflate.rbs +17 -0
  83. data/sig/plugins/compression/gzip.rbs +29 -0
  84. data/sig/plugins/cookies.rbs +26 -0
  85. data/sig/plugins/cookies/cookie.rbs +50 -0
  86. data/sig/plugins/cookies/jar.rbs +27 -0
  87. data/sig/plugins/digest_authentication.rbs +33 -0
  88. data/sig/plugins/expect.rbs +19 -0
  89. data/sig/plugins/follow_redirects.rbs +37 -0
  90. data/sig/plugins/h2c.rbs +26 -0
  91. data/sig/plugins/multipart.rbs +44 -0
  92. data/sig/plugins/persistent.rbs +17 -0
  93. data/sig/plugins/proxy.rbs +47 -0
  94. data/sig/plugins/proxy/http.rbs +14 -0
  95. data/sig/plugins/proxy/socks4.rbs +33 -0
  96. data/sig/plugins/proxy/socks5.rbs +36 -0
  97. data/sig/plugins/proxy/ssh.rbs +18 -0
  98. data/sig/plugins/push_promise.rbs +22 -0
  99. data/sig/plugins/rate_limiter.rbs +11 -0
  100. data/sig/plugins/retries.rbs +48 -0
  101. data/sig/plugins/stream.rbs +39 -0
  102. data/sig/pool.rbs +36 -0
  103. data/sig/registry.rbs +9 -0
  104. data/sig/request.rbs +61 -0
  105. data/sig/resolver.rbs +26 -0
  106. data/sig/resolver/https.rbs +51 -0
  107. data/sig/resolver/native.rbs +60 -0
  108. data/sig/resolver/resolver_mixin.rbs +27 -0
  109. data/sig/resolver/system.rbs +17 -0
  110. data/sig/response.rbs +87 -0
  111. data/sig/selector.rbs +20 -0
  112. data/sig/session.rbs +49 -0
  113. data/sig/timeout.rbs +29 -0
  114. data/sig/transcoder.rbs +18 -0
  115. data/sig/transcoder/body.rbs +20 -0
  116. data/sig/transcoder/chunker.rbs +32 -0
  117. data/sig/transcoder/form.rbs +22 -0
  118. data/sig/transcoder/json.rbs +16 -0
  119. metadata +99 -59
  120. data/lib/httpx/resolver/options.rb +0 -25
@@ -65,7 +65,6 @@ module Faraday
65
65
  plugin(:compression)
66
66
  plugin(:persistent)
67
67
 
68
- # :nocov:
69
68
  module ReasonPlugin
70
69
  if RUBY_VERSION < "2.5"
71
70
  def self.load_dependencies(*)
@@ -88,7 +87,6 @@ module Faraday
88
87
  end
89
88
  end
90
89
  end
91
- # :nocov:
92
90
  plugin(ReasonPlugin)
93
91
  end
94
92
 
@@ -121,7 +119,7 @@ module Faraday
121
119
  end
122
120
 
123
121
  def respond_to_missing?(meth)
124
- @env.respond_to?(meth)
122
+ @env.respond_to?(meth) || super
125
123
  end
126
124
 
127
125
  def method_missing(meth, *args, &blk)
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebMock
4
+ module HttpLibAdapters
5
+ if RUBY_VERSION < "2.5"
6
+ require "webrick/httpstatus"
7
+ HTTP_REASONS = WEBrick::HTTPStatus::StatusMessage
8
+ else
9
+ require "net/http/status"
10
+ HTTP_REASONS = Net::HTTP::STATUS_CODES
11
+ end
12
+
13
+ #
14
+ # HTTPX plugin for webmock.
15
+ #
16
+ # Requests are "hijacked" at the session, before they're distributed to a connection.
17
+ #
18
+ module Plugin
19
+ module InstanceMethods
20
+ private
21
+
22
+ def send_requests(*requests, options)
23
+ request_signatures = requests.map do |request|
24
+ request_signature = _build_webmock_request_signature(request)
25
+ WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)
26
+ request_signature
27
+ end
28
+
29
+ responses = request_signatures.map do |request_signature|
30
+ WebMock::StubRegistry.instance.response_for_request(request_signature)
31
+ end
32
+
33
+ real_requests = {}
34
+
35
+ requests.each_with_index.each_with_object([request_signatures, responses]) do |(request, idx), (sig_reqs, mock_responses)|
36
+ if (webmock_response = mock_responses[idx])
37
+ mock_responses[idx] = _build_from_webmock_response(request, webmock_response)
38
+ WebMock::CallbackRegistry.invoke_callbacks({ lib: :httpx }, sig_reqs[idx], webmock_response)
39
+ log { "mocking #{request.uri} with #{mock_responses[idx].inspect}" }
40
+ elsif WebMock.net_connect_allowed?(sig_reqs[idx].uri)
41
+ log { "performing #{request.uri}" }
42
+ real_requests[request] = idx
43
+ else
44
+ raise WebMock::NetConnectNotAllowedError, sig_reqs[idx]
45
+ end
46
+ end
47
+
48
+ unless real_requests.empty?
49
+ reqs = real_requests.keys
50
+ reqs.zip(super(*reqs, options)).each do |req, res|
51
+ idx = real_requests[req]
52
+
53
+ if WebMock::CallbackRegistry.any_callbacks?
54
+ webmock_response = _build_webmock_response(req, res)
55
+ WebMock::CallbackRegistry.invoke_callbacks(
56
+ { lib: :httpx, real_request: true }, request_signatures[idx],
57
+ webmock_response
58
+ )
59
+ end
60
+
61
+ responses[idx] = res
62
+ end
63
+ end
64
+
65
+ responses
66
+ end
67
+
68
+ def _build_webmock_request_signature(request)
69
+ uri = WebMock::Util::URI.heuristic_parse(request.uri)
70
+ uri.path = uri.normalized_path.gsub("[^:]//", "/")
71
+
72
+ WebMock::RequestSignature.new(
73
+ request.verb,
74
+ uri.to_s,
75
+ body: request.body.each.to_a.join,
76
+ headers: request.headers.to_h
77
+ )
78
+ end
79
+
80
+ def _build_webmock_response(_request, response)
81
+ webmock_response = WebMock::Response.new
82
+ webmock_response.status = [response.status, HTTP_REASONS[response.status]]
83
+ webmock_response.body = response.body.to_s
84
+ webmock_response.headers = response.headers.to_h
85
+ webmock_response
86
+ end
87
+
88
+ def _build_from_webmock_response(request, webmock_response)
89
+ return ErrorResponse.new(request, webmock_response.exception, request.options) if webmock_response.exception
90
+
91
+ response = request.options.response_class.new(request,
92
+ webmock_response.status[0],
93
+ "2.0",
94
+ webmock_response.headers)
95
+ response << webmock_response.body.dup
96
+ response
97
+ end
98
+ end
99
+ end
100
+
101
+ class HttpxAdapter < HttpLibAdapter
102
+ adapter_for :httpx
103
+
104
+ class << self
105
+ def enable!
106
+ @original_session = ::HTTPX::Session
107
+
108
+ webmock_session = ::HTTPX.plugin(Plugin)
109
+
110
+ ::HTTPX.send(:remove_const, :Session)
111
+ ::HTTPX.send(:const_set, :Session, webmock_session.class)
112
+ end
113
+
114
+ def disable!
115
+ return unless @original_session
116
+
117
+ HTTPX.send(:remove_const, :Session)
118
+ HTTPX.send(:const_set, :Session, @original_session)
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -34,20 +34,23 @@ module HTTPX
34
34
  branch(default_options).wrap(&blk)
35
35
  end
36
36
 
37
- def plugin(*args, **opts)
37
+ def plugin(*args, **opts, &blk)
38
38
  klass = is_a?(Session) ? self.class : Session
39
39
  klass = Class.new(klass)
40
40
  klass.instance_variable_set(:@default_options, klass.default_options.merge(default_options))
41
- klass.plugin(*args, **opts).new
41
+ klass.plugin(*args, **opts, &blk).new
42
42
  end
43
43
 
44
44
  # deprecated
45
+ # :nocov:
45
46
  def plugins(*args, **opts)
47
+ warn ":#{__method__} is deprecated, use :plugin instead"
46
48
  klass = is_a?(Session) ? self.class : Session
47
49
  klass = Class.new(klass)
48
50
  klass.instance_variable_set(:@default_options, klass.default_options.merge(default_options))
49
51
  klass.plugins(*args, **opts).new
50
52
  end
53
+ # :nocov:
51
54
 
52
55
  def with(options, &blk)
53
56
  branch(default_options.merge(options), &blk)
@@ -56,7 +59,7 @@ module HTTPX
56
59
  private
57
60
 
58
61
  def default_options
59
- @options || Options.new
62
+ @options || Session.default_options
60
63
  end
61
64
 
62
65
  def branch(options, &blk)
@@ -66,12 +69,10 @@ module HTTPX
66
69
  end
67
70
 
68
71
  def method_missing(meth, *args, **options)
69
- if meth =~ /\Awith_(.+)/
70
- option = Regexp.last_match(1).to_sym
71
- with(option => (args.first || options))
72
- else
73
- super
74
- end
72
+ return super unless meth =~ /\Awith_(.+)/
73
+
74
+ option = Regexp.last_match(1).to_sym
75
+ with(option => (args.first || options))
75
76
  end
76
77
 
77
78
  def respond_to_missing?(meth, *args)
@@ -51,7 +51,7 @@ module HTTPX
51
51
  def initialize(type, uri, options)
52
52
  @type = type
53
53
  @origins = [uri.origin]
54
- @origin = URI(uri.origin)
54
+ @origin = Utils.uri(uri.origin)
55
55
  @options = Options.new(options)
56
56
  @window_size = @options.window_size
57
57
  @read_buffer = Buffer.new(BUFFER_SIZE)
@@ -120,8 +120,8 @@ module HTTPX
120
120
  end
121
121
  end
122
122
 
123
- def create_idle
124
- self.class.new(@type, @origin, @options)
123
+ def create_idle(options = {})
124
+ self.class.new(@type, @origin, @options.merge(options))
125
125
  end
126
126
 
127
127
  def merge(connection)
@@ -131,18 +131,7 @@ module HTTPX
131
131
  end
132
132
  end
133
133
 
134
- def unmerge(connection)
135
- @origins -= connection.instance_variable_get(:@origins)
136
- purge_pending do |request|
137
- request.uri.origin == connection.origin && begin
138
- request.transition(:idle)
139
- connection.send(request)
140
- true
141
- end
142
- end
143
- end
144
-
145
- def purge_pending
134
+ def purge_pending(&block)
146
135
  pendings = []
147
136
  if @parser
148
137
  @inflight -= @parser.pending.size
@@ -150,9 +139,7 @@ module HTTPX
150
139
  end
151
140
  pendings << @pending
152
141
  pendings.each do |pending|
153
- pending.reject! do |request|
154
- yield request
155
- end
142
+ pending.reject!(&block)
156
143
  end
157
144
  end
158
145
 
@@ -293,8 +280,6 @@ module HTTPX
293
280
  break
294
281
  end
295
282
 
296
- log { "READ: #{siz} bytes..." }
297
-
298
283
  parser << @read_buffer.to_s
299
284
 
300
285
  break if @state == :closing || @state == :closed
@@ -317,7 +302,6 @@ module HTTPX
317
302
  on_error(ex)
318
303
  return
319
304
  end
320
- log { "WRITE: #{siz} bytes..." }
321
305
 
322
306
  if siz.zero?
323
307
  write_drained = !@write_buffer.empty?
@@ -404,7 +388,7 @@ module HTTPX
404
388
  parser.on(:error) do |request, ex|
405
389
  case ex
406
390
  when MisdirectedRequestError
407
- emit(:uncoalesce, request.uri)
391
+ emit(:misdirected, request)
408
392
  else
409
393
  response = ErrorResponse.new(request, ex, @options)
410
394
  request.emit(:response, response)
@@ -440,7 +424,7 @@ module HTTPX
440
424
  remove_instance_variable(:@total_timeout)
441
425
  end
442
426
 
443
- @io.close
427
+ @io.close if @io
444
428
  @read_buffer.clear
445
429
  if @keep_alive_timer
446
430
  @keep_alive_timer.cancel
@@ -460,7 +444,6 @@ module HTTPX
460
444
  throw(:jump_tick)
461
445
  rescue Errno::ECONNREFUSED,
462
446
  Errno::EADDRNOTAVAIL,
463
- Errno::EHOSTUNREACH,
464
447
  OpenSSL::SSL::SSLError => e
465
448
  # connect errors, exit gracefully
466
449
  handle_error(e)
@@ -21,6 +21,7 @@ module HTTPX
21
21
  @version = [1, 1]
22
22
  @pending = []
23
23
  @requests = []
24
+ @handshake_completed = false
24
25
  end
25
26
 
26
27
  def interests
@@ -155,6 +156,7 @@ module HTTPX
155
156
  @parser.reset!
156
157
  @max_requests -= 1
157
158
  manage_connection(response)
159
+
158
160
  send(@pending.shift) unless @pending.empty?
159
161
  end
160
162
 
@@ -181,18 +183,29 @@ module HTTPX
181
183
  def manage_connection(response)
182
184
  connection = response.headers["connection"]
183
185
  case connection
184
- when /keep\-alive/i
186
+ when /keep-alive/i
187
+ if @handshake_completed
188
+ if @max_requests.zero?
189
+ @pending.concat(@requests)
190
+ @requests.clear
191
+ emit(:exhausted)
192
+ end
193
+ return
194
+ end
195
+
185
196
  keep_alive = response.headers["keep-alive"]
186
197
  return unless keep_alive
187
198
 
188
199
  parameters = Hash[keep_alive.split(/ *, */).map do |pair|
189
200
  pair.split(/ *= */)
190
201
  end]
191
- @max_requests = parameters["max"].to_i if parameters.key?("max")
202
+ @max_requests = parameters["max"].to_i - 1 if parameters.key?("max")
203
+
192
204
  if parameters.key?("timeout")
193
205
  keep_alive_timeout = parameters["timeout"].to_i
194
206
  emit(:timeout, keep_alive_timeout)
195
207
  end
208
+ @handshake_completed = true
196
209
  when /close/i
197
210
  disable
198
211
  when nil
@@ -51,12 +51,8 @@ module HTTPX
51
51
  :rw
52
52
  end
53
53
 
54
- def reset
55
- init_connection
56
- end
57
-
58
- def close(*args)
59
- @connection.goaway(*args) unless @connection.state == :closed
54
+ def close
55
+ @connection.goaway unless @connection.state == :closed
60
56
  emit(:close)
61
57
  end
62
58
 
@@ -163,14 +159,17 @@ module HTTPX
163
159
  @connection.send_connection_preface
164
160
  end
165
161
 
162
+ alias_method :reset, :init_connection
163
+ public :reset
164
+
166
165
  def handle_stream(stream, request)
167
- stream.on(:close, &method(:on_stream_close).curry[stream, request])
166
+ stream.on(:close, &method(:on_stream_close).curry(3)[stream, request])
168
167
  stream.on(:half_close) do
169
168
  log(level: 2) { "#{stream.id}: waiting for response..." }
170
169
  end
171
- stream.on(:altsvc, &method(:on_altsvc).curry[request.origin])
172
- stream.on(:headers, &method(:on_stream_headers).curry[stream, request])
173
- stream.on(:data, &method(:on_stream_data).curry[stream, request])
170
+ stream.on(:altsvc, &method(:on_altsvc).curry(2)[request.origin])
171
+ stream.on(:headers, &method(:on_stream_headers).curry(3)[stream, request])
172
+ stream.on(:data, &method(:on_stream_data).curry(3)[stream, request])
174
173
  end
175
174
 
176
175
  def join_headers(stream, request)
@@ -270,16 +269,16 @@ module HTTPX
270
269
  end
271
270
 
272
271
  def on_close(_last_frame, error, _payload)
272
+ is_connection_closed = @connection.state == :closed
273
273
  if error && error != :no_error
274
+ @buffer.clear if is_connection_closed
274
275
  ex = Error.new(0, error)
275
276
  ex.set_backtrace(caller)
276
- @streams.each_key do |request|
277
- emit(:error, request, ex)
278
- end
277
+ handle_error(ex)
279
278
  end
280
- return unless @connection.state == :closed && @streams.size.zero?
279
+ return unless is_connection_closed && @streams.size.zero?
281
280
 
282
- emit(:close)
281
+ emit(:close, is_connection_closed)
283
282
  end
284
283
 
285
284
  def on_frame_sent(frame)
@@ -317,7 +316,7 @@ module HTTPX
317
316
  end
318
317
 
319
318
  def on_pong(ping)
320
- if !@pings.delete(ping)
319
+ if !@pings.delete(ping.to_s)
321
320
  close(:protocol_error, "ping payload did not match")
322
321
  else
323
322
  emit(:pong)
@@ -0,0 +1,438 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # domain_name.rb - Domain Name manipulation library for Ruby
5
+ #
6
+ # Copyright (C) 2011-2017 Akinori MUSHA, All rights reserved.
7
+ #
8
+ # Redistribution and use in source and binary forms, with or without
9
+ # modification, are permitted provided that the following conditions
10
+ # are met:
11
+ # 1. Redistributions of source code must retain the above copyright
12
+ # notice, this list of conditions and the following disclaimer.
13
+ # 2. Redistributions in binary form must reproduce the above copyright
14
+ # notice, this list of conditions and the following disclaimer in the
15
+ # documentation and/or other materials provided with the distribution.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23
+ # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24
+ # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
+ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26
+ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27
+ # SUCH DAMAGE.
28
+
29
+ require "ipaddr"
30
+
31
+ module HTTPX
32
+ # Represents a domain name ready for extracting its registered domain
33
+ # and TLD.
34
+ class DomainName
35
+ include Comparable
36
+
37
+ # The full host name normalized, ASCII-ized and downcased using the
38
+ # Unicode NFC rules and the Punycode algorithm. If initialized with
39
+ # an IP address, the string representation of the IP address
40
+ # suitable for opening a connection to.
41
+ attr_reader :hostname
42
+
43
+ # The Unicode representation of the #hostname property.
44
+ #
45
+ # :attr_reader: hostname_idn
46
+
47
+ # The least "universally original" domain part of this domain name.
48
+ # For example, "example.co.uk" for "www.sub.example.co.uk". This
49
+ # may be nil if the hostname does not have one, like when it is an
50
+ # IP address, an effective TLD or higher itself, or of a
51
+ # non-canonical domain.
52
+ attr_reader :domain
53
+
54
+ DOT = "." # :nodoc:
55
+
56
+ class << self
57
+ def new(domain)
58
+ return domain if domain.is_a?(self)
59
+
60
+ super(domain)
61
+ end
62
+
63
+ # Normalizes a _domain_ using the Punycode algorithm as necessary.
64
+ # The result will be a downcased, ASCII-only string.
65
+ def normalize(domain)
66
+ domain = domain.ascii_only? ? domain : domain.chomp(DOT).unicode_normalize(:nfc)
67
+ Punycode.encode_hostname(domain).downcase
68
+ end
69
+ end
70
+
71
+ # Parses _hostname_ into a DomainName object. An IP address is also
72
+ # accepted. An IPv6 address may be enclosed in square brackets.
73
+ def initialize(hostname)
74
+ hostname = String(hostname)
75
+
76
+ raise ArgumentError, "domain name must not start with a dot: #{hostname}" if hostname.start_with?(DOT)
77
+
78
+ begin
79
+ @ipaddr = IPAddr.new(hostname)
80
+ @hostname = @ipaddr.to_s
81
+ return
82
+ rescue IPAddr::Error
83
+ nil
84
+ end
85
+
86
+ @hostname = DomainName.normalize(hostname)
87
+ tld = if (last_dot = @hostname.rindex(DOT))
88
+ @hostname[(last_dot + 1)..-1]
89
+ else
90
+ @hostname
91
+ end
92
+
93
+ # unknown/local TLD
94
+ @domain = if last_dot
95
+ # fallback - accept cookies down to second level
96
+ # cf. http://www.dkim-reputation.org/regdom-libs/
97
+ if (penultimate_dot = @hostname.rindex(DOT, last_dot - 1))
98
+ @hostname[(penultimate_dot + 1)..-1]
99
+ else
100
+ @hostname
101
+ end
102
+ else
103
+ # no domain part - must be a local hostname
104
+ tld
105
+ end
106
+ end
107
+
108
+ # Checks if the server represented by this domain is qualified to
109
+ # send and receive cookies with a domain attribute value of
110
+ # _domain_. A true value given as the second argument represents
111
+ # cookies without a domain attribute value, in which case only
112
+ # hostname equality is checked.
113
+ def cookie_domain?(domain, host_only = false)
114
+ # RFC 6265 #5.3
115
+ # When the user agent "receives a cookie":
116
+ return self == @domain if host_only
117
+
118
+ domain = DomainName.new(domain)
119
+
120
+ # RFC 6265 #5.1.3
121
+ # Do not perform subdomain matching against IP addresses.
122
+ @hostname == domain.hostname if @ipaddr
123
+
124
+ # RFC 6265 #4.1.1
125
+ # Domain-value must be a subdomain.
126
+ @domain && self <= domain && domain <= @domain ? true : false
127
+ end
128
+
129
+ # def ==(other)
130
+ # other = DomainName.new(other)
131
+ # other.hostname == @hostname
132
+ # end
133
+
134
+ def <=>(other)
135
+ other = DomainName.new(other)
136
+ othername = other.hostname
137
+ if othername == @hostname
138
+ 0
139
+ elsif @hostname.end_with?(othername) && @hostname[-othername.size - 1, 1] == DOT
140
+ # The other is higher
141
+ -1
142
+ else
143
+ # The other is lower
144
+ 1
145
+ end
146
+ end
147
+
148
+ # :nocov:
149
+ # rubocop:disable all
150
+ # -*- coding: utf-8 -*-
151
+ #--
152
+ # punycode.rb - PunyCode encoder for the Domain Name library
153
+ #
154
+ # Copyright (C) 2011-2017 Akinori MUSHA, All rights reserved.
155
+ #
156
+ # Ported from puny.c, a part of VeriSign XCode (encode/decode) IDN
157
+ # Library.
158
+ #
159
+ # Copyright (C) 2000-2002 Verisign Inc., All rights reserved.
160
+ #
161
+ # Redistribution and use in source and binary forms, with or
162
+ # without modification, are permitted provided that the following
163
+ # conditions are met:
164
+ #
165
+ # 1) Redistributions of source code must retain the above copyright
166
+ # notice, this list of conditions and the following disclaimer.
167
+ #
168
+ # 2) Redistributions in binary form must reproduce the above copyright
169
+ # notice, this list of conditions and the following disclaimer in
170
+ # the documentation and/or other materials provided with the
171
+ # distribution.
172
+ #
173
+ # 3) Neither the name of the VeriSign Inc. nor the names of its
174
+ # contributors may be used to endorse or promote products derived
175
+ # from this software without specific prior written permission.
176
+ #
177
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
178
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
179
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
180
+ # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
181
+ # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
182
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
183
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
184
+ # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
185
+ # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
186
+ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
187
+ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
188
+ # POSSIBILITY OF SUCH DAMAGE.
189
+ #
190
+ # This software is licensed under the BSD open source license. For more
191
+ # information visit www.opensource.org.
192
+ #
193
+ # Authors:
194
+ # John Colosi (VeriSign)
195
+ # Srikanth Veeramachaneni (VeriSign)
196
+ # Nagesh Chigurupati (Verisign)
197
+ # Praveen Srinivasan(Verisign)
198
+ #++
199
+ module Punycode
200
+ BASE = 36
201
+ TMIN = 1
202
+ TMAX = 26
203
+ SKEW = 38
204
+ DAMP = 700
205
+ INITIAL_BIAS = 72
206
+ INITIAL_N = 0x80
207
+ DELIMITER = "-"
208
+
209
+ MAXINT = (1 << 32) - 1
210
+
211
+ LOBASE = BASE - TMIN
212
+ CUTOFF = LOBASE * TMAX / 2
213
+
214
+ RE_NONBASIC = /[^\x00-\x7f]/.freeze
215
+
216
+ # Returns the numeric value of a basic code point (for use in
217
+ # representing integers) in the range 0 to base-1, or nil if cp
218
+ # is does not represent a value.
219
+ DECODE_DIGIT = {}.tap do |map|
220
+ # ASCII A..Z map to 0..25
221
+ # ASCII a..z map to 0..25
222
+ (0..25).each { |i| map[65 + i] = map[97 + i] = i }
223
+ # ASCII 0..9 map to 26..35
224
+ (26..35).each { |i| map[22 + i] = i }
225
+ end
226
+
227
+ # Returns the basic code point whose value (when used for
228
+ # representing integers) is d, which must be in the range 0 to
229
+ # BASE-1. The lowercase form is used unless flag is true, in
230
+ # which case the uppercase form is used. The behavior is
231
+ # undefined if flag is nonzero and digit d has no uppercase
232
+ # form.
233
+ ENCODE_DIGIT = proc { |d, flag|
234
+ (d + 22 + (d < 26 ? 75 : 0) - (flag ? (1 << 5) : 0)).chr
235
+ # 0..25 map to ASCII a..z or A..Z
236
+ # 26..35 map to ASCII 0..9
237
+ }
238
+
239
+ DOT = "."
240
+ PREFIX = "xn--"
241
+
242
+ # Most errors we raise are basically kind of ArgumentError.
243
+ class ArgumentError < ::ArgumentError; end
244
+ class BufferOverflowError < ArgumentError; end
245
+
246
+ class << self
247
+ # Encode a +string+ in Punycode
248
+ def encode(string)
249
+ input = string.unpack("U*")
250
+ output = +""
251
+
252
+ # Initialize the state
253
+ n = INITIAL_N
254
+ delta = 0
255
+ bias = INITIAL_BIAS
256
+
257
+ # Handle the basic code points
258
+ input.each { |cp| output << cp.chr if cp < 0x80 }
259
+
260
+ h = b = output.length
261
+
262
+ # h is the number of code points that have been handled, b is the
263
+ # number of basic code points, and out is the number of characters
264
+ # that have been output.
265
+
266
+ output << DELIMITER if b > 0
267
+
268
+ # Main encoding loop
269
+
270
+ while h < input.length
271
+ # All non-basic code points < n have been handled already. Find
272
+ # the next larger one
273
+
274
+ m = MAXINT
275
+ input.each do |cp|
276
+ m = cp if (n...m) === cp
277
+ end
278
+
279
+ # Increase delta enough to advance the decoder's <n,i> state to
280
+ # <m,0>, but guard against overflow
281
+
282
+ delta += (m - n) * (h + 1)
283
+ raise BufferOverflowError if delta > MAXINT
284
+
285
+ n = m
286
+
287
+ input.each do |cp|
288
+ # AMC-ACE-Z can use this simplified version instead
289
+ if cp < n
290
+ delta += 1
291
+ raise BufferOverflowError if delta > MAXINT
292
+ elsif cp == n
293
+ # Represent delta as a generalized variable-length integer
294
+ q = delta
295
+ k = BASE
296
+ loop do
297
+ t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
298
+ break if q < t
299
+
300
+ q, r = (q - t).divmod(BASE - t)
301
+ output << ENCODE_DIGIT[t + r, false]
302
+ k += BASE
303
+ end
304
+
305
+ output << ENCODE_DIGIT[q, false]
306
+
307
+ # Adapt the bias
308
+ delta = h == b ? delta / DAMP : delta >> 1
309
+ delta += delta / (h + 1)
310
+ bias = 0
311
+ while delta > CUTOFF
312
+ delta /= LOBASE
313
+ bias += BASE
314
+ end
315
+ bias += (LOBASE + 1) * delta / (delta + SKEW)
316
+
317
+ delta = 0
318
+ h += 1
319
+ end
320
+ end
321
+
322
+ delta += 1
323
+ n += 1
324
+ end
325
+
326
+ output
327
+ end
328
+
329
+ # Encode a hostname using IDN/Punycode algorithms
330
+ def encode_hostname(hostname)
331
+ hostname.match(RE_NONBASIC) || (return hostname)
332
+
333
+ hostname.split(DOT).map do |name|
334
+ if name.match(RE_NONBASIC)
335
+ PREFIX + encode(name)
336
+ else
337
+ name
338
+ end
339
+ end.join(DOT)
340
+ end
341
+
342
+ # Decode a +string+ encoded in Punycode
343
+ def decode(string)
344
+ # Initialize the state
345
+ n = INITIAL_N
346
+ i = 0
347
+ bias = INITIAL_BIAS
348
+
349
+ if j = string.rindex(DELIMITER)
350
+ b = string[0...j]
351
+
352
+ b.match(RE_NONBASIC) &&
353
+ raise(ArgumentError, "Illegal character is found in basic part: #{string.inspect}")
354
+
355
+ # Handle the basic code points
356
+
357
+ output = b.unpack("U*")
358
+ u = string[(j + 1)..-1]
359
+ else
360
+ output = []
361
+ u = string
362
+ end
363
+
364
+ # Main decoding loop: Start just after the last delimiter if any
365
+ # basic code points were copied; start at the beginning
366
+ # otherwise.
367
+
368
+ input = u.unpack("C*")
369
+ input_length = input.length
370
+ h = 0
371
+ out = output.length
372
+
373
+ while h < input_length
374
+ # Decode a generalized variable-length integer into delta,
375
+ # which gets added to i. The overflow checking is easier
376
+ # if we increase i as we go, then subtract off its starting
377
+ # value at the end to obtain delta.
378
+
379
+ oldi = i
380
+ w = 1
381
+ k = BASE
382
+
383
+ loop do
384
+ (digit = DECODE_DIGIT[input[h]]) ||
385
+ raise(ArgumentError, "Illegal character is found in non-basic part: #{string.inspect}")
386
+ h += 1
387
+ i += digit * w
388
+ raise BufferOverflowError if i > MAXINT
389
+
390
+ t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
391
+ break if digit < t
392
+
393
+ w *= BASE - t
394
+ raise BufferOverflowError if w > MAXINT
395
+
396
+ k += BASE
397
+ (h < input_length) || raise(ArgumentError, "Malformed input given: #{string.inspect}")
398
+ end
399
+
400
+ # Adapt the bias
401
+ delta = oldi == 0 ? i / DAMP : (i - oldi) >> 1
402
+ delta += delta / (out + 1)
403
+ bias = 0
404
+ while delta > CUTOFF
405
+ delta /= LOBASE
406
+ bias += BASE
407
+ end
408
+ bias += (LOBASE + 1) * delta / (delta + SKEW)
409
+
410
+ # i was supposed to wrap around from out+1 to 0, incrementing
411
+ # n each time, so we'll fix that now:
412
+
413
+ q, i = i.divmod(out + 1)
414
+ n += q
415
+ raise BufferOverflowError if n > MAXINT
416
+
417
+ # Insert n at position i of the output:
418
+
419
+ output[i, 0] = n
420
+
421
+ out += 1
422
+ i += 1
423
+ end
424
+ output.pack("U*")
425
+ end
426
+
427
+ # Decode a hostname using IDN/Punycode algorithms
428
+ def decode_hostname(hostname)
429
+ hostname.gsub(/(\A|#{Regexp.quote(DOT)})#{Regexp.quote(PREFIX)}([^#{Regexp.quote(DOT)}]*)/o) do
430
+ Regexp.last_match(1) << decode(Regexp.last_match(2))
431
+ end
432
+ end
433
+ end
434
+ # rubocop:enable all
435
+ # :nocov:
436
+ end
437
+ end
438
+ end