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 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