httpx 0.14.2 → 0.15.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_13_2.md +1 -1
  3. data/doc/release_notes/0_14_3.md +5 -0
  4. data/doc/release_notes/0_14_4.md +5 -0
  5. data/doc/release_notes/0_14_5.md +11 -0
  6. data/doc/release_notes/0_15_0.md +53 -0
  7. data/doc/release_notes/0_15_1.md +8 -0
  8. data/lib/httpx.rb +1 -0
  9. data/lib/httpx/connection.rb +14 -3
  10. data/lib/httpx/connection/http1.rb +26 -2
  11. data/lib/httpx/connection/http2.rb +14 -0
  12. data/lib/httpx/domain_name.rb +0 -290
  13. data/lib/httpx/errors.rb +2 -0
  14. data/lib/httpx/extensions.rb +1 -1
  15. data/lib/httpx/options.rb +2 -0
  16. data/lib/httpx/plugins/digest_authentication.rb +19 -21
  17. data/lib/httpx/plugins/grpc.rb +1 -1
  18. data/lib/httpx/plugins/grpc/call.rb +1 -2
  19. data/lib/httpx/plugins/ntlm_authentication.rb +66 -0
  20. data/lib/httpx/plugins/proxy/socks4.rb +4 -0
  21. data/lib/httpx/plugins/proxy/socks5.rb +4 -0
  22. data/lib/httpx/pmatch_extensions.rb +33 -0
  23. data/lib/httpx/punycode.rb +304 -0
  24. data/lib/httpx/request.rb +1 -1
  25. data/lib/httpx/response.rb +2 -0
  26. data/lib/httpx/selector.rb +31 -31
  27. data/lib/httpx/utils.rb +6 -4
  28. data/lib/httpx/version.rb +1 -1
  29. data/sig/chainable.rbs +5 -0
  30. data/sig/connection/http1.rbs +2 -0
  31. data/sig/connection/http2.rbs +2 -0
  32. data/sig/options.rbs +1 -1
  33. data/sig/plugins/aws_sdk_authentication.rbs +2 -0
  34. data/sig/plugins/basic_authentication.rbs +1 -1
  35. data/sig/plugins/digest_authentication.rbs +1 -1
  36. data/sig/plugins/follow_redirects.rbs +1 -1
  37. data/sig/plugins/grpc.rbs +93 -0
  38. data/sig/plugins/multipart.rbs +1 -1
  39. data/sig/plugins/ntlm_authentication.rbs +27 -0
  40. data/sig/plugins/proxy/socks4.rbs +1 -0
  41. data/sig/plugins/proxy/socks5.rbs +1 -0
  42. data/sig/utils.rbs +7 -0
  43. metadata +18 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5049a0cfb698c15800ea618a2775831183765f24ccb946c2adfb6b067c976c4d
4
- data.tar.gz: 498a626981251e0ffbff3dde9d63fcc57e0b3183d29fafbc1ecd921d3bfe8172
3
+ metadata.gz: 71d64789176a94e104957666f409c327a17827e4384e1387c14c2471e1b98994
4
+ data.tar.gz: 0a516128ac747a041ae8e5d86da78c7687d124f830850484ed372c83d67e4745
5
5
  SHA512:
6
- metadata.gz: 6fb23f5a45bd521bf30c25b8e30af3b35e4359093a470fb29f9ab6c8f5d0f09674a08b28ba2c20f92c51fe46702f4f3751b855b6b31f202be01728f56273d6a2
7
- data.tar.gz: f64d1057e2483502dc3751a74489cfec0e7672d906f5901a81f98227c98ee907dc6be0b8d0ad7f4c4688546a2a7c22886f437807e28ba5b2a5e09c6b588de023
6
+ metadata.gz: 83f6d5041140e1670e7175db3d9a3269a3766a5b8b29076ba7ee4025ef035411a4458c5df25cce4d70a10fd34fdc1cbda30fa315b3a77c585d58672ec35f737d
7
+ data.tar.gz: '05298bf9e3fc41c8fe7315558a6842af2dbe915c8f5cfb119ca019cc8e83dbde93a9d57222bbf0098603b3fde00a5e8f1de05f4d62cf5fe81a6f7213a8c8107d'
@@ -1,4 +1,4 @@
1
- # 0.13.1
1
+ # 0.13.2
2
2
 
3
3
  ## Improvements
4
4
 
