httpx 0.7.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +48 -0
  3. data/README.md +9 -5
  4. data/doc/release_notes/0_0_1.md +7 -0
  5. data/doc/release_notes/0_0_2.md +9 -0
  6. data/doc/release_notes/0_0_3.md +9 -0
  7. data/doc/release_notes/0_0_4.md +7 -0
  8. data/doc/release_notes/0_0_5.md +5 -0
  9. data/doc/release_notes/0_10_0.md +66 -0
  10. data/doc/release_notes/0_1_0.md +9 -0
  11. data/doc/release_notes/0_2_0.md +5 -0
  12. data/doc/release_notes/0_2_1.md +16 -0
  13. data/doc/release_notes/0_3_0.md +12 -0
  14. data/doc/release_notes/0_3_1.md +6 -0
  15. data/doc/release_notes/0_4_0.md +51 -0
  16. data/doc/release_notes/0_4_1.md +3 -0
  17. data/doc/release_notes/0_5_0.md +15 -0
  18. data/doc/release_notes/0_5_1.md +14 -0
  19. data/doc/release_notes/0_6_0.md +5 -0
  20. data/doc/release_notes/0_6_1.md +6 -0
  21. data/doc/release_notes/0_6_2.md +6 -0
  22. data/doc/release_notes/0_6_3.md +13 -0
  23. data/doc/release_notes/0_6_4.md +21 -0
  24. data/doc/release_notes/0_6_5.md +22 -0
  25. data/doc/release_notes/0_6_6.md +19 -0
  26. data/doc/release_notes/0_6_7.md +5 -0
  27. data/doc/release_notes/0_7_0.md +46 -0
  28. data/doc/release_notes/0_8_0.md +27 -0
  29. data/doc/release_notes/0_8_1.md +8 -0
  30. data/doc/release_notes/0_8_2.md +7 -0
  31. data/doc/release_notes/0_9_0.md +38 -0
  32. data/lib/httpx.rb +2 -0
  33. data/lib/httpx/adapters/faraday.rb +1 -1
  34. data/lib/httpx/altsvc.rb +18 -2
  35. data/lib/httpx/chainable.rb +9 -8
  36. data/lib/httpx/connection.rb +177 -72
  37. data/lib/httpx/connection/http1.rb +44 -13
  38. data/lib/httpx/connection/http2.rb +77 -34
  39. data/lib/httpx/domain_name.rb +440 -0
  40. data/lib/httpx/errors.rb +1 -0
  41. data/lib/httpx/extensions.rb +23 -3
  42. data/lib/httpx/headers.rb +2 -2
  43. data/lib/httpx/io/ssl.rb +11 -4
  44. data/lib/httpx/io/tcp.rb +16 -5
  45. data/lib/httpx/io/udp.rb +4 -1
  46. data/lib/httpx/loggable.rb +6 -6
  47. data/lib/httpx/options.rb +22 -15
  48. data/lib/httpx/parser/http1.rb +14 -17
  49. data/lib/httpx/plugins/compression.rb +49 -64
  50. data/lib/httpx/plugins/compression/brotli.rb +10 -14
  51. data/lib/httpx/plugins/compression/deflate.rb +7 -6
  52. data/lib/httpx/plugins/compression/gzip.rb +45 -17
  53. data/lib/httpx/plugins/cookies.rb +21 -60
  54. data/lib/httpx/plugins/cookies/cookie.rb +173 -0
  55. data/lib/httpx/plugins/cookies/jar.rb +74 -0
  56. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +142 -0
  57. data/lib/httpx/plugins/digest_authentication.rb +2 -0
  58. data/lib/httpx/plugins/expect.rb +12 -1
  59. data/lib/httpx/plugins/follow_redirects.rb +20 -2
  60. data/lib/httpx/plugins/h2c.rb +1 -1
  61. data/lib/httpx/plugins/multipart.rb +0 -8
  62. data/lib/httpx/plugins/persistent.rb +6 -1
  63. data/lib/httpx/plugins/proxy.rb +16 -12
  64. data/lib/httpx/plugins/proxy/http.rb +7 -2
  65. data/lib/httpx/plugins/proxy/socks4.rb +4 -2
  66. data/lib/httpx/plugins/proxy/socks5.rb +5 -1
  67. data/lib/httpx/plugins/push_promise.rb +2 -2
  68. data/lib/httpx/plugins/rate_limiter.rb +51 -0
  69. data/lib/httpx/plugins/retries.rb +13 -6
  70. data/lib/httpx/plugins/stream.rb +109 -13
  71. data/lib/httpx/pool.rb +13 -15
  72. data/lib/httpx/registry.rb +2 -1
  73. data/lib/httpx/request.rb +14 -19
  74. data/lib/httpx/resolver.rb +7 -8
  75. data/lib/httpx/resolver/https.rb +22 -5
  76. data/lib/httpx/resolver/native.rb +27 -33
  77. data/lib/httpx/resolver/options.rb +2 -2
  78. data/lib/httpx/resolver/resolver_mixin.rb +1 -1
  79. data/lib/httpx/response.rb +22 -17
  80. data/lib/httpx/selector.rb +96 -97
  81. data/lib/httpx/session.rb +32 -24
  82. data/lib/httpx/timeout.rb +7 -1
  83. data/lib/httpx/transcoder/chunker.rb +0 -2
  84. data/lib/httpx/transcoder/form.rb +0 -6
  85. data/lib/httpx/transcoder/json.rb +0 -4
  86. data/lib/httpx/utils.rb +45 -0
  87. data/lib/httpx/version.rb +1 -1
  88. data/sig/buffer.rbs +24 -0
  89. data/sig/callbacks.rbs +14 -0
  90. data/sig/chainable.rbs +37 -0
  91. data/sig/connection.rbs +2 -0
  92. data/sig/connection/http2.rbs +4 -0
  93. data/sig/domain_name.rbs +17 -0
  94. data/sig/errors.rbs +3 -0
  95. data/sig/headers.rbs +42 -0
  96. data/sig/httpx.rbs +14 -0
  97. data/sig/loggable.rbs +11 -0
  98. data/sig/missing.rbs +12 -0
  99. data/sig/options.rbs +118 -0
  100. data/sig/parser/http1.rbs +50 -0
  101. data/sig/plugins/authentication.rbs +11 -0
  102. data/sig/plugins/basic_authentication.rbs +13 -0
  103. data/sig/plugins/compression.rbs +55 -0
  104. data/sig/plugins/compression/brotli.rbs +21 -0
  105. data/sig/plugins/compression/deflate.rbs +17 -0
  106. data/sig/plugins/compression/gzip.rbs +29 -0
  107. data/sig/plugins/cookies.rbs +26 -0
  108. data/sig/plugins/cookies/cookie.rbs +50 -0
  109. data/sig/plugins/cookies/jar.rbs +27 -0
  110. data/sig/plugins/digest_authentication.rbs +33 -0
  111. data/sig/plugins/expect.rbs +19 -0
  112. data/sig/plugins/follow_redirects.rbs +37 -0
  113. data/sig/plugins/h2c.rbs +26 -0
  114. data/sig/plugins/multipart.rbs +19 -0
  115. data/sig/plugins/persistent.rbs +17 -0
  116. data/sig/plugins/proxy.rbs +47 -0
  117. data/sig/plugins/proxy/http.rbs +14 -0
  118. data/sig/plugins/proxy/socks4.rbs +33 -0
  119. data/sig/plugins/proxy/socks5.rbs +36 -0
  120. data/sig/plugins/proxy/ssh.rbs +18 -0
  121. data/sig/plugins/push_promise.rbs +22 -0
  122. data/sig/plugins/rate_limiter.rbs +11 -0
  123. data/sig/plugins/retries.rbs +48 -0
  124. data/sig/plugins/stream.rbs +39 -0
  125. data/sig/pool.rbs +2 -0
  126. data/sig/registry.rbs +9 -0
  127. data/sig/request.rbs +61 -0
  128. data/sig/response.rbs +87 -0
  129. data/sig/session.rbs +49 -0
  130. data/sig/test.rbs +9 -0
  131. data/sig/timeout.rbs +29 -0
  132. data/sig/transcoder.rbs +16 -0
  133. data/sig/transcoder/body.rbs +18 -0
  134. data/sig/transcoder/chunker.rbs +32 -0
  135. data/sig/transcoder/form.rbs +16 -0
  136. data/sig/transcoder/json.rbs +14 -0
  137. metadata +120 -21
