http 5.0.4 → 5.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92001458b2f36332dfd042ab8750595408d2be8e941ed52be9237e11c071fef7
4
- data.tar.gz: de00f9d0a8f59f3af1605f479e503269741c751b397d62d618c8866e58929140
3
+ metadata.gz: ef55bbff996952784d0917931b8eaa6f12a1adfc9cc480daf098cbc8cd8f6107
4
+ data.tar.gz: 222f8e8723969f89994fe2a0692c41786e450c200c45b84f5c8418f736254a64
5
5
  SHA512:
6
- metadata.gz: 63249cd08800a312ffba0ed4b9a508e3890b3088058956e5433c71a60957bfe9985070225855b926d78f5a64c3b587a122c52137788b54e10da65076b39f541f
7
- data.tar.gz: 89605690a8dd8ac3bf23447de8a75f90bc37a77d8ff0e85d84b0748daf2cf06b69011d8e19cc4b22067e479decdfc9d8ca46f7fc0495ec90c25a433e10c1eac8
6
+ metadata.gz: 49b0c9e508fb02fca9d9e8a26d707202c5ac67bbdf73fce15b616b321545a3a32be96eb9e23f4d389687ad1720372eaba4412e18d800c5d29fab5bb0f4bc0d93
7
+ data.tar.gz: 1b2e22b2b33abe8059e78535556a15c53959b321fb225cd84153d5a8ea608ba4d03ead9cf018720f63b1e7c27c477f8c3f1b151cae60f2d50b6811e7dd9f5bcc
@@ -16,11 +16,11 @@ jobs:
16
16
 
17
17
  strategy:
18
18
  matrix:
19
- ruby: [ ruby-2.5, ruby-2.6, ruby-2.7, ruby-3.0 ]
19
+ ruby: [ ruby-2.6, ruby-2.7, ruby-3.0, ruby-3.1 ]
20
20
  os: [ ubuntu-latest ]
21
21
 
22
22
  steps:
23
- - uses: actions/checkout@v2
23
+ - uses: actions/checkout@v3
24
24
 
25
25
  - uses: ruby/setup-ruby@v1
26
26
  with:
@@ -38,6 +38,26 @@ jobs:
38
38
  path-to-lcov: ./coverage/lcov/lcov.info
39
39
  parallel: true
40
40
 
41
+ test-flaky:
42
+ runs-on: ${{ matrix.os }}
43
+
44
+ strategy:
45
+ matrix:
46
+ ruby: [ jruby-9.3 ]
47
+ os: [ ubuntu-latest ]
48
+
49
+ steps:
50
+ - uses: actions/checkout@v3
51
+
52
+ - uses: ruby/setup-ruby@v1
53
+ with:
54
+ ruby-version: ${{ matrix.ruby }}
55
+ bundler-cache: true
56
+
57
+ - name: bundle exec rspec
58
+ continue-on-error: true
59
+ run: bundle exec rspec --format progress --force-colour
60
+
41
61
  coveralls:
42
62
  needs: test
43
63
  runs-on: ubuntu-latest
@@ -52,11 +72,11 @@ jobs:
52
72
  runs-on: ubuntu-latest
53
73
 
54
74
  steps:
55
- - uses: actions/checkout@v2
75
+ - uses: actions/checkout@v3
56
76
 
57
77
  - uses: ruby/setup-ruby@v1
58
78
  with:
59
- ruby-version: 2.5
79
+ ruby-version: 2.6
60
80
  bundler-cache: true
61
81
 
62
82
  - name: bundle exec rubocop
data/.rubocop.yml CHANGED
@@ -7,4 +7,4 @@ AllCops:
7
7
  DefaultFormatter: fuubar
8
8
  DisplayCopNames: true
9
9
  NewCops: enable
10
- TargetRubyVersion: 2.5
10
+ TargetRubyVersion: 2.6
data/.rubocop_todo.yml CHANGED
@@ -1,13 +1,21 @@
1
1
  # This configuration was generated by
2
- # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 42`
3
- # on 2021-04-10 09:49:03 UTC using RuboCop version 1.12.1.
2
+ # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 100`
3
+ # on 2022-06-16 14:35:44 UTC using RuboCop version 1.30.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
+ # Offense count: 1
10
+ # This cop supports safe autocorrection (--autocorrect).
11
+ # Configuration parameters: Include.
12
+ # Include: **/*.gemspec
13
+ Gemspec/DeprecatedAttributeAssignment:
14
+ Exclude:
15
+ - 'http.gemspec'
16
+
9
17
  # Offense count: 53
10
- # Cop supports --auto-correct.
18
+ # This cop supports safe autocorrection (--autocorrect).
11
19
  # Configuration parameters: EnforcedStyle.
