httpx 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +48 -0
  3. data/README.md +2 -0
  4. data/doc/release_notes/0_10_0.md +66 -0
  5. data/lib/httpx.rb +2 -0
  6. data/lib/httpx/adapters/faraday.rb +1 -1
  7. data/lib/httpx/chainable.rb +2 -2
  8. data/lib/httpx/connection.rb +3 -9
  9. data/lib/httpx/connection/http1.rb +1 -1
  10. data/lib/httpx/domain_name.rb +440 -0
  11. data/lib/httpx/errors.rb +1 -0
  12. data/lib/httpx/extensions.rb +21 -1
  13. data/lib/httpx/io/ssl.rb +0 -1
  14. data/lib/httpx/io/tcp.rb +6 -5
  15. data/lib/httpx/io/udp.rb +4 -1
  16. data/lib/httpx/options.rb +2 -0
  17. data/lib/httpx/parser/http1.rb +14 -17
  18. data/lib/httpx/plugins/compression.rb +28 -63
  19. data/lib/httpx/plugins/compression/brotli.rb +10 -14
  20. data/lib/httpx/plugins/compression/deflate.rb +7 -6
  21. data/lib/httpx/plugins/compression/gzip.rb +23 -5
  22. data/lib/httpx/plugins/cookies.rb +21 -60
  23. data/lib/httpx/plugins/cookies/cookie.rb +173 -0
  24. data/lib/httpx/plugins/cookies/jar.rb +74 -0
  25. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +142 -0
  26. data/lib/httpx/plugins/expect.rb +3 -5
  27. data/lib/httpx/plugins/follow_redirects.rb +20 -2
  28. data/lib/httpx/plugins/h2c.rb +1 -1
  29. data/lib/httpx/plugins/multipart.rb +0 -8
  30. data/lib/httpx/plugins/persistent.rb +6 -1
  31. data/lib/httpx/plugins/proxy/socks4.rb +3 -1
  32. data/lib/httpx/plugins/rate_limiter.rb +51 -0
  33. data/lib/httpx/plugins/retries.rb +3 -2
  34. data/lib/httpx/plugins/stream.rb +109 -13
  35. data/lib/httpx/pool.rb +6 -6
  36. data/lib/httpx/request.rb +7 -19
  37. data/lib/httpx/resolver/https.rb +7 -2
  38. data/lib/httpx/resolver/native.rb +7 -3
  39. data/lib/httpx/response.rb +16 -23
  40. data/lib/httpx/selector.rb +2 -4
  41. data/lib/httpx/session.rb +17 -11
  42. data/lib/httpx/transcoder/chunker.rb +0 -2
  43. data/lib/httpx/transcoder/form.rb +0 -6
  44. data/lib/httpx/transcoder/json.rb +0 -4
  45. data/lib/httpx/utils.rb +45 -0
  46. data/lib/httpx/version.rb +1 -1
  47. data/sig/buffer.rbs +24 -0
  48. data/sig/callbacks.rbs +14 -0
  49. data/sig/chainable.rbs +37 -0
  50. data/sig/connection.rbs +2 -0
  51. data/sig/connection/http2.rbs +4 -0
  52. data/sig/domain_name.rbs +17 -0
  53. data/sig/errors.rbs +3 -0
  54. data/sig/headers.rbs +42 -0
  55. data/sig/httpx.rbs +14 -0
  56. data/sig/loggable.rbs +11 -0
  57. data/sig/missing.rbs +12 -0
  58. data/sig/options.rbs +118 -0
  59. data/sig/parser/http1.rbs +50 -0
  60. data/sig/plugins/authentication.rbs +11 -0
  61. data/sig/plugins/basic_authentication.rbs +13 -0
  62. data/sig/plugins/compression.rbs +55 -0
  63. data/sig/plugins/compression/brotli.rbs +21 -0
  64. data/sig/plugins/compression/deflate.rbs +17 -0
  65. data/sig/plugins/compression/gzip.rbs +29 -0
  66. data/sig/plugins/cookies.rbs +26 -0
  67. data/sig/plugins/cookies/cookie.rbs +50 -0
  68. data/sig/plugins/cookies/jar.rbs +27 -0
  69. data/sig/plugins/digest_authentication.rbs +33 -0
  70. data/sig/plugins/expect.rbs +19 -0
  71. data/sig/plugins/follow_redirects.rbs +37 -0
  72. data/sig/plugins/h2c.rbs +26 -0
  73. data/sig/plugins/multipart.rbs +19 -0
  74. data/sig/plugins/persistent.rbs +17 -0
  75. data/sig/plugins/proxy.rbs +47 -0
  76. data/sig/plugins/proxy/http.rbs +14 -0
  77. data/sig/plugins/proxy/socks4.rbs +33 -0
  78. data/sig/plugins/proxy/socks5.rbs +36 -0
  79. data/sig/plugins/proxy/ssh.rbs +18 -0
  80. data/sig/plugins/push_promise.rbs +22 -0
  81. data/sig/plugins/rate_limiter.rbs +11 -0
  82. data/sig/plugins/retries.rbs +48 -0
  83. data/sig/plugins/stream.rbs +39 -0
  84. data/sig/pool.rbs +2 -0
  85. data/sig/registry.rbs +9 -0
  86. data/sig/request.rbs +61 -0
  87. data/sig/response.rbs +87 -0
  88. data/sig/session.rbs +49 -0
  89. data/sig/test.rbs +9 -0
  90. data/sig/timeout.rbs +29 -0
  91. data/sig/transcoder.rbs +16 -0
  92. data/sig/transcoder/body.rbs +18 -0
  93. data/sig/transcoder/chunker.rbs +32 -0
  94. data/sig/transcoder/form.rbs +16 -0
  95. data/sig/transcoder/json.rbs +14 -0
  96. metadata +60 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e15b3e81842118829ecc251093ce5e80ad0c82dada2c7b2588004b2bf7313f63
