http 5.3.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +241 -41
- data/LICENSE.txt +1 -1
- data/README.md +110 -13
- data/UPGRADING.md +491 -0
- data/http.gemspec +32 -29
- data/lib/http/base64.rb +11 -1
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +232 -136
- data/lib/http/client.rb +158 -127
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +126 -97
- data/lib/http/content_type.rb +61 -6
- data/lib/http/errors.rb +25 -1
- data/lib/http/feature.rb +65 -5
- data/lib/http/features/auto_deflate.rb +124 -17
- data/lib/http/features/auto_inflate.rb +38 -15
- 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 +97 -17
- data/lib/http/features/logging.rb +183 -5
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +18 -3
- 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 +3 -0
- data/lib/http/headers/normalizer.rb +17 -36
- data/lib/http/headers.rb +172 -65
- data/lib/http/mime_type/adapter.rb +24 -9
- data/lib/http/mime_type/json.rb +19 -4
- data/lib/http/mime_type.rb +21 -3
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +172 -125
- data/lib/http/redirector.rb +80 -75
- data/lib/http/request/body.rb +87 -6
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +76 -16
- data/lib/http/request.rb +214 -98
- data/lib/http/response/body.rb +103 -18
- data/lib/http/response/inflater.rb +35 -7
- data/lib/http/response/parser.rb +98 -4
- data/lib/http/response/status/reasons.rb +2 -4
- data/lib/http/response/status.rb +141 -31
- data/lib/http/response.rb +219 -61
- data/lib/http/retriable/delay_calculator.rb +38 -11
- data/lib/http/retriable/errors.rb +21 -0
- data/lib/http/retriable/performer.rb +82 -38
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +147 -34
- data/lib/http/timeout/null.rb +155 -9
- data/lib/http/timeout/per_operation.rb +139 -18
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +289 -124
- data/lib/http/version.rb +2 -1
- data/lib/http.rb +11 -2
- 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/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/{spec → test}/support/fakeio.rb +2 -2
- 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/{spec → test}/support/simplecov.rb +11 -2
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +108 -168
- data/.github/workflows/ci.yml +0 -67
- data/.gitignore +0 -15
- data/.rspec +0 -1
- data/.rubocop/layout.yml +0 -8
- data/.rubocop/metrics.yml +0 -4
- data/.rubocop/rspec.yml +0 -9
- data/.rubocop/style.yml +0 -32
- data/.rubocop.yml +0 -11
- data/.rubocop_todo.yml +0 -219
- data/.yardopts +0 -2
- data/CHANGES_OLD.md +0 -1002
- data/Gemfile +0 -51
- data/Guardfile +0 -18
- data/Rakefile +0 -64
- data/lib/http/headers/mixin.rb +0 -34
- data/lib/http/retriable/client.rb +0 -37
- data/logo.png +0 -0
- data/spec/lib/http/client_spec.rb +0 -556
- data/spec/lib/http/connection_spec.rb +0 -88
- data/spec/lib/http/content_type_spec.rb +0 -47
- data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
- data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
- data/spec/lib/http/features/instrumentation_spec.rb +0 -81
- data/spec/lib/http/features/logging_spec.rb +0 -65
- data/spec/lib/http/features/raise_error_spec.rb +0 -62
- data/spec/lib/http/headers/mixin_spec.rb +0 -36
- data/spec/lib/http/headers/normalizer_spec.rb +0 -52
- data/spec/lib/http/headers_spec.rb +0 -527
- data/spec/lib/http/options/body_spec.rb +0 -15
- data/spec/lib/http/options/features_spec.rb +0 -33
- data/spec/lib/http/options/form_spec.rb +0 -15
- data/spec/lib/http/options/headers_spec.rb +0 -24
- data/spec/lib/http/options/json_spec.rb +0 -15
- data/spec/lib/http/options/merge_spec.rb +0 -68
- data/spec/lib/http/options/new_spec.rb +0 -30
- data/spec/lib/http/options/proxy_spec.rb +0 -20
- data/spec/lib/http/options_spec.rb +0 -13
- data/spec/lib/http/redirector_spec.rb +0 -530
- data/spec/lib/http/request/body_spec.rb +0 -211
- data/spec/lib/http/request/writer_spec.rb +0 -121
- data/spec/lib/http/request_spec.rb +0 -234
- data/spec/lib/http/response/body_spec.rb +0 -85
- data/spec/lib/http/response/parser_spec.rb +0 -74
- data/spec/lib/http/response/status_spec.rb +0 -253
- data/spec/lib/http/response_spec.rb +0 -262
- data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
- data/spec/lib/http/retriable/performer_spec.rb +0 -302
- data/spec/lib/http/uri/normalizer_spec.rb +0 -95
- data/spec/lib/http/uri_spec.rb +0 -71
- data/spec/lib/http_spec.rb +0 -535
- data/spec/regression_specs.rb +0 -24
- data/spec/spec_helper.rb +0 -89
- data/spec/support/black_hole.rb +0 -13
- data/spec/support/dummy_server/servlet.rb +0 -203
- data/spec/support/dummy_server.rb +0 -44
- data/spec/support/fuubar.rb +0 -21
- data/spec/support/http_handling_shared.rb +0 -190
- data/spec/support/proxy_server.rb +0 -39
- data/spec/support/servers/config.rb +0 -11
- data/spec/support/servers/runner.rb +0 -19
- data/spec/support/ssl_helper.rb +0 -104
- /data/{spec → test}/support/capture_warning.rb +0 -0
data/lib/http/redirector.rb
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "set"
|
|
4
|
-
|
|
5
3
|
require "http/headers"
|
|
6
4
|
|
|
7
5
|
module HTTP
|
|
6
|
+
# Follows HTTP redirects according to configured policy
|
|
8
7
|
class Redirector
|
|
9
8
|
# Notifies that we reached max allowed redirect hops
|
|
10
9
|
class TooManyRedirectsError < ResponseError; end
|
|
@@ -26,117 +25,123 @@ module HTTP
|
|
|
26
25
|
# Verbs which will remain unchanged upon See Other response.
|
|
27
26
|
SEE_OTHER_ALLOWED_VERBS = %i[get head].to_set.freeze
|
|
28
27
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
28
|
+
# Returns redirector policy
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# redirector.strict # => true
|
|
32
|
+
#
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
# @api public
|
|
32
35
|
attr_reader :strict
|
|
33
36
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
+
# Returns maximum allowed hops
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# redirector.max_hops # => 5
|
|
41
|
+
#
|
|
42
|
+
# @return [Fixnum]
|
|
43
|
+
# @api public
|
|
37
44
|
attr_reader :max_hops
|
|
38
45
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
# @
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
46
60
|
end
|
|
47
61
|
|
|
48
62
|
# Follows redirects until non-redirect response found
|
|
49
|
-
|
|
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, &)
|
|
50
72
|
@request = request
|
|
51
73
|
@response = response
|
|
52
74
|
@visited = []
|
|
53
|
-
collect_cookies_from_request
|
|
54
|
-
collect_cookies_from_response
|
|
55
|
-
|
|
56
|
-
while REDIRECT_CODES.include? @response.status.code
|
|
57
|
-
@visited << "#{@request.verb} #{@request.uri}"
|
|
58
|
-
|
|
59
|
-
raise TooManyRedirectsError if too_many_hops?
|
|
60
|
-
raise EndlessRedirectError if endless_loop?
|
|
61
75
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# XXX(ixti): using `Array#inject` to return `nil` if no Location header.
|
|
65
|
-
@request = redirect_to(@response.headers.get(Headers::LOCATION).inject(:+))
|
|
66
|
-
unless cookie_jar.empty?
|
|
67
|
-
@request.headers.set(Headers::COOKIE, cookie_jar.cookies.map { |c| "#{c.name}=#{c.value}" }.join("; "))
|
|
68
|
-
end
|
|
69
|
-
@on_redirect.call @response, @request if @on_redirect.respond_to?(:call)
|
|
70
|
-
@response = yield @request
|
|
71
|
-
collect_cookies_from_response
|
|
72
|
-
end
|
|
76
|
+
follow_redirects(&) while REDIRECT_CODES.include?(@response.code)
|
|
73
77
|
|
|
74
78
|
@response
|
|
75
79
|
end
|
|
76
80
|
|
|
77
81
|
private
|
|
78
82
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
# Perform a single redirect step
|
|
84
|
+
#
|
|
85
|
+
# @api private
|
|
86
|
+
# @return [void]
|
|
87
|
+
def follow_redirects
|
|
88
|
+
@visited << visit_key
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
cookies.each do |key, value|
|
|
96
|
-
cookie_jar.add(HTTP::Cookie.new(key, value, :path => @request.uri.path, :domain => @request.host))
|
|
97
|
-
end
|
|
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
98
|
end
|
|
99
99
|
|
|
100
|
-
#
|
|
101
|
-
# carrying them to the next request as well.
|
|
100
|
+
# Extracts the redirect URI from the Location header
|
|
102
101
|
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
def
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if cookie.value == ""
|
|
109
|
-
cookie_jar.delete(cookie)
|
|
110
|
-
else
|
|
111
|
-
cookie_jar.add(cookie)
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# I wish we could just do @response.cookes = cookie_jar
|
|
116
|
-
cookie_jar.each do |cookie|
|
|
117
|
-
@response.cookies.add(cookie)
|
|
118
|
-
end
|
|
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?
|
|
119
107
|
end
|
|
120
108
|
|
|
121
109
|
# Check if we reached max amount of redirect hops
|
|
110
|
+
#
|
|
111
|
+
# @api private
|
|
122
112
|
# @return [Boolean]
|
|
123
113
|
def too_many_hops?
|
|
124
|
-
|
|
114
|
+
@max_hops.positive? && @visited.length > @max_hops
|
|
125
115
|
end
|
|
126
116
|
|
|
127
117
|
# Check if we got into an endless loop
|
|
118
|
+
#
|
|
119
|
+
# @api private
|
|
128
120
|
# @return [Boolean]
|
|
129
121
|
def endless_loop?
|
|
130
|
-
|
|
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]}"
|
|
131
134
|
end
|
|
132
135
|
|
|
133
136
|
# Redirect policy for follow
|
|
137
|
+
#
|
|
138
|
+
# @api private
|
|
134
139
|
# @return [Request]
|
|
135
140
|
def redirect_to(uri)
|
|
136
141
|
raise StateError, "no Location header in redirect" unless uri
|
|
137
142
|
|
|
138
143
|
verb = @request.verb
|
|
139
|
-
code = @response.
|
|
144
|
+
code = @response.code
|
|
140
145
|
|
|
141
146
|
if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code)
|
|
142
147
|
raise StateError, "can't follow #{@response.status} redirect" if @strict
|
|
@@ -144,7 +149,7 @@ module HTTP
|
|
|
144
149
|
verb = :get
|
|
145
150
|
end
|
|
146
151
|
|
|
147
|
-
verb = :get if !SEE_OTHER_ALLOWED_VERBS.include?(verb) && 303
|
|
152
|
+
verb = :get if !SEE_OTHER_ALLOWED_VERBS.include?(verb) && code.eql?(303)
|
|
148
153
|
|
|
149
154
|
@request.redirect(uri, verb)
|
|
150
155
|
end
|
data/lib/http/request/body.rb
CHANGED
|
@@ -2,18 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
module HTTP
|
|
4
4
|
class Request
|
|
5
|
+
# Represents an HTTP request body with streaming support
|
|
5
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
|
|
6
14
|
attr_reader :source
|
|
7
15
|
|
|
16
|
+
# Initialize a new request body
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# Body.new("hello world")
|
|
20
|
+
#
|
|
21
|
+
# @return [HTTP::Request::Body]
|
|
22
|
+
# @api public
|
|
8
23
|
def initialize(source)
|
|
9
24
|
@source = source
|
|
10
25
|
|
|
11
26
|
validate_source_type!
|
|
12
27
|
end
|
|
13
28
|
|
|
14
|
-
#
|
|
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
|
|
15
62
|
#
|
|
16
63
|
# @return [Integer]
|
|
64
|
+
# @api public
|
|
17
65
|
def size
|
|
18
66
|
if @source.is_a?(String)
|
|
19
67
|
@source.bytesize
|
|
@@ -24,33 +72,49 @@ module HTTP
|
|
|
24
72
|
elsif @source.nil?
|
|
25
73
|
0
|
|
26
74
|
else
|
|
27
|
-
raise RequestError,
|
|
75
|
+
raise RequestError,
|
|
76
|
+
"cannot determine size of body: #{@source.inspect}; " \
|
|
77
|
+
"set the Content-Length header explicitly or use chunked Transfer-Encoding"
|
|
28
78
|
end
|
|
29
79
|
end
|
|
30
80
|
|
|
31
|
-
# Yields chunks of content to be streamed
|
|
81
|
+
# Yields chunks of content to be streamed
|
|
82
|
+
#
|
|
83
|
+
# @example
|
|
84
|
+
# body.each { |chunk| socket.write(chunk) }
|
|
32
85
|
#
|
|
33
86
|
# @yieldparam [String]
|
|
87
|
+
# @return [self]
|
|
88
|
+
# @api public
|
|
34
89
|
def each(&block)
|
|
35
90
|
if @source.is_a?(String)
|
|
36
91
|
yield @source
|
|
37
92
|
elsif @source.respond_to?(:read)
|
|
38
93
|
IO.copy_stream(@source, ProcIO.new(block))
|
|
39
94
|
rewind(@source)
|
|
40
|
-
elsif @source
|
|
95
|
+
elsif @source
|
|
41
96
|
@source.each(&block)
|
|
42
97
|
end
|
|
43
98
|
|
|
44
99
|
self
|
|
45
100
|
end
|
|
46
101
|
|
|
47
|
-
#
|
|
102
|
+
# Check equality based on source
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# body == other_body
|
|
106
|
+
#
|
|
107
|
+
# @return [Boolean]
|
|
108
|
+
# @api public
|
|
48
109
|
def ==(other)
|
|
49
|
-
self.class
|
|
110
|
+
other.is_a?(self.class) && source == other.source
|
|
50
111
|
end
|
|
51
112
|
|
|
52
113
|
private
|
|
53
114
|
|
|
115
|
+
# Rewind an IO source if possible
|
|
116
|
+
# @return [void]
|
|
117
|
+
# @api private
|
|
54
118
|
def rewind(io)
|
|
55
119
|
io.rewind if io.respond_to? :rewind
|
|
56
120
|
rescue Errno::ESPIPE, Errno::EPIPE
|
|
@@ -73,6 +137,9 @@ module HTTP
|
|
|
73
137
|
nil
|
|
74
138
|
end
|
|
75
139
|
|
|
140
|
+
# Validate that source is a supported type
|
|
141
|
+
# @return [void]
|
|
142
|
+
# @api private
|
|
76
143
|
def validate_source_type!
|
|
77
144
|
return if @source.is_a?(String)
|
|
78
145
|
return if @source.respond_to?(:read)
|
|
@@ -86,10 +153,24 @@ module HTTP
|
|
|
86
153
|
# #write simply calling the proc, which we can pass in as the
|
|
87
154
|
# "destination IO" in IO.copy_stream.
|
|
88
155
|
class ProcIO
|
|
156
|
+
# Initialize a new ProcIO wrapper
|
|
157
|
+
#
|
|
158
|
+
# @example
|
|
159
|
+
# ProcIO.new(block)
|
|
160
|
+
#
|
|
161
|
+
# @return [ProcIO]
|
|
162
|
+
# @api public
|
|
89
163
|
def initialize(block)
|
|
90
164
|
@block = block
|
|
91
165
|
end
|
|
92
166
|
|
|
167
|
+
# Write data by calling the wrapped proc
|
|
168
|
+
#
|
|
169
|
+
# @example
|
|
170
|
+
# proc_io.write("hello")
|
|
171
|
+
#
|
|
172
|
+
# @return [Integer]
|
|
173
|
+
# @api public
|
|
93
174
|
def write(data)
|
|
94
175
|
@block.call(data)
|
|
95
176
|
data.bytesize
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
require "http/form_data"
|
|
6
|
+
require "http/headers"
|
|
7
|
+
require "http/connection"
|
|
8
|
+
require "http/uri"
|
|
9
|
+
|
|
10
|
+
module HTTP
|
|
11
|
+
class Request
|
|
12
|
+
# Builds HTTP::Request objects from resolved options
|
|
13
|
+
#
|
|
14
|
+
# @example Build a request from options
|
|
15
|
+
# options = HTTP::Options.new(headers: {"Accept" => "application/json"})
|
|
16
|
+
# builder = HTTP::Request::Builder.new(options)
|
|
17
|
+
# request = builder.build(:get, "https://example.com")
|
|
18
|
+
#
|
|
19
|
+
# @see Options
|
|
20
|
+
class Builder
|
|
21
|
+
# Pattern matching HTTP or HTTPS URI schemes
|
|
22
|
+
HTTP_OR_HTTPS_RE = %r{\Ahttps?://}i
|
|
23
|
+
|
|
24
|
+
# Initialize a new Request Builder
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# HTTP::Request::Builder.new(HTTP::Options.new)
|
|
28
|
+
#
|
|
29
|
+
# @param options [HTTP::Options] resolved request options
|
|
30
|
+
# @return [HTTP::Request::Builder]
|
|
31
|
+
# @api public
|
|
32
|
+
def initialize(options)
|
|
33
|
+
@options = options
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Build an HTTP request
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# builder.build(:get, "https://example.com")
|
|
40
|
+
#
|
|
41
|
+
# @param verb [Symbol] the HTTP method
|
|
42
|
+
# @param uri [#to_s] the URI to request
|
|
43
|
+
# @return [HTTP::Request] the built request object
|
|
44
|
+
# @api public
|
|
45
|
+
def build(verb, uri)
|
|
46
|
+
uri = make_request_uri(uri)
|
|
47
|
+
headers = make_request_headers
|
|
48
|
+
body = make_request_body(headers)
|
|
49
|
+
|
|
50
|
+
req = HTTP::Request.new(
|
|
51
|
+
verb: verb,
|
|
52
|
+
uri: uri,
|
|
53
|
+
uri_normalizer: @options.feature(:normalize_uri)&.normalizer,
|
|
54
|
+
proxy: @options.proxy,
|
|
55
|
+
headers: headers,
|
|
56
|
+
body: body
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
wrap(req)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Wrap a request through feature middleware
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# builder.wrap(redirect_request)
|
|
66
|
+
#
|
|
67
|
+
# @param request [HTTP::Request] the request to wrap
|
|
68
|
+
# @return [HTTP::Request] the wrapped request
|
|
69
|
+
# @api public
|
|
70
|
+
def wrap(request)
|
|
71
|
+
@options.features.inject(request) do |req, (_name, feature)|
|
|
72
|
+
feature.wrap_request(req)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Merges query params if needed
|
|
79
|
+
#
|
|
80
|
+
# @param uri [#to_s] the URI to process
|
|
81
|
+
# @return [HTTP::URI] the constructed URI
|
|
82
|
+
# @api private
|
|
83
|
+
def make_request_uri(uri)
|
|
84
|
+
uri = uri.to_s
|
|
85
|
+
|
|
86
|
+
if @options.base_uri? && uri !~ HTTP_OR_HTTPS_RE
|
|
87
|
+
uri = resolve_against_base(uri)
|
|
88
|
+
elsif @options.persistent? && uri !~ HTTP_OR_HTTPS_RE
|
|
89
|
+
uri = "#{@options.persistent}#{uri}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
uri = HTTP::URI.parse uri
|
|
93
|
+
|
|
94
|
+
merge_query_params!(uri)
|
|
95
|
+
|
|
96
|
+
# Some proxies (seen on WEBrick) fail if URL has
|
|
97
|
+
# empty path (e.g. `http://example.com`) while it's RFC-compliant:
|
|
98
|
+
# http://tools.ietf.org/html/rfc1738#section-3.1
|
|
99
|
+
uri.path = "/" if uri.path.empty?
|
|
100
|
+
|
|
101
|
+
uri
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Resolve a relative URI against the configured base URI
|
|
105
|
+
#
|
|
106
|
+
# Ensures the base URI path has a trailing slash so that relative
|
|
107
|
+
# paths are appended rather than replacing the last path segment,
|
|
108
|
+
# per the convention described in RFC 3986 Section 5.
|
|
109
|
+
#
|
|
110
|
+
# @param uri [String] the relative URI to resolve
|
|
111
|
+
# @return [String] the resolved absolute URI
|
|
112
|
+
# @api private
|
|
113
|
+
def resolve_against_base(uri)
|
|
114
|
+
base = @options.base_uri or raise Error, "base_uri is not set"
|
|
115
|
+
|
|
116
|
+
unless base.path.end_with?("/")
|
|
117
|
+
base = base.dup
|
|
118
|
+
base.path = "#{base.path}/"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
String(base.join(uri))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Merge query parameters into URI
|
|
125
|
+
#
|
|
126
|
+
# @return [void]
|
|
127
|
+
# @api private
|
|
128
|
+
def merge_query_params!(uri)
|
|
129
|
+
return unless @options.params && !@options.params.empty?
|
|
130
|
+
|
|
131
|
+
existing = ::URI.decode_www_form(uri.query || "")
|
|
132
|
+
uri.query = ::URI.encode_www_form(existing.concat(@options.params.to_a))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Creates request headers
|
|
136
|
+
#
|
|
137
|
+
# @return [HTTP::Headers] the constructed headers
|
|
138
|
+
# @api private
|
|
139
|
+
def make_request_headers
|
|
140
|
+
headers = @options.headers
|
|
141
|
+
|
|
142
|
+
# Tell the server to keep the conn open
|
|
143
|
+
headers[Headers::CONNECTION] = @options.persistent? ? Connection::KEEP_ALIVE : Connection::CLOSE
|
|
144
|
+
|
|
145
|
+
headers
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Create the request body object to send
|
|
149
|
+
#
|
|
150
|
+
# @return [String, HTTP::FormData, nil] the request body
|
|
151
|
+
# @api private
|
|
152
|
+
def make_request_body(headers)
|
|
153
|
+
if @options.body
|
|
154
|
+
@options.body
|
|
155
|
+
elsif @options.form
|
|
156
|
+
form = make_form_data(@options.form)
|
|
157
|
+
headers[Headers::CONTENT_TYPE] ||= form.content_type
|
|
158
|
+
form
|
|
159
|
+
elsif @options.json
|
|
160
|
+
make_json_body(@options.json, headers)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Encode JSON body and set content type header
|
|
165
|
+
# @return [String] the encoded JSON body
|
|
166
|
+
# @api private
|
|
167
|
+
def make_json_body(data, headers)
|
|
168
|
+
body = MimeType[:json].encode data
|
|
169
|
+
headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name.downcase}"
|
|
170
|
+
body
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Coerce form data into an HTTP::FormData object
|
|
174
|
+
# @return [HTTP::FormData::Multipart, HTTP::FormData::Urlencoded] form data
|
|
175
|
+
# @api private
|
|
176
|
+
def make_form_data(form)
|
|
177
|
+
return form if form.is_a? FormData::Multipart
|
|
178
|
+
return form if form.is_a? FormData::Urlencoded
|
|
179
|
+
|
|
180
|
+
FormData.create(form)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HTTP
|
|
4
|
+
class Request
|
|
5
|
+
# Proxy-related methods for HTTP requests
|
|
6
|
+
module Proxy
|
|
7
|
+
# Merges proxy headers into the request headers
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# request.include_proxy_headers
|
|
11
|
+
#
|
|
12
|
+
# @return [void]
|
|
13
|
+
# @api public
|
|
14
|
+
def include_proxy_headers
|
|
15
|
+
headers.merge!(proxy[:proxy_headers]) if proxy.key?(:proxy_headers)
|
|
16
|
+
include_proxy_authorization_header if using_authenticated_proxy?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Compute and add the Proxy-Authorization header
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# request.include_proxy_authorization_header
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
# @api public
|
|
26
|
+
def include_proxy_authorization_header
|
|
27
|
+
headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Build the Proxy-Authorization header value
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# request.proxy_authorization_header
|
|
34
|
+
#
|
|
35
|
+
# @return [String]
|
|
36
|
+
# @api public
|
|
37
|
+
def proxy_authorization_header
|
|
38
|
+
digest = encode64(format("%s:%s", proxy.fetch(:proxy_username), proxy.fetch(:proxy_password)))
|
|
39
|
+
"Basic #{digest}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Setup tunnel through proxy for SSL request
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# request.connect_using_proxy(socket)
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
48
|
+
# @api public
|
|
49
|
+
def connect_using_proxy(socket)
|
|
50
|
+
Writer.new(socket, nil, proxy_connect_headers, proxy_connect_header).connect_through_proxy
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Compute HTTP request header SSL proxy connection
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# request.proxy_connect_header
|
|
57
|
+
#
|
|
58
|
+
# @return [String]
|
|
59
|
+
# @api public
|
|
60
|
+
def proxy_connect_header
|
|
61
|
+
"CONNECT #{host}:#{port} HTTP/#{version}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Headers to send with proxy connect request
|
|
65
|
+
#
|
|
66
|
+
# @example
|
|
67
|
+
# request.proxy_connect_headers
|
|
68
|
+
#
|
|
69
|
+
# @return [HTTP::Headers]
|
|
70
|
+
# @api public
|
|
71
|
+
def proxy_connect_headers
|
|
72
|
+
connect_headers = Headers.coerce(
|
|
73
|
+
Headers::HOST => headers[Headers::HOST],
|
|
74
|
+
Headers::USER_AGENT => headers[Headers::USER_AGENT]
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
connect_headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header if using_authenticated_proxy?
|
|
78
|
+
connect_headers.merge!(proxy[:proxy_headers]) if proxy.key?(:proxy_headers)
|
|
79
|
+
connect_headers
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|