@@ -23,6 +23,19 @@ module HTTPX
23
23
  @requests = []
24
24
  end
25
25
 
26
+ def interests
27
+ # this means we're processing incoming response already
28
+ return :r if @request
29
+
30
+ return if @requests.empty?
31
+
32
+ request = @requests.first
33
+
34
+ return :w if request.interests == :w || !@buffer.empty?
35
+
36
+ :r
37
+ end
38
+
26
39
  def reset
27
40
  @max_requests = @options.max_requests || MAX_REQUESTS
28
41
  @parser.reset!
@@ -30,7 +43,7 @@ module HTTPX
30
43
 
31
44
  def close
32
45
  reset
33
- emit(:close)
46
+ emit(:close, true)
34
47
  end
35
48
 
36
49
  def exhausted?
@@ -53,16 +66,19 @@ module HTTPX
53
66
  return
54
67
  end
55
68
 
56
- unless @requests.include?(request)
57
- @requests << request
58
- @pipelining = true if @requests.size > 1
59
- end
69
+ return if @requests.include?(request)
60
70
 
61
- handle(request)
71
+ @requests << request
72
+ @pipelining = true if @requests.size > 1
62
73
  end
63
74
 
64
75
  def consume
65
- @requests.each do |request|
76
+ requests_limit = [@max_concurrent_requests, @max_requests, @requests.size].min
77
+ @requests.each_with_index do |request, idx|
78
+ break if idx >= requests_limit
79
+ next if request.state == :done
80
+
81
+ request.headers["connection"] ||= request.options.persistent || idx < requests_limit - 1 ? "keep-alive" : "close"
66
82
  handle(request)