4
- data.tar.gz: 9d4efcb47f0c269f2c5b9a3c950fe8069c3b76ffb2f9624b557d29a702704712
3
+ metadata.gz: 3e3727a5374ad3c2d6d7c5dbfd61040e3aa3ea6c74aa6ad12d662a5bd0b9a7af
4
+ data.tar.gz: 7b1a2aa15418b03fc276b81cfaf0565bbce59dd531c2bfc10f19ddc60506213b
5
5
  SHA512:
6
- metadata.gz: 9fbd146e351da8f603c8b00c236d92c7a2a3c383296736f271c88b0ac310086adf0127af7b630e9087ec45c4a22494128b2efafdb2592de43df4aae4a68ef1f3
7
- data.tar.gz: eecb817b76bc0f217b728c7e862f73758e4b2db9c6a943311e0c7d10d8e7fe6bfa8e56a7c0d4593158fb605927813a1e720ec85a9d7e70aae8ecce015f5da647
6
+ metadata.gz: cda3ab9929396bdbad4ff38472dea4d7af34946e34cf995228ad5d52d05e37664b53f66e0a0ac8dd10193e55de52b4f7575f203bea8202b5474f0c3cca450d24
7
+ data.tar.gz: 025a985fc7c05abbb1bd66d23356d5165f7536d6565464e0729c618da624fc9f428e1046bba7c09dc5d7c23ada0587f73fb23e42128c96847857bcc1ccd6d408
@@ -189,3 +189,51 @@
189
189
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190
190
  See the License for the specific language governing permissions and
191
191
  limitations under the License.
