http 6.0.0-java
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 +7 -0
- data/CHANGELOG.md +267 -0
- data/CONTRIBUTING.md +26 -0
- data/LICENSE.txt +20 -0
- data/README.md +263 -0
- data/SECURITY.md +17 -0
- data/UPGRADING.md +491 -0
- data/http.gemspec +48 -0
- data/lib/http/base64.rb +22 -0
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +377 -0
- data/lib/http/client.rb +230 -0
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +265 -0
- data/lib/http/content_type.rb +89 -0
- data/lib/http/errors.rb +67 -0
- data/lib/http/feature.rb +86 -0
- data/lib/http/features/auto_deflate.rb +230 -0
- data/lib/http/features/auto_inflate.rb +64 -0
- data/lib/http/features/caching/entry.rb +178 -0
- data/lib/http/features/caching/in_memory_store.rb +63 -0
- data/lib/http/features/caching.rb +216 -0
- data/lib/http/features/digest_auth.rb +234 -0
- data/lib/http/features/instrumentation.rb +149 -0
- data/lib/http/features/logging.rb +231 -0
- data/lib/http/features/normalize_uri.rb +34 -0
- data/lib/http/features/raise_error.rb +37 -0
- data/lib/http/form_data/composite_io.rb +106 -0
- data/lib/http/form_data/file.rb +95 -0
- data/lib/http/form_data/multipart/param.rb +62 -0
- data/lib/http/form_data/multipart.rb +106 -0
- data/lib/http/form_data/part.rb +52 -0
- data/lib/http/form_data/readable.rb +58 -0
- data/lib/http/form_data/urlencoded.rb +175 -0
- data/lib/http/form_data/version.rb +8 -0
- data/lib/http/form_data.rb +102 -0
- data/lib/http/headers/known.rb +90 -0
- data/lib/http/headers/normalizer.rb +50 -0
- data/lib/http/headers.rb +343 -0
- data/lib/http/mime_type/adapter.rb +43 -0
- data/lib/http/mime_type/json.rb +41 -0
- data/lib/http/mime_type.rb +96 -0
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +241 -0
- data/lib/http/redirector.rb +157 -0
- data/lib/http/request/body.rb +181 -0
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +186 -0
- data/lib/http/request.rb +375 -0
- data/lib/http/response/body.rb +172 -0
- data/lib/http/response/inflater.rb +60 -0
- data/lib/http/response/parser.rb +223 -0
- data/lib/http/response/status/reasons.rb +79 -0
- data/lib/http/response/status.rb +263 -0
- data/lib/http/response.rb +350 -0
- data/lib/http/retriable/delay_calculator.rb +91 -0
- data/lib/http/retriable/errors.rb +35 -0
- data/lib/http/retriable/performer.rb +197 -0
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +229 -0
- data/lib/http/timeout/null.rb +225 -0
- data/lib/http/timeout/per_operation.rb +197 -0
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +376 -0
- data/lib/http/version.rb +6 -0
- data/lib/http.rb +36 -0
- data/sig/deps.rbs +122 -0
- data/sig/http.rbs +1619 -0
- data/test/http/base64_test.rb +28 -0
- data/test/http/client_test.rb +739 -0
- data/test/http/connection_test.rb +1533 -0
- data/test/http/content_type_test.rb +190 -0
- data/test/http/errors_test.rb +28 -0
- data/test/http/feature_test.rb +49 -0
- data/test/http/features/auto_deflate_test.rb +317 -0
- data/test/http/features/auto_inflate_test.rb +213 -0
- data/test/http/features/caching_test.rb +942 -0
- data/test/http/features/digest_auth_test.rb +996 -0
- data/test/http/features/instrumentation_test.rb +246 -0
- data/test/http/features/logging_test.rb +654 -0
- data/test/http/features/normalize_uri_test.rb +41 -0
- data/test/http/features/raise_error_test.rb +77 -0
- data/test/http/form_data/composite_io_test.rb +215 -0
- data/test/http/form_data/file_test.rb +255 -0
- data/test/http/form_data/fixtures/the-http-gem.info +1 -0
- data/test/http/form_data/multipart_test.rb +303 -0
- data/test/http/form_data/part_test.rb +90 -0
- data/test/http/form_data/urlencoded_test.rb +164 -0
- data/test/http/form_data_test.rb +232 -0
- data/test/http/headers/normalizer_test.rb +93 -0
- data/test/http/headers_test.rb +888 -0
- data/test/http/mime_type/json_test.rb +39 -0
- data/test/http/mime_type_test.rb +150 -0
- data/test/http/options/base_uri_test.rb +148 -0
- data/test/http/options/body_test.rb +21 -0
- data/test/http/options/features_test.rb +38 -0
- data/test/http/options/form_test.rb +21 -0
- data/test/http/options/headers_test.rb +32 -0
- data/test/http/options/json_test.rb +21 -0
- data/test/http/options/merge_test.rb +78 -0
- data/test/http/options/new_test.rb +37 -0
- data/test/http/options/proxy_test.rb +32 -0
- data/test/http/options_test.rb +575 -0
- data/test/http/redirector_test.rb +639 -0
- data/test/http/request/body_test.rb +318 -0
- data/test/http/request/builder_test.rb +623 -0
- data/test/http/request/writer_test.rb +391 -0
- data/test/http/request_test.rb +1733 -0
- data/test/http/response/body_test.rb +292 -0
- data/test/http/response/parser_test.rb +105 -0
- data/test/http/response/status_test.rb +322 -0
- data/test/http/response_test.rb +502 -0
- data/test/http/retriable/delay_calculator_test.rb +194 -0
- data/test/http/retriable/errors_test.rb +71 -0
- data/test/http/retriable/performer_test.rb +551 -0
- data/test/http/session_test.rb +424 -0
- data/test/http/timeout/global_test.rb +239 -0
- data/test/http/timeout/null_test.rb +218 -0
- data/test/http/timeout/per_operation_test.rb +220 -0
- data/test/http/uri/normalizer_test.rb +89 -0
- data/test/http/uri_test.rb +1140 -0
- data/test/http/version_test.rb +15 -0
- data/test/http_test.rb +818 -0
- data/test/regression_tests.rb +27 -0
- data/test/support/capture_warning.rb +10 -0
- data/test/support/dummy_server/encoding_routes.rb +47 -0
- data/test/support/dummy_server/routes.rb +201 -0
- data/test/support/dummy_server/servlet.rb +81 -0
- data/test/support/dummy_server.rb +200 -0
- data/test/support/fakeio.rb +21 -0
- data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
- data/test/support/http_handling_shared/timeout_tests.rb +134 -0
- data/test/support/http_handling_shared.rb +11 -0
- data/test/support/proxy_server.rb +207 -0
- data/test/support/servers/runner.rb +67 -0
- data/test/support/simplecov.rb +28 -0
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +218 -0
data/lib/http/client.rb
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
require "http/form_data"
|
|
6
|
+
require "http/retriable/performer"
|
|
7
|
+
require "http/options"
|
|
8
|
+
require "http/feature"
|
|
9
|
+
require "http/headers"
|
|
10
|
+
require "http/connection"
|
|
11
|
+
require "http/redirector"
|
|
12
|
+
require "http/request/builder"
|
|
13
|
+
require "http/uri"
|
|
14
|
+
|
|
15
|
+
module HTTP
|
|
16
|
+
# Clients make requests and receive responses
|
|
17
|
+
class Client
|
|
18
|
+
extend Forwardable
|
|
19
|
+
include Chainable
|
|
20
|
+
|
|
21
|
+
# Initialize a new HTTP Client
|
|
22
|
+
#
|
|
23
|
+
# @example
|
|
24
|
+
# client = HTTP::Client.new(headers: {"Accept" => "application/json"})
|
|
25
|
+
#
|
|
26
|
+
# @param default_options [HTTP::Options, nil] existing options instance
|
|
27
|
+
# @param options [Hash] keyword options (see HTTP::Options#initialize)
|
|
28
|
+
# @return [HTTP::Client] a new client instance
|
|
29
|
+
# @api public
|
|
30
|
+
def initialize(default_options = nil, **)
|
|
31
|
+
@default_options = HTTP::Options.new(default_options, **)
|
|
32
|
+
@connection = nil
|
|
33
|
+
@state = :clean
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Make an HTTP request
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# client.request(:get, "https://example.com")
|
|
40
|
+
#
|
|
41
|
+
# @param verb [Symbol] the HTTP method
|
|
42
|
+
# @param uri [#to_s] the URI to request
|
|
43
|
+
# @return [HTTP::Response] the response
|
|
44
|
+
# @api public
|
|
45
|
+
def request(verb, uri,
|
|
46
|
+
headers: nil, params: nil, form: nil, json: nil, body: nil,
|
|
47
|
+
response: nil, encoding: nil, follow: nil, ssl: nil, ssl_context: nil,
|
|
48
|
+
proxy: nil, nodelay: nil, features: nil, retriable: nil,
|
|
49
|
+
socket_class: nil, ssl_socket_class: nil, timeout_class: nil,
|
|
50
|
+
timeout_options: nil, keep_alive_timeout: nil, base_uri: nil, persistent: nil)
|
|
51
|
+
opts = { headers: headers, params: params, form: form, json: json, body: body,
|
|
52
|
+
response: response, encoding: encoding, follow: follow, ssl: ssl,
|
|
53
|
+
ssl_context: ssl_context, proxy: proxy, nodelay: nodelay, features: features,
|
|
54
|
+
retriable: retriable, socket_class: socket_class, ssl_socket_class: ssl_socket_class,
|
|
55
|
+
timeout_class: timeout_class, timeout_options: timeout_options,
|
|
56
|
+
keep_alive_timeout: keep_alive_timeout, base_uri: base_uri, persistent: persistent }.compact
|
|
57
|
+
opts = @default_options.merge(opts)
|
|
58
|
+
builder = Request::Builder.new(opts)
|
|
59
|
+
req = builder.build(verb, uri)
|
|
60
|
+
res = perform(req, opts)
|
|
61
|
+
return res unless opts.follow
|
|
62
|
+
|
|
63
|
+
Redirector.new(**opts.follow).perform(req, res) do |request|
|
|
64
|
+
perform(builder.wrap(request), opts)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @!method persistent?
|
|
69
|
+
# Indicate whether the client has persistent connections
|
|
70
|
+
#
|
|
71
|
+
# @example
|
|
72
|
+
# client.persistent?
|
|
73
|
+
#
|
|
74
|
+
# @see Options#persistent?
|
|
75
|
+
# @return [Boolean] whenever client is persistent
|
|
76
|
+
# @api public
|
|
77
|
+
def_delegator :default_options, :persistent?
|
|
78
|
+
|
|
79
|
+
# Perform a single (no follow) HTTP request
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# client.perform(request, options)
|
|
83
|
+
#
|
|
84
|
+
# @param req [HTTP::Request] the request to perform
|
|
85
|
+
# @param options [HTTP::Options] request options
|
|
86
|
+
# @return [HTTP::Response] the response
|
|
87
|
+
# @api public
|
|
88
|
+
def perform(req, options)
|
|
89
|
+
if options.retriable
|
|
90
|
+
perform_with_retry(req, options)
|
|
91
|
+
else
|
|
92
|
+
perform_once(req, options)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Close the connection and reset state
|
|
97
|
+
#
|
|
98
|
+
# @example
|
|
99
|
+
# client.close
|
|
100
|
+
#
|
|
101
|
+
# @return [void]
|
|
102
|
+
# @api public
|
|
103
|
+
def close
|
|
104
|
+
@connection&.close
|
|
105
|
+
@connection = nil
|
|
106
|
+
@state = :clean
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
# Execute a single HTTP request without retry logic
|
|
112
|
+
#
|
|
113
|
+
# @param req [HTTP::Request] the request to perform
|
|
114
|
+
# @param options [HTTP::Options] request options
|
|
115
|
+
# @return [HTTP::Response] the response
|
|
116
|
+
# @api private
|
|
117
|
+
def perform_once(req, options)
|
|
118
|
+
res = perform_exchange(req, options)
|
|
119
|
+
|
|
120
|
+
@connection.finish_response if res.request.verb == :head
|
|
121
|
+
@state = :clean
|
|
122
|
+
|
|
123
|
+
res
|
|
124
|
+
rescue
|
|
125
|
+
close
|
|
126
|
+
raise
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Execute a request with retry logic
|
|
130
|
+
#
|
|
131
|
+
# @param req [HTTP::Request] the request to perform
|
|
132
|
+
# @param options [HTTP::Options] request options
|
|
133
|
+
# @return [HTTP::Response] the response
|
|
134
|
+
# @api private
|
|
135
|
+
def perform_with_retry(req, options)
|
|
136
|
+
Retriable::Performer.new(**options.retriable).perform(self, req) do
|
|
137
|
+
perform_once(req, options)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Send request over the connection, handling proxy and errors
|
|
142
|
+
# @return [void]
|
|
143
|
+
# @api private
|
|
144
|
+
def send_request(req, options)
|
|
145
|
+
notify_features(req, options)
|
|
146
|
+
|
|
147
|
+
@connection ||= HTTP::Connection.new(req, options)
|
|
148
|
+
|
|
149
|
+
unless @connection.failed_proxy_connect?
|
|
150
|
+
@connection.send_request(req)
|
|
151
|
+
@connection.read_headers!
|
|
152
|
+
end
|
|
153
|
+
rescue Error => e
|
|
154
|
+
options.features.each_value { |feature| feature.on_error(req, e) }
|
|
155
|
+
raise
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Build response and apply feature wrapping
|
|
159
|
+
# @return [HTTP::Response] the wrapped response
|
|
160
|
+
# @api private
|
|
161
|
+
def build_wrapped_response(req, options)
|
|
162
|
+
res = build_response(req, options)
|
|
163
|
+
|
|
164
|
+
options.features.values.reverse.inject(res) do |response, feature|
|
|
165
|
+
feature.wrap_response(response)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Notify features of an upcoming request attempt
|
|
170
|
+
# @return [void]
|
|
171
|
+
# @api private
|
|
172
|
+
def notify_features(req, options)
|
|
173
|
+
options.features.each_value { |feature| feature.on_request(req) }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Execute the HTTP exchange wrapped by feature around_request hooks
|
|
177
|
+
# @return [HTTP::Response] the response
|
|
178
|
+
# @api private
|
|
179
|
+
def perform_exchange(req, options)
|
|
180
|
+
around_request(req, options) do |request|
|
|
181
|
+
verify_connection!(request.uri)
|
|
182
|
+
@state = :dirty
|
|
183
|
+
send_request(request, options)
|
|
184
|
+
build_wrapped_response(request, options)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Compose around_request chains from all features
|
|
189
|
+
# @return [HTTP::Response] the response
|
|
190
|
+
# @api private
|
|
191
|
+
def around_request(request, options, &block)
|
|
192
|
+
options.features.values.reverse.reduce(block) do |inner, feature|
|
|
193
|
+
->(req) { feature.around_request(req) { |r| inner.call(r) } }
|
|
194
|
+
end.call(request)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Build a response from the current connection
|
|
198
|
+
# @return [HTTP::Response] the built response
|
|
199
|
+
# @api private
|
|
200
|
+
def build_response(req, options)
|
|
201
|
+
Response.new(
|
|
202
|
+
status: @connection.status_code,
|
|
203
|
+
version: @connection.http_version,
|
|
204
|
+
headers: @connection.headers,
|
|
205
|
+
proxy_headers: @connection.proxy_response_headers,
|
|
206
|
+
connection: @connection,
|
|
207
|
+
encoding: options.encoding,
|
|
208
|
+
request: req
|
|
209
|
+
).tap { |res| @connection.pending_response = res }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Verify our request isn't going to be made against another URI
|
|
213
|
+
#
|
|
214
|
+
# @return [void]
|
|
215
|
+
# @api private
|
|
216
|
+
def verify_connection!(uri)
|
|
217
|
+
if default_options.persistent? && uri.origin != default_options.persistent
|
|
218
|
+
raise StateError, "Persistence is enabled for #{default_options.persistent}, but we got #{uri.origin}"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# We re-create the connection object because we want to let prior requests
|
|
222
|
+
# lazily load the body as long as possible, and this mimics prior functionality.
|
|
223
|
+
return close if @connection && (!@connection.keep_alive? || @connection.expired?)
|
|
224
|
+
|
|
225
|
+
# If we get into a bad state (eg, Timeout.timeout ensure being killed)
|
|
226
|
+
# close the connection to prevent potential for mixed responses.
|
|
227
|
+
close if @state == :dirty
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTP
|
|
4
|
+
class Connection
|
|
5
|
+
# Internal private methods for Connection
|
|
6
|
+
module Internals
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# Flush the pending response body so the connection can be reused
|
|
10
|
+
# @return [void]
|
|
11
|
+
# @api private
|
|
12
|
+
def flush_pending_response
|
|
13
|
+
response = @pending_response
|
|
14
|
+
unless response.respond_to?(:flush)
|
|
15
|
+
close
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
flush_or_close_response(response)
|
|
20
|
+
rescue
|
|
21
|
+
close
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Flush the response or close if the body exceeds the size limit
|
|
25
|
+
# @param response [HTTP::Response] the response to flush
|
|
26
|
+
# @return [void]
|
|
27
|
+
# @api private
|
|
28
|
+
def flush_or_close_response(response)
|
|
29
|
+
content_length = response.content_length
|
|
30
|
+
if content_length && content_length > MAX_FLUSH_SIZE
|
|
31
|
+
close
|
|
32
|
+
else
|
|
33
|
+
response.flush
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Sets up SSL context and starts TLS if needed
|
|
38
|
+
# @param (see Connection#initialize)
|
|
39
|
+
# @return [void]
|
|
40
|
+
# @api private
|
|
41
|
+
def start_tls(req, options)
|
|
42
|
+
return unless req.uri.https? && !failed_proxy_connect?
|
|
43
|
+
|
|
44
|
+
ssl_context = options.ssl_context
|
|
45
|
+
|
|
46
|
+
unless ssl_context
|
|
47
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
48
|
+
ssl_context.set_params(options.ssl || {})
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@socket.start_tls(req.uri.host, options.ssl_socket_class, ssl_context)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Open tunnel through proxy
|
|
55
|
+
# @return [void]
|
|
56
|
+
# @api private
|
|
57
|
+
def send_proxy_connect_request(req)
|
|
58
|
+
return unless req.uri.https? && req.using_proxy?
|
|
59
|
+
|
|
60
|
+
@pending_request = true
|
|
61
|
+
|
|
62
|
+
req.connect_using_proxy @socket
|
|
63
|
+
|
|
64
|
+
@pending_request = false
|
|
65
|
+
@pending_response = true
|
|
66
|
+
|
|
67
|
+
read_headers!
|
|
68
|
+
handle_proxy_connect_response
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Process the proxy connect response
|
|
72
|
+
# @return [void]
|
|
73
|
+
# @api private
|
|
74
|
+
def handle_proxy_connect_response
|
|
75
|
+
@proxy_response_headers = @parser.headers
|
|
76
|
+
|
|
77
|
+
if @parser.status_code != 200
|
|
78
|
+
@failed_proxy_connect = true
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
@parser.reset
|
|
83
|
+
@pending_response = false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Resets expiration of persistent connection
|
|
87
|
+
# @return [void]
|
|
88
|
+
# @api private
|
|
89
|
+
def reset_timer
|
|
90
|
+
@conn_expires_at = Time.now + @keep_alive_timeout if @persistent
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Store keep-alive state from parser
|
|
94
|
+
# @return [void]
|
|
95
|
+
# @api private
|
|
96
|
+
def set_keep_alive
|
|
97
|
+
return @keep_alive = false unless @persistent
|
|
98
|
+
|
|
99
|
+
@keep_alive =
|
|
100
|
+
case @parser.http_version
|
|
101
|
+
when HTTP_1_0 # HTTP/1.0 requires opt in for Keep Alive
|
|
102
|
+
@parser.headers[Headers::CONNECTION] == KEEP_ALIVE
|
|
103
|
+
when HTTP_1_1 # HTTP/1.1 is opt-out
|
|
104
|
+
@parser.headers[Headers::CONNECTION] != CLOSE
|
|
105
|
+
else # Anything else we assume doesn't support it
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Check if the response body has a known framing mechanism
|
|
111
|
+
#
|
|
112
|
+
# @example
|
|
113
|
+
# body_framed?
|
|
114
|
+
#
|
|
115
|
+
# @return [Boolean]
|
|
116
|
+
# @api private
|
|
117
|
+
def body_framed?
|
|
118
|
+
@parser.headers.include?(Headers::TRANSFER_ENCODING) ||
|
|
119
|
+
@parser.headers.include?(Headers::CONTENT_LENGTH)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Feeds some more data into parser
|
|
123
|
+
# @return [void]
|
|
124
|
+
# @raise [SocketReadError] when unable to read from socket
|
|
125
|
+
# @api private
|
|
126
|
+
def read_more(size)
|
|
127
|
+
return if @parser.finished?
|
|
128
|
+
|
|
129
|
+
value = @socket.readpartial(size, @buffer)
|
|
130
|
+
if value == :eof
|
|
131
|
+
@parser << ""
|
|
132
|
+
:eof
|
|
133
|
+
elsif value
|
|
134
|
+
@parser << value
|
|
135
|
+
end
|
|
136
|
+
rescue IOError, SocketError, SystemCallError => e
|
|
137
|
+
raise SocketReadError, "error reading from socket: #{e}", e.backtrace
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
require "http/connection/internals"
|
|
6
|
+
require "http/headers"
|
|
7
|
+
|
|
8
|
+
module HTTP
|
|
9
|
+
# A connection to the HTTP server
|
|
10
|
+
class Connection
|
|
11
|
+
extend Forwardable
|
|
12
|
+
include Internals
|
|
13
|
+
|
|
14
|
+
# Allowed values for CONNECTION header
|
|
15
|
+
KEEP_ALIVE = "Keep-Alive"
|
|
16
|
+
# Connection: close header value
|
|
17
|
+
CLOSE = "close"
|
|
18
|
+
|
|
19
|
+
# Attempt to read this much data
|
|
20
|
+
BUFFER_SIZE = 16_384
|
|
21
|
+
|
|
22
|
+
# Maximum response body size (in bytes) to auto-flush when reusing
|
|
23
|
+
# a connection. Bodies larger than this cause the connection to close
|
|
24
|
+
# instead, to avoid blocking on huge downloads.
|
|
25
|
+
MAX_FLUSH_SIZE = 1_048_576
|
|
26
|
+
|
|
27
|
+
# HTTP/1.0
|
|
28
|
+
HTTP_1_0 = "1.0"
|
|
29
|
+
|
|
30
|
+
# HTTP/1.1
|
|
31
|
+
HTTP_1_1 = "1.1"
|
|
32
|
+
|
|
33
|
+
# Returned after HTTP CONNECT (via proxy)
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# connection.proxy_response_headers
|
|
37
|
+
#
|
|
38
|
+
# @return [HTTP::Headers, nil]
|
|
39
|
+
# @api public
|
|
40
|
+
attr_reader :proxy_response_headers
|
|
41
|
+
|
|
42
|
+
# Initialize a new connection to an HTTP server
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# Connection.new(req, options)
|
|
46
|
+
#
|
|
47
|
+
# @param [HTTP::Request] req
|
|
48
|
+
# @param [HTTP::Options] options
|
|
49
|
+
# @return [Connection]
|
|
50
|
+
# @raise [HTTP::ConnectionError] when failed to connect
|
|
51
|
+
# @api public
|
|
52
|
+
def initialize(req, options)
|
|
53
|
+
init_state(options)
|
|
54
|
+
connect_socket(req, options)
|
|
55
|
+
rescue IO::TimeoutError => e
|
|
56
|
+
close
|
|
57
|
+
raise ConnectTimeoutError, e.message, e.backtrace
|
|
58
|
+
rescue IOError, SocketError, SystemCallError => e
|
|
59
|
+
raise ConnectionError, "failed to connect: #{e}", e.backtrace
|
|
60
|
+
rescue TimeoutError
|
|
61
|
+
close
|
|
62
|
+
raise
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @see (HTTP::Response::Parser#status_code)
|
|
66
|
+
def_delegator :@parser, :status_code
|
|
67
|
+
|
|
68
|
+
# @see (HTTP::Response::Parser#http_version)
|
|
69
|
+
def_delegator :@parser, :http_version
|
|
70
|
+
|
|
71
|
+
# @see (HTTP::Response::Parser#headers)
|
|
72
|
+
def_delegator :@parser, :headers
|
|
73
|
+
|
|
74
|
+
# Whether the proxy CONNECT request failed
|
|
75
|
+
#
|
|
76
|
+
# @example
|
|
77
|
+
# connection.failed_proxy_connect?
|
|
78
|
+
#
|
|
79
|
+
# @return [Boolean] whenever proxy connect failed
|
|
80
|
+
# @api public
|
|
81
|
+
def failed_proxy_connect?
|
|
82
|
+
@failed_proxy_connect
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Set the pending response for auto-flushing before the next request
|
|
86
|
+
#
|
|
87
|
+
# @example
|
|
88
|
+
# connection.pending_response = response
|
|
89
|
+
#
|
|
90
|
+
# @param [HTTP::Response, false] response
|
|
91
|
+
# @return [void]
|
|
92
|
+
# @api public
|
|
93
|
+
attr_writer :pending_response
|
|
94
|
+
|
|
95
|
+
# Send a request to the server
|
|
96
|
+
#
|
|
97
|
+
# @example
|
|
98
|
+
# connection.send_request(req)
|
|
99
|
+
#
|
|
100
|
+
# @param [Request] req Request to send to the server
|
|
101
|
+
# @return [nil]
|
|
102
|
+
# @api public
|
|
103
|
+
def send_request(req)
|
|
104
|
+
flush_pending_response if @pending_response
|
|
105
|
+
|
|
106
|
+
if @pending_request
|
|
107
|
+
raise StateError, "Tried to send a request while a response is pending. Make sure you read off the body."
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@pending_request = true
|
|
111
|
+
|
|
112
|
+
req.stream @socket
|
|
113
|
+
|
|
114
|
+
@pending_response = true
|
|
115
|
+
@pending_request = false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Read a chunk of the body
|
|
119
|
+
#
|
|
120
|
+
# @example
|
|
121
|
+
# connection.readpartial
|
|
122
|
+
#
|
|
123
|
+
# @param [Integer] size maximum bytes to read
|
|
124
|
+
# @param [String, nil] outbuf buffer to fill with data
|
|
125
|
+
# @return [String] data chunk
|
|
126
|
+
# @raise [EOFError] when no more data left
|
|
127
|
+
# @api public
|
|
128
|
+
def readpartial(size = BUFFER_SIZE, outbuf = nil)
|
|
129
|
+
raise EOFError unless @pending_response
|
|
130
|
+
|
|
131
|
+
chunk = @parser.read(size)
|
|
132
|
+
unless chunk
|
|
133
|
+
eof = read_more(size) == :eof
|
|
134
|
+
check_premature_eof(eof)
|
|
135
|
+
finished = eof || @parser.finished?
|
|
136
|
+
chunk = @parser.read(size) || "".b
|
|
137
|
+
finish_response if finished
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
outbuf ? outbuf.replace(chunk) : chunk
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Reads data from socket up until headers are loaded
|
|
144
|
+
#
|
|
145
|
+
# @example
|
|
146
|
+
# connection.read_headers!
|
|
147
|
+
#
|
|
148
|
+
# @return [void]
|
|
149
|
+
# @raise [ResponseHeaderError] when unable to read response headers
|
|
150
|
+
# @api public
|
|
151
|
+
def read_headers!
|
|
152
|
+
until @parser.headers?
|
|
153
|
+
result = read_more(BUFFER_SIZE)
|
|
154
|
+
raise ResponseHeaderError, "couldn't read response headers" if result == :eof
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
set_keep_alive
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Callback for when we've reached the end of a response
|
|
161
|
+
#
|
|
162
|
+
# @example
|
|
163
|
+
# connection.finish_response
|
|
164
|
+
#
|
|
165
|
+
# @return [void]
|
|
166
|
+
# @api public
|
|
167
|
+
def finish_response
|
|
168
|
+
close unless keep_alive?
|
|
169
|
+
|
|
170
|
+
@parser.reset
|
|
171
|
+
@socket.reset_counter if @socket.respond_to?(:reset_counter)
|
|
172
|
+
reset_timer
|
|
173
|
+
|
|
174
|
+
@pending_response = false
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Close the connection
|
|
178
|
+
#
|
|
179
|
+
# @example
|
|
180
|
+
# connection.close
|
|
181
|
+
#
|
|
182
|
+
# @return [void]
|
|
183
|
+
# @api public
|
|
184
|
+
def close
|
|
185
|
+
@socket.close unless @socket&.closed?
|
|
186
|
+
|
|
187
|
+
@pending_response = false
|
|
188
|
+
@pending_request = false
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Whether there are no pending requests or responses
|
|
192
|
+
#
|
|
193
|
+
# @example
|
|
194
|
+
# connection.finished_request?
|
|
195
|
+
#
|
|
196
|
+
# @return [Boolean]
|
|
197
|
+
# @api public
|
|
198
|
+
def finished_request?
|
|
199
|
+
!@pending_request && !@pending_response
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Whether we're keeping the conn alive
|
|
203
|
+
#
|
|
204
|
+
# @example
|
|
205
|
+
# connection.keep_alive?
|
|
206
|
+
#
|
|
207
|
+
# @return [Boolean]
|
|
208
|
+
# @api public
|
|
209
|
+
def keep_alive?
|
|
210
|
+
@keep_alive && !@socket.closed?
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Whether our connection has expired
|
|
214
|
+
#
|
|
215
|
+
# @example
|
|
216
|
+
# connection.expired?
|
|
217
|
+
#
|
|
218
|
+
# @return [Boolean]
|
|
219
|
+
# @api public
|
|
220
|
+
def expired?
|
|
221
|
+
!@conn_expires_at || @conn_expires_at < Time.now
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
# Initialize connection state
|
|
227
|
+
# @return [void]
|
|
228
|
+
# @api private
|
|
229
|
+
def init_state(options)
|
|
230
|
+
@persistent = options.persistent?
|
|
231
|
+
@keep_alive_timeout = options.keep_alive_timeout.to_f
|
|
232
|
+
@pending_request = false
|
|
233
|
+
@pending_response = false
|
|
234
|
+
@failed_proxy_connect = false
|
|
235
|
+
@buffer = "".b
|
|
236
|
+
@parser = Response::Parser.new
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Check for premature end-of-file and raise if detected
|
|
240
|
+
#
|
|
241
|
+
# @example
|
|
242
|
+
# check_premature_eof(:eof)
|
|
243
|
+
#
|
|
244
|
+
# @return [void]
|
|
245
|
+
# @api private
|
|
246
|
+
def check_premature_eof(eof)
|
|
247
|
+
return unless eof && !@parser.finished? && body_framed?
|
|
248
|
+
|
|
249
|
+
close
|
|
250
|
+
raise ConnectionError, "response body ended prematurely"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Connect socket and set up proxy/TLS
|
|
254
|
+
# @return [void]
|
|
255
|
+
# @api private
|
|
256
|
+
def connect_socket(req, options)
|
|
257
|
+
@socket = options.timeout_class.new(**options.timeout_options) # steep:ignore
|
|
258
|
+
@socket.connect(options.socket_class, req.socket_host, req.socket_port, nodelay: options.nodelay)
|
|
259
|
+
|
|
260
|
+
send_proxy_connect_request(req)
|
|
261
|
+
start_tls(req, options)
|
|
262
|
+
reset_timer
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|