http 5.0.4 → 5.1.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/.github/workflows/ci.yml +24 -4
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +27 -13
- data/CHANGES.md +33 -1
- data/Gemfile +1 -1
- data/LICENSE.txt +1 -1
- data/README.md +3 -2
- data/SECURITY.md +5 -0
- data/http.gemspec +6 -5
- data/lib/http/errors.rb +3 -0
- data/lib/http/headers.rb +1 -1
- data/lib/http/redirector.rb +53 -3
- data/lib/http/response/status.rb +1 -1
- data/lib/http/response.rb +7 -1
- data/lib/http/timeout/global.rb +1 -1
- data/lib/http/timeout/per_operation.rb +1 -1
- data/lib/http/uri.rb +46 -1
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +5 -3
- data/spec/lib/http/redirector_spec.rb +88 -3
- data/spec/lib/http/response/body_spec.rb +1 -1
- data/spec/lib/http/response_spec.rb +36 -0
- data/spec/lib/http/uri_spec.rb +39 -0
- data/spec/support/dummy_server/servlet.rb +2 -0
- data/spec/support/http_handling_shared.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef55bbff996952784d0917931b8eaa6f12a1adfc9cc480daf098cbc8cd8f6107
|
|
4
|
+
data.tar.gz: 222f8e8723969f89994fe2a0692c41786e450c200c45b84f5c8418f736254a64
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49b0c9e508fb02fca9d9e8a26d707202c5ac67bbdf73fce15b616b321545a3a32be96eb9e23f4d389687ad1720372eaba4412e18d800c5d29fab5bb0f4bc0d93
|
|
7
|
+
data.tar.gz: 1b2e22b2b33abe8059e78535556a15c53959b321fb225cd84153d5a8ea608ba4d03ead9cf018720f63b1e7c27c477f8c3f1b151cae60f2d50b6811e7dd9f5bcc
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -16,11 +16,11 @@ jobs:
|
|
|
16
16
|
|
|
17
17
|
strategy:
|
|
18
18
|
matrix:
|
|
19
|
-
ruby: [ ruby-2.
|
|
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@
|
|
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@
|
|
75
|
+
- uses: actions/checkout@v3
|
|
56
76
|
|
|
57
77
|
- uses: ruby/setup-ruby@v1
|
|
58
78
|
with:
|
|
59
|
-
ruby-version: 2.
|
|
79
|
+
ruby-version: 2.6
|
|
60
80
|
bundler-cache: true
|
|
61
81
|
|
|
62
82
|
- name: bundle exec rubocop
|
data/.rubocop.yml
CHANGED
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
|
|
3
|
-
# on
|
|
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
|
-
#
|
|
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:
|
|
26
|
-
#
|
|
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:
|
|
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:
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
187
|
-
# Configuration parameters:
|
|
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
data/LICENSE.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copyright (c) 2011-
|
|
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
|
-
-
|
|
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-
|
|
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
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.
|
|
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"
|
|
39
|
-
"wiki_uri"
|
|
40
|
-
"bug_tracker_uri"
|
|
41
|
-
"changelog_uri"
|
|
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
data/lib/http/redirector.rb
CHANGED
|
@@ -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
|
|
44
|
-
@max_hops
|
|
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
|
|
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?
|
data/lib/http/response/status.rb
CHANGED
|
@@ -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.
|
|
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
|
|
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]
|
data/lib/http/timeout/global.rb
CHANGED
|
@@ -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,
|
|
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,
|
|
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,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::
|
|
393
|
-
expect(feature_instance.captured_error).to be_a(HTTP::
|
|
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
|
|
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)
|
|
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
|
data/spec/lib/http/uri_spec.rb
CHANGED
|
@@ -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
|
|
@@ -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::
|
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
205
|
+
version: '2.6'
|
|
204
206
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
205
207
|
requirements:
|
|
206
208
|
- - ">="
|