192
+
193
+
194
+ * lib/httpx/domain_name.rb
195
+
196
+ This file is derived from the implementation of punycode available at
197
+ here:
198
+
199
+ https://www.verisign.com/en_US/channel-resources/domain-registry-products/idn-sdks/index.xhtml
200
+
201
+ Copyright (C) 2000-2002 Verisign Inc., All rights reserved.
202
+
203
+ Redistribution and use in source and binary forms, with or
204
+ without modification, are permitted provided that the following
205
+ conditions are met:
206
+
207
+ 1) Redistributions of source code must retain the above copyright
208
+ notice, this list of conditions and the following disclaimer.
209
+
210
+ 2) Redistributions in binary form must reproduce the above copyright
211
+ notice, this list of conditions and the following disclaimer in
212
+ the documentation and/or other materials provided with the
213
+ distribution.
214
+
215
+ 3) Neither the name of the VeriSign Inc. nor the names of its
216
+ contributors may be used to endorse or promote products derived
217
+ from this software without specific prior written permission.
218
+
219
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
220
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
221
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
222
+ FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
223
+ COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
224
+ INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
225
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
226
+ OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
227
+ AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
228
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
229
+ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
230
+ POSSIBILITY OF SUCH DAMAGE.
231
+
232
+ This software is licensed under the BSD open source license. For more
233
+ information visit www.opensource.org.
234
+
235
+ Authors:
236
+ John Colosi (VeriSign)
237
+ Srikanth Veeramachaneni (VeriSign)
238
+ Nagesh Chigurupati (Verisign)
239
+ Praveen Srinivasan(Verisign)
data/README.md CHANGED
@@ -18,6 +18,7 @@ Among its features, it supports:
18
18
  And also:
19
19
 
20
20
  * Compression (gzip, deflate, brotli)
21
+ * Streaming Requests
21
22
  * Authentication (Basic Auth, Digest Auth)
22
23
  * Expect 100-continue
23
24
  * Multipart Requests
@@ -25,6 +26,7 @@ And also:
25
26
  * HTTP/2 Server Push
26
27
  * H2C Upgrade
27
28
  * Automatic follow redirects
29
+ * International Domain Names
28
30
 
29
31
  ## How
30
32
 