67
83
  end
68
84
  end
@@ -121,7 +137,7 @@ module HTTPX
121
137
 
122
138
  def dispatch
123
139
  if @request.expects?
124
- reset
140
+ @parser.reset!
125
141
  return handle(@request)
126
142
  end
127
143
 
@@ -136,10 +152,10 @@ module HTTPX
136
152
  throw(:called)
137
153
  end
138
154
 
139
- reset
155
+ @parser.reset!
140
156
  @max_requests -= 1
141
- send(@pending.shift) unless @pending.empty?
142
157
  manage_connection(response)
158
+ send(@pending.shift) unless @pending.empty?
143
159
  end
144
160
 
145
161
  def handle_error(ex)
@@ -149,15 +165,23 @@ module HTTPX
149
165
  @requests.each do |request|
150
166
  emit(:error, request, ex)
151
167
  end
168
+ @pending.each do |request|
169
+ emit(:error, request, ex)
170
+ end
152
171
  end
153
172
  end
154
173
 
174
+ def ping
175
+ emit(:reset)
176
+ emit(:exhausted)
177
+ end
178
+
155
179
  private
156
180
 
157
181
  def manage_connection(response)
158
182
  connection = response.headers["connection"]
159
183
  case connection
160
- when /keep\-alive/i
184
+ when /keep-alive/i
161
185
  keep_alive = response.headers["keep-alive"]
162
186
  return unless keep_alive
163
187
 
@@ -188,7 +212,14 @@ module HTTPX
188
212
  def disable_pipelining
189
213
  return if @requests.empty?
190
214
 
191
- @requests.each { |r| r.transition(:idle) }
215
+ @requests.each do |r|
216
+ r.transition(:idle)
217
+
218
+ # when we disable pipelining, we still want to try keep-alive.
219
+ # only when keep-alive with one request fails, do we fallback to
220
+ # connection: close.
221
+ r.headers["connection"] = "close" if @max_concurrent_requests == 1
222
+ end
192
223
  # server doesn't handle pipelining, and probably
193
224
  # doesn't support keep-alive. Fallback to send only
194
225
  # 1 keep alive request.
@@ -198,7 +229,7 @@ module HTTPX
198
229
 
199
230
  def set_request_headers(request)
200
231
  request.headers["host"] ||= request.authority
