http 5.1.1 → 5.2.0
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 +6 -24
- data/.rubocop/metrics.yml +4 -0
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +41 -0
- data/{CHANGES.md → CHANGES_OLD.md} +1 -1
- data/README.md +4 -2
- data/SECURITY.md +13 -1
- data/http.gemspec +3 -2
- data/lib/http/client.rb +1 -1
- data/lib/http/connection.rb +8 -1
- data/lib/http/features/instrumentation.rb +6 -1
- data/lib/http/request.rb +12 -3
- data/lib/http/timeout/null.rb +8 -5
- data/lib/http/uri.rb +18 -2
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +1 -0
- data/spec/lib/http/connection_spec.rb +23 -1
- data/spec/lib/http/features/instrumentation_spec.rb +19 -0
- data/spec/lib/http/options/headers_spec.rb +5 -1
- data/spec/lib/http/uri/normalizer_spec.rb +95 -0
- data/spec/lib/http_spec.rb +20 -3
- data/spec/support/dummy_server/servlet.rb +6 -6
- metadata +25 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8593353cd2283b2ac49c557ff71d5f1a668066dbac59b3d2a6f59429b9b2f5e8
|
4
|
+
data.tar.gz: e9316b3c4dd519825eeaa255a4f86932ce4a9e7f374f9be8273ca4051f5b9c3d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ae34b411a233ca8601aebe2295f2a1d8e0bec7e742a95fdd6e075b4f7dd3c68d42448cfbac1146b645ee59424e91af8eb3878c43e7a7fb17c3569681cf4ccf3
|
7
|
+
data.tar.gz: a76bc0a41338e67677370014d803ea79e3a34078172cc81491f31b085613394c7290b2402e199a2e1de77c7fecf7e05fde8fc2d9924016105ce429751f202e85
|
data/.github/workflows/ci.yml
CHANGED
@@ -2,9 +2,9 @@ name: CI
|
|
2
2
|
|
3
3
|
on:
|
4
4
|
push:
|
5
|
-
branches: [ main ]
|
5
|
+
branches: [ main, 5-x-stable ]
|
6
6
|
pull_request:
|
7
|
-
branches: [ main ]
|
7
|
+
branches: [ main, 5-x-stable ]
|
8
8
|
|
9
9
|
env:
|
10
10
|
BUNDLE_WITHOUT: "development"
|
@@ -16,11 +16,11 @@ jobs:
|
|
16
16
|
|
17
17
|
strategy:
|
18
18
|
matrix:
|
19
|
-
ruby: [ ruby-2.6, ruby-2.7, ruby-3.0, ruby-3.1 ]
|
19
|
+
ruby: [ ruby-2.6, ruby-2.7, ruby-3.0, ruby-3.1, ruby-3.2, ruby-3.3 ]
|
20
20
|
os: [ ubuntu-latest ]
|
21
21
|
|
22
22
|
steps:
|
23
|
-
- uses: actions/checkout@
|
23
|
+
- uses: actions/checkout@v4
|
24
24
|
|
25
25
|
- uses: ruby/setup-ruby@v1
|
26
26
|
with:
|
@@ -30,14 +30,6 @@ jobs:
|
|
30
30
|
- name: bundle exec rspec
|
31
31
|
run: bundle exec rspec --format progress --force-colour
|
32
32
|
|
33
|
-
- name: Prepare Coveralls test coverage report
|
34
|
-
uses: coverallsapp/github-action@v1.1.2
|
35
|
-
with:
|
36
|
-
github-token: ${{ secrets.GITHUB_TOKEN }}
|
37
|
-
flag-name: "${{ matrix.ruby }} @${{ matrix.os }}"
|
38
|
-
path-to-lcov: ./coverage/lcov/lcov.info
|
39
|
-
parallel: true
|
40
|
-
|
41
33
|
test-flaky:
|
42
34
|
runs-on: ${{ matrix.os }}
|
43
35
|
|
@@ -47,7 +39,7 @@ jobs:
|
|
47
39
|
os: [ ubuntu-latest ]
|
48
40
|
|
49
41
|
steps:
|
50
|
-
- uses: actions/checkout@
|
42
|
+
- uses: actions/checkout@v4
|
51
43
|
|
52
44
|
- uses: ruby/setup-ruby@v1
|
53
45
|
with:
|
@@ -58,21 +50,11 @@ jobs:
|
|
58
50
|
continue-on-error: true
|
59
51
|
run: bundle exec rspec --format progress --force-colour
|
60
52
|
|
61
|
-
coveralls:
|
62
|
-
needs: test
|
63
|
-
runs-on: ubuntu-latest
|
64
|
-
steps:
|
65
|
-
- name: Finalize Coveralls test coverage report
|
66
|
-
uses: coverallsapp/github-action@master
|
67
|
-
with:
|
68
|
-
github-token: ${{ secrets.GITHUB_TOKEN }}
|
69
|
-
parallel-finished: true
|
70
|
-
|
71
53
|
lint:
|
72
54
|
runs-on: ubuntu-latest
|
73
55
|
|
74
56
|
steps:
|
75
|
-
- uses: actions/checkout@
|
57
|
+
- uses: actions/checkout@v4
|
76
58
|
|
77
59
|
- uses: ruby/setup-ruby@v1
|
78
60
|
with:
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [Unreleased]
|
9
|
+
|
10
|
+
## [5.2.0] - 2024-02-05
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- Add `Connection#finished_request?`
|
15
|
+
([#743](https://github.com/httprb/http/pull/743))
|
16
|
+
- Add `Instrumentation#on_error`
|
17
|
+
([#746](https://github.com/httprb/http/pull/746))
|
18
|
+
- Add `base64` dependency (suppresses warnings on Ruby 3.0)
|
19
|
+
([#759](https://github.com/httprb/http/pull/759))
|
20
|
+
- Add `PURGE` HTTP verb
|
21
|
+
([#757](https://github.com/httprb/http/pull/757))
|
22
|
+
- Add Ruby-3.3 support
|
23
|
+
|
24
|
+
### Changed
|
25
|
+
|
26
|
+
- **BREAKING** Process features in reverse order
|
27
|
+
([#766](https://github.com/httprb/http/pull/766))
|
28
|
+
- **BREAKING** Downcase Content-Type charset name
|
29
|
+
([#753](https://github.com/httprb/http/pull/753))
|
30
|
+
- **BREAKING** Make URI normalization more conservative
|
31
|
+
([#758](https://github.com/httprb/http/pull/758))
|
32
|
+
|
33
|
+
### Fixed
|
34
|
+
|
35
|
+
- Close sockets on initialize failure
|
36
|
+
([#762](https://github.com/httprb/http/pull/762))
|
37
|
+
- Prevent CRLF injection due to broken URL normalizer
|
38
|
+
([#765](https://github.com/httprb/http/pull/765))
|
39
|
+
|
40
|
+
[unreleased]: https://github.com/httprb/http/compare/v5.2.0...5-x-stable
|
41
|
+
[5.2.0]: https://github.com/httprb/http/compare/v5.1.1...v5.2.0
|
data/README.md
CHANGED
@@ -110,11 +110,13 @@ and call `#readpartial` on it repeatedly until it returns `nil`:
|
|
110
110
|
This library aims to support and is [tested against][build-link]
|
111
111
|
the following Ruby versions:
|
112
112
|
|
113
|
+
- JRuby 9.3
|
113
114
|
- Ruby 2.6
|
114
115
|
- Ruby 2.7
|
115
116
|
- Ruby 3.0
|
116
117
|
- Ruby 3.1
|
117
|
-
-
|
118
|
+
- Ruby 3.2
|
119
|
+
- Ruby 3.3
|
118
120
|
|
119
121
|
If something doesn't work on one of these versions, it's a bug.
|
120
122
|
|
@@ -160,5 +162,5 @@ See LICENSE.txt for further details.
|
|
160
162
|
[//]: # (links)
|
161
163
|
|
162
164
|
[documentation]: https://github.com/httprb/http/wiki
|
163
|
-
[requests]:
|
165
|
+
[requests]: https://docs.python-requests.org/en/latest/
|
164
166
|
[llhttp]: https://llhttp.org/
|
data/SECURITY.md
CHANGED
@@ -1,5 +1,17 @@
|
|
1
1
|
# Security Policy
|
2
2
|
|
3
|
+
## Supported Versions
|
4
|
+
|
5
|
+
Security updates are applied only to the most recent release.
|
6
|
+
|
3
7
|
## Reporting a Vulnerability
|
4
8
|
|
5
|
-
|
9
|
+
If you have discovered a security vulnerability in this project, please report
|
10
|
+
it privately. **Do not disclose it as a public issue.** This gives us time to
|
11
|
+
work with you to fix the issue before public exposure, reducing the chance that
|
12
|
+
the exploit will be used before a patch is released.
|
13
|
+
|
14
|
+
Please disclose it at [security advisory](https://github.com/httprb/http/security/advisories/new).
|
15
|
+
|
16
|
+
This project is maintained by a team of volunteers on a reasonable-effort basis.
|
17
|
+
As such, please give us at least 90 days to work on a fix before public exposure.
|
data/http.gemspec
CHANGED
@@ -28,9 +28,10 @@ Gem::Specification.new do |gem|
|
|
28
28
|
gem.required_ruby_version = ">= 2.6"
|
29
29
|
|
30
30
|
gem.add_runtime_dependency "addressable", "~> 2.8"
|
31
|
+
gem.add_runtime_dependency "base64", "~> 0.1"
|
31
32
|
gem.add_runtime_dependency "http-cookie", "~> 1.0"
|
32
33
|
gem.add_runtime_dependency "http-form_data", "~> 2.2"
|
33
|
-
gem.add_runtime_dependency "llhttp-ffi", "~> 0.
|
34
|
+
gem.add_runtime_dependency "llhttp-ffi", "~> 0.5.0"
|
34
35
|
|
35
36
|
gem.add_development_dependency "bundler", "~> 2.0"
|
36
37
|
|
@@ -38,7 +39,7 @@ Gem::Specification.new do |gem|
|
|
38
39
|
"source_code_uri" => "https://github.com/httprb/http",
|
39
40
|
"wiki_uri" => "https://github.com/httprb/http/wiki",
|
40
41
|
"bug_tracker_uri" => "https://github.com/httprb/http/issues",
|
41
|
-
"changelog_uri" => "https://github.com/httprb/http/blob/v#{HTTP::VERSION}/
|
42
|
+
"changelog_uri" => "https://github.com/httprb/http/blob/v#{HTTP::VERSION}/CHANGELOG.md",
|
42
43
|
"rubygems_mfa_required" => "true"
|
43
44
|
}
|
44
45
|
end
|
data/lib/http/client.rb
CHANGED
@@ -184,7 +184,7 @@ module HTTP
|
|
184
184
|
form
|
185
185
|
when opts.json
|
186
186
|
body = MimeType[:json].encode opts.json
|
187
|
-
headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name}"
|
187
|
+
headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name.downcase}"
|
188
188
|
body
|
189
189
|
end
|
190
190
|
end
|
data/lib/http/connection.rb
CHANGED
@@ -46,6 +46,9 @@ module HTTP
|
|
46
46
|
reset_timer
|
47
47
|
rescue IOError, SocketError, SystemCallError => e
|
48
48
|
raise ConnectionError, "failed to connect: #{e}", e.backtrace
|
49
|
+
rescue TimeoutError
|
50
|
+
close
|
51
|
+
raise
|
49
52
|
end
|
50
53
|
|
51
54
|
# @see (HTTP::Response::Parser#status_code)
|
@@ -126,12 +129,16 @@ module HTTP
|
|
126
129
|
# Close the connection
|
127
130
|
# @return [void]
|
128
131
|
def close
|
129
|
-
@socket.close unless @socket
|
132
|
+
@socket.close unless @socket&.closed?
|
130
133
|
|
131
134
|
@pending_response = false
|
132
135
|
@pending_request = false
|
133
136
|
end
|
134
137
|
|
138
|
+
def finished_request?
|
139
|
+
!@pending_request && !@pending_response
|
140
|
+
end
|
141
|
+
|
135
142
|
# Whether we're keeping the conn alive
|
136
143
|
# @return [Boolean]
|
137
144
|
def keep_alive?
|
@@ -19,11 +19,12 @@ module HTTP
|
|
19
19
|
# and `finish` so the duration of the request can be calculated.
|
20
20
|
#
|
21
21
|
class Instrumentation < Feature
|
22
|
-
attr_reader :instrumenter, :name
|
22
|
+
attr_reader :instrumenter, :name, :error_name
|
23
23
|
|
24
24
|
def initialize(instrumenter: NullInstrumenter.new, namespace: "http")
|
25
25
|
@instrumenter = instrumenter
|
26
26
|
@name = "request.#{namespace}"
|
27
|
+
@error_name = "error.#{namespace}"
|
27
28
|
end
|
28
29
|
|
29
30
|
def wrap_request(request)
|
@@ -39,6 +40,10 @@ module HTTP
|
|
39
40
|
response
|
40
41
|
end
|
41
42
|
|
43
|
+
def on_error(request, error)
|
44
|
+
instrumenter.instrument(error_name, :request => request, :error => error)
|
45
|
+
end
|
46
|
+
|
42
47
|
HTTP::Options.register_feature(:instrumentation, self)
|
43
48
|
|
44
49
|
class NullInstrumenter
|
data/lib/http/request.rb
CHANGED
@@ -49,7 +49,10 @@ module HTTP
|
|
49
49
|
:search,
|
50
50
|
|
51
51
|
# RFC 4791: Calendaring Extensions to WebDAV -- CalDAV
|
52
|
-
:mkcalendar
|
52
|
+
:mkcalendar,
|
53
|
+
|
54
|
+
# Implemented by several caching servers, like Squid, Varnish or Fastly
|
55
|
+
:purge
|
53
56
|
].freeze
|
54
57
|
|
55
58
|
# Allowed schemes
|
@@ -172,7 +175,9 @@ module HTTP
|
|
172
175
|
uri.omit(:fragment)
|
173
176
|
else
|
174
177
|
uri.request_uri
|
175
|
-
end
|
178
|
+
end.to_s
|
179
|
+
|
180
|
+
raise RequestError, "Invalid request URI: #{request_uri.inspect}" if request_uri.match?(/\s/)
|
176
181
|
|
177
182
|
"#{verb.to_s.upcase} #{request_uri} HTTP/#{version}"
|
178
183
|
end
|
@@ -230,7 +235,11 @@ module HTTP
|
|
230
235
|
|
231
236
|
# @return [String] Default host (with port if needed) header value.
|
232
237
|
def default_host_header_value
|
233
|
-
PORTS[@scheme] == port ? host : "#{host}:#{port}"
|
238
|
+
value = PORTS[@scheme] == port ? host : "#{host}:#{port}"
|
239
|
+
|
240
|
+
raise RequestError, "Invalid host: #{value.inspect}" if value.match?(/\s/)
|
241
|
+
|
242
|
+
value
|
234
243
|
end
|
235
244
|
|
236
245
|
def prepare_body(body)
|
data/lib/http/timeout/null.rb
CHANGED
@@ -1,15 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "forwardable"
|
4
3
|
require "io/wait"
|
5
4
|
|
6
5
|
module HTTP
|
7
6
|
module Timeout
|
8
7
|
class Null
|
9
|
-
extend Forwardable
|
10
|
-
|
11
|
-
def_delegators :@socket, :close, :closed?
|
12
|
-
|
13
8
|
attr_reader :options, :socket
|
14
9
|
|
15
10
|
def initialize(options = {})
|
@@ -27,6 +22,14 @@ module HTTP
|
|
27
22
|
@socket.connect
|
28
23
|
end
|
29
24
|
|
25
|
+
def close
|
26
|
+
@socket&.close
|
27
|
+
end
|
28
|
+
|
29
|
+
def closed?
|
30
|
+
@socket&.closed?
|
31
|
+
end
|
32
|
+
|
30
33
|
# Configures the SSL connection and starts the connection
|
31
34
|
def start_tls(host, ssl_socket_class, ssl_context)
|
32
35
|
@socket = ssl_socket_class.new(socket, ssl_context)
|
data/lib/http/uri.rb
CHANGED
@@ -37,6 +37,9 @@ module HTTP
|
|
37
37
|
# @private
|
38
38
|
HTTPS_SCHEME = "https"
|
39
39
|
|
40
|
+
# @private
|
41
|
+
PERCENT_ENCODE = /[^\x21-\x7E]+/.freeze
|
42
|
+
|
40
43
|
# @private
|
41
44
|
NORMALIZER = lambda do |uri|
|
42
45
|
uri = HTTP::URI.parse uri
|
@@ -44,8 +47,8 @@ module HTTP
|
|
44
47
|
HTTP::URI.new(
|
45
48
|
:scheme => uri.normalized_scheme,
|
46
49
|
:authority => uri.normalized_authority,
|
47
|
-
:path => uri.
|
48
|
-
:query => uri.query,
|
50
|
+
:path => uri.path.empty? ? "/" : percent_encode(Addressable::URI.normalize_path(uri.path)),
|
51
|
+
:query => percent_encode(uri.query),
|
49
52
|
:fragment => uri.normalized_fragment
|
50
53
|
)
|
51
54
|
end
|
@@ -71,6 +74,19 @@ module HTTP
|
|
71
74
|
Addressable::URI.form_encode(form_values, sort)
|
72
75
|
end
|
73
76
|
|
77
|
+
# Percent-encode all characters matching a regular expression.
|
78
|
+
#
|
79
|
+
# @param [String] string raw string
|
80
|
+
#
|
81
|
+
# @return [String] encoded value
|
82
|
+
#
|
83
|
+
# @private
|
84
|
+
def self.percent_encode(string)
|
85
|
+
string&.gsub(PERCENT_ENCODE) do |substr|
|
86
|
+
substr.encode(Encoding::UTF_8).bytes.map { |c| format("%%%02X", c) }.join
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
74
90
|
# Creates an HTTP::URI instance from the given options
|
75
91
|
#
|
76
92
|
# @param [Hash, Addressable::URI] options_or_uri
|
data/lib/http/version.rb
CHANGED
@@ -261,6 +261,7 @@ RSpec.describe HTTP::Client do
|
|
261
261
|
|
262
262
|
expect(HTTP::Request).to receive(:new) do |opts|
|
263
263
|
expect(opts[:body]).to eq '{"foo":"bar"}'
|
264
|
+
expect(opts[:headers]["Content-Type"]).to eq "application/json; charset=utf-8"
|
264
265
|
end
|
265
266
|
|
266
267
|
client.get("http://example.com/", :json => {:foo => :bar})
|
@@ -8,11 +8,31 @@ RSpec.describe HTTP::Connection do
|
|
8
8
|
:headers => {}
|
9
9
|
)
|
10
10
|
end
|
11
|
-
let(:socket) { double(:connect => nil) }
|
11
|
+
let(:socket) { double(:connect => nil, :close => nil) }
|
12
12
|
let(:timeout_class) { double(:new => socket) }
|
13
13
|
let(:opts) { HTTP::Options.new(:timeout_class => timeout_class) }
|
14
14
|
let(:connection) { HTTP::Connection.new(req, opts) }
|
15
15
|
|
16
|
+
describe "#initialize times out" do
|
17
|
+
let(:req) do
|
18
|
+
HTTP::Request.new(
|
19
|
+
:verb => :get,
|
20
|
+
:uri => "https://example.com/",
|
21
|
+
:headers => {}
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
before do
|
26
|
+
expect(socket).to receive(:start_tls).and_raise(HTTP::TimeoutError)
|
27
|
+
expect(socket).to receive(:closed?) { false }
|
28
|
+
expect(socket).to receive(:close)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "closes the connection" do
|
32
|
+
expect { connection }.to raise_error(HTTP::TimeoutError)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
16
36
|
describe "#read_headers!" do
|
17
37
|
before do
|
18
38
|
connection.instance_variable_set(:@pending_response, true)
|
@@ -58,9 +78,11 @@ RSpec.describe HTTP::Connection do
|
|
58
78
|
connection.read_headers!
|
59
79
|
buffer = String.new
|
60
80
|
while (s = connection.readpartial(3))
|
81
|
+
expect(connection.finished_request?).to be false if s != ""
|
61
82
|
buffer << s
|
62
83
|
end
|
63
84
|
expect(buffer).to eq "1234567890"
|
85
|
+
expect(connection.finished_request?).to be true
|
64
86
|
end
|
65
87
|
end
|
66
88
|
end
|
@@ -59,4 +59,23 @@ RSpec.describe HTTP::Features::Instrumentation do
|
|
59
59
|
expect(instrumenter.output[:finish]).to eq(:response => response)
|
60
60
|
end
|
61
61
|
end
|
62
|
+
|
63
|
+
describe "logging errors" do
|
64
|
+
let(:request) do
|
65
|
+
HTTP::Request.new(
|
66
|
+
:verb => :post,
|
67
|
+
:uri => "https://example.com/",
|
68
|
+
:headers => {:accept => "application/json"},
|
69
|
+
:body => '{"hello": "world!"}'
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
let(:error) { HTTP::TimeoutError.new }
|
74
|
+
|
75
|
+
it "should log the error" do
|
76
|
+
feature.on_error(request, error)
|
77
|
+
|
78
|
+
expect(instrumenter.output[:finish]).to eq(:request => request, :error => error)
|
79
|
+
end
|
80
|
+
end
|
62
81
|
end
|
@@ -14,7 +14,11 @@ RSpec.describe HTTP::Options, "headers" do
|
|
14
14
|
end
|
15
15
|
|
16
16
|
it "accepts any object that respond to :to_hash" do
|
17
|
-
x =
|
17
|
+
x = if RUBY_VERSION >= "3.2.0"
|
18
|
+
Data.define(:to_hash).new(:to_hash => { "accept" => "json" })
|
19
|
+
else
|
20
|
+
Struct.new(:to_hash).new({ "accept" => "json" })
|
21
|
+
end
|
18
22
|
expect(opts.with_headers(x).headers["accept"]).to eq("json")
|
19
23
|
end
|
20
24
|
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::URI::NORMALIZER do
|
4
|
+
describe "scheme" do
|
5
|
+
it "lower-cases scheme" do
|
6
|
+
expect(HTTP::URI::NORMALIZER.call("HttP://example.com").scheme).to eq "http"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "hostname" do
|
11
|
+
it "lower-cases hostname" do
|
12
|
+
expect(HTTP::URI::NORMALIZER.call("http://EXAMPLE.com").host).to eq "example.com"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "decodes percent-encoded hostname" do
|
16
|
+
expect(HTTP::URI::NORMALIZER.call("http://ex%61mple.com").host).to eq "example.com"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "removes trailing period in hostname" do
|
20
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com.").host).to eq "example.com"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "IDN-encodes non-ASCII hostname" do
|
24
|
+
expect(HTTP::URI::NORMALIZER.call("http://exämple.com").host).to eq "xn--exmple-cua.com"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "path" do
|
29
|
+
it "ensures path is not empty" do
|
30
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com").path).to eq "/"
|
31
|
+
end
|
32
|
+
|
33
|
+
it "preserves double slashes in path" do
|
34
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com//a///b").path).to eq "//a///b"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "resolves single-dot segments in path" do
|
38
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/a/./b").path).to eq "/a/b"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "resolves double-dot segments in path" do
|
42
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/a/b/../c").path).to eq "/a/c"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "resolves leading double-dot segments in path" do
|
46
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/../a/b").path).to eq "/a/b"
|
47
|
+
end
|
48
|
+
|
49
|
+
it "percent-encodes control characters in path" do
|
50
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/\x00\x7F\n").path).to eq "/%00%7F%0A"
|
51
|
+
end
|
52
|
+
|
53
|
+
it "percent-encodes space in path" do
|
54
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/a b").path).to eq "/a%20b"
|
55
|
+
end
|
56
|
+
|
57
|
+
it "percent-encodes non-ASCII characters in path" do
|
58
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/キョ").path).to eq "/%E3%82%AD%E3%83%A7"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "does not percent-encode non-special characters in path" do
|
62
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/~.-_!$&()*,;=:@{}").path).to eq "/~.-_!$&()*,;=:@{}"
|
63
|
+
end
|
64
|
+
|
65
|
+
it "preserves escape sequences in path" do
|
66
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/%41").path).to eq "/%41"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "query" do
|
71
|
+
it "allows no query" do
|
72
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com").query).to be_nil
|
73
|
+
end
|
74
|
+
|
75
|
+
it "percent-encodes control characters in query" do
|
76
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/?\x00\x7F\n").query).to eq "%00%7F%0A"
|
77
|
+
end
|
78
|
+
|
79
|
+
it "percent-encodes space in query" do
|
80
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/?a b").query).to eq "a%20b"
|
81
|
+
end
|
82
|
+
|
83
|
+
it "percent-encodes non-ASCII characters in query" do
|
84
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com?キョ").query).to eq "%E3%82%AD%E3%83%A7"
|
85
|
+
end
|
86
|
+
|
87
|
+
it "does not percent-encode non-special characters in query" do
|
88
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/?~.-_!$&()*,;=:@{}?").query).to eq "~.-_!$&()*,;=:@{}?"
|
89
|
+
end
|
90
|
+
|
91
|
+
it "preserves escape sequences in query" do
|
92
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/?%41").query).to eq "%41"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/spec/lib/http_spec.rb
CHANGED
@@ -460,20 +460,37 @@ RSpec.describe HTTP do
|
|
460
460
|
|
461
461
|
context "with :normalize_uri" do
|
462
462
|
it "normalizes URI" do
|
463
|
-
response = HTTP.get "#{dummy.endpoint}/
|
463
|
+
response = HTTP.get "#{dummy.endpoint}/héllö-wörld"
|
464
464
|
expect(response.to_s).to eq("hello world")
|
465
465
|
end
|
466
466
|
|
467
467
|
it "uses the custom URI Normalizer method" do
|
468
468
|
client = HTTP.use(:normalize_uri => {:normalizer => :itself.to_proc})
|
469
|
-
response = client.get("#{dummy.endpoint}/
|
469
|
+
response = client.get("#{dummy.endpoint}/héllö-wörld")
|
470
470
|
expect(response.status).to eq(400)
|
471
471
|
end
|
472
472
|
|
473
|
+
it "raises if custom URI Normalizer returns invalid path" do
|
474
|
+
client = HTTP.use(:normalize_uri => {:normalizer => :itself.to_proc})
|
475
|
+
expect { client.get("#{dummy.endpoint}/hello\nworld") }.
|
476
|
+
to raise_error HTTP::RequestError, 'Invalid request URI: "/hello\nworld"'
|
477
|
+
end
|
478
|
+
|
479
|
+
it "raises if custom URI Normalizer returns invalid host" do
|
480
|
+
normalizer = lambda do |uri|
|
481
|
+
uri.port = nil
|
482
|
+
uri.instance_variable_set(:@host, "example\ncom")
|
483
|
+
uri
|
484
|
+
end
|
485
|
+
client = HTTP.use(:normalize_uri => {:normalizer => normalizer})
|
486
|
+
expect { client.get(dummy.endpoint) }.
|
487
|
+
to raise_error HTTP::RequestError, 'Invalid host: "example\ncom"'
|
488
|
+
end
|
489
|
+
|
473
490
|
it "uses the default URI normalizer" do
|
474
491
|
client = HTTP.use :normalize_uri
|
475
492
|
expect(HTTP::URI::NORMALIZER).to receive(:call).and_call_original
|
476
|
-
response = client.get("#{dummy.endpoint}/
|
493
|
+
response = client.get("#{dummy.endpoint}/héllö-wörld")
|
477
494
|
expect(response.to_s).to eq("hello world")
|
478
495
|
end
|
479
496
|
end
|
@@ -9,9 +9,9 @@ class DummyServer < WEBrick::HTTPServer
|
|
9
9
|
@sockets ||= []
|
10
10
|
end
|
11
11
|
|
12
|
-
def not_found(
|
12
|
+
def not_found(req, res)
|
13
13
|
res.status = 404
|
14
|
-
res.body = "
|
14
|
+
res.body = "#{req.unparsed_uri} not found"
|
15
15
|
end
|
16
16
|
|
17
17
|
def self.handlers
|
@@ -27,7 +27,7 @@ class DummyServer < WEBrick::HTTPServer
|
|
27
27
|
def do_#{method.upcase}(req, res)
|
28
28
|
handler = self.class.handlers["#{method}:\#{req.path}"]
|
29
29
|
return instance_exec(req, res, &handler) if handler
|
30
|
-
not_found
|
30
|
+
not_found(req, res)
|
31
31
|
end
|
32
32
|
RUBY
|
33
33
|
end
|
@@ -68,7 +68,7 @@ class DummyServer < WEBrick::HTTPServer
|
|
68
68
|
end
|
69
69
|
|
70
70
|
get "/params" do |req, res|
|
71
|
-
next not_found unless "foo=bar" == req.query_string
|
71
|
+
next not_found(req, res) unless "foo=bar" == req.query_string
|
72
72
|
|
73
73
|
res.status = 200
|
74
74
|
res.body = "Params!"
|
@@ -77,7 +77,7 @@ class DummyServer < WEBrick::HTTPServer
|
|
77
77
|
get "/multiple-params" do |req, res|
|
78
78
|
params = CGI.parse req.query_string
|
79
79
|
|
80
|
-
next not_found unless {"foo" => ["bar"], "baz" => ["quux"]} == params
|
80
|
+
next not_found(req, res) unless {"foo" => ["bar"], "baz" => ["quux"]} == params
|
81
81
|
|
82
82
|
res.status = 200
|
83
83
|
res.body = "More Params!"
|
@@ -149,7 +149,7 @@ class DummyServer < WEBrick::HTTPServer
|
|
149
149
|
res.body = req.body
|
150
150
|
end
|
151
151
|
|
152
|
-
get "/
|
152
|
+
get "/héllö-wörld".b do |_req, res|
|
153
153
|
res.status = 200
|
154
154
|
res.body = "hello world"
|
155
155
|
end
|
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.2.0
|
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: 2024-02-05 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: addressable
|
@@ -27,6 +27,20 @@ dependencies:
|
|
27
27
|
- - "~>"
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '2.8'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: base64
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
requirements:
|
34
|
+
- - "~>"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0.1'
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0.1'
|
30
44
|
- !ruby/object:Gem::Dependency
|
31
45
|
name: http-cookie
|
32
46
|
requirement: !ruby/object:Gem::Requirement
|
@@ -61,14 +75,14 @@ dependencies:
|
|
61
75
|
requirements:
|
62
76
|
- - "~>"
|
63
77
|
- !ruby/object:Gem::Version
|
64
|
-
version: 0.
|
78
|
+
version: 0.5.0
|
65
79
|
type: :runtime
|
66
80
|
prerelease: false
|
67
81
|
version_requirements: !ruby/object:Gem::Requirement
|
68
82
|
requirements:
|
69
83
|
- - "~>"
|
70
84
|
- !ruby/object:Gem::Version
|
71
|
-
version: 0.
|
85
|
+
version: 0.5.0
|
72
86
|
- !ruby/object:Gem::Dependency
|
73
87
|
name: bundler
|
74
88
|
requirement: !ruby/object:Gem::Requirement
|
@@ -96,10 +110,12 @@ files:
|
|
96
110
|
- ".rspec"
|
97
111
|
- ".rubocop.yml"
|
98
112
|
- ".rubocop/layout.yml"
|
113
|
+
- ".rubocop/metrics.yml"
|
99
114
|
- ".rubocop/style.yml"
|
100
115
|
- ".rubocop_todo.yml"
|
101
116
|
- ".yardopts"
|
102
|
-
-
|
117
|
+
- CHANGELOG.md
|
118
|
+
- CHANGES_OLD.md
|
103
119
|
- CONTRIBUTING.md
|
104
120
|
- Gemfile
|
105
121
|
- Guardfile
|
@@ -169,6 +185,7 @@ files:
|
|
169
185
|
- spec/lib/http/response/parser_spec.rb
|
170
186
|
- spec/lib/http/response/status_spec.rb
|
171
187
|
- spec/lib/http/response_spec.rb
|
188
|
+
- spec/lib/http/uri/normalizer_spec.rb
|
172
189
|
- spec/lib/http/uri_spec.rb
|
173
190
|
- spec/lib/http_spec.rb
|
174
191
|
- spec/regression_specs.rb
|
@@ -192,7 +209,7 @@ metadata:
|
|
192
209
|
source_code_uri: https://github.com/httprb/http
|
193
210
|
wiki_uri: https://github.com/httprb/http/wiki
|
194
211
|
bug_tracker_uri: https://github.com/httprb/http/issues
|
195
|
-
changelog_uri: https://github.com/httprb/http/blob/v5.
|
212
|
+
changelog_uri: https://github.com/httprb/http/blob/v5.2.0/CHANGELOG.md
|
196
213
|
rubygems_mfa_required: 'true'
|
197
214
|
post_install_message:
|
198
215
|
rdoc_options: []
|
@@ -209,7 +226,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
209
226
|
- !ruby/object:Gem::Version
|
210
227
|
version: '0'
|
211
228
|
requirements: []
|
212
|
-
rubygems_version: 3.
|
229
|
+
rubygems_version: 3.5.4
|
213
230
|
signing_key:
|
214
231
|
specification_version: 4
|
215
232
|
summary: HTTP should be easy
|
@@ -240,6 +257,7 @@ test_files:
|
|
240
257
|
- spec/lib/http/response/parser_spec.rb
|
241
258
|
- spec/lib/http/response/status_spec.rb
|
242
259
|
- spec/lib/http/response_spec.rb
|
260
|
+
- spec/lib/http/uri/normalizer_spec.rb
|
243
261
|
- spec/lib/http/uri_spec.rb
|
244
262
|
- spec/lib/http_spec.rb
|
245
263
|
- spec/regression_specs.rb
|