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.
- checksums.yaml +4 -4
- data/LICENSE.txt +48 -0
- data/README.md +13 -3
- data/doc/release_notes/0_10_0.md +66 -0
- data/doc/release_notes/0_10_1.md +37 -0
- data/doc/release_notes/0_10_2.md +5 -0
- data/doc/release_notes/0_11_0.md +76 -0
- data/doc/release_notes/0_11_1.md +1 -0
- data/lib/httpx.rb +2 -0
- data/lib/httpx/adapters/datadog.rb +205 -0
- data/lib/httpx/adapters/faraday.rb +1 -3
- data/lib/httpx/adapters/webmock.rb +123 -0
- data/lib/httpx/chainable.rb +10 -9
- data/lib/httpx/connection.rb +7 -24
- data/lib/httpx/connection/http1.rb +15 -2
- data/lib/httpx/connection/http2.rb +15 -16
- data/lib/httpx/domain_name.rb +438 -0
- data/lib/httpx/errors.rb +4 -1
- data/lib/httpx/extensions.rb +21 -1
- data/lib/httpx/headers.rb +1 -0
- data/lib/httpx/io/ssl.rb +4 -9
- data/lib/httpx/io/tcp.rb +6 -5
- data/lib/httpx/io/udp.rb +8 -4
- data/lib/httpx/options.rb +2 -0
- data/lib/httpx/parser/http1.rb +14 -17
- data/lib/httpx/plugins/compression.rb +28 -63
- data/lib/httpx/plugins/compression/brotli.rb +10 -14
- data/lib/httpx/plugins/compression/deflate.rb +7 -6
- data/lib/httpx/plugins/compression/gzip.rb +23 -5
- data/lib/httpx/plugins/cookies.rb +21 -60
- data/lib/httpx/plugins/cookies/cookie.rb +173 -0
- data/lib/httpx/plugins/cookies/jar.rb +74 -0
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +142 -0
- data/lib/httpx/plugins/expect.rb +34 -11
- data/lib/httpx/plugins/follow_redirects.rb +20 -2
- data/lib/httpx/plugins/h2c.rb +1 -1
- data/lib/httpx/plugins/multipart.rb +41 -30
- data/lib/httpx/plugins/multipart/encoder.rb +115 -0
- data/lib/httpx/plugins/multipart/mime_type_detector.rb +64 -0
- data/lib/httpx/plugins/multipart/part.rb +34 -0
- data/lib/httpx/plugins/persistent.rb +6 -1
- data/lib/httpx/plugins/proxy.rb +16 -2
- data/lib/httpx/plugins/proxy/socks4.rb +14 -14
- data/lib/httpx/plugins/proxy/socks5.rb +3 -2
- data/lib/httpx/plugins/push_promise.rb +2 -2
- data/lib/httpx/plugins/rate_limiter.rb +51 -0
- data/lib/httpx/plugins/retries.rb +3 -2
- data/lib/httpx/plugins/stream.rb +109 -13
- data/lib/httpx/pool.rb +14 -20
- data/lib/httpx/request.rb +29 -31
- data/lib/httpx/resolver.rb +7 -6
- data/lib/httpx/resolver/https.rb +25 -25
- data/lib/httpx/resolver/native.rb +29 -22
- data/lib/httpx/resolver/resolver_mixin.rb +4 -2
- data/lib/httpx/resolver/system.rb +3 -3
- data/lib/httpx/response.rb +16 -23
- data/lib/httpx/selector.rb +11 -17
- data/lib/httpx/session.rb +39 -30
- data/lib/httpx/transcoder.rb +20 -0
- data/lib/httpx/transcoder/chunker.rb +0 -2
- data/lib/httpx/transcoder/form.rb +9 -7
- data/lib/httpx/transcoder/json.rb +0 -4
- data/lib/httpx/utils.rb +45 -0
- data/lib/httpx/version.rb +1 -1
- data/sig/buffer.rbs +24 -0
- data/sig/callbacks.rbs +14 -0
- data/sig/chainable.rbs +37 -0
- data/sig/connection.rbs +85 -0
- data/sig/connection/http1.rbs +66 -0
- data/sig/connection/http2.rbs +77 -0
- data/sig/domain_name.rbs +17 -0
- data/sig/errors.rbs +3 -0
- data/sig/headers.rbs +45 -0
- data/sig/httpx.rbs +15 -0
- data/sig/loggable.rbs +11 -0
- data/sig/options.rbs +118 -0
- data/sig/parser/http1.rbs +50 -0
- data/sig/plugins/authentication.rbs +11 -0
- data/sig/plugins/basic_authentication.rbs +13 -0
- data/sig/plugins/compression.rbs +55 -0
- data/sig/plugins/compression/brotli.rbs +21 -0
- data/sig/plugins/compression/deflate.rbs +17 -0
- data/sig/plugins/compression/gzip.rbs +29 -0
- data/sig/plugins/cookies.rbs +26 -0
- data/sig/plugins/cookies/cookie.rbs +50 -0
- data/sig/plugins/cookies/jar.rbs +27 -0
- data/sig/plugins/digest_authentication.rbs +33 -0
- data/sig/plugins/expect.rbs +19 -0
- data/sig/plugins/follow_redirects.rbs +37 -0
- data/sig/plugins/h2c.rbs +26 -0
- data/sig/plugins/multipart.rbs +44 -0
- data/sig/plugins/persistent.rbs +17 -0
- data/sig/plugins/proxy.rbs +47 -0
- data/sig/plugins/proxy/http.rbs +14 -0
- data/sig/plugins/proxy/socks4.rbs +33 -0
- data/sig/plugins/proxy/socks5.rbs +36 -0
- data/sig/plugins/proxy/ssh.rbs +18 -0
- data/sig/plugins/push_promise.rbs +22 -0
- data/sig/plugins/rate_limiter.rbs +11 -0
- data/sig/plugins/retries.rbs +48 -0
- data/sig/plugins/stream.rbs +39 -0
- data/sig/pool.rbs +36 -0
- data/sig/registry.rbs +9 -0
- data/sig/request.rbs +61 -0
- data/sig/resolver.rbs +26 -0
- data/sig/resolver/https.rbs +51 -0
- data/sig/resolver/native.rbs +60 -0
- data/sig/resolver/resolver_mixin.rbs +27 -0
- data/sig/resolver/system.rbs +17 -0
- data/sig/response.rbs +87 -0
- data/sig/selector.rbs +20 -0
- data/sig/session.rbs +49 -0
- data/sig/timeout.rbs +29 -0
- data/sig/transcoder.rbs +18 -0
- data/sig/transcoder/body.rbs +20 -0
- data/sig/transcoder/chunker.rbs +32 -0
- data/sig/transcoder/form.rbs +22 -0
- data/sig/transcoder/json.rbs +16 -0
- metadata +99 -59
- 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
|
data/lib/httpx/chainable.rb
CHANGED
|
@@ -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 ||
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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)
|
data/lib/httpx/connection.rb
CHANGED
|
@@ -51,7 +51,7 @@ module HTTPX
|
|
|
51
51
|
def initialize(type, uri, options)
|
|
52
52
|
@type = type
|
|
53
53
|
@origins = [uri.origin]
|
|
54
|
-
@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
|
|
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!
|
|
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(:
|
|
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
|
|
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
|
|
55
|
-
|
|
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
|
-
|
|
277
|
-
emit(:error, request, ex)
|
|
278
|
-
end
|
|
277
|
+
handle_error(ex)
|
|
279
278
|
end
|
|
280
|
-
return unless
|
|
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
|