http 5.1.1 → 5.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|