@@ -0,0 +1,66 @@
1
+ # 0.10.0
2
+
3
+ ## Features
4
+
5
+ ### Streaming Requests
6
+
7
+ The `stream` plugin adds functionality to handle long-lived stream responses, such as the Twitter Streaming API:
8
+
9
+ ```ruby
10
+ http = HTTPX.plugin(:stream)
11
+
12
+ http.get(stream_api_endpoint, stream: true).each_line do |line|
13
+ payload = JSON.parse(line)
14
+ # do smth with this
15
+ end
16
+ ```
17
+
18
+ https://gitlab.com/honeyryderchuck/httpx/-/wikis/Stream
19
+
20
+ ### Rate Limiter
21
+
22
+ The `rate_limiter` plugin adds functionality for automatically hooking into rate-limiting responses coming from the server, and waits-and-retries them according to what the server advertises.
23
+
24
+ ```ruby
25
+ HTTPX.plugin(:rate_limiter).get(rate_limited_api_endpoint)
26
+ # => 429 Too Many Requests .... Retry-After: 3
27
+ # waits 3 seconds before retrying
28
+ ```
29
+
30
+ https://gitlab.com/honeyryderchuck/httpx/-/wikis/Rate-Limiter
31
+
32
+ ### Ruby 3
33
+
34
+ This release is the first testing against and targeting Ruby 3 and some of the new features.
35
+
36
+ It ships with RBS signatures for all of the client-facing APIs. There's non 100% typinng coverage yet, but I'm gradually (pun intended) working on it.
37
+
38
+
39
+ ## Improvements
40
+
41
+ ### IDN support
42
+
43
+ Requests where the domains are formed by non-ASCII characters, are now supported (if you're using ruby 2.3 or more recent).
44
+
45
+ ```ruby
46
+ HTTPX.get("http://bücher.ch") # it works!
47
+ ```
48
+
49
+ ### cookies plugin full implementation
50
+
51
+
52
+ The `cookies` plugin is now independent of 3rd-party gems. The motivation for this was that `http-cookie` was dependent of both `domain_name` and `unf` gems, which are currently unusable in ruby 3, and haven't received any update in the last 3 years.
53
+
54
+ The implementation is still compliant with RFC6265, and all of the features provided in earlier versions were ported, exceptwhen loading the cookie jar stored in a Netscape-format file or Mozilla sqlite database, which were not documented for `httpx` anyway, and I considered too niche to backport. If you feel `httpx` should support those, do let me know.
55
+
56
+ Some code from these gems, including the ruby punycode implementation, is now part of the source tree, along with its licenses and attribution mentions.
57
+
58
+ ## Bugfixes
59
+
60
+
61
+ Several edge-case bugs have been fixed solely by the integration of RBS runtime type checking, including some bugs around closing a connection pool that can cause loops.
62
+
63
+
64
+ ## Regressions
65
+
66
+ `HTTPX::ErrorResponse`'s methods `#headers` and `#reason` were removed, as they didn't provide much value. Consider calling `#raise_for_status` or checking the API (`is_a?(HTTPX::ErrorResponse)` or `respond_to?(:error)` are strategies for this).
@@ -5,6 +5,8 @@ require "httpx/version"
5
5
  require "httpx/extensions"
6
6
 
7
7
  require "httpx/errors"
8
+ require "httpx/utils"
9
+ require "httpx/domain_name"
8
10
  require "httpx/altsvc"
9
11
  require "httpx/callbacks"
10
12
  require "httpx/loggable"
@@ -121,7 +121,7 @@ module Faraday
121
121
  end
122
122
 
123
123
  def respond_to_missing?(meth)
124
- @env.respond_to?(meth)
124
+ @env.respond_to?(meth) || super
125
125
  end
126
126
 
127
127
  def method_missing(meth, *args, &blk)
@@ -34,11 +34,11 @@ 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
@@ -51,7 +51,7 @@ module HTTPX
51
51
  def initialize(type, uri, options)
52
52
  @type = type
53
53
  @origins = [uri.origin]
54
- @origin = URI(uri.origin)
54
+ @origin = Utils.uri(uri.origin)
55
55
  @options = Options.new(options)
56
56
  @window_size = @options.window_size
57
57
  @read_buffer = Buffer.new(BUFFER_SIZE)
@@ -142,7 +142,7 @@ module HTTPX
142
142
  end
143
143
  end
144
144
 
145
- def purge_pending
145
+ def purge_pending(&block)
146
146
  pendings = []
147
147
  if @parser
148
148
  @inflight -= @parser.pending.size
@@ -150,9 +150,7 @@ module HTTPX
150
150
  end
151
151
  pendings << @pending
152
152
  pendings.each do |pending|
153
- pending.reject! do |request|
154
- yield request
155
- end
153
+ pending.reject!(&block)
156
154
  end
157
155
  end
158
156
 
@@ -293,8 +291,6 @@ module HTTPX
293
291
  break
294
292
  end
295
293
 
296
- log { "READ: #{siz} bytes..." }
297
-
298
294
  parser << @read_buffer.to_s
299
295
 
300
296
  break if @state == :closing || @state == :closed
@@ -317,7 +313,6 @@ module HTTPX
317
313
  on_error(ex)
318
314
  return
319
315
  end
320
- log { "WRITE: #{siz} bytes..." }
321
316
 
322
317
  if siz.zero?
323
318
  write_drained = !@write_buffer.empty?
@@ -460,7 +455,6 @@ module HTTPX
460
455
  throw(:jump_tick)
461
456
  rescue Errno::ECONNREFUSED,
462
457
  Errno::EADDRNOTAVAIL,
463
- Errno::EHOSTUNREACH,
464
458
  OpenSSL::SSL::SSLError => e
465
459
  # connect errors, exit gracefully
466
460
  handle_error(e)
@@ -181,7 +181,7 @@ module HTTPX
181
181
  def manage_connection(response)
182
182
  connection = response.headers["connection"]
183
183
  case connection
184
- when /keep\-alive/i
184
+ when /keep-alive/i
185
185
  keep_alive = response.headers["keep-alive"]
186
186
  return unless keep_alive
187
187
 
@@ -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