http 3.1.0 → 5.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +67 -0
  3. data/.gitignore +6 -9
  4. data/.rspec +0 -4
  5. data/.rubocop/layout.yml +8 -0
  6. data/.rubocop/metrics.yml +4 -0
  7. data/.rubocop/rspec.yml +9 -0
  8. data/.rubocop/style.yml +32 -0
  9. data/.rubocop.yml +9 -108
  10. data/.rubocop_todo.yml +219 -0
  11. data/.yardopts +1 -1
  12. data/CHANGELOG.md +67 -0
  13. data/{CHANGES.md → CHANGES_OLD.md} +358 -0
  14. data/Gemfile +19 -10
  15. data/LICENSE.txt +1 -1
  16. data/README.md +53 -85
  17. data/Rakefile +3 -11
  18. data/SECURITY.md +17 -0
  19. data/http.gemspec +15 -6
  20. data/lib/http/base64.rb +12 -0
  21. data/lib/http/chainable.rb +71 -41
  22. data/lib/http/client.rb +73 -52
  23. data/lib/http/connection.rb +28 -18
  24. data/lib/http/content_type.rb +12 -7
  25. data/lib/http/errors.rb +19 -0
  26. data/lib/http/feature.rb +18 -1
  27. data/lib/http/features/auto_deflate.rb +27 -6
  28. data/lib/http/features/auto_inflate.rb +32 -6
  29. data/lib/http/features/instrumentation.rb +69 -0
  30. data/lib/http/features/logging.rb +53 -0
  31. data/lib/http/features/normalize_uri.rb +17 -0
  32. data/lib/http/features/raise_error.rb +22 -0
  33. data/lib/http/headers/known.rb +3 -0
  34. data/lib/http/headers/normalizer.rb +69 -0
  35. data/lib/http/headers.rb +72 -49
  36. data/lib/http/mime_type/adapter.rb +3 -1
  37. data/lib/http/mime_type/json.rb +1 -0
  38. data/lib/http/options.rb +31 -28
  39. data/lib/http/redirector.rb +56 -4
  40. data/lib/http/request/body.rb +31 -0
  41. data/lib/http/request/writer.rb +29 -9
  42. data/lib/http/request.rb +76 -41
  43. data/lib/http/response/body.rb +6 -4
  44. data/lib/http/response/inflater.rb +1 -1
  45. data/lib/http/response/parser.rb +78 -26
  46. data/lib/http/response/status.rb +4 -3
  47. data/lib/http/response.rb +45 -27
  48. data/lib/http/retriable/client.rb +37 -0
  49. data/lib/http/retriable/delay_calculator.rb +64 -0
  50. data/lib/http/retriable/errors.rb +14 -0
  51. data/lib/http/retriable/performer.rb +153 -0
  52. data/lib/http/timeout/global.rb +29 -47
  53. data/lib/http/timeout/null.rb +12 -8
  54. data/lib/http/timeout/per_operation.rb +32 -57
  55. data/lib/http/uri.rb +75 -1
  56. data/lib/http/version.rb +1 -1
  57. data/lib/http.rb +2 -2
  58. data/spec/lib/http/client_spec.rb +189 -36
  59. data/spec/lib/http/connection_spec.rb +31 -6
  60. data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
  61. data/spec/lib/http/features/instrumentation_spec.rb +81 -0
  62. data/spec/lib/http/features/logging_spec.rb +65 -0
  63. data/spec/lib/http/features/raise_error_spec.rb +62 -0
  64. data/spec/lib/http/headers/normalizer_spec.rb +52 -0
  65. data/spec/lib/http/headers_spec.rb +53 -18
  66. data/spec/lib/http/options/headers_spec.rb +6 -2
  67. data/spec/lib/http/options/merge_spec.rb +16 -16
  68. data/spec/lib/http/redirector_spec.rb +147 -3
  69. data/spec/lib/http/request/body_spec.rb +71 -4
  70. data/spec/lib/http/request/writer_spec.rb +45 -2
  71. data/spec/lib/http/request_spec.rb +11 -5
  72. data/spec/lib/http/response/body_spec.rb +5 -5
  73. data/spec/lib/http/response/parser_spec.rb +74 -0
  74. data/spec/lib/http/response/status_spec.rb +3 -3
  75. data/spec/lib/http/response_spec.rb +83 -7
  76. data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
  77. data/spec/lib/http/retriable/performer_spec.rb +302 -0
  78. data/spec/lib/http/uri/normalizer_spec.rb +95 -0
  79. data/spec/lib/http/uri_spec.rb +39 -0
  80. data/spec/lib/http_spec.rb +121 -68
  81. data/spec/regression_specs.rb +7 -0
  82. data/spec/spec_helper.rb +22 -21
  83. data/spec/support/black_hole.rb +1 -1
  84. data/spec/support/dummy_server/servlet.rb +42 -11
  85. data/spec/support/dummy_server.rb +9 -8
  86. data/spec/support/fuubar.rb +21 -0
  87. data/spec/support/http_handling_shared.rb +62 -66
  88. data/spec/support/simplecov.rb +19 -0
  89. data/spec/support/ssl_helper.rb +4 -4
  90. metadata +66 -27
  91. data/.coveralls.yml +0 -1
  92. data/.ruby-version +0 -1
  93. data/.travis.yml +0 -36
