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/options.rb
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "http/headers"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "http/uri"
|
|
7
|
+
|
|
8
|
+
module HTTP
|
|
9
|
+
# Configuration options for HTTP requests and clients
|
|
10
|
+
class Options
|
|
11
|
+
@default_socket_class = TCPSocket
|
|
12
|
+
@default_ssl_socket_class = OpenSSL::SSL::SSLSocket
|
|
13
|
+
@default_timeout_class = HTTP::Timeout::Null
|
|
14
|
+
@available_features = {}
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
# Default TCP socket class
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# HTTP::Options.default_socket_class # => TCPSocket
|
|
21
|
+
#
|
|
22
|
+
# @return [Class] default socket class
|
|
23
|
+
# @api public
|
|
24
|
+
attr_accessor :default_socket_class
|
|
25
|
+
|
|
26
|
+
# Default SSL socket class
|
|
27
|
+
#
|
|
28
|
+
# @example
|
|
29
|
+
# HTTP::Options.default_ssl_socket_class
|
|
30
|
+
#
|
|
31
|
+
# @return [Class] default SSL socket class
|
|
32
|
+
# @api public
|
|
33
|
+
attr_accessor :default_ssl_socket_class
|
|
34
|
+
|
|
35
|
+
# Default timeout handler class
|
|
36
|
+
#
|
|
37
|
+
# @example
|
|
38
|
+
# HTTP::Options.default_timeout_class
|
|
39
|
+
#
|
|
40
|
+
# @return [Class] default timeout class
|
|
41
|
+
# @api public
|
|
42
|
+
attr_accessor :default_timeout_class
|
|
43
|
+
|
|
44
|
+
# Registered feature implementations
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# HTTP::Options.available_features
|
|
48
|
+
#
|
|
49
|
+
# @return [Hash] registered feature implementations
|
|
50
|
+
# @api public
|
|
51
|
+
attr_reader :available_features
|
|
52
|
+
|
|
53
|
+
# Returns existing Options or creates new one
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# HTTP::Options.new(response: :auto)
|
|
57
|
+
#
|
|
58
|
+
# @param [HTTP::Options, Hash, nil] options existing Options or Hash to convert
|
|
59
|
+
# @api public
|
|
60
|
+
# @return [HTTP::Options]
|
|
61
|
+
def new(options = nil, **kwargs)
|
|
62
|
+
return options if options.is_a?(self)
|
|
63
|
+
|
|
64
|
+
super(**(options || kwargs)) # steep:ignore
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns list of defined option names
|
|
68
|
+
#
|
|
69
|
+
# @example
|
|
70
|
+
# HTTP::Options.defined_options
|
|
71
|
+
#
|
|
72
|
+
# @api semipublic
|
|
73
|
+
# @return [Array<Symbol>]
|
|
74
|
+
def defined_options
|
|
75
|
+
@defined_options ||= []
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Registers a feature by name and implementation
|
|
79
|
+
#
|
|
80
|
+
# @example
|
|
81
|
+
# HTTP::Options.register_feature(:auto_inflate, AutoInflate)
|
|
82
|
+
#
|
|
83
|
+
# @param [Symbol] name
|
|
84
|
+
# @param [Class] impl
|
|
85
|
+
# @api public
|
|
86
|
+
# @return [Class]
|
|
87
|
+
def register_feature(name, impl)
|
|
88
|
+
@available_features[name] = impl
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
protected
|
|
92
|
+
|
|
93
|
+
# Defines an option with accessor and with_ method
|
|
94
|
+
#
|
|
95
|
+
# @param [Symbol] name
|
|
96
|
+
# @param [Boolean] reader_only
|
|
97
|
+
# @api private
|
|
98
|
+
# @return [void]
|
|
99
|
+
def def_option(name, reader_only: false, &interpreter)
|
|
100
|
+
defined_options << name.to_sym
|
|
101
|
+
interpreter ||= ->(v) { v }
|
|
102
|
+
|
|
103
|
+
def_option_accessor(name, reader_only: reader_only)
|
|
104
|
+
|
|
105
|
+
define_method(:"with_#{name}") do |value|
|
|
106
|
+
dup { |opts| opts.send(:"#{name}=", instance_exec(value, &interpreter)) } # steep:ignore
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Define accessor methods for an option
|
|
111
|
+
#
|
|
112
|
+
# @example
|
|
113
|
+
# def_option_accessor(:timeout, reader_only: false)
|
|
114
|
+
#
|
|
115
|
+
# @return [void]
|
|
116
|
+
# @api private
|
|
117
|
+
def def_option_accessor(name, reader_only:)
|
|
118
|
+
if reader_only
|
|
119
|
+
attr_reader name
|
|
120
|
+
else
|
|
121
|
+
attr_accessor name
|
|
122
|
+
protected :"#{name}="
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Initializes options with keyword arguments
|
|
128
|
+
#
|
|
129
|
+
# @example
|
|
130
|
+
# HTTP::Options.new(response: :auto, follow: true)
|
|
131
|
+
#
|
|
132
|
+
# @api public
|
|
133
|
+
# @return [HTTP::Options]
|
|
134
|
+
def initialize(
|
|
135
|
+
response: :auto,
|
|
136
|
+
encoding: nil,
|
|
137
|
+
nodelay: false,
|
|
138
|
+
keep_alive_timeout: 5,
|
|
139
|
+
proxy: {},
|
|
140
|
+
ssl: {},
|
|
141
|
+
headers: {},
|
|
142
|
+
features: {},
|
|
143
|
+
timeout_class: self.class.default_timeout_class,
|
|
144
|
+
timeout_options: {},
|
|
145
|
+
socket_class: self.class.default_socket_class,
|
|
146
|
+
ssl_socket_class: self.class.default_ssl_socket_class,
|
|
147
|
+
params: nil,
|
|
148
|
+
form: nil,
|
|
149
|
+
json: nil,
|
|
150
|
+
body: nil,
|
|
151
|
+
follow: nil,
|
|
152
|
+
retriable: nil,
|
|
153
|
+
base_uri: nil,
|
|
154
|
+
persistent: nil,
|
|
155
|
+
ssl_context: nil
|
|
156
|
+
)
|
|
157
|
+
assign_options(binding)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Merges two Options objects
|
|
161
|
+
#
|
|
162
|
+
# @example
|
|
163
|
+
# opts = HTTP::Options.new.merge(HTTP::Options.new(response: :body))
|
|
164
|
+
#
|
|
165
|
+
# @param [HTTP::Options, Hash] other
|
|
166
|
+
# @api public
|
|
167
|
+
# @return [HTTP::Options]
|
|
168
|
+
def merge(other)
|
|
169
|
+
merged = to_hash.merge(other.to_hash) do |k, v1, v2|
|
|
170
|
+
k == :headers ? v1.merge(v2) : v2
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
self.class.new(**merged)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Converts options to a Hash
|
|
177
|
+
#
|
|
178
|
+
# @example
|
|
179
|
+
# HTTP::Options.new.to_hash
|
|
180
|
+
#
|
|
181
|
+
# @api public
|
|
182
|
+
# @return [Hash]
|
|
183
|
+
def to_hash
|
|
184
|
+
self.class.defined_options.to_h { |opt_name| [opt_name, public_send(opt_name)] }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Duplicates the options object
|
|
188
|
+
#
|
|
189
|
+
# @example
|
|
190
|
+
# opts = HTTP::Options.new
|
|
191
|
+
# opts.dup
|
|
192
|
+
#
|
|
193
|
+
# @api public
|
|
194
|
+
# @return [HTTP::Options]
|
|
195
|
+
def dup
|
|
196
|
+
dupped = super
|
|
197
|
+
yield(dupped) if block_given?
|
|
198
|
+
dupped
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Returns a feature by name
|
|
202
|
+
#
|
|
203
|
+
# @example
|
|
204
|
+
# opts = HTTP::Options.new
|
|
205
|
+
# opts.feature(:auto_inflate)
|
|
206
|
+
#
|
|
207
|
+
# @param [Symbol] name
|
|
208
|
+
# @api public
|
|
209
|
+
# @return [Feature, nil]
|
|
210
|
+
def feature(name)
|
|
211
|
+
features[name]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
# Assigns all option values from the initialize binding
|
|
217
|
+
#
|
|
218
|
+
# @param [Binding] env binding from initialize with keyword argument values
|
|
219
|
+
# @api private
|
|
220
|
+
# @return [void]
|
|
221
|
+
def assign_options(env)
|
|
222
|
+
self.class.defined_options.each do |name|
|
|
223
|
+
value = env.local_variable_get(name)
|
|
224
|
+
value = Headers.coerce(value) if name.eql?(:headers)
|
|
225
|
+
__send__(:"#{name}=", value)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Raises an argument error with adjusted backtrace
|
|
230
|
+
#
|
|
231
|
+
# @api private
|
|
232
|
+
# @return [void]
|
|
233
|
+
def argument_error!(message)
|
|
234
|
+
error = Error.new(message)
|
|
235
|
+
error.set_backtrace(caller(1) || [])
|
|
236
|
+
raise error
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
require "http/options/definitions"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "http/headers"
|
|
4
|
+
|
|
5
|
+
module HTTP
|
|
6
|
+
# Follows HTTP redirects according to configured policy
|
|
7
|
+
class Redirector
|
|
8
|
+
# Notifies that we reached max allowed redirect hops
|
|
9
|
+
class TooManyRedirectsError < ResponseError; end
|
|
10
|
+
|
|
11
|
+
# Notifies that following redirects got into an endless loop
|
|
12
|
+
class EndlessRedirectError < TooManyRedirectsError; end
|
|
13
|
+
|
|
14
|
+
# HTTP status codes which indicate redirects
|
|
15
|
+
REDIRECT_CODES = [300, 301, 302, 303, 307, 308].to_set.freeze
|
|
16
|
+
|
|
17
|
+
# Codes which which should raise StateError in strict mode if original
|
|
18
|
+
# request was any of {UNSAFE_VERBS}
|
|
19
|
+
STRICT_SENSITIVE_CODES = [300, 301, 302].to_set.freeze
|
|
20
|
+
|
|
21
|
+
# Insecure http verbs, which should trigger StateError in strict mode
|
|
22
|
+
# upon {STRICT_SENSITIVE_CODES}
|
|
23
|
+
UNSAFE_VERBS = %i[put delete post].to_set.freeze
|
|
24
|
+
|
|
25
|
+
# Verbs which will remain unchanged upon See Other response.
|
|
26
|
+
SEE_OTHER_ALLOWED_VERBS = %i[get head].to_set.freeze
|
|
27
|
+
|
|
28
|
+
# Returns redirector policy
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# redirector.strict # => true
|
|
32
|
+
#
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
# @api public
|
|
35
|
+
attr_reader :strict
|
|
36
|
+
|
|
37
|
+
# Returns maximum allowed hops
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# redirector.max_hops # => 5
|
|
41
|
+
#
|
|
42
|
+
# @return [Fixnum]
|
|
43
|
+
# @api public
|
|
44
|
+
attr_reader :max_hops
|
|
45
|
+
|
|
46
|
+
# Initializes a new Redirector
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# HTTP::Redirector.new(strict: true, max_hops: 5)
|
|
50
|
+
#
|
|
51
|
+
# @param [Boolean] strict (true) redirector hops policy
|
|
52
|
+
# @param [#to_i] max_hops (5) maximum allowed amount of hops
|
|
53
|
+
# @param [#call, nil] on_redirect optional redirect callback
|
|
54
|
+
# @api public
|
|
55
|
+
# @return [HTTP::Redirector]
|
|
56
|
+
def initialize(strict: true, max_hops: 5, on_redirect: nil)
|
|
57
|
+
@strict = strict
|
|
58
|
+
@max_hops = Integer(max_hops)
|
|
59
|
+
@on_redirect = on_redirect
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Follows redirects until non-redirect response found
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# redirector.perform(request, response) { |req| client.perform(req) }
|
|
66
|
+
#
|
|
67
|
+
# @param [HTTP::Request] request
|
|
68
|
+
# @param [HTTP::Response] response
|
|
69
|
+
# @api public
|
|
70
|
+
# @return [HTTP::Response]
|
|
71
|
+
def perform(request, response, &)
|
|
72
|
+
@request = request
|
|
73
|
+
@response = response
|
|
74
|
+
@visited = []
|
|
75
|
+
|
|
76
|
+
follow_redirects(&) while REDIRECT_CODES.include?(@response.code)
|
|
77
|
+
|
|
78
|
+
@response
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Perform a single redirect step
|
|
84
|
+
#
|
|
85
|
+
# @api private
|
|
86
|
+
# @return [void]
|
|
87
|
+
def follow_redirects
|
|
88
|
+
@visited << visit_key
|
|
89
|
+
|
|
90
|
+
raise TooManyRedirectsError if too_many_hops?
|
|
91
|
+
raise EndlessRedirectError if endless_loop?
|
|
92
|
+
|
|
93
|
+
@response.flush
|
|
94
|
+
|
|
95
|
+
@request = redirect_to(redirect_uri)
|
|
96
|
+
@on_redirect&.call @response, @request
|
|
97
|
+
@response = yield @request
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Extracts the redirect URI from the Location header
|
|
101
|
+
#
|
|
102
|
+
# @api private
|
|
103
|
+
# @return [String, nil] URI string or nil if no Location header
|
|
104
|
+
def redirect_uri
|
|
105
|
+
location = @response.headers.get(Headers::LOCATION)
|
|
106
|
+
location.join unless location.empty?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Check if we reached max amount of redirect hops
|
|
110
|
+
#
|
|
111
|
+
# @api private
|
|
112
|
+
# @return [Boolean]
|
|
113
|
+
def too_many_hops?
|
|
114
|
+
@max_hops.positive? && @visited.length > @max_hops
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Check if we got into an endless loop
|
|
118
|
+
#
|
|
119
|
+
# @api private
|
|
120
|
+
# @return [Boolean]
|
|
121
|
+
def endless_loop?
|
|
122
|
+
@visited.count(@visited.last) > 1
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Build a visit key for the current request
|
|
126
|
+
#
|
|
127
|
+
# Includes verb, URI, and Cookie header so that requests to the same URL
|
|
128
|
+
# with different cookies are not falsely detected as an endless loop.
|
|
129
|
+
#
|
|
130
|
+
# @api private
|
|
131
|
+
# @return [String]
|
|
132
|
+
def visit_key
|
|
133
|
+
"#{@request.verb} #{@request.uri} #{@request.headers[Headers::COOKIE]}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Redirect policy for follow
|
|
137
|
+
#
|
|
138
|
+
# @api private
|
|
139
|
+
# @return [Request]
|
|
140
|
+
def redirect_to(uri)
|
|
141
|
+
raise StateError, "no Location header in redirect" unless uri
|
|
142
|
+
|
|
143
|
+
verb = @request.verb
|
|
144
|
+
code = @response.code
|
|
145
|
+
|
|
146
|
+
if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code)
|
|
147
|
+
raise StateError, "can't follow #{@response.status} redirect" if @strict
|
|
148
|
+
|
|
149
|
+
verb = :get
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
verb = :get if !SEE_OTHER_ALLOWED_VERBS.include?(verb) && code.eql?(303)
|
|
153
|
+
|
|
154
|
+
@request.redirect(uri, verb)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTP
|
|
4
|
+
class Request
|
|
5
|
+
# Represents an HTTP request body with streaming support
|
|
6
|
+
class Body
|
|
7
|
+
# The source data for this body
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# body.source # => "hello world"
|
|
11
|
+
#
|
|
12
|
+
# @return [String, Enumerable, IO, nil]
|
|
13
|
+
# @api public
|
|
14
|
+
attr_reader :source
|
|
15
|
+
|
|
16
|
+
# Initialize a new request body
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# Body.new("hello world")
|
|
20
|
+
#
|
|
21
|
+
# @return [HTTP::Request::Body]
|
|
22
|
+
# @api public
|
|
23
|
+
def initialize(source)
|
|
24
|
+
@source = source
|
|
25
|
+
|
|
26
|
+
validate_source_type!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Whether the body is empty
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# body.empty? # => true
|
|
33
|
+
#
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
# @api public
|
|
36
|
+
def empty?
|
|
37
|
+
@source.nil?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Whether the body content can be accessed for logging
|
|
41
|
+
#
|
|
42
|
+
# Returns true for String sources (the content can be inspected).
|
|
43
|
+
# Returns false for IO streams and Enumerables (which cannot be
|
|
44
|
+
# read without consuming them), and for nil bodies.
|
|
45
|
+
#
|
|
46
|
+
# The logging feature checks the string encoding separately to
|
|
47
|
+
# decide whether to log the content as text or format it as binary.
|
|
48
|
+
#
|
|
49
|
+
# @example
|
|
50
|
+
# body.loggable? # => true
|
|
51
|
+
#
|
|
52
|
+
# @return [Boolean]
|
|
53
|
+
# @api public
|
|
54
|
+
def loggable?
|
|
55
|
+
@source.is_a?(String)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns size for the "Content-Length" header
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# body.size
|
|
62
|
+
#
|
|
63
|
+
# @return [Integer]
|
|
64
|
+
# @api public
|
|
65
|
+
def size
|
|
66
|
+
if @source.is_a?(String)
|
|
67
|
+
@source.bytesize
|
|
68
|
+
elsif @source.respond_to?(:read)
|
|
69
|
+
raise RequestError, "IO object must respond to #size" unless @source.respond_to?(:size)
|
|
70
|
+
|
|
71
|
+
@source.size
|
|
72
|
+
elsif @source.nil?
|
|
73
|
+
0
|
|
74
|
+
else
|
|
75
|
+
raise RequestError,
|
|
76
|
+
"cannot determine size of body: #{@source.inspect}; " \
|
|
77
|
+
"set the Content-Length header explicitly or use chunked Transfer-Encoding"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Yields chunks of content to be streamed
|
|
82
|
+
#
|
|
83
|
+
# @example
|
|
84
|
+
# body.each { |chunk| socket.write(chunk) }
|
|
85
|
+
#
|
|
86
|
+
# @yieldparam [String]
|
|
87
|
+
# @return [self]
|
|
88
|
+
# @api public
|
|
89
|
+
def each(&block)
|
|
90
|
+
if @source.is_a?(String)
|
|
91
|
+
yield @source
|
|
92
|
+
elsif @source.respond_to?(:read)
|
|
93
|
+
IO.copy_stream(@source, ProcIO.new(block))
|
|
94
|
+
rewind(@source)
|
|
95
|
+
elsif @source
|
|
96
|
+
@source.each(&block)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check equality based on source
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# body == other_body
|
|
106
|
+
#
|
|
107
|
+
# @return [Boolean]
|
|
108
|
+
# @api public
|
|
109
|
+
def ==(other)
|
|
110
|
+
other.is_a?(self.class) && source == other.source
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# Rewind an IO source if possible
|
|
116
|
+
# @return [void]
|
|
117
|
+
# @api private
|
|
118
|
+
def rewind(io)
|
|
119
|
+
io.rewind if io.respond_to? :rewind
|
|
120
|
+
rescue Errno::ESPIPE, Errno::EPIPE
|
|
121
|
+
# Pipe IOs respond to `:rewind` but fail when you call it.
|
|
122
|
+
#
|
|
123
|
+
# Calling `IO#rewind` on a pipe, fails with *ESPIPE* on MRI,
|
|
124
|
+
# but *EPIPE* on jRuby.
|
|
125
|
+
#
|
|
126
|
+
# - **ESPIPE** -- "Illegal seek."
|
|
127
|
+
# Invalid seek operation (such as on a pipe).
|
|
128
|
+
#
|
|
129
|
+
# - **EPIPE** -- "Broken pipe."
|
|
130
|
+
# There is no process reading from the other end of a pipe. Every
|
|
131
|
+
# library function that returns this error code also generates
|
|
132
|
+
# a SIGPIPE signal; this signal terminates the program if not handled
|
|
133
|
+
# or blocked. Thus, your program will never actually see EPIPE unless
|
|
134
|
+
# it has handled or blocked SIGPIPE.
|
|
135
|
+
#
|
|
136
|
+
# See: https://www.gnu.org/software/libc/manual/html_node/Error-Codes.html
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Validate that source is a supported type
|
|
141
|
+
# @return [void]
|
|
142
|
+
# @api private
|
|
143
|
+
def validate_source_type!
|
|
144
|
+
return if @source.is_a?(String)
|
|
145
|
+
return if @source.respond_to?(:read)
|
|
146
|
+
return if @source.is_a?(Enumerable)
|
|
147
|
+
return if @source.nil?
|
|
148
|
+
|
|
149
|
+
raise RequestError, "body of wrong type: #{@source.class}"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# This class provides a "writable IO" wrapper around a proc object, with
|
|
153
|
+
# #write simply calling the proc, which we can pass in as the
|
|
154
|
+
# "destination IO" in IO.copy_stream.
|
|
155
|
+
class ProcIO
|
|
156
|
+
# Initialize a new ProcIO wrapper
|
|
157
|
+
#
|
|
158
|
+
# @example
|
|
159
|
+
# ProcIO.new(block)
|
|
160
|
+
#
|
|
161
|
+
# @return [ProcIO]
|
|
162
|
+
# @api public
|
|
163
|
+
def initialize(block)
|
|
164
|
+
@block = block
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Write data by calling the wrapped proc
|
|
168
|
+
#
|
|
169
|
+
# @example
|
|
170
|
+
# proc_io.write("hello")
|
|
171
|
+
#
|
|
172
|
+
# @return [Integer]
|
|
173
|
+
# @api public
|
|
174
|
+
def write(data)
|
|
175
|
+
@block.call(data)
|
|
176
|
+
data.bytesize
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|