http 5.2.0 → 6.0.2
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/LICENSE.txt +1 -1
- data/README.md +110 -13
- data/http.gemspec +38 -35
- 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 +249 -129
- data/lib/http/client.rb +158 -127
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +128 -97
- data/lib/http/content_type.rb +61 -6
- data/lib/http/errors.rb +41 -1
- data/lib/http/feature.rb +67 -6
- 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 +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 +3 -0
- data/lib/http/headers/normalizer.rb +50 -0
- data/lib/http/headers.rb +185 -92
- 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 +78 -17
- data/lib/http/request.rb +216 -99
- 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 +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 +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 -1
- data/sig/http.rbs +1619 -0
- metadata +42 -175
- 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/style.yml +0 -32
- data/.rubocop.yml +0 -11
- data/.rubocop_todo.yml +0 -206
- data/.yardopts +0 -2
- data/CHANGELOG.md +0 -41
- data/CHANGES_OLD.md +0 -1002
- data/CONTRIBUTING.md +0 -26
- data/Gemfile +0 -50
- data/Guardfile +0 -18
- data/Rakefile +0 -64
- data/SECURITY.md +0 -17
- data/lib/http/headers/mixin.rb +0 -34
- 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/headers/mixin_spec.rb +0 -36
- 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 -529
- 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/uri/normalizer_spec.rb +0 -95
- data/spec/lib/http/uri_spec.rb +0 -71
- data/spec/lib/http_spec.rb +0 -506
- data/spec/regression_specs.rb +0 -24
- data/spec/spec_helper.rb +0 -88
- data/spec/support/black_hole.rb +0 -13
- data/spec/support/capture_warning.rb +0 -10
- data/spec/support/dummy_server/servlet.rb +0 -190
- data/spec/support/dummy_server.rb +0 -43
- data/spec/support/fakeio.rb +0 -21
- 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/simplecov.rb +0 -19
- data/spec/support/ssl_helper.rb +0 -104
data/lib/http/request/writer.rb
CHANGED
|
@@ -4,19 +4,24 @@ require "http/headers"
|
|
|
4
4
|
|
|
5
5
|
module HTTP
|
|
6
6
|
class Request
|
|
7
|
+
# Streams HTTP requests to a socket
|
|
7
8
|
class Writer
|
|
8
9
|
# CRLF is the universal HTTP delimiter
|
|
9
10
|
CRLF = "\r\n"
|
|
10
11
|
|
|
11
|
-
# Chunked data
|
|
12
|
+
# Chunked data terminator
|
|
12
13
|
ZERO = "0"
|
|
13
14
|
|
|
14
|
-
# Chunked transfer encoding
|
|
15
|
-
CHUNKED = "chunked"
|
|
16
|
-
|
|
17
15
|
# End of a chunked transfer
|
|
18
|
-
CHUNKED_END = "#{ZERO}#{CRLF}#{CRLF}"
|
|
16
|
+
CHUNKED_END = "#{ZERO}#{CRLF}#{CRLF}".freeze
|
|
19
17
|
|
|
18
|
+
# Initialize a new request writer
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# Writer.new(socket, body, headers, "GET / HTTP/1.1")
|
|
22
|
+
#
|
|
23
|
+
# @return [HTTP::Request::Writer]
|
|
24
|
+
# @api public
|
|
20
25
|
def initialize(socket, body, headers, headline)
|
|
21
26
|
@body = body
|
|
22
27
|
@socket = socket
|
|
@@ -24,7 +29,13 @@ module HTTP
|
|
|
24
29
|
@request_header = [headline]
|
|
25
30
|
end
|
|
26
31
|
|
|
27
|
-
# Adds headers to the request header
|
|
32
|
+
# Adds headers to the request header array
|
|
33
|
+
#
|
|
34
|
+
# @example
|
|
35
|
+
# writer.add_headers
|
|
36
|
+
#
|
|
37
|
+
# @return [void]
|
|
38
|
+
# @api public
|
|
28
39
|
def add_headers
|
|
29
40
|
@headers.each do |field, value|
|
|
30
41
|
@request_header << "#{field}: #{value}"
|
|
@@ -32,6 +43,12 @@ module HTTP
|
|
|
32
43
|
end
|
|
33
44
|
|
|
34
45
|
# Stream the request to a socket
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# writer.stream
|
|
49
|
+
#
|
|
50
|
+
# @return [void]
|
|
51
|
+
# @api public
|
|
35
52
|
def stream
|
|
36
53
|
add_headers
|
|
37
54
|
add_body_type_headers
|
|
@@ -39,32 +56,54 @@ module HTTP
|
|
|
39
56
|
end
|
|
40
57
|
|
|
41
58
|
# Send headers needed to connect through proxy
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# writer.connect_through_proxy
|
|
62
|
+
#
|
|
63
|
+
# @return [void]
|
|
64
|
+
# @api public
|
|
42
65
|
def connect_through_proxy
|
|
43
66
|
add_headers
|
|
44
67
|
write(join_headers)
|
|
45
68
|
end
|
|
46
69
|
|
|
47
|
-
# Adds
|
|
48
|
-
#
|
|
70
|
+
# Adds content length or transfer encoding headers
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
# writer.add_body_type_headers
|
|
74
|
+
#
|
|
75
|
+
# @return [void]
|
|
76
|
+
# @api public
|
|
49
77
|
def add_body_type_headers
|
|
50
78
|
return if @headers[Headers::CONTENT_LENGTH] || chunked? || (
|
|
51
79
|
@body.source.nil? && %w[GET HEAD DELETE CONNECT].any? do |method|
|
|
52
|
-
@request_header
|
|
80
|
+
@request_header.fetch(0).start_with?("#{method} ")
|
|
53
81
|
end
|
|
54
82
|
)
|
|
55
83
|
|
|
56
84
|
@request_header << "#{Headers::CONTENT_LENGTH}: #{@body.size}"
|
|
57
85
|
end
|
|
58
86
|
|
|
59
|
-
# Joins
|
|
60
|
-
#
|
|
87
|
+
# Joins headers into an HTTP request header string
|
|
88
|
+
#
|
|
89
|
+
# @example
|
|
90
|
+
# writer.join_headers
|
|
91
|
+
#
|
|
92
|
+
# @return [String]
|
|
93
|
+
# @api public
|
|
61
94
|
def join_headers
|
|
62
95
|
# join the headers array with crlfs, stick two on the end because
|
|
63
96
|
# that ends the request header
|
|
64
97
|
@request_header.join(CRLF) + (CRLF * 2)
|
|
65
98
|
end
|
|
66
99
|
|
|
67
|
-
# Writes HTTP request data into the socket
|
|
100
|
+
# Writes HTTP request data into the socket
|
|
101
|
+
#
|
|
102
|
+
# @example
|
|
103
|
+
# writer.send_request
|
|
104
|
+
#
|
|
105
|
+
# @return [void]
|
|
106
|
+
# @api public
|
|
68
107
|
def send_request
|
|
69
108
|
each_chunk { |chunk| write chunk }
|
|
70
109
|
rescue Errno::EPIPE
|
|
@@ -72,12 +111,18 @@ module HTTP
|
|
|
72
111
|
nil
|
|
73
112
|
end
|
|
74
113
|
|
|
75
|
-
# Yields chunks of request data
|
|
114
|
+
# Yields chunks of request data for the socket
|
|
76
115
|
#
|
|
77
116
|
# It's important to send the request in a single write call when possible
|
|
78
117
|
# in order to play nicely with Nagle's algorithm. Making two writes in a
|
|
79
118
|
# row triggers a pathological case where Nagle is expecting a third write
|
|
80
119
|
# that never happens.
|
|
120
|
+
#
|
|
121
|
+
# @example
|
|
122
|
+
# writer.each_chunk { |chunk| socket.write(chunk) }
|
|
123
|
+
#
|
|
124
|
+
# @return [void]
|
|
125
|
+
# @api public
|
|
81
126
|
def each_chunk
|
|
82
127
|
data = join_headers
|
|
83
128
|
|
|
@@ -92,7 +137,13 @@ module HTTP
|
|
|
92
137
|
yield CHUNKED_END if chunked?
|
|
93
138
|
end
|
|
94
139
|
|
|
95
|
-
# Returns
|
|
140
|
+
# Returns chunk encoded per Transfer-Encoding header
|
|
141
|
+
#
|
|
142
|
+
# @example
|
|
143
|
+
# writer.encode_chunk("hello")
|
|
144
|
+
#
|
|
145
|
+
# @return [String]
|
|
146
|
+
# @api public
|
|
96
147
|
def encode_chunk(chunk)
|
|
97
148
|
if chunked?
|
|
98
149
|
chunk.bytesize.to_s(16) << CRLF << chunk << CRLF
|
|
@@ -101,13 +152,23 @@ module HTTP
|
|
|
101
152
|
end
|
|
102
153
|
end
|
|
103
154
|
|
|
104
|
-
# Returns true if
|
|
155
|
+
# Returns true if using chunked transfer encoding
|
|
156
|
+
#
|
|
157
|
+
# @example
|
|
158
|
+
# writer.chunked?
|
|
159
|
+
#
|
|
160
|
+
# @return [Boolean]
|
|
161
|
+
# @api public
|
|
105
162
|
def chunked?
|
|
106
|
-
@headers[Headers::TRANSFER_ENCODING]
|
|
163
|
+
@headers[Headers::TRANSFER_ENCODING].eql?(Headers::CHUNKED)
|
|
107
164
|
end
|
|
108
165
|
|
|
109
166
|
private
|
|
110
167
|
|
|
168
|
+
# Write data to the underlying socket
|
|
169
|
+
# @return [void]
|
|
170
|
+
# @raise [SocketWriteError] when unable to write to socket
|
|
171
|
+
# @api private
|
|
111
172
|
def write(data)
|
|
112
173
|
until data.empty?
|
|
113
174
|
length = @socket.write(data)
|
|
@@ -118,7 +179,7 @@ module HTTP
|
|
|
118
179
|
rescue Errno::EPIPE
|
|
119
180
|
raise
|
|
120
181
|
rescue IOError, SocketError, SystemCallError => e
|
|
121
|
-
raise
|
|
182
|
+
raise SocketWriteError, "error writing to socket: #{e}", e.backtrace
|
|
122
183
|
end
|
|
123
184
|
end
|
|
124
185
|
end
|
data/lib/http/request.rb
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "forwardable"
|
|
4
|
-
require "base64"
|
|
5
4
|
require "time"
|
|
6
5
|
|
|
6
|
+
require "http/base64"
|
|
7
7
|
require "http/errors"
|
|
8
8
|
require "http/headers"
|
|
9
9
|
require "http/request/body"
|
|
10
|
+
require "http/request/proxy"
|
|
10
11
|
require "http/request/writer"
|
|
11
12
|
require "http/version"
|
|
12
13
|
require "http/uri"
|
|
13
14
|
|
|
14
15
|
module HTTP
|
|
16
|
+
# Represents an HTTP request with verb, URI, headers, and body
|
|
15
17
|
class Request
|
|
16
18
|
extend Forwardable
|
|
17
19
|
|
|
18
|
-
include HTTP::
|
|
20
|
+
include HTTP::Base64
|
|
21
|
+
include Proxy
|
|
19
22
|
|
|
20
23
|
# The method given was not understood
|
|
21
24
|
class UnsupportedMethodError < RequestError; end
|
|
@@ -24,8 +27,9 @@ module HTTP
|
|
|
24
27
|
class UnsupportedSchemeError < RequestError; end
|
|
25
28
|
|
|
26
29
|
# Default User-Agent header value
|
|
27
|
-
USER_AGENT = "http.rb/#{HTTP::VERSION}"
|
|
30
|
+
USER_AGENT = "http.rb/#{HTTP::VERSION}".freeze
|
|
28
31
|
|
|
32
|
+
# Supported HTTP methods
|
|
29
33
|
METHODS = [
|
|
30
34
|
# RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
|
|
31
35
|
:options, :get, :head, :post, :put, :delete, :trace, :connect,
|
|
@@ -60,115 +64,176 @@ module HTTP
|
|
|
60
64
|
|
|
61
65
|
# Default ports of supported schemes
|
|
62
66
|
PORTS = {
|
|
63
|
-
:
|
|
64
|
-
:
|
|
65
|
-
:
|
|
66
|
-
:
|
|
67
|
+
http: 80,
|
|
68
|
+
https: 443,
|
|
69
|
+
ws: 80,
|
|
70
|
+
wss: 443
|
|
67
71
|
}.freeze
|
|
68
72
|
|
|
69
|
-
#
|
|
73
|
+
# HTTP method as a lowercase symbol
|
|
74
|
+
#
|
|
75
|
+
# @example
|
|
76
|
+
# request.verb # => :get
|
|
77
|
+
#
|
|
78
|
+
# @return [Symbol]
|
|
79
|
+
# @api public
|
|
70
80
|
attr_reader :verb
|
|
71
81
|
|
|
72
|
-
#
|
|
82
|
+
# URI scheme as a lowercase symbol
|
|
83
|
+
#
|
|
84
|
+
# @example
|
|
85
|
+
# request.scheme # => :https
|
|
86
|
+
#
|
|
87
|
+
# @return [Symbol]
|
|
88
|
+
# @api public
|
|
73
89
|
attr_reader :scheme
|
|
74
90
|
|
|
91
|
+
# URI normalizer callable
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# request.uri_normalizer
|
|
95
|
+
#
|
|
96
|
+
# @return [#call]
|
|
97
|
+
# @api public
|
|
75
98
|
attr_reader :uri_normalizer
|
|
76
99
|
|
|
77
|
-
#
|
|
78
|
-
#
|
|
100
|
+
# Request URI
|
|
101
|
+
#
|
|
102
|
+
# @example
|
|
103
|
+
# request.uri # => #<HTTP::URI ...>
|
|
104
|
+
#
|
|
105
|
+
# @return [HTTP::URI]
|
|
106
|
+
# @api public
|
|
79
107
|
attr_reader :uri
|
|
80
|
-
attr_reader :proxy, :body, :version
|
|
81
|
-
|
|
82
|
-
# @option opts [String] :version
|
|
83
|
-
# @option opts [#to_s] :verb HTTP request method
|
|
84
|
-
# @option opts [#call] :uri_normalizer (HTTP::URI::NORMALIZER)
|
|
85
|
-
# @option opts [HTTP::URI, #to_s] :uri
|
|
86
|
-
# @option opts [Hash] :headers
|
|
87
|
-
# @option opts [Hash] :proxy
|
|
88
|
-
# @option opts [String, Enumerable, IO, nil] :body
|
|
89
|
-
def initialize(opts)
|
|
90
|
-
@verb = opts.fetch(:verb).to_s.downcase.to_sym
|
|
91
|
-
@uri_normalizer = opts[:uri_normalizer] || HTTP::URI::NORMALIZER
|
|
92
|
-
|
|
93
|
-
@uri = @uri_normalizer.call(opts.fetch(:uri))
|
|
94
|
-
@scheme = @uri.scheme.to_s.downcase.to_sym if @uri.scheme
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
# Proxy configuration hash
|
|
110
|
+
#
|
|
111
|
+
# @example
|
|
112
|
+
# request.proxy
|
|
113
|
+
#
|
|
114
|
+
# @return [Hash]
|
|
115
|
+
# @api public
|
|
116
|
+
attr_reader :proxy
|
|
117
|
+
|
|
118
|
+
# Request body object
|
|
119
|
+
#
|
|
120
|
+
# @example
|
|
121
|
+
# request.body
|
|
122
|
+
#
|
|
123
|
+
# @return [HTTP::Request::Body]
|
|
124
|
+
# @api public
|
|
125
|
+
attr_reader :body
|
|
98
126
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
127
|
+
# The HTTP headers collection
|
|
128
|
+
#
|
|
129
|
+
# @example
|
|
130
|
+
# request.headers
|
|
131
|
+
#
|
|
132
|
+
# @return [HTTP::Headers]
|
|
133
|
+
# @api public
|
|
134
|
+
attr_reader :headers
|
|
135
|
+
|
|
136
|
+
# HTTP version string
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# request.version # => "1.1"
|
|
140
|
+
#
|
|
141
|
+
# @return [String]
|
|
142
|
+
# @api public
|
|
143
|
+
attr_reader :version
|
|
144
|
+
|
|
145
|
+
# Create a new HTTP request
|
|
146
|
+
#
|
|
147
|
+
# @param [#to_s] verb HTTP request method
|
|
148
|
+
# @param [HTTP::URI, #to_s] uri
|
|
149
|
+
# @param [Hash] headers
|
|
150
|
+
# @param [Hash] proxy
|
|
151
|
+
# @param [String, Enumerable, IO, nil] body
|
|
152
|
+
# @param [String] version
|
|
153
|
+
# @param [#call] uri_normalizer
|
|
154
|
+
#
|
|
155
|
+
# @example
|
|
156
|
+
# Request.new(verb: :get, uri: "https://example.com")
|
|
157
|
+
#
|
|
158
|
+
# @return [HTTP::Request]
|
|
159
|
+
# @api public
|
|
160
|
+
def initialize(verb:, uri:, headers: nil, proxy: {}, body: nil, version: "1.1",
|
|
161
|
+
uri_normalizer: nil)
|
|
162
|
+
@uri_normalizer = uri_normalizer || HTTP::URI::NORMALIZER
|
|
163
|
+
@verb = verb.to_s.downcase.to_sym
|
|
164
|
+
parse_uri!(uri)
|
|
165
|
+
validate_method_and_scheme!
|
|
166
|
+
|
|
167
|
+
@proxy = proxy
|
|
168
|
+
@version = version
|
|
169
|
+
@headers = prepare_headers(headers)
|
|
170
|
+
@body = prepare_body(body)
|
|
103
171
|
end
|
|
104
172
|
|
|
105
173
|
# Returns new Request with updated uri
|
|
174
|
+
#
|
|
175
|
+
# @example
|
|
176
|
+
# request.redirect("https://example.com/new")
|
|
177
|
+
#
|
|
178
|
+
# @return [HTTP::Request]
|
|
179
|
+
# @api public
|
|
106
180
|
def redirect(uri, verb = @verb)
|
|
107
|
-
|
|
108
|
-
headers
|
|
109
|
-
|
|
110
|
-
new_body = body.source
|
|
111
|
-
if verb == :get
|
|
112
|
-
# request bodies should not always be resubmitted when following a redirect
|
|
113
|
-
# some servers will close the connection after receiving the request headers
|
|
114
|
-
# which may cause Errno::ECONNRESET: Connection reset by peer
|
|
115
|
-
# see https://github.com/httprb/http/issues/649
|
|
116
|
-
# new_body = Request::Body.new(nil)
|
|
117
|
-
new_body = nil
|
|
118
|
-
# the CONTENT_TYPE header causes problems if set on a get request w/ an empty body
|
|
119
|
-
# the server might assume that there should be content if it is set to multipart
|
|
120
|
-
# rack raises EmptyContentError if this happens
|
|
121
|
-
headers.delete(Headers::CONTENT_TYPE)
|
|
122
|
-
end
|
|
181
|
+
redirect_uri = @uri.join(uri)
|
|
182
|
+
headers = redirect_headers(redirect_uri, verb)
|
|
183
|
+
new_body = verb == :get ? nil : body.source
|
|
123
184
|
|
|
124
185
|
self.class.new(
|
|
125
|
-
:
|
|
126
|
-
:
|
|
127
|
-
:
|
|
128
|
-
:
|
|
129
|
-
:
|
|
130
|
-
:
|
|
131
|
-
:
|
|
186
|
+
verb: verb,
|
|
187
|
+
uri: redirect_uri,
|
|
188
|
+
headers: headers,
|
|
189
|
+
proxy: proxy,
|
|
190
|
+
body: new_body,
|
|
191
|
+
version: version,
|
|
192
|
+
uri_normalizer: uri_normalizer
|
|
132
193
|
)
|
|
133
194
|
end
|
|
134
195
|
|
|
135
196
|
# Stream the request to a socket
|
|
197
|
+
#
|
|
198
|
+
# @example
|
|
199
|
+
# request.stream(socket)
|
|
200
|
+
#
|
|
201
|
+
# @return [void]
|
|
202
|
+
# @api public
|
|
136
203
|
def stream(socket)
|
|
137
204
|
include_proxy_headers if using_proxy? && !@uri.https?
|
|
138
205
|
Request::Writer.new(socket, body, headers, headline).stream
|
|
139
206
|
end
|
|
140
207
|
|
|
141
208
|
# Is this request using a proxy?
|
|
209
|
+
#
|
|
210
|
+
# @example
|
|
211
|
+
# request.using_proxy?
|
|
212
|
+
#
|
|
213
|
+
# @return [Boolean]
|
|
214
|
+
# @api public
|
|
142
215
|
def using_proxy?
|
|
143
216
|
proxy && proxy.keys.size >= 2
|
|
144
217
|
end
|
|
145
218
|
|
|
146
219
|
# Is this request using an authenticated proxy?
|
|
220
|
+
#
|
|
221
|
+
# @example
|
|
222
|
+
# request.using_authenticated_proxy?
|
|
223
|
+
#
|
|
224
|
+
# @return [Boolean]
|
|
225
|
+
# @api public
|
|
147
226
|
def using_authenticated_proxy?
|
|
148
227
|
proxy && proxy.keys.size >= 4
|
|
149
228
|
end
|
|
150
229
|
|
|
151
|
-
def include_proxy_headers
|
|
152
|
-
headers.merge!(proxy[:proxy_headers]) if proxy.key?(:proxy_headers)
|
|
153
|
-
include_proxy_authorization_header if using_authenticated_proxy?
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# Compute and add the Proxy-Authorization header
|
|
157
|
-
def include_proxy_authorization_header
|
|
158
|
-
headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def proxy_authorization_header
|
|
162
|
-
digest = Base64.strict_encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}")
|
|
163
|
-
"Basic #{digest}"
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Setup tunnel through proxy for SSL request
|
|
167
|
-
def connect_using_proxy(socket)
|
|
168
|
-
Request::Writer.new(socket, nil, proxy_connect_headers, proxy_connect_header).connect_through_proxy
|
|
169
|
-
end
|
|
170
|
-
|
|
171
230
|
# Compute HTTP request header for direct or proxy request
|
|
231
|
+
#
|
|
232
|
+
# @example
|
|
233
|
+
# request.headline
|
|
234
|
+
#
|
|
235
|
+
# @return [String]
|
|
236
|
+
# @api public
|
|
172
237
|
def headline
|
|
173
238
|
request_uri =
|
|
174
239
|
if using_proxy? && !uri.https?
|
|
@@ -182,34 +247,29 @@ module HTTP
|
|
|
182
247
|
"#{verb.to_s.upcase} #{request_uri} HTTP/#{version}"
|
|
183
248
|
end
|
|
184
249
|
|
|
185
|
-
# Compute HTTP request header SSL proxy connection
|
|
186
|
-
def proxy_connect_header
|
|
187
|
-
"CONNECT #{host}:#{port} HTTP/#{version}"
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Headers to send with proxy connect request
|
|
191
|
-
def proxy_connect_headers
|
|
192
|
-
connect_headers = HTTP::Headers.coerce(
|
|
193
|
-
Headers::HOST => headers[Headers::HOST],
|
|
194
|
-
Headers::USER_AGENT => headers[Headers::USER_AGENT]
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
connect_headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header if using_authenticated_proxy?
|
|
198
|
-
connect_headers.merge!(proxy[:proxy_headers]) if proxy.key?(:proxy_headers)
|
|
199
|
-
connect_headers
|
|
200
|
-
end
|
|
201
|
-
|
|
202
250
|
# Host for tcp socket
|
|
251
|
+
#
|
|
252
|
+
# @example
|
|
253
|
+
# request.socket_host
|
|
254
|
+
#
|
|
255
|
+
# @return [String]
|
|
256
|
+
# @api public
|
|
203
257
|
def socket_host
|
|
204
258
|
using_proxy? ? proxy[:proxy_address] : host
|
|
205
259
|
end
|
|
206
260
|
|
|
207
261
|
# Port for tcp socket
|
|
262
|
+
#
|
|
263
|
+
# @example
|
|
264
|
+
# request.socket_port
|
|
265
|
+
#
|
|
266
|
+
# @return [Integer]
|
|
267
|
+
# @api public
|
|
208
268
|
def socket_port
|
|
209
269
|
using_proxy? ? proxy[:proxy_port] : port
|
|
210
270
|
end
|
|
211
271
|
|
|
212
|
-
# Human-readable representation of base request info
|
|
272
|
+
# Human-readable representation of base request info
|
|
213
273
|
#
|
|
214
274
|
# @example
|
|
215
275
|
#
|
|
@@ -217,23 +277,54 @@ module HTTP
|
|
|
217
277
|
# # => #<HTTP::Request/1.1 GET https://example.com>
|
|
218
278
|
#
|
|
219
279
|
# @return [String]
|
|
280
|
+
# @api public
|
|
220
281
|
def inspect
|
|
221
|
-
"
|
|
282
|
+
format("#<%s/%s %s %s>", self.class, @version, String(verb).upcase, uri)
|
|
222
283
|
end
|
|
223
284
|
|
|
224
285
|
private
|
|
225
286
|
|
|
287
|
+
# Build headers for a redirect request
|
|
288
|
+
#
|
|
289
|
+
# Strips the Host header (it will be regenerated), sensitive credentials
|
|
290
|
+
# on cross-origin redirects, and Content-Type on GET verb changes.
|
|
291
|
+
#
|
|
292
|
+
# @param [HTTP::URI] redirect_uri the target URI
|
|
293
|
+
# @param [Symbol] verb the HTTP verb for the redirected request
|
|
294
|
+
# @return [HTTP::Headers] headers for the redirect
|
|
295
|
+
# @api private
|
|
296
|
+
def redirect_headers(redirect_uri, verb)
|
|
297
|
+
headers = self.headers.dup
|
|
298
|
+
headers.delete(Headers::HOST)
|
|
299
|
+
|
|
300
|
+
# Strip sensitive headers when redirecting to a different origin
|
|
301
|
+
# (scheme + host + port) to prevent credential leakage.
|
|
302
|
+
unless @uri.origin == redirect_uri.origin
|
|
303
|
+
headers.delete(Headers::AUTHORIZATION)
|
|
304
|
+
headers.delete(Headers::COOKIE)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
headers.delete(Headers::CONTENT_TYPE) if verb == :get
|
|
308
|
+
|
|
309
|
+
headers
|
|
310
|
+
end
|
|
311
|
+
|
|
226
312
|
# @!attribute [r] host
|
|
313
|
+
# Host from the URI
|
|
227
314
|
# @return [String]
|
|
315
|
+
# @api private
|
|
228
316
|
def_delegator :@uri, :host
|
|
229
317
|
|
|
230
|
-
#
|
|
231
|
-
#
|
|
318
|
+
# Return the port for the request URI
|
|
319
|
+
# @return [Fixnum]
|
|
320
|
+
# @api private
|
|
232
321
|
def port
|
|
233
322
|
@uri.port || @uri.default_port
|
|
234
323
|
end
|
|
235
324
|
|
|
236
|
-
#
|
|
325
|
+
# Build default Host header value
|
|
326
|
+
# @return [String]
|
|
327
|
+
# @api private
|
|
237
328
|
def default_host_header_value
|
|
238
329
|
value = PORTS[@scheme] == port ? host : "#{host}:#{port}"
|
|
239
330
|
|
|
@@ -242,12 +333,38 @@ module HTTP
|
|
|
242
333
|
value
|
|
243
334
|
end
|
|
244
335
|
|
|
336
|
+
# Parse and normalize the URI, setting scheme
|
|
337
|
+
# @return [void]
|
|
338
|
+
# @api private
|
|
339
|
+
def parse_uri!(uri)
|
|
340
|
+
raise ArgumentError, "uri is nil" if uri.nil?
|
|
341
|
+
raise ArgumentError, "uri is empty" if uri.is_a?(String) && uri.empty?
|
|
342
|
+
|
|
343
|
+
@uri = @uri_normalizer.call(uri)
|
|
344
|
+
@scheme = String(@uri.scheme).downcase.to_sym if @uri.scheme
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Validate HTTP method and URI scheme
|
|
348
|
+
# @return [void]
|
|
349
|
+
# @api private
|
|
350
|
+
def validate_method_and_scheme!
|
|
351
|
+
raise(UnsupportedMethodError, "unknown method: #{verb}") unless METHODS.include?(@verb)
|
|
352
|
+
raise(HTTP::URI::InvalidError, "invalid URI: #{@uri}") unless @scheme
|
|
353
|
+
raise(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Coerce input into a Body object
|
|
357
|
+
# @return [HTTP::Request::Body]
|
|
358
|
+
# @api private
|
|
245
359
|
def prepare_body(body)
|
|
246
|
-
body.is_a?(
|
|
360
|
+
body.is_a?(Body) ? body : Body.new(body)
|
|
247
361
|
end
|
|
248
362
|
|
|
363
|
+
# Build headers with default values
|
|
364
|
+
# @return [HTTP::Headers]
|
|
365
|
+
# @api private
|
|
249
366
|
def prepare_headers(headers)
|
|
250
|
-
headers =
|
|
367
|
+
headers = Headers.coerce(headers || {})
|
|
251
368
|
|
|
252
369
|
headers[Headers::HOST] ||= default_host_header_value
|
|
253
370
|
headers[Headers::USER_AGENT] ||= USER_AGENT
|