@@ -3,27 +3,25 @@
3
3
  require "timeout"
4
4
  require "io/wait"
5
5
 
6
- require "http/timeout/per_operation"
6
+ require "http/timeout/null"
7
7
 
8
8
  module HTTP
9
9
  module Timeout
10
- class Global < PerOperation
11
- attr_reader :time_left, :total_timeout
12
-
10
+ class Global < Null
13
11
  def initialize(*args)
14
12
  super
15
- reset_counter
13
+
14
+ @timeout = @time_left = options.fetch(:global_timeout)
16
15
  end
17
16
 
18
17
  # To future me: Don't remove this again, past you was smarter.
19
18
  def reset_counter
20
- @time_left = connect_timeout + read_timeout + write_timeout
21
- @total_timeout = time_left
19
+ @time_left = @timeout
22
20
  end
23
21
 
24
22
  def connect(socket_class, host, port, nodelay = false)
25
23
  reset_timer
26
- ::Timeout.timeout(time_left, TimeoutError) do
24
+ ::Timeout.timeout(@time_left, ConnectTimeoutError) do
27
25
  @socket = socket_class.open(host, port)
28
26
  @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
29
27
  end
@@ -37,19 +35,17 @@ module HTTP
37
35
  begin
38
36
  @socket.connect_nonblock
39
37
  rescue IO::WaitReadable
40
- IO.select([@socket], nil, nil, time_left)
41
- log_time
38
+ wait_readable_or_timeout
42
39
  retry
43
40
  rescue IO::WaitWritable
44
- IO.select(nil, [@socket], nil, time_left)
45
- log_time
41
+ wait_writable_or_timeout
46
42
  retry
47
43
  end
48
44
  end
49
45
 
50
46
  # Read from the socket
51
- def readpartial(size)
52
- perform_io { read_nonblock(size) }
47
+ def readpartial(size, buffer = nil)
48
+ perform_io { read_nonblock(size, buffer) }
53
49
  end
54
50
 
55
51
  # Write to the socket
@@ -61,22 +57,12 @@ module HTTP
61
57
 
62
58
  private
63
59
 
64
- if RUBY_VERSION < "2.1.0"
65
- def read_nonblock(size)
66
- @socket.read_nonblock(size)
67
- end
68
-
69
- def write_nonblock(data)
70
- @socket.write_nonblock(data)
71
- end
72
- else
73
- def read_nonblock(size)
74
- @socket.read_nonblock(size, :exception => false)
75
- end
60
+ def read_nonblock(size, buffer = nil)
61
+ @socket.read_nonblock(size, buffer, :exception => false)
62
+ end
76
63
 
77
- def write_nonblock(data)
78
- @socket.write_nonblock(data, :exception => false)
79
- end
64
+ def write_nonblock(data)
65
+ @socket.write_nonblock(data, :exception => false)
80
66
  end
81
67
 
82
68
  # Perform the given I/O operation with the given argument
@@ -84,20 +70,18 @@ module HTTP
84
70
  reset_timer