201
- request.headers["connection"] ||= "keep-alive"
232
+ request.headers["connection"] ||= request.options.persistent ? "keep-alive" : "close"
202
233
  if !request.headers.key?("content-length") &&
203
234
  request.body.bytesize == Float::INFINITY
204
235
  request.chunk!
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securerandom"
3
4
  require "io/wait"
4
5
  require "http/2/next"
5
6
 
@@ -25,13 +26,38 @@ module HTTPX
25
26
  @pending = []
26
27
  @streams = {}
27
28
  @drains = {}
29
+ @pings = []
28
30
  @buffer = buffer
29
31
  @handshake_completed = false
30
32
  init_connection
31
33
  end
32
34
 
33
- def close
34
- @connection.goaway unless @connection.state == :closed
35
+ def interests
36
+ # waiting for WINDOW_UPDATE frames
37
+ return :r if @buffer.full?
38
+
39
+ return :w if @connection.state == :closed
40
+
41
+ unless (@connection.state == :connected && @handshake_completed)
42
+ return @buffer.empty? ? :r : :rw
43
+ end
44
+
45
+ return :w unless @pending.empty?
46
+
47
+ return :w if @streams.each_key.any? { |r| r.interests == :w }
48
+
49
+ return :r if @buffer.empty?
50
+
51
+ :rw
52
+ end
53
+
54
+ def reset
55
+ init_connection
56
+ end
57
+
58
+ def close(*args)
59
+ @connection.goaway(*args) unless @connection.state == :closed
60
+ emit(:close)
35
61
  end
36
62
 
37
63
  def empty?
@@ -39,6 +65,8 @@ module HTTPX
39
65
  end
40
66
 
41
67
  def exhausted?
68
+ return false if @max_requests.zero? && @connection.active_stream_count.zero?
69
+
42
70
  @connection.active_stream_count >= @max_requests
43
71
  end
44
72
 
@@ -68,6 +96,8 @@ module HTTPX
68
96
 
69
97
  def consume
70
98
  @streams.each do |request, stream|
99
+ next if request.state == :done
100
+
71
101
  handle(request, stream)
72
102
  end
73
103
  end
@@ -81,6 +111,13 @@ module HTTPX
81
111
  end
82
112
  end
83
113
 
114
+ def ping
115
+ ping = SecureRandom.gen_random(8)
116
+ @connection.ping(ping)
117
+ ensure
118
+ @pings << ping
119
+ end
120
+
84
121
  private
85
122
 
86
123
  def send_pending
@@ -115,6 +152,7 @@ module HTTPX
115
152
  @connection.on(:promise, &method(:on_promise))
116
153
  @connection.on(:altsvc) { |frame| on_altsvc(frame[:origin], frame) }
117
154
  @connection.on(:settings_ack, &method(:on_settings))
155
+ @connection.on(:ack, &method(:on_pong))
118
156
  @connection.on(:goaway, &method(:on_close))
119
157
  #
120
158
  # Some servers initiate HTTP/2 negotiation right away, some don't.
@@ -128,7 +166,7 @@ module HTTPX
128
166
  def handle_stream(stream, request)
129
167
  stream.on(:close, &method(:on_stream_close).curry[stream, request])
130
168
  stream.on(:half_close) do
131
- log(level: 2, label: "#{stream.id}: ") { "waiting for response..." }
169
+ log(level: 2) { "#{stream.id}: waiting for response..." }
132
170
  end
133
171
  stream.on(:altsvc, &method(:on_altsvc).curry[request.origin])
134
172
  stream.on(:headers, &method(:on_stream_headers).curry[stream, request])
@@ -143,8 +181,8 @@ module HTTPX
143
181
  headers[":path"] = headline_uri(request)
144
182
  headers[":authority"] = request.authority
145
183
  headers = headers.merge(request.headers)
146
- log(level: 1, label: "#{stream.id}: ", color: :yellow) do
147
- headers.map { |k, v| "-> HEADER: #{k}: #{v}" }.join("\n")
184
+ log(level: 1, color: :yellow) do
185
+ headers.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
148
186
  end
149
187
  stream.headers(headers, end_stream: request.empty?)
150
188
  end
@@ -155,8 +193,8 @@ module HTTPX
155
193
  chunk = @drains.delete(request) || request.drain_body
156
194
  while chunk
157
195
  next_chunk = request.drain_body
158
- log(level: 1, label: "#{stream.id}: ", color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
159
- log(level: 2, label: "#{stream.id}: ", color: :green) { "-> #{chunk.inspect}" }
196
+ log(level: 1, color: :green) { "#{stream.id}: -> DATA: #{chunk.bytesize} bytes..." }
197
+ log(level: 2, color: :green) { "#{stream.id}: -> #{chunk.inspect}" }
160
198
  stream.data(chunk, end_stream: !next_chunk)
161
199
  if next_chunk && @buffer.full?
162
200
  @drains[request] = next_chunk
@@ -171,25 +209,25 @@ module HTTPX
171
209
  ######
172
210
 
173
211
  def on_stream_headers(stream, request, h)
174
- log(label: "#{stream.id}:", color: :yellow) do
175
- h.map { |k, v| "<- HEADER: #{k}: #{v}" }.join("\n")
212
+ log(color: :yellow) do
213
+ h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{v}" }.join("\n")
176
214
  end
177
215
  _, status = h.shift
178
216
  headers = request.options.headers_class.new(h)
179
217
  response = request.options.response_class.new(request, status, "2.0", headers)
180
218
  request.response = response
181
219
  @streams[request] = stream
220
+
221
+ handle(request, stream) if request.expects?
182
222
  end
183
223
 
184
224
  def on_stream_data(stream, request, data)
185
- log(level: 1, label: "#{stream.id}: ", color: :green) { "<- DATA: #{data.bytesize} bytes..." }
186
- log(level: 2, label: "#{stream.id}: ", color: :green) { "<- #{data.inspect}" }
225
+ log(level: 1, color: :green) { "#{stream.id}: <- DATA: #{data.bytesize} bytes..." }
226
+ log(level: 2, color: :green) { "#{stream.id}: <- #{data.inspect}" }
187
227
  request.response << data
188
228
  end
189
229
 
190
230
  def on_stream_close(stream, request, error)
191
- return handle(request, stream) if request.expects?
192
-
193
231
  if error && error != :no_error
194
232
  ex = Error.new(stream.id, error)
195
233
  ex.set_backtrace(caller)
@@ -204,14 +242,13 @@ module HTTPX
204
242
  emit(:response, request, response)
205
243
  end
206
244
  end
207
- log(level: 2, label: "#{stream.id}: ") { "closing stream" }
245
+ log(level: 2) { "#{stream.id}: closing stream" }
208
246
 
209
247
  @streams.delete(request)
210
248
  send(@pending.shift) unless @pending.empty?
211
249
  return unless @streams.empty? && exhausted?
212
250
 
213
251
  close
214
- emit(:close)
215
252
  emit(:exhausted) unless @pending.empty?
216
253
  end
217
254
 
@@ -222,7 +259,11 @@ module HTTPX
222
259
  def on_settings(*)
223
260
  @handshake_completed = true
224
261
 
225
- @max_requests = [@max_requests, @connection.remote_settings[:settings_max_concurrent_streams]].max
262
+ if @max_requests.zero?
263
+ @max_requests = @connection.remote_settings[:settings_max_concurrent_streams]
264
+
265
+ @connection.max_streams = @max_requests if @connection.respond_to?(:max_streams=) && @max_requests.positive?
266
+ end
226
267
 
227
268
  @max_concurrent_requests = [@max_concurrent_requests, @max_requests].min
228
269
  send_pending
@@ -242,32 +283,26 @@ module HTTPX
242
283
  end
243
284
 
244
285
  def on_frame_sent(frame)
245
- log(level: 2, label: "#{frame[:stream]}: ") { "frame was sent!" }
246
- log(level: 2, label: "#{frame[:stream]}: ", color: :blue) do
247
- case frame[:type]
248
- when :data
249
- frame.merge(payload: frame[:payload].bytesize).inspect
250
- else
251
- frame.inspect
252
- end
286
+ log(level: 2) { "#{frame[:stream]}: frame was sent!" }
287
+ log(level: 2, color: :blue) do
288
+ payload = frame
289
+ payload = payload.merge(payload: frame[:payload].bytesize) if frame[:type] == :data
290
+ "#{frame[:stream]}: #{payload}"
253
291
  end
254
292
  end
255
293
 
256
294
  def on_frame_received(frame)
257
- log(level: 2, label: "#{frame[:stream]}: ") { "frame was received!" }
258
- log(level: 2, label: "#{frame[:stream]}: ", color: :magenta) do
259
- case frame[:type]
260
- when :data
261
- frame.merge(payload: frame[:payload].bytesize).inspect
262
- else
263
- frame.inspect
264
- end
295
+ log(level: 2) { "#{frame[:stream]}: frame was received!" }
296
+ log(level: 2, color: :magenta) do
297
+ payload = frame
298
+ payload = payload.merge(payload: frame[:payload].bytesize) if frame[:type] == :data
299
+ "#{frame[:stream]}: #{payload}"
265
300
  end
266
301
  end
267
302
 
268
303
  def on_altsvc(origin, frame)
269
- log(level: 2, label: "#{frame[:stream]}: ") { "altsvc frame was received" }
270
- log(level: 2, label: "#{frame[:stream]}: ") { frame.inspect }
304
+ log(level: 2) { "#{frame[:stream]}: altsvc frame was received" }
305
+ log(level: 2) { "#{frame[:stream]}: #{frame.inspect}" }
271
306
  alt_origin = URI.parse("#{frame[:proto]}://#{frame[:host]}:#{frame[:port]}")
272
307
  params = { "ma" => frame[:max_age] }
273
308
  emit(:altsvc, origin, alt_origin, origin, params)
@@ -281,6 +316,14 @@ module HTTPX
281
316
  emit(:origin, origin)
282
317
  end
283
318
 
319
+ def on_pong(ping)
320
+ if !@pings.delete(ping)
321
+ close(:protocol_error, "ping payload did not match")
322
+ else
323
+ emit(:pong)
324
+ end
325
+ end
326
+
284
327
  def respond_to_missing?(meth, *args)
285
328
  @connection.respond_to?(meth, *args) || super
286
329
  end
@@ -0,0 +1,440 @@
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
+ elsif othername.end_with?(@hostname) && othername[-@hostname.size - 1, 1] == DOT
143
+ # The other is lower
144
+ 1
145
+ else
146
+ 1
147
+ end
148
+ end
149
+
150
+ # :nocov:
151
+ # rubocop:disable all
152
+ # -*- coding: utf-8 -*-
153
+ #--
154
+ # punycode.rb - PunyCode encoder for the Domain Name library
155
+ #
156
+ # Copyright (C) 2011-2017 Akinori MUSHA, All rights reserved.
157
+ #
158
+ # Ported from puny.c, a part of VeriSign XCode (encode/decode) IDN
159
+ # Library.
160
+ #
161
+ # Copyright (C) 2000-2002 Verisign Inc., All rights reserved.
162
+ #
163
+ # Redistribution and use in source and binary forms, with or
164
+ # without modification, are permitted provided that the following
165
+ # conditions are met:
166
+ #
167
+ # 1) Redistributions of source code must retain the above copyright
168
+ # notice, this list of conditions and the following disclaimer.
169
+ #
170
+ # 2) Redistributions in binary form must reproduce the above copyright
171
+ # notice, this list of conditions and the following disclaimer in
172
+ # the documentation and/or other materials provided with the
173
+ # distribution.
174
+ #
175
+ # 3) Neither the name of the VeriSign Inc. nor the names of its
176
+ # contributors may be used to endorse or promote products derived
177
+ # from this software without specific prior written permission.
178
+ #
179
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
180
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
181
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
182
+ # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
183
+ # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
184
+ # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
185
+ # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
186
+ # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
187
+ # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
188
+ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
189
+ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
190
+ # POSSIBILITY OF SUCH DAMAGE.
191
+ #
192
+ # This software is licensed under the BSD open source license. For more
193
+ # information visit www.opensource.org.
194
+ #
195
+ # Authors:
196
+ # John Colosi (VeriSign)
197
+ # Srikanth Veeramachaneni (VeriSign)
198
+ # Nagesh Chigurupati (Verisign)
199
+ # Praveen Srinivasan(Verisign)
200
+ #++
201
+ module Punycode
202
+ BASE = 36
203
+ TMIN = 1
204
+ TMAX = 26
205
+ SKEW = 38
206
+ DAMP = 700
207
+ INITIAL_BIAS = 72
208
+ INITIAL_N = 0x80
209
+ DELIMITER = "-"
210
+
211
+ MAXINT = (1 << 32) - 1
212
+
213
+ LOBASE = BASE - TMIN
214
+ CUTOFF = LOBASE * TMAX / 2
215
+
216
+ RE_NONBASIC = /[^\x00-\x7f]/.freeze
217
+
218
+ # Returns the numeric value of a basic code point (for use in
219
+ # representing integers) in the range 0 to base-1, or nil if cp
220
+ # is does not represent a value.
221
+ DECODE_DIGIT = {}.tap do |map|
222
+ # ASCII A..Z map to 0..25
223
+ # ASCII a..z map to 0..25
224
+ (0..25).each { |i| map[65 + i] = map[97 + i] = i }
225
+ # ASCII 0..9 map to 26..35
226
+ (26..35).each { |i| map[22 + i] = i }
227
+ end
228
+
229
+ # Returns the basic code point whose value (when used for
230
+ # representing integers) is d, which must be in the range 0 to
231
+ # BASE-1. The lowercase form is used unless flag is true, in
232
+ # which case the uppercase form is used. The behavior is
233
+ # undefined if flag is nonzero and digit d has no uppercase
234
+ # form.
235
+ ENCODE_DIGIT = proc { |d, flag|
236
+ (d + 22 + (d < 26 ? 75 : 0) - (flag ? (1 << 5) : 0)).chr
237
+ # 0..25 map to ASCII a..z or A..Z
238
+ # 26..35 map to ASCII 0..9
239
+ }
240
+
241
+ DOT = "."
242
+ PREFIX = "xn--"
243
+
244
+ # Most errors we raise are basically kind of ArgumentError.
245
+ class ArgumentError < ::ArgumentError; end
246
+ class BufferOverflowError < ArgumentError; end
247
+
248
+ class << self
249
+ # Encode a +string+ in Punycode
250
+ def encode(string)
251
+ input = string.unpack("U*")
252
+ output = +""
253
+
254
+ # Initialize the state
255
+ n = INITIAL_N
256
+ delta = 0
257
+ bias = INITIAL_BIAS
258
+
259
+ # Handle the basic code points
260
+ input.each { |cp| output << cp.chr if cp < 0x80 }
261
+
262
+ h = b = output.length
263
+
264
+ # h is the number of code points that have been handled, b is the
265
+ # number of basic code points, and out is the number of characters
266
+ # that have been output.
267
+
268
+ output << DELIMITER if b > 0
269
+
270
+ # Main encoding loop
271
+
272
+ while h < input.length
273
+ # All non-basic code points < n have been handled already. Find
274
+ # the next larger one
275
+
276
+ m = MAXINT
277
+ input.each do |cp|
278
+ m = cp if (n...m) === cp
279
+ end
280
+
281
+ # Increase delta enough to advance the decoder's <n,i> state to
282
+ # <m,0>, but guard against overflow
283
+
284
+ delta += (m - n) * (h + 1)
285
+ raise BufferOverflowError if delta > MAXINT
286
+
287
+ n = m
288
+
289
+ input.each do |cp|
290
+ # AMC-ACE-Z can use this simplified version instead
291
+ if cp < n
292
+ delta += 1
293
+ raise BufferOverflowError if delta > MAXINT
294
+ elsif cp == n
295
+ # Represent delta as a generalized variable-length integer
296
+ q = delta
297
+ k = BASE
298
+ loop do
299
+ t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
300
+ break if q < t
301
+
302
+ q, r = (q - t).divmod(BASE - t)
303
+ output << ENCODE_DIGIT[t + r, false]
304
+ k += BASE
305
+ end
306
+
307
+ output << ENCODE_DIGIT[q, false]
308
+
309
+ # Adapt the bias
310
+ delta = h == b ? delta / DAMP : delta >> 1
311
+ delta += delta / (h + 1)
312
+ bias = 0
313
+ while delta > CUTOFF
314
+ delta /= LOBASE
315
+ bias += BASE
316
+ end
317
+ bias += (LOBASE + 1) * delta / (delta + SKEW)
318
+
319
+ delta = 0
320
+ h += 1
321
+ end
322
+ end
323
+
324
+ delta += 1
325
+ n += 1
326
+ end
327
+
328
+ output
329
+ end
330
+
331
+ # Encode a hostname using IDN/Punycode algorithms
332
+ def encode_hostname(hostname)
333
+ hostname.match(RE_NONBASIC) || (return hostname)
334
+
335
+ hostname.split(DOT).map do |name|
336
+ if name.match(RE_NONBASIC)
337
+ PREFIX + encode(name)
338
+ else
339
+ name
340
+ end
341
+ end.join(DOT)
342
+ end
343
+
344
+ # Decode a +string+ encoded in Punycode
345
+ def decode(string)
346
+ # Initialize the state
347
+ n = INITIAL_N
348
+ i = 0
349
+ bias = INITIAL_BIAS
350
+
351
+ if j = string.rindex(DELIMITER)
352
+ b = string[0...j]
353
+
354
+ b.match(RE_NONBASIC) &&
355
+ raise(ArgumentError, "Illegal character is found in basic part: #{string.inspect}")
356
+
357
+ # Handle the basic code points
358
+
359
+ output = b.unpack("U*")
360
+ u = string[(j + 1)..-1]
361
+ else
362
+ output = []
363
+ u = string
364
+ end
365
+
366
+ # Main decoding loop: Start just after the last delimiter if any
367
+ # basic code points were copied; start at the beginning
368
+ # otherwise.
369
+
370
+ input = u.unpack("C*")
371
+ input_length = input.length
372
+ h = 0
373
+ out = output.length
374
+
375
+ while h < input_length
376
+ # Decode a generalized variable-length integer into delta,
377
+ # which gets added to i. The overflow checking is easier
378
+ # if we increase i as we go, then subtract off its starting
379
+ # value at the end to obtain delta.
380
+
381
+ oldi = i
382
+ w = 1
383
+ k = BASE
384
+
385
+ loop do
386
+ (digit = DECODE_DIGIT[input[h]]) ||
387
+ raise(ArgumentError, "Illegal character is found in non-basic part: #{string.inspect}")
388
+ h += 1
389
+ i += digit * w
390
+ raise BufferOverflowError if i > MAXINT
391
+
392
+ t = k <= bias ? TMIN : k - bias >= TMAX ? TMAX : k - bias
393
+ break if digit < t
394
+
395
+ w *= BASE - t
396
+ raise BufferOverflowError if w > MAXINT
397
+
398
+ k += BASE
399
+ (h < input_length) || raise(ArgumentError, "Malformed input given: #{string.inspect}")
400
+ end
401
+
402
+ # Adapt the bias
403
+ delta = oldi == 0 ? i / DAMP : (i - oldi) >> 1
404
+ delta += delta / (out + 1)
405
+ bias = 0
406
+ while delta > CUTOFF
407
+ delta /= LOBASE
408
+ bias += BASE
409
+ end
410
+ bias += (LOBASE + 1) * delta / (delta + SKEW)
411
+
412
+ # i was supposed to wrap around from out+1 to 0, incrementing
413
+ # n each time, so we'll fix that now:
414
+
415
+ q, i = i.divmod(out + 1)
416
+ n += q
417
+ raise BufferOverflowError if n > MAXINT
418
+
419
+ # Insert n at position i of the output:
420
+
421
+ output[i, 0] = n
422
+
423
+ out += 1
424
+ i += 1
425
+ end
426
+ output.pack("U*")
427
+ end
428
+
429
+ # Decode a hostname using IDN/Punycode algorithms
430
+ def decode_hostname(hostname)
431
+ hostname.gsub(/(\A|#{Regexp.quote(DOT)})#{Regexp.quote(PREFIX)}([^#{Regexp.quote(DOT)}]*)/o) do
432
+ Regexp.last_match(1) << decode(Regexp.last_match(2))
433
+ end
434
+ end
435
+ end
436
+ # rubocop:enable all
437
+ # :nocov:
438
+ end
439
+ end
440
+ end