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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef55bbff996952784d0917931b8eaa6f12a1adfc9cc480daf098cbc8cd8f6107
4
- data.tar.gz: 222f8e8723969f89994fe2a0692c41786e450c200c45b84f5c8418f736254a64
3
+ metadata.gz: 8593353cd2283b2ac49c557ff71d5f1a668066dbac59b3d2a6f59429b9b2f5e8
4
+ data.tar.gz: e9316b3c4dd519825eeaa255a4f86932ce4a9e7f374f9be8273ca4051f5b9c3d
5
5
  SHA512:
6
- metadata.gz: 49b0c9e508fb02fca9d9e8a26d707202c5ac67bbdf73fce15b616b321545a3a32be96eb9e23f4d389687ad1720372eaba4412e18d800c5d29fab5bb0f4bc0d93
7
- data.tar.gz: 1b2e22b2b33abe8059e78535556a15c53959b321fb225cd84153d5a8ea608ba4d03ead9cf018720f63b1e7c27c477f8c3f1b151cae60f2d50b6811e7dd9f5bcc
6
+ metadata.gz: 0ae34b411a233ca8601aebe2295f2a1d8e0bec7e742a95fdd6e075b4f7dd3c68d42448cfbac1146b645ee59424e91af8eb3878c43e7a7fb17c3569681cf4ccf3
7
+ data.tar.gz: a76bc0a41338e67677370014d803ea79e3a34078172cc81491f31b085613394c7290b2402e199a2e1de77c7fecf7e05fde8fc2d9924016105ce429751f202e85
@@ -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@v3
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@v3
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@v3
57
+ - uses: actions/checkout@v4
76
58
 
77
59
  - uses: ruby/setup-ruby@v1
78
60
  with:
@@ -0,0 +1,4 @@
1
+ Metrics/BlockLength:
2
+ Exclude:
3
+ - 'spec/**/*.rb'
4
+ - '*.gemspec'
data/.rubocop.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  inherit_from:
2
2
  - .rubocop_todo.yml
3
3
  - .rubocop/layout.yml
4
+ - .rubocop/metrics.yml
4
5
  - .rubocop/style.yml
5
6
 
6
7
  AllCops:
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
@@ -54,7 +54,7 @@
54
54
  Use features on redirected requests.
55
55
  ([@nomis])
56
56
 
57
- * [#678](https://github.com/schwern)
57
+ * [#678](https://github.com/httprb/http/pull/678)
58
58
  Restore `HTTP::Response` `:uri` option for backwards compatibility.
59
59
  ([@schwern])
60
60
 
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
- - JRuby 9.3
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]: http://docs.python-requests.org/en/latest/
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
- Please report security issues to `bascule@gmail.com`
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.4.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}/CHANGES.md",
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
@@ -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.closed?
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)
@@ -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.normalized_path,
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- VERSION = "5.1.1"
4
+ VERSION = "5.2.0"
5
5
  end
@@ -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 = Struct.new(:to_hash).new("accept" => "json")
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
@@ -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}/hello world"
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}/hello world")
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}/hello world")
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(_req, res)
12
+ def not_found(req, res)
13
13
  res.status = 404
14
- res.body = "Not Found"
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 "/hello world" do |_req, res|
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.1.1
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: 2022-12-17 00:00:00.000000000 Z
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.4.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.4.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
- - CHANGES.md
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.1.1/CHANGES.md
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.0.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