85
71
 
86
72
  loop do
87
- begin
88
- result = yield
89
-
90
- case result
91
- when :wait_readable then wait_readable_or_timeout
92
- when :wait_writable then wait_writable_or_timeout
93
- when NilClass then return :eof
94
- else return result
95
- end
96
- rescue IO::WaitReadable
97
- wait_readable_or_timeout
98
- rescue IO::WaitWritable
99
- wait_writable_or_timeout
73
+ result = yield
74
+
75
+ case result
76
+ when :wait_readable then wait_readable_or_timeout
77
+ when :wait_writable then wait_writable_or_timeout
78
+ when NilClass then return :eof
79
+ else return result
100
80
  end
81
+ rescue IO::WaitReadable
82
+ wait_readable_or_timeout
83
+ rescue IO::WaitWritable
84
+ wait_writable_or_timeout
101
85
  end
102
86
  rescue EOFError
103
87
  :eof
@@ -105,13 +89,13 @@ module HTTP
105
89
 
106
90
  # Wait for a socket to become readable
107
91
  def wait_readable_or_timeout
108
- @socket.to_io.wait_readable(time_left)
92
+ @socket.to_io.wait_readable(@time_left)
109
93
  log_time
110
94
  end
111
95
 
112
96
  # Wait for a socket to become writable
113
97
  def wait_writable_or_timeout
114
- @socket.to_io.wait_writable(time_left)
98
+ @socket.to_io.wait_writable(@time_left)
115
99
  log_time
116
100
  end
117
101
 
@@ -123,9 +107,7 @@ module HTTP
123
107
 
124
108
  def log_time
125
109
  @time_left -= (Time.now - @started)
126
- if time_left <= 0
127
- raise TimeoutError, "Timed out after using the allocated #{total_timeout} seconds"
128
- end
110
+ raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds" if @time_left <= 0
129
111
 
130
112
  reset_timer
131
113
  end
@@ -1,18 +1,13 @@
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
- def initialize(options = {}) # rubocop:disable Style/OptionHash
10
+ def initialize(options = {})
16
11
  @options = options
17
12
  end
18
13
 
@@ -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)
@@ -36,13 +39,14 @@ module HTTP
36
39
  connect_ssl
37
40
 
38
41
  return unless ssl_context.verify_mode == OpenSSL::SSL::VERIFY_PEER
42
+ return if ssl_context.respond_to?(:verify_hostname) && !ssl_context.verify_hostname
39
43
 
40
44
  @socket.post_connection_check(host)
41
45
  end
42
46
 
43
47
  # Read from the socket
44
- def readpartial(size)
45
- @socket.readpartial(size)
48
+ def readpartial(size, buffer = nil)
49
+ @socket.readpartial(size, buffer)
46
50
  rescue EOFError
47
51
  :eof
48
52
  end
@@ -11,8 +11,6 @@ module HTTP
11
11
  WRITE_TIMEOUT = 0.25
12
12
  READ_TIMEOUT = 0.25
13
13
 
14
- attr_reader :read_timeout, :write_timeout, :connect_timeout
15
-
16
14
  def initialize(*args)
17
15
  super
18
16
 
@@ -22,7 +20,7 @@ module HTTP
22
20
  end
23
21
 
24
22
  def connect(socket_class, host, port, nodelay = false)
25
- ::Timeout.timeout(connect_timeout, TimeoutError) do
23
+ ::Timeout.timeout(@connect_timeout, ConnectTimeoutError) do
26
24
  @socket = socket_class.open(host, port)
27
25
  @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
28
26
  end
@@ -36,65 +34,42 @@ module HTTP
36
34
  end
37
35
  end
38
36
 