12
20
  # SupportedStyles: leading, trailing
13
21
  Layout/DotPosition:
@@ -22,8 +30,8 @@ Layout/DotPosition:
22
30
  - 'spec/lib/http_spec.rb'
23
31
  - 'spec/support/http_handling_shared.rb'
24
32
 
25
- # Offense count: 174
26
- # Cop supports --auto-correct.
33
+ # Offense count: 176
34
+ # This cop supports safe autocorrection (--autocorrect).
27
35
  # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
28
36
  # SupportedStyles: space, no_space, compact
29
37
  # SupportedStylesForEmptyBraces: space, no_space
@@ -66,7 +74,7 @@ Metrics/AbcSize:
66
74
  - 'lib/http/request.rb'
67
75
  - 'lib/http/response.rb'
68
76
 
69
- # Offense count: 66
77
+ # Offense count: 70
70
78
  # Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, IgnoredMethods.
71
79
  # IgnoredMethods: refine
72
80
  Metrics/BlockLength:
@@ -90,6 +98,7 @@ Metrics/BlockLength:
90
98
  - 'spec/lib/http/response/parser_spec.rb'
91
99
  - 'spec/lib/http/response/status_spec.rb'
92
100
  - 'spec/lib/http/response_spec.rb'
101
+ - 'spec/lib/http/uri_spec.rb'
93
102
  - 'spec/lib/http_spec.rb'
94
103
  - 'spec/support/http_handling_shared.rb'
95
104
 
@@ -109,7 +118,7 @@ Metrics/CyclomaticComplexity:
109
118
  - 'lib/http/chainable.rb'
110
119
  - 'lib/http/client.rb'
111
120
 
112
- # Offense count: 19
121
+ # Offense count: 18
113
122
  # Configuration parameters: CountComments, Max, CountAsOne, ExcludedMethods, IgnoredMethods.
114
123
  Metrics/MethodLength:
115
124
  Exclude:
@@ -132,8 +141,13 @@ Metrics/ModuleLength:
132
141
  Exclude:
133
142
  - 'lib/http/chainable.rb'
134
143
 
144
+ # Offense count: 1
145
+ Security/CompoundHash:
146
+ Exclude:
147
+ - 'lib/http/uri.rb'
148
+
135
149
  # Offense count: 2
136
- # Cop supports --auto-correct.
150
+ # This cop supports safe autocorrection (--autocorrect).
137
151
  # Configuration parameters: EnforcedStyle.
138
152
  # SupportedStyles: separated, grouped
139
153
  Style/AccessorGrouping:
@@ -141,7 +155,7 @@ Style/AccessorGrouping:
141
155
  - 'lib/http/request.rb'
142
156
 
143
157
  # Offense count: 4
144
- # Cop supports --auto-correct.
158
+ # This cop supports safe autocorrection (--autocorrect).
145
159
  Style/EmptyCaseCondition:
146
160
  Exclude:
147
161
  - 'lib/http/client.rb'
@@ -150,7 +164,7 @@ Style/EmptyCaseCondition:
150
164
  - 'lib/http/response/status.rb'
151
165
 
152
166
  # Offense count: 5
153
- # Cop supports --auto-correct.
167
+ # This cop supports safe autocorrection (--autocorrect).
154
168
  Style/Encoding:
155
169
  Exclude:
156
170
  - 'spec/lib/http/client_spec.rb'
@@ -160,7 +174,7 @@ Style/Encoding:
160
174
  - 'spec/support/dummy_server/servlet.rb'
161
175
 
162
176
  # Offense count: 17
163
- # Configuration parameters: SuspiciousParamNames.
177
+ # Configuration parameters: SuspiciousParamNames, Allowlist.
164
178
  # SuspiciousParamNames: options, opts, args, params, parameters
165
179
  Style/OptionHash:
166
180
  Exclude:
@@ -183,8 +197,8 @@ Style/OptionalBooleanParameter:
183
197
  - 'lib/http/uri.rb'
184
198
 
185
199
  # Offense count: 3
186
- # Cop supports --auto-correct.
187
- # Configuration parameters: AutoCorrect, Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
200
+ # This cop supports safe autocorrection (--autocorrect).
201
+ # Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
188
202
  # URISchemes: http, https
189
203
  Layout/LineLength:
190
204
  Exclude:
data/CHANGES.md CHANGED
@@ -1,3 +1,29 @@
1
+ ## 5.1.1 (2022-12-17)
2
+
3
+ * [#731](https://github.com/httprb/http/pull/731)
4
+ Strip brackets from IPv6 addresses in `HTTP::URI`.
5
+ ([@jeraki])
6
+
7
+ * [#722](https://github.com/httprb/http/pull/722)
8
+ Add `on_redirect` callback.
9
+ ([@benubois])
10
+
11
+ ## 5.1.0 (2022-06-17)
12
+
13
+ * Drop ruby-2.5 support.
14
+
15
+ * [#715](https://github.com/httprb/http/pull/715)
16
+ Set default encoding to UTF-8 for `application/json`.
17
+ ([@drwl])
18
+
19
+ * [#712](https://github.com/httprb/http/pull/712)
20
+ Recognize cookies set by redirect.
21
+ ([@tkellogg])
22
+
23
+ * [#707](https://github.com/httprb/http/pull/707)
24
+ Distinguish connection timeouts.
25
+ ([@YuLeven])
26
+
1
27
  ## 5.0.4 (2021-10-07)
2
28
 
3
29
  * [#698](https://github.com/httprb/http/pull/698)
@@ -42,7 +68,7 @@
42
68
 
43
69
  * [#638](https://github.com/httprb/http/pull/638)
44
70
  DNS failover handling.
45
- ([@midnight-wonderer])
71
+ ([@midnight-wonderer])
46
72
 
47
73
 
48
74
  ## 5.0.1 (2021-06-26)
@@ -112,6 +138,7 @@
112
138
 
113
139
  * [#576](https://github.com/httprb/http/pull/576)
114
140
  [#524](https://github.com/httprb/http/issues/524)
141
+ **BREAKING CHANGE**
115
142
  Preserve header names casing.
116
143
  ([@joshuaflanagan])
117
144
 
@@ -968,3 +995,8 @@ end
968
995
  [@matheussilvasantos]: https://github.com/matheussilvasantos
969
996
  [@PhilCoggins]: https://github.com/PhilCoggins
970
997
  [@flosacca]: https://github.com/flosacca
998
+ [@YuLeven]: https://github.com/YuLeven
999
+ [@drwl]: https://github.com/drwl
1000
+ [@tkellogg]: https://github.com/tkellogg
1001
+ [@jeraki]: https://github.com/jeraki
1002
+ [@benubois]: https://github.com/benubois
data/Gemfile CHANGED
@@ -27,7 +27,7 @@ group :test do
27
27
 
28
28
  gem "backports"
29
29
 
30
- gem "rubocop", "~> 1.21"
30
+ gem "rubocop", "~> 1.30.0"
31
31
  gem "rubocop-performance"
32
32
  gem "rubocop-rake"
33
33
  gem "rubocop-rspec"
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011-2021 Tony Arcieri, Erik Michaels-Ober, Alexey V. Zapparov, Zachary Anker
1
+ Copyright (c) 2011-2022 Tony Arcieri, Erik Michaels-Ober, Alexey V. Zapparov, Zachary Anker
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -113,7 +113,8 @@ the following Ruby versions:
113
113
  - Ruby 2.6
114
114
  - Ruby 2.7
115
115
  - Ruby 3.0
116
- - JRuby 9.2
116
+ - Ruby 3.1
117
+ - JRuby 9.3
117
118
 
118
119
  If something doesn't work on one of these versions, it's a bug.
119
120
 
@@ -141,7 +142,7 @@ dropped.
141
142
 
142
143
  ## Copyright
143
144
 
144
- Copyright © 2011-2021 Tony Arcieri, Alexey V. Zapparov, Erik Michaels-Ober, Zachary Anker.
145
+ Copyright © 2011-2022 Tony Arcieri, Alexey V. Zapparov, Erik Michaels-Ober, Zachary Anker.
145
146
  See LICENSE.txt for further details.
146
147
 
147
148
 
data/SECURITY.md ADDED
@@ -0,0 +1,5 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ Please report security issues to `bascule@gmail.com`
data/http.gemspec CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |gem|
25
25
  gem.require_paths = ["lib"]
26
26
  gem.version = HTTP::VERSION
27
27
 
28
- gem.required_ruby_version = ">= 2.5"
28
+ gem.required_ruby_version = ">= 2.6"
29
29
 
30
30
  gem.add_runtime_dependency "addressable", "~> 2.8"
31
31
  gem.add_runtime_dependency "http-cookie", "~> 1.0"
@@ -35,9 +35,10 @@ Gem::Specification.new do |gem|
35
35
  gem.add_development_dependency "bundler", "~> 2.0"
36
36
 
37
37
  gem.metadata = {
38
- "source_code_uri" => "https://github.com/httprb/http",
39
- "wiki_uri" => "https://github.com/httprb/http/wiki",
40
- "bug_tracker_uri" => "https://github.com/httprb/http/issues",
41
- "changelog_uri" => "https://github.com/httprb/http/blob/v#{HTTP::VERSION}/CHANGES.md"
38
+ "source_code_uri" => "https://github.com/httprb/http",
39
+ "wiki_uri" => "https://github.com/httprb/http/wiki",
40
+ "bug_tracker_uri" => "https://github.com/httprb/http/issues",
41
+ "changelog_uri" => "https://github.com/httprb/http/blob/v#{HTTP::VERSION}/CHANGES.md",
42
+ "rubygems_mfa_required" => "true"
42
43
  }
43
44
  end
data/lib/http/errors.rb CHANGED
@@ -19,6 +19,9 @@ module HTTP
19
19
  # Generic Timeout error
20
20
  class TimeoutError < Error; end
21
21
 
22
+ # Timeout when first establishing the conncetion
23
+ class ConnectTimeoutError < TimeoutError; end
24
+
22
25
  # Header value is of unexpected format (similar to Net::HTTPHeaderSyntaxError)
23
26
  class HeaderError < Error; end
24
27
  end
data/lib/http/headers.rb CHANGED
@@ -111,7 +111,7 @@ module HTTP
111
111
  #
112
112
  # @return [Hash]
113
113
  def to_h
114
- keys.map { |k| [k, self[k]] }.to_h
114
+ keys.to_h { |k| [k, self[k]] }
115
115
  end
116
116
  alias to_hash to_h
117
117
 
@@ -40,8 +40,9 @@ module HTTP
40
40
  # @option opts [Boolean] :strict (true) redirector hops policy
41
41
  # @option opts [#to_i] :max_hops (5) maximum allowed amount of hops
42
42
  def initialize(opts = {})
43
- @strict = opts.fetch(:strict, true)
44
- @max_hops = opts.fetch(:max_hops, 5).to_i
43
+ @strict = opts.fetch(:strict, true)
44
+ @max_hops = opts.fetch(:max_hops, 5).to_i
45
+ @on_redirect = opts.fetch(:on_redirect, nil)
45
46
  end
46
47
 
47
48
  # Follows redirects until non-redirect response found
@@ -49,6 +50,8 @@ module HTTP
49
50
  @request = request
50
51
  @response = response
51
52
  @visited = []
53
+ collect_cookies_from_request
54
+ collect_cookies_from_response
52
55
 
53
56
  while REDIRECT_CODES.include? @response.status.code
54
57
  @visited << "#{@request.verb} #{@request.uri}"
@@ -59,8 +62,13 @@ module HTTP
59
62
  @response.flush
60
63
 
61
64
  # XXX(ixti): using `Array#inject` to return `nil` if no Location header.
62
- @request = redirect_to(@response.headers.get(Headers::LOCATION).inject(:+))
65
+ @request = redirect_to(@response.headers.get(Headers::LOCATION).inject(:+))
66
+ unless cookie_jar.empty?
67
+ @request.headers.set(Headers::COOKIE, cookie_jar.cookies.map { |c| "#{c.name}=#{c.value}" }.join("; "))
68
+ end
69
+ @on_redirect.call @response, @request if @on_redirect.respond_to?(:call)
63
70
  @response = yield @request
71
+ collect_cookies_from_response
64
72
  end
65
73
 
66
74
  @response
@@ -68,6 +76,48 @@ module HTTP
68
76
 
69
77
  private
70
78
 
79
+ # All known cookies. On the original request, this is only the original cookies, but after that,
80
+ # Set-Cookie headers can add, set or delete cookies.
81
+ def cookie_jar
82
+ # it seems that @response.cookies instance is reused between responses, so we have to "clone"
83
+ @cookie_jar ||= HTTP::CookieJar.new
84
+ end
85
+
86
+ def collect_cookies_from_request
87
+ request_cookie_header = @request.headers["Cookie"]
88
+ cookies =
89
+ if request_cookie_header
90
+ HTTP::Cookie.cookie_value_to_hash(request_cookie_header)
91
+ else
92
+ {}
93
+ end
94
+
95
+ cookies.each do |key, value|
96
+ cookie_jar.add(HTTP::Cookie.new(key, value, :path => @request.uri.path, :domain => @request.host))
97
+ end
98
+ end
99
+
100
+ # Carry cookies from one response to the next. Carrying cookies to the next response ends up
101
+ # carrying them to the next request as well.
102
+ #
103
+ # Note that this isn't part of the IETF standard, but all major browsers support setting cookies
104
+ # on redirect: https://blog.dubbelboer.com/2012/11/25/302-cookie.html
105
+ def collect_cookies_from_response
106
+ # Overwrite previous cookies
107
+ @response.cookies.each do |cookie|
108
+ if cookie.value == ""
109
+ cookie_jar.delete(cookie)
110
+ else
111
+ cookie_jar.add(cookie)
112
+ end
113
+ end
114
+
115
+ # I wish we could just do @response.cookes = cookie_jar
116
+ cookie_jar.each do |cookie|
117
+ @response.cookies.add(cookie)
118
+ end
119
+ end
120
+
71
121
  # Check if we reached max amount of redirect hops
72
122
  # @return [Boolean]
73
123
  def too_many_hops?
@@ -69,7 +69,7 @@ module HTTP
69
69
  # SYMBOL_CODES[:im_a_teapot] # => 418
70
70
  #
71
71
  # @return [Hash<Symbol => Fixnum>]
72
- SYMBOL_CODES = SYMBOLS.map { |k, v| [v, k] }.to_h.freeze
72
+ SYMBOL_CODES = SYMBOLS.to_h { |k, v| [v, k] }.freeze
73
73
 
74
74
  # @return [Fixnum] status code
75
75
  attr_reader :code
data/lib/http/response.rb CHANGED
@@ -53,7 +53,7 @@ module HTTP
53
53
  @body = opts.fetch(:body)
54
54
  else
55
55
  connection = opts.fetch(:connection)
56
- encoding = opts[:encoding] || charset || Encoding::BINARY
56
+ encoding = opts[:encoding] || charset || default_encoding
57
57
 
58
58
  @body = Response::Body.new(connection, :encoding => encoding)
59
59
  end
@@ -168,6 +168,12 @@ module HTTP
168
168
 
169
169
  private
170
170
 
171
+ def default_encoding
172
+ return Encoding::UTF_8 if mime_type == "application/json"
173
+
174
+ Encoding::BINARY
175
+ end
176
+
171
177
  # Initialize an HTTP::Request from options.
172
178
  #
173
179
  # @return [HTTP::Request]
@@ -21,7 +21,7 @@ module HTTP
21
21
 
22
22
  def connect(socket_class, host, port, nodelay = false)
23
23
  reset_timer
24
- ::Timeout.timeout(@time_left, TimeoutError) do
24
+ ::Timeout.timeout(@time_left, ConnectTimeoutError) do
25
25
  @socket = socket_class.open(host, port)
26
26
  @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
27
27
  end
@@ -20,7 +20,7 @@ module HTTP
20
20
  end
21
21
 
22
22
  def connect(socket_class, host, port, nodelay = false)
23
- ::Timeout.timeout(@connect_timeout, TimeoutError) do
23
+ ::Timeout.timeout(@connect_timeout, ConnectTimeoutError) do
24
24
  @socket = socket_class.open(host, port)
25
25
  @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
26
26
  end
data/lib/http/uri.rb CHANGED
@@ -9,7 +9,6 @@ module HTTP
9
9
  def_delegators :@uri, :scheme, :normalized_scheme, :scheme=
10
10
  def_delegators :@uri, :user, :normalized_user, :user=
11
11
  def_delegators :@uri, :password, :normalized_password, :password=
12
- def_delegators :@uri, :host, :normalized_host, :host=
13
12
  def_delegators :@uri, :authority, :normalized_authority, :authority=
14
13
  def_delegators :@uri, :origin, :origin=
15
14
  def_delegators :@uri, :normalized_port, :port=
@@ -20,6 +19,18 @@ module HTTP
20
19
  def_delegators :@uri, :fragment, :normalized_fragment, :fragment=
21
20
  def_delegators :@uri, :omit, :join, :normalize
22
21
 
22
+ # Host, either a domain name or IP address. If the host is an IPv6 address, it will be returned
23
+ # without brackets surrounding it.
24
+ #
25
+ # @return [String] The host of the URI
26
+ attr_reader :host
27
+
28
+ # Normalized host, either a domain name or IP address. If the host is an IPv6 address, it will
29
+ # be returned without brackets surrounding it.
30
+ #
31
+ # @return [String] The normalized host of the URI
32
+ attr_reader :normalized_host
33
+
23
34
  # @private
24
35
  HTTP_SCHEME = "http"
25
36
 
@@ -83,6 +94,9 @@ module HTTP
83
94
  else
84
95
  raise TypeError, "expected Hash for options, got #{options_or_uri.class}"
85
96
  end
97
+
98
+ @host = process_ipv6_brackets(@uri.host)
99
+ @normalized_host = process_ipv6_brackets(@uri.normalized_host)
86
100
  end
87
101
 
88
102
  # Are these URI objects equal? Normalizes both URIs prior to comparison
@@ -110,6 +124,17 @@ module HTTP
110
124
  @hash ||= to_s.hash * -1
111
125
  end
112
126
 
127
+ # Sets the host component for the URI.
128
+ #
129
+ # @param [String, #to_str] new_host The new host component.
130
+ # @return [void]
131
+ def host=(new_host)
132
+ @uri.host = process_ipv6_brackets(new_host, :brackets => true)
133
+
134
+ @host = process_ipv6_brackets(@uri.host)
135
+ @normalized_host = process_ipv6_brackets(@uri.normalized_host)
136
+ end
137
+
113
138
  # Port number, either as specified or the default if unspecified
114
139
  #
115
140
  # @return [Integer] port number
@@ -146,5 +171,25 @@ module HTTP
146
171
  def inspect
147
172
  format("#<%s:0x%014x URI:%s>", self.class.name, object_id << 1, to_s)
148
173
  end
174
+
175
+ private
176
+
177
+ # Process a URI host, adding or removing surrounding brackets if the host is an IPv6 address.
178
+ #
179
+ # @param [Boolean] brackets When true, brackets will be added to IPv6 addresses if missing. When
180
+ # false, they will be removed if present.
181
+ #
182
+ # @return [String] Host with IPv6 address brackets added or removed
183
+ def process_ipv6_brackets(raw_host, brackets: false)
184
+ ip = IPAddr.new(raw_host)
185
+
186
+ if ip.ipv6?
187
+ brackets ? "[#{ip}]" : ip.to_s
188
+ else
189
+ raw_host
190
+ end
191
+ rescue IPAddr::Error
192
+ raw_host
193
+ end
149
194
  end
150
195
  end
data/lib/http/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- VERSION = "5.0.4"
4
+ VERSION = "5.1.1"
5
5
  end
@@ -1,10 +1,12 @@
1
1
  # coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "cgi"
5
+ require "logger"
6
+
4
7
  require "support/http_handling_shared"
5
8
  require "support/dummy_server"
6
9
  require "support/ssl_helper"
7
- require "logger"
8
10
 
9
11
  RSpec.describe HTTP::Client do
10
12
  run_server(:dummy) { DummyServer.new }
@@ -389,8 +391,8 @@ RSpec.describe HTTP::Client do
389
391
  client.use(:test_feature => feature_instance).
390
392
  timeout(0.001).
391
393
  request(:post, sleep_url)
392
- end.to raise_error(HTTP::TimeoutError)
393
- expect(feature_instance.captured_error).to be_a(HTTP::TimeoutError)
394
+ end.to raise_error(HTTP::ConnectTimeoutError)
395
+ expect(feature_instance.captured_error).to be_a(HTTP::ConnectTimeoutError)
394
396
  end
395
397
  end
396
398
  end
@@ -11,8 +11,12 @@ RSpec.describe HTTP::Redirector do
11
11
  )
12
12
  end
13
13
 
14
- def redirect_response(status, location)
15
- simple_response status, "", "Location" => location
14
+ def redirect_response(status, location, set_cookie = {})
15
+ res = simple_response status, "", "Location" => location
16
+ set_cookie.each do |name, value|
17
+ res.headers.add("Set-Cookie", "#{name}=#{value}; path=/; httponly; secure; SameSite=none; Secure")
18
+ end
19
+ res
16
20
  end
17
21
 
18
22
  describe "#strict" do
@@ -89,6 +93,87 @@ RSpec.describe HTTP::Redirector do
89
93
  expect(res.to_s).to eq "http://example.com/123"
90
94
  end
91
95
 
96
+ it "returns cookies in response" do
97
+ req = HTTP::Request.new :verb => :head, :uri => "http://example.com"
98
+ hops = [
99
+ redirect_response(301, "http://example.com/1", {"foo" => "42"}),
100
+ redirect_response(301, "http://example.com/2", {"bar" => "53", "deleted" => "foo"}),
101
+ redirect_response(301, "http://example.com/3", {"baz" => "64", "deleted" => ""}),
102
+ redirect_response(301, "http://example.com/4", {"baz" => "65"}),
103
+ simple_response(200, "bar")
104
+ ]
105
+
106
+ request_cookies = [
107
+ {"foo" => "42"},
108
+ {"foo" => "42", "bar" => "53", "deleted" => "foo"},
109
+ {"foo" => "42", "bar" => "53", "baz" => "64"},
110
+ {"foo" => "42", "bar" => "53", "baz" => "65"}
111
+ ]
112
+
113
+ res = redirector.perform(req, hops.shift) do |request|
114
+ req_cookie = HTTP::Cookie.cookie_value_to_hash(request.headers["Cookie"] || "")
115
+ expect(req_cookie).to eq request_cookies.shift
116
+ hops.shift
117
+ end
118
+ expect(res.to_s).to eq "bar"
119
+ cookies = res.cookies.cookies.to_h { |c| [c.name, c.value] }
120
+ expect(cookies["foo"]).to eq "42"
121
+ expect(cookies["bar"]).to eq "53"
122
+ expect(cookies["baz"]).to eq "65"
123
+ expect(cookies["deleted"]).to eq nil
124
+ end
125
+
126
+ it "returns original cookies in response" do
127
+ req = HTTP::Request.new :verb => :head, :uri => "http://example.com"
128
+ req.headers.set("Cookie", "foo=42; deleted=baz")
129
+ hops = [
130
+ redirect_response(301, "http://example.com/1", {"bar" => "64", "deleted" => ""}),
131
+ simple_response(200, "bar")
132
+ ]
133
+
134
+ request_cookies = [
135
+ {"foo" => "42", "bar" => "64"},
136
+ {"foo" => "42", "bar" => "64"}
137
+ ]
138
+
139
+ res = redirector.perform(req, hops.shift) do |request|
140
+ req_cookie = HTTP::Cookie.cookie_value_to_hash(request.headers["Cookie"] || "")
141
+ expect(req_cookie).to eq request_cookies.shift
142
+ hops.shift
143
+ end
144
+ expect(res.to_s).to eq "bar"
145
+ cookies = res.cookies.cookies.to_h { |c| [c.name, c.value] }
146
+ expect(cookies["foo"]).to eq "42"
147
+ expect(cookies["bar"]).to eq "64"
148
+ expect(cookies["deleted"]).to eq nil
149
+ end
150
+
151
+ context "with on_redirect callback" do
152
+ let(:options) do
153
+ {
154
+ :on_redirect => proc do |response, location|
155
+ @redirect_response = response
156
+ @redirect_location = location
157
+ end
158
+ }
159
+ end
160
+
161
+ it "calls on_redirect" do
162
+ req = HTTP::Request.new :verb => :head, :uri => "http://example.com"
163
+ hops = [
164
+ redirect_response(301, "http://example.com/1"),
165
+ redirect_response(301, "http://example.com/2"),
166
+ simple_response(200, "foo")
167
+ ]
168
+
169
+ redirector.perform(req, hops.shift) do |prev_req, _|
170
+ expect(@redirect_location.uri.to_s).to eq prev_req.uri.to_s
171
+ expect(@redirect_response.code).to eq 301
172
+ hops.shift
173
+ end
174
+ end
175
+ end
176
+
92
177
  context "following 300 redirect" do
93
178
  context "with strict mode" do
94
179
  let(:options) { {:strict => true} }
@@ -400,7 +485,7 @@ RSpec.describe HTTP::Redirector do
400
485
  describe "changing verbs during redirects" do
401
486
  let(:options) { {:strict => false} }
402
487
  let(:post_body) { HTTP::Request::Body.new("i might be way longer in real life") }
403
- let(:cookie) { "dont eat my cookies" }
488
+ let(:cookie) { "dont=eat my cookies" }
404
489
 
405
490
  def a_dangerous_request(verb)
406
491
  HTTP::Request.new(
@@ -59,7 +59,7 @@ RSpec.describe HTTP::Response::Body do
59
59
  let(:chunks) do
60
60
  body = Zlib::Deflate.deflate("Hi, HTTP here ☺")
61
61
  len = body.length
62
- [body[0, len / 2], body[(len / 2)..-1]]
62
+ [body[0, len / 2], body[(len / 2)..]]
63
63
  end
64
64
  subject(:body) do
65
65
  inflater = HTTP::Response::Inflater.new(connection)
@@ -223,4 +223,40 @@ RSpec.describe HTTP::Response do
223
223
  end
224
224
  end
225
225
  end
226
+
227
+ describe "#body" do
228
+ let(:connection) { double(:sequence_id => 0) }
229
+ let(:chunks) { ["Hello, ", "World!"] }
230
+
231
+ subject(:response) do
232
+ HTTP::Response.new(
233
+ :status => 200,
234
+ :version => "1.1",
235
+ :headers => headers,
236
+ :request => request,
237
+ :connection => connection
238
+ )
239
+ end
240
+
241
+ before do
242
+ allow(connection).to receive(:readpartial) { chunks.shift }
243
+ allow(connection).to receive(:body_completed?) { chunks.empty? }
244
+ end
245
+
246
+ context "with no Content-Type" do
247
+ let(:headers) { {} }
248
+
249
+ it "returns a body with default binary encoding" do
250
+ expect(response.body.to_s.encoding).to eq Encoding::BINARY
251
+ end
252
+ end
253
+
254
+ context "with Content-Type: application/json" do
255
+ let(:headers) { {"Content-Type" => "application/json"} }
256
+
257
+ it "returns a body with a default UTF_8 encoding" do
258
+ expect(response.body.to_s.encoding).to eq Encoding::UTF_8
259
+ end
260
+ end
261
+ end
226
262
  end
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe HTTP::URI do
4
+ let(:example_ipv6_address) { "2606:2800:220:1:248:1893:25c8:1946" }
5
+
4
6
  let(:example_http_uri_string) { "http://example.com" }
5
7
  let(:example_https_uri_string) { "https://example.com" }
8
+ let(:example_ipv6_uri_string) { "https://[#{example_ipv6_address}]" }
6
9
 
7
10
  subject(:http_uri) { described_class.parse(example_http_uri_string) }
8
11
  subject(:https_uri) { described_class.parse(example_https_uri_string) }
12
+ subject(:ipv6_uri) { described_class.parse(example_ipv6_uri_string) }
9
13
 
10
14
  it "knows URI schemes" do
11
15
  expect(http_uri.scheme).to eq "http"
@@ -20,6 +24,41 @@ RSpec.describe HTTP::URI do
20
24
  expect(https_uri.port).to eq 443
21
25
  end
22
26
 
27
+ describe "#host" do
28
+ it "strips brackets from IPv6 addresses" do
29
+ expect(ipv6_uri.host).to eq("2606:2800:220:1:248:1893:25c8:1946")
30
+ end
31
+ end
32
+
33
+ describe "#normalized_host" do
34
+ it "strips brackets from IPv6 addresses" do
35
+ expect(ipv6_uri.normalized_host).to eq("2606:2800:220:1:248:1893:25c8:1946")
36
+ end
37
+ end
38
+
39
+ describe "#host=" do
40
+ it "updates cached values for #host and #normalized_host" do
41
+ expect(http_uri.host).to eq("example.com")
42
+ expect(http_uri.normalized_host).to eq("example.com")
43
+
44
+ http_uri.host = "[#{example_ipv6_address}]"
45
+
46
+ expect(http_uri.host).to eq(example_ipv6_address)
47
+ expect(http_uri.normalized_host).to eq(example_ipv6_address)
48
+ end
49
+
50
+ it "ensures IPv6 addresses are bracketed in the inner Addressable::URI" do
51
+ expect(http_uri.host).to eq("example.com")
52
+ expect(http_uri.normalized_host).to eq("example.com")
53
+
54
+ http_uri.host = example_ipv6_address
55
+
56
+ expect(http_uri.host).to eq(example_ipv6_address)
57
+ expect(http_uri.normalized_host).to eq(example_ipv6_address)
58
+ expect(http_uri.instance_variable_get(:@uri).host).to eq("[#{example_ipv6_address}]")
59
+ end
60
+ end
61
+
23
62
  describe "#dup" do
24
63
  it "doesn't share internal value between duplicates" do
25
64
  duplicated_uri = http_uri.dup
@@ -1,6 +1,8 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "cgi"
5
+
4
6
  class DummyServer < WEBrick::HTTPServer
5
7
  class Servlet < WEBrick::HTTPServlet::AbstractServlet # rubocop:disable Metrics/ClassLength
6
8
  def self.sockets
@@ -77,7 +77,7 @@ RSpec.shared_context "HTTP handling" do
77
77
  sleep 1.25
78
78
  end
79
79
 
80
- expect { response }.to raise_error(HTTP::TimeoutError, /execution/)
80
+ expect { response }.to raise_error(HTTP::ConnectTimeoutError, /execution/)
81
81
  end
82
82
 
83
83
  it "errors if reading takes too long" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.4
4
+ version: 5.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Arcieri
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2021-10-07 00:00:00.000000000 Z
14
+ date: 2022-12-17 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: addressable
@@ -106,6 +106,7 @@ files:
106
106
  - LICENSE.txt
107
107
  - README.md
108
108
  - Rakefile
109
+ - SECURITY.md
109
110
  - http.gemspec
110
111
  - lib/http.rb
111
112
  - lib/http/chainable.rb
@@ -191,7 +192,8 @@ metadata:
191
192
  source_code_uri: https://github.com/httprb/http
192
193
  wiki_uri: https://github.com/httprb/http/wiki
193
194
  bug_tracker_uri: https://github.com/httprb/http/issues
194
- changelog_uri: https://github.com/httprb/http/blob/v5.0.4/CHANGES.md
195
+ changelog_uri: https://github.com/httprb/http/blob/v5.1.1/CHANGES.md
196
+ rubygems_mfa_required: 'true'
195
197
  post_install_message:
196
198
  rdoc_options: []
197
199
  require_paths:
@@ -200,7 +202,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
200
202
  requirements:
201
203
  - - ">="
202
204
  - !ruby/object:Gem::Version
203
- version: '2.5'
205
+ version: '2.6'
204
206
  required_rubygems_version: !ruby/object:Gem::Requirement
205
207
  requirements:
206
208
  - - ">="