@@ -0,0 +1,5 @@
1
+ # 0.14.3
2
+
3
+ ## Bugfixes
4
+
5
+ * fixed: HTTP/1 "connection: close" header was "leaking" into subsequent redirect follow, including HTTP/2 requests which would fail due to the invalid header.
@@ -0,0 +1,5 @@
1
+ # 0.14.4
2
+
3
+ ## Bugfixes
4
+
5
+ * The HTTP/1 handler was miscalculating the last request for a given connection, and potentially freezing it.
@@ -0,0 +1,11 @@
1
+ # 0.14.5
2
+
3
+ ## Bugfixes
4
+
5
+ * After a connection had been initiated, sending multiple concurrent requests (ex: `open_httpx.request(req1, req2, req3)`) could freeze; this happened when the first request would fill the write buffer (like a file upload request), and the subsequent requests would never be buffered afterwards; this was fixed by making pending requests flushing a part of a connection's consumption loop.
6
+ * Fixing v0.14.1's fixed bug again; The HTTP/1 "Connection: close" header was not being set in the last possible request on a connection, due to ann off-by-one error on connection bookkeeping;
7
+ * HTTP/1 connections didn't respect a server-set max nunmber of requests after a reconnect; Fixed by making this accounting part of the reset process;
8
+
9
+ ## Chore
10
+
11
+ * Added regression test suite, which reproduce reported bugs before the fix (backported all 0.14.x releases here)
@@ -0,0 +1,53 @@
1
+ # 0.15.0
2
+
3
+ ## Features
4
+
5
+ ### HTTP response pattern-matching (ruby 3 only)
6
+
7
+ You can now apply pattern matching in responses:
8
+
9
+ ```ruby
10
+ case response = HTTPX.get("https://google.com")
11
+ in { status: 200..399, headers: [*, ["x-special-token", token], *], body: }
12
+ puts "success: #{body.to_s}"
13
+ in { status: 400..499, body: }
14
+ puts "client error: #{body.to_s}"
15
+ in { status: 500.., body: }
16
+ puts "server error: #{body.to_s}"
17
+ in { error: error }
18
+ puts "error: #{error.message}"
19
+ else
20
+ raise "unexpected: #{response}"
21
+ end
22
+ ```
23
+
24
+ ### NTLM Authentication
25
+
26
+ A new plugin, `:ntml_authentication`, is now available. Like the name suggests, it allows authenticating requests via [NTLM](https://docs.microsoft.com/en-us/windows-server/security/kerberos/ntlm-overview).
27
+
28
+ ```ruby
29
+ ntlm_http = HTTPX.plugin(:ntlm_authentication)
30
+
31
+ ntlm.ntlm_authentication("user", "password").get("http://protected-area-requiring-ntlm.net")
32
+ # or for a specific domain
33
+ ntlm.ntlm_authentication("user", "password", "Domain\\User").get("http://protected-area-requiring-ntlm.net")
34
+ ```
35
+
36
+ ## Improvemennts
37
+
38
+ A new timeout option, `settings_timeout`, is supported for the HTTP/2 handshake; after the TCP and TLS handshakes are complete, and initiating the HTTP/2 handshake, the client terminates the connection with SETTINGS_TIMEOUT error code, if it doesn't receive the server settings for the amount of seconds set in `settings_timeout` (by default, 10 seconds).
39
+
40
+ ```ruby
41
+ # if you want to change
42
+ HTTPX.with(timeout: {settings_timeout: 5})....
43
+
44
+ ```
45
+
46
+ IDNA 2008 support is now possibly, by integrating [idnx](https://github.com/HoneyryderChuck/idnx) into your dependencies:
47
+
48
+
49
+ ```ruby
50
+ # in Gemfile
51
+ gem "httpx"
52
+ gem "idnx"
53
+ ```
@@ -0,0 +1,8 @@
1
+ # 0.15.1
2
+
3
+ ## Bugfixes
4
+
5
+ Fixed HTTP/1 connection accounting on requests:
6
+
7
+ * when persistent, Connection: close will be set based on the request position on the batch against the allowed requests on the open connection.
8
+ * when not persistent, Connnection: close will be set on the last request of the batch, being the batch a subset based on allowed requests, or the whole of it.
data/lib/httpx.rb CHANGED
@@ -6,6 +6,7 @@ require "httpx/extensions"
6
6
 
7
7
  require "httpx/errors"
8
8
  require "httpx/utils"
9
+ require "httpx/punycode"
9
10
  require "httpx/domain_name"
10
11
  require "httpx/altsvc"
11
12
  require "httpx/callbacks"
@@ -51,7 +51,7 @@ module HTTPX
51
51
  def initialize(type, uri, options)
52
52
  @type = type
53
53
  @origins = [uri.origin]
54
- @origin = Utils.uri(uri.origin)
54
+ @origin = Utils.to_uri(uri.origin)
55
55
  @options = Options.new(options)
56
56
  @window_size = @options.window_size
57
57
  @read_buffer = Buffer.new(BUFFER_SIZE)
@@ -254,6 +254,8 @@ module HTTPX
254
254
  end
255
255
 
256
256
  def consume
257
+ return unless @io
258
+
257
259
  catch(:called) do
258
260
  epiped = false
259
261
  loop do
@@ -311,7 +313,7 @@ module HTTPX
311
313
 
312
314
  # exit #consume altogether if all outstanding requests have been dealt with
313
315
  return if @pending.size.zero? && @inflight.zero?
314
- end unless (interests == :w || @state == :closing) && !epiped
316
+ end unless (interests.nil? || interests == :w || @state == :closing) && !epiped
315
317
 
316
318
  #
317
319
  # tight write loop.
@@ -360,6 +362,8 @@ module HTTPX
360
362
  write_drained = false
361
363
  end unless interests == :r
362
364
 
365
+ send_pending if @state == :open
366
+
363
367
  # return if socket is drained
364
368
  next unless (interests != :r || read_drained) &&
365
369
  (interests != :w || write_drained)
@@ -422,6 +426,9 @@ module HTTPX
422
426
  emit(:close)
423
427
  end
424
428
  end
429
+ parser.on(:close_handshake) do
430
+ consume
431
+ end
425
432
  parser.on(:reset) do
426
433
  if parser.empty?
427
434
  reset
@@ -434,6 +441,9 @@ module HTTPX
434
441
  transition(:open)
435
442
  end
436
443
  end
444
+ parser.on(:current_timeout) do
445
+ @current_timeout = @timeout = parser.timeout
446
+ end
437
447
  parser.on(:timeout) do |tout|
438
448
  @timeout = tout
439
449
  end
@@ -464,10 +474,11 @@ module HTTPX
464
474
 
465
475
  send_pending
466
476
 
467
- @timeout = @current_timeout = @options.timeout[:operation_timeout]
477
+ @timeout = @current_timeout = parser.timeout
468
478
  emit(:open)
469
479
  when :closing
470
480
  return unless @state == :open
481
+
471
482
  when :closed
472
483
  return unless @state == :closing
473
484
  return unless @write_buffer.empty?
@@ -24,6 +24,10 @@ module HTTPX
24
24
  @handshake_completed = false
25
25
  end
26
26
 
27
+ def timeout
28
+ @options.timeout[:operation_timeout]
29
+ end
30
+
27
31
  def interests
28
32
  # this means we're processing incoming response already
29
33
  return :r if @request
@@ -40,6 +44,7 @@ module HTTPX
40
44
  def reset
41
45
  @max_requests = @options.max_requests || MAX_REQUESTS
42
46
  @parser.reset!
47
+ @handshake_completed = false
43
48
  end
44
49
 
45
50
  def close
@@ -80,7 +85,6 @@ module HTTPX
80
85
  break if idx >= concurrent_requests_limit
81
86
  next if request.state == :done
82
87
 
83
- request.headers["connection"] ||= request.options.persistent || idx < requests_limit - 1 ? "keep-alive" : "close"
84
88
  handle(request)
85
89
  end
86
90
  end
@@ -259,9 +263,29 @@ module HTTPX
259
263
  request.chunk!
260
264
  end
261
265
 
266
+ connection = if request.options.persistent
267
+ # when in a persistent connection, the request can't be at
268
+ # the edge of a renegotiation
269
+ if @requests.index(request) + 1 < @max_requests
270
+ "keep-alive"
271
+ else
272
+ "close"
273
+ end
274
+ else
275
+ # when it's not a persistent connection, it sets "Connection: close" always
276
+ # on the last request of the possible batch (either allowed max requests,
277
+ # or if smaller, the size of the batch itself)
278
+ requests_limit = [@max_requests, @requests.size].min
279
+ if request != @requests[requests_limit - 1]
280
+ "keep-alive"
281
+ else
282
+ "close"
283
+ end
284
+ end
285
+
262
286
  {
263
287
  "host" => (request.headers["host"] || request.authority),
264
- "connection" => (request.headers["connection"] || (request.options.persistent ? "keep-alive" : "close")),
288
+ "connection" => connection,
265
289
  }
266
290
  end
267
291
 
@@ -34,6 +34,12 @@ module HTTPX
34
34
  init_connection
35
35
  end
36
36
 
37
+ def timeout
38
+ return @options.timeout[:operation_timeout] if @handshake_completed
39
+
40
+ @options.timeout[:settings_timeout]
41
+ end
42
+
37
43
  def interests
38
44
  # waiting for WINDOW_UPDATE frames
39
45
  return :r if @buffer.full?
@@ -117,6 +123,13 @@ module HTTPX
117
123
  end
118
124
 
119
125
  def handle_error(ex)
126
+ if ex.instance_of?(TimeoutError) && !@handshake_completed && @connection.state != :closed
127
+ @connection.goaway(:settings_timeout, "closing due to settings timeout")
128
+ emit(:close_handshake)
129
+ settings_ex = SettingsTimeoutError.new(ex.timeout, ex.message)
130
+ settings_ex.set_backtrace(ex.backtrace)
131
+ ex = settings_ex
132
+ end
120
133
  @streams.each_key do |request|
121
134
  emit(:error, request, ex)
122
135
  end
@@ -312,6 +325,7 @@ module HTTPX
312
325
 
313
326
  def on_settings(*)
314
327
  @handshake_completed = true
328
+ emit(:current_timeout)
315
329
 
316
330
  if @max_requests.zero?
317
331
  @max_requests = @connection.remote_settings[:settings_max_concurrent_streams]
@@ -144,295 +144,5 @@ module HTTPX
144
144
  1
145
145
  end
146
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
147
  end
438
148
  end