39
- # NIO with exceptions
40
- if RUBY_VERSION < "2.1.0"
41
- # Read data from the socket
42
- def readpartial(size)
43
- rescue_readable do
44
- @socket.read_nonblock(size)
45
- end
46
- rescue EOFError
47
- :eof
48
- end
49
-
50
- # Write data to the socket
51
- def write(data)
52
- rescue_writable do
53
- @socket.write_nonblock(data)
54
- end
55
- rescue EOFError
56
- :eof
57
- end
58
-
59
- # NIO without exceptions
60
- else
61
- # Read data from the socket
62
- def readpartial(size)
63
- timeout = false
64
- loop do
65
- result = @socket.read_nonblock(size, :exception => false)
66
-
67
- return :eof if result.nil?
68
- return result if result != :wait_readable
69
-
70
- raise TimeoutError, "Read timed out after #{read_timeout} seconds" if timeout
71
- # marking the socket for timeout. Why is this not being raised immediately?
72
- # it seems there is some race-condition on the network level between calling
73
- # #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
74
- # for reads, and when waiting for x seconds, it returns nil suddenly without completing
75
- # the x seconds. In a normal case this would be a timeout on wait/read, but it can
76
- # also mean that the socket has been closed by the server. Therefore we "mark" the
77
- # socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
78
- # timeout. Else, the first timeout was a proper timeout.
79
- # This hack has to be done because io/wait#wait_readable doesn't provide a value for when
80
- # the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
81
- timeout = true unless @socket.to_io.wait_readable(read_timeout)
82
- end
37
+ # Read data from the socket
38
+ def readpartial(size, buffer = nil)
39
+ timeout = false
40
+ loop do
41
+ result = @socket.read_nonblock(size, buffer, :exception => false)
42
+
43
+ return :eof if result.nil?
44
+ return result if result != :wait_readable
45
+
46
+ raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
47
+
48
+ # marking the socket for timeout. Why is this not being raised immediately?
49
+ # it seems there is some race-condition on the network level between calling
50
+ # #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
51
+ # for reads, and when waiting for x seconds, it returns nil suddenly without completing
52
+ # the x seconds. In a normal case this would be a timeout on wait/read, but it can
53
+ # also mean that the socket has been closed by the server. Therefore we "mark" the
54
+ # socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
55
+ # timeout. Else, the first timeout was a proper timeout.
56
+ # This hack has to be done because io/wait#wait_readable doesn't provide a value for when
57
+ # the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
58
+ timeout = true unless @socket.to_io.wait_readable(@read_timeout)
83
59
  end
60
+ end
84
61
 
85
- # Write data to the socket
86
- def write(data)
87
- timeout = false
88
- loop do
89
- result = @socket.write_nonblock(data, :exception => false)
90
- return result unless result == :wait_writable
62
+ # Write data to the socket
63
+ def write(data)
64
+ timeout = false
65
+ loop do
66
+ result = @socket.write_nonblock(data, :exception => false)
67
+ return result unless result == :wait_writable
91
68
 
92
- raise TimeoutError, "Write timed out after #{write_timeout} seconds" if timeout
69
+ raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
93
70
 
94
- timeout = true unless @socket.to_io.wait_writable(write_timeout)
95
- end
71
+ timeout = true unless @socket.to_io.wait_writable(@write_timeout)
96
72
  end
97
-
98
73
  end
99
74
  end
100
75
  end
data/lib/http/uri.rb CHANGED
@@ -9,7 +9,6 @@ module HTTP
9
9
  def_delegators :@uri, :scheme, :normalized_scheme, :scheme=
10
10
  def_delegators :@uri, :user, :normalized_user, :user=
11
11
  def_delegators :@uri, :password, :normalized_password, :password=
12
- def_delegators :@uri, :host, :normalized_host, :host=
13
12
  def_delegators :@uri, :authority, :normalized_authority, :authority=
14
13
  def_delegators :@uri, :origin, :origin=
15
14
  def_delegators :@uri, :normalized_port, :port=
@@ -20,12 +19,40 @@ module HTTP
20
19
  def_delegators :@uri, :fragment, :normalized_fragment, :fragment=
21
20
  def_delegators :@uri, :omit, :join, :normalize
22
21
 
22
+ # Host, either a domain name or IP address. If the host is an IPv6 address, it will be returned
23
+ # without brackets surrounding it.
24
+ #
25
+ # @return [String] The host of the URI
26
+ attr_reader :host
27
+
28
+ # Normalized host, either a domain name or IP address. If the host is an IPv6 address, it will
29
+ # be returned without brackets surrounding it.
30
+ #
31
+ # @return [String] The normalized host of the URI
32
+ attr_reader :normalized_host
33
+
23
34
  # @private
