httpx 0.7.0 → 0.10.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 (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