24
35
  HTTP_SCHEME = "http"
25
36
 
26
37
  # @private
27
38
  HTTPS_SCHEME = "https"
28
39
 
40
+ # @private
41
+ PERCENT_ENCODE = /[^\x21-\x7E]+/.freeze
42
+
43
+ # @private
44
+ NORMALIZER = lambda do |uri|
45
+ uri = HTTP::URI.parse uri
46
+
47
+ HTTP::URI.new(
48
+ :scheme => uri.normalized_scheme,
49
+ :authority => uri.normalized_authority,
50
+ :path => uri.path.empty? ? "/" : percent_encode(Addressable::URI.normalize_path(uri.path)),
51
+ :query => percent_encode(uri.query),
52
+ :fragment => uri.normalized_fragment
53
+ )
54
+ end
55
+
29
56
  # Parse the given URI string, returning an HTTP::URI object
30
57
  #
31
58
  # @param [HTTP::URI, String, #to_str] uri to parse
@@ -47,6 +74,19 @@ module HTTP
47
74
  Addressable::URI.form_encode(form_values, sort)
48
75
  end
49
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
+
50
90
  # Creates an HTTP::URI instance from the given options
51
91
  #
52
92
  # @param [Hash, Addressable::URI] options_or_uri
@@ -70,6 +110,9 @@ module HTTP
70
110
  else
71
111
  raise TypeError, "expected Hash for options, got #{options_or_uri.class}"
72
112
  end
113
+
114
+ @host = process_ipv6_brackets(@uri.host)
115
+ @normalized_host = process_ipv6_brackets(@uri.normalized_host)
73
116
  end
74
117
 
75
118
  # Are these URI objects equal? Normalizes both URIs prior to comparison
@@ -97,6 +140,17 @@ module HTTP
97
140
  @hash ||= to_s.hash * -1
98
141
  end
99
142
 
143
+ # Sets the host component for the URI.
144
+ #
145
+ # @param [String, #to_str] new_host The new host component.
146
+ # @return [void]
147
+ def host=(new_host)
148
+ @uri.host = process_ipv6_brackets(new_host, :brackets => true)
149
+
150
+ @host = process_ipv6_brackets(@uri.host)
151
+ @normalized_host = process_ipv6_brackets(@uri.normalized_host)
152
+ end
153
+
100
154
  # Port number, either as specified or the default if unspecified
101
155
  #
102
156
  # @return [Integer] port number
@@ -133,5 +187,25 @@ module HTTP
133
187
  def inspect
134
188
  format("#<%s:0x%014x URI:%s>", self.class.name, object_id << 1, to_s)
135
189
  end
190
+
191
+ private
192
+
193
+ # Process a URI host, adding or removing surrounding brackets if the host is an IPv6 address.
194
+ #
195
+ # @param [Boolean] brackets When true, brackets will be added to IPv6 addresses if missing. When
196
+ # false, they will be removed if present.
197
+ #
198
+ # @return [String] Host with IPv6 address brackets added or removed
199
+ def process_ipv6_brackets(raw_host, brackets: false)
200
+ ip = IPAddr.new(raw_host)
201
+
202
+ if ip.ipv6?
203
+ brackets ? "[#{ip}]" : ip.to_s
204
+ else
205
+ raw_host
206
+ end
207
+ rescue IPAddr::Error
208
+ raw_host
209
+ end
136
210
  end
137
211
  end
data/lib/http/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- VERSION = "3.1.0"
4
+ VERSION = "5.3.1"
5
5
  end
data/lib/http.rb CHANGED
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "http/parser"
4
-
5
3
  require "http/errors"
6
4
  require "http/timeout/null"
7
5
  require "http/timeout/per_operation"
8
6
  require "http/timeout/global"
9
7
  require "http/chainable"
10
8
  require "http/client"
9
+ require "http/retriable/client"
11
10
  require "http/connection"
12
11
  require "http/options"
12
+ require "http/feature"
13
13
  require "http/request"
14
14
  require "http/request/writer"
15
15
  require "http/response"