http 3.1.0 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +67 -0
- data/.gitignore +6 -9
- data/.rspec +0 -4
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/metrics.yml +4 -0
- data/.rubocop/rspec.yml +9 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop.yml +9 -108
- data/.rubocop_todo.yml +219 -0
- data/.yardopts +1 -1
- data/CHANGELOG.md +67 -0
- data/{CHANGES.md → CHANGES_OLD.md} +358 -0
- data/Gemfile +19 -10
- data/LICENSE.txt +1 -1
- data/README.md +53 -85
- data/Rakefile +3 -11
- data/SECURITY.md +17 -0
- data/http.gemspec +15 -6
- data/lib/http/base64.rb +12 -0
- data/lib/http/chainable.rb +71 -41
- data/lib/http/client.rb +73 -52
- data/lib/http/connection.rb +28 -18
- data/lib/http/content_type.rb +12 -7
- data/lib/http/errors.rb +19 -0
- data/lib/http/feature.rb +18 -1
- data/lib/http/features/auto_deflate.rb +27 -6
- data/lib/http/features/auto_inflate.rb +32 -6
- data/lib/http/features/instrumentation.rb +69 -0
- data/lib/http/features/logging.rb +53 -0
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +22 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +69 -0
- data/lib/http/headers.rb +72 -49
- data/lib/http/mime_type/adapter.rb +3 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +31 -28
- data/lib/http/redirector.rb +56 -4
- data/lib/http/request/body.rb +31 -0
- data/lib/http/request/writer.rb +29 -9
- data/lib/http/request.rb +76 -41
- data/lib/http/response/body.rb +6 -4
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +78 -26
- data/lib/http/response/status.rb +4 -3
- data/lib/http/response.rb +45 -27
- data/lib/http/retriable/client.rb +37 -0
- data/lib/http/retriable/delay_calculator.rb +64 -0
- data/lib/http/retriable/errors.rb +14 -0
- data/lib/http/retriable/performer.rb +153 -0
- data/lib/http/timeout/global.rb +29 -47
- data/lib/http/timeout/null.rb +12 -8
- data/lib/http/timeout/per_operation.rb +32 -57
- data/lib/http/uri.rb +75 -1
- data/lib/http/version.rb +1 -1
- data/lib/http.rb +2 -2
- data/spec/lib/http/client_spec.rb +189 -36
- data/spec/lib/http/connection_spec.rb +31 -6
- data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
- data/spec/lib/http/features/instrumentation_spec.rb +81 -0
- data/spec/lib/http/features/logging_spec.rb +65 -0
- data/spec/lib/http/features/raise_error_spec.rb +62 -0
- data/spec/lib/http/headers/normalizer_spec.rb +52 -0
- data/spec/lib/http/headers_spec.rb +53 -18
- data/spec/lib/http/options/headers_spec.rb +6 -2
- data/spec/lib/http/options/merge_spec.rb +16 -16
- data/spec/lib/http/redirector_spec.rb +147 -3
- data/spec/lib/http/request/body_spec.rb +71 -4
- data/spec/lib/http/request/writer_spec.rb +45 -2
- data/spec/lib/http/request_spec.rb +11 -5
- data/spec/lib/http/response/body_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +74 -0
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +83 -7
- data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
- data/spec/lib/http/retriable/performer_spec.rb +302 -0
- data/spec/lib/http/uri/normalizer_spec.rb +95 -0
- data/spec/lib/http/uri_spec.rb +39 -0
- data/spec/lib/http_spec.rb +121 -68
- data/spec/regression_specs.rb +7 -0
- data/spec/spec_helper.rb +22 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server/servlet.rb +42 -11
- data/spec/support/dummy_server.rb +9 -8
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/http_handling_shared.rb +62 -66
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +66 -27
- data/.coveralls.yml +0 -1
- data/.ruby-version +0 -1
- data/.travis.yml +0 -36
data/lib/http/options.rb
CHANGED
|
@@ -1,46 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# rubocop:disable Metrics/ClassLength, Style/RedundantSelf
|
|
4
|
-
|
|
5
3
|
require "http/headers"
|
|
6
4
|
require "openssl"
|
|
7
5
|
require "socket"
|
|
8
6
|
require "http/uri"
|
|
9
|
-
require "http/feature"
|
|
10
|
-
require "http/features/auto_inflate"
|
|
11
|
-
require "http/features/auto_deflate"
|
|
12
7
|
|
|
13
8
|
module HTTP
|
|
14
|
-
class Options
|
|
9
|
+
class Options # rubocop:disable Metrics/ClassLength
|
|
15
10
|
@default_socket_class = TCPSocket
|
|
16
11
|
@default_ssl_socket_class = OpenSSL::SSL::SSLSocket
|
|
17
12
|
@default_timeout_class = HTTP::Timeout::Null
|
|
18
|
-
@available_features = {
|
|
19
|
-
:auto_inflate => Features::AutoInflate,
|
|
20
|
-
:auto_deflate => Features::AutoDeflate
|
|
21
|
-
}
|
|
13
|
+
@available_features = {}
|
|
22
14
|
|
|
23
15
|
class << self
|
|
24
16
|
attr_accessor :default_socket_class, :default_ssl_socket_class, :default_timeout_class
|
|
25
17
|
attr_reader :available_features
|
|
26
18
|
|
|
27
|
-
def new(options = {})
|
|
28
|
-
|
|
29
|
-
super
|
|
19
|
+
def new(options = {})
|
|
20
|
+
options.is_a?(self) ? options : super
|
|
30
21
|
end
|
|
31
22
|
|
|
32
23
|
def defined_options
|
|
33
24
|
@defined_options ||= []
|
|
34
25
|
end
|
|
35
26
|
|
|
27
|
+
def register_feature(name, impl)
|
|
28
|
+
@available_features[name] = impl
|
|
29
|
+
end
|
|
30
|
+
|
|
36
31
|
protected
|
|
37
32
|
|
|
38
|
-
def def_option(name, &interpreter)
|
|
33
|
+
def def_option(name, reader_only: false, &interpreter)
|
|
39
34
|
defined_options << name.to_sym
|
|
40
|
-
interpreter ||=
|
|
35
|
+
interpreter ||= ->(v) { v }
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
if reader_only
|
|
38
|
+
attr_reader name
|
|
39
|
+
else
|
|
40
|
+
attr_accessor name
|
|
41
|
+
protected :"#{name}="
|
|
42
|
+
end
|
|
44
43
|
|
|
45
44
|
define_method(:"with_#{name}") do |value|
|
|
46
45
|
dup { |opts| opts.send(:"#{name}=", instance_exec(value, &interpreter)) }
|
|
@@ -48,7 +47,7 @@ module HTTP
|
|
|
48
47
|
end
|
|
49
48
|
end
|
|
50
49
|
|
|
51
|
-
def initialize(options = {})
|
|
50
|
+
def initialize(options = {})
|
|
52
51
|
defaults = {
|
|
53
52
|
:response => :auto,
|
|
54
53
|
:proxy => {},
|
|
@@ -70,12 +69,12 @@ module HTTP
|
|
|
70
69
|
opts_w_defaults.each { |(k, v)| self[k] = v }
|
|
71
70
|
end
|
|
72
71
|
|
|
73
|
-
def_option :headers do |
|
|
74
|
-
|
|
72
|
+
def_option :headers do |new_headers|
|
|
73
|
+
headers.merge(new_headers)
|
|
75
74
|
end
|
|
76
75
|
|
|
77
|
-
def_option :cookies do |
|
|
78
|
-
|
|
76
|
+
def_option :cookies do |new_cookies|
|
|
77
|
+
new_cookies.each_with_object cookies.dup do |(k, v), jar|
|
|
79
78
|
cookie = k.is_a?(Cookie) ? k : Cookie.new(k.to_s, v.to_s)
|
|
80
79
|
jar[cookie.name] = cookie.cookie_value
|
|
81
80
|
end
|
|
@@ -85,7 +84,7 @@ module HTTP
|
|
|
85
84
|
self.encoding = Encoding.find(encoding)
|
|
86
85
|
end
|
|
87
86
|
|
|
88
|
-
def_option :features do |
|
|
87
|
+
def_option :features, :reader_only => true do |new_features|
|
|
89
88
|
# Normalize features from:
|
|
90
89
|
#
|
|
91
90
|
# [{feature_one: {opt: 'val'}}, :feature_two]
|
|
@@ -93,7 +92,7 @@ module HTTP
|
|
|
93
92
|
# into:
|
|
94
93
|
#
|
|
95
94
|
# {feature_one: {opt: 'val'}, feature_two: {}}
|
|
96
|
-
|
|
95
|
+
normalized_features = new_features.each_with_object({}) do |feature, h|
|
|
97
96
|
if feature.is_a?(Hash)
|
|
98
97
|
h.merge!(feature)
|
|
99
98
|
else
|
|
@@ -101,7 +100,7 @@ module HTTP
|
|
|
101
100
|
end
|
|
102
101
|
end
|
|
103
102
|
|
|
104
|
-
|
|
103
|
+
features.merge(normalized_features)
|
|
105
104
|
end
|
|
106
105
|
|
|
107
106
|
def features=(features)
|
|
@@ -112,19 +111,21 @@ module HTTP
|
|
|
112
111
|
unless (feature = self.class.available_features[name])
|
|
113
112
|
argument_error! "Unsupported feature: #{name}"
|
|
114
113
|
end
|
|
115
|
-
feature.new(opts_or_feature)
|
|
114
|
+
feature.new(**opts_or_feature)
|
|
116
115
|
end
|
|
117
116
|
end
|
|
118
117
|
end
|
|
119
118
|
|
|
120
119
|
%w[
|
|
121
|
-
proxy params form json body
|
|
120
|
+
proxy params form json body response
|
|
122
121
|
socket_class nodelay ssl_socket_class ssl_context ssl
|
|
123
|
-
|
|
122
|
+
keep_alive_timeout timeout_class timeout_options
|
|
124
123
|
].each do |method_name|
|
|
125
124
|
def_option method_name
|
|
126
125
|
end
|
|
127
126
|
|
|
127
|
+
def_option :follow, :reader_only => true
|
|
128
|
+
|
|
128
129
|
def follow=(value)
|
|
129
130
|
@follow =
|
|
130
131
|
case
|
|
@@ -135,6 +136,8 @@ module HTTP
|
|
|
135
136
|
end
|
|
136
137
|
end
|
|
137
138
|
|
|
139
|
+
def_option :persistent, :reader_only => true
|
|
140
|
+
|
|
138
141
|
def persistent=(value)
|
|
139
142
|
@persistent = value ? HTTP::URI.parse(value).origin : nil
|
|
140
143
|
end
|
data/lib/http/redirector.rb
CHANGED
|
@@ -39,9 +39,10 @@ module HTTP
|
|
|
39
39
|
# @param [Hash] opts
|
|
40
40
|
# @option opts [Boolean] :strict (true) redirector hops policy
|
|
41
41
|
# @option opts [#to_i] :max_hops (5) maximum allowed amount of hops
|
|
42
|
-
def initialize(opts = {})
|
|
43
|
-
@strict
|
|
44
|
-
@max_hops
|
|
42
|
+
def initialize(opts = {})
|
|
43
|
+
@strict = opts.fetch(:strict, true)
|
|
44
|
+
@max_hops = opts.fetch(:max_hops, 5).to_i
|
|
45
|
+
@on_redirect = opts.fetch(:on_redirect, nil)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
# Follows redirects until non-redirect response found
|
|
@@ -49,6 +50,8 @@ module HTTP
|
|
|
49
50
|
@request = request
|
|
50
51
|
@response = response
|
|
51
52
|
@visited = []
|
|
53
|
+
collect_cookies_from_request
|
|
54
|
+
collect_cookies_from_response
|
|
52
55
|
|
|
53
56
|
while REDIRECT_CODES.include? @response.status.code
|
|
54
57
|
@visited << "#{@request.verb} #{@request.uri}"
|
|
@@ -58,8 +61,14 @@ module HTTP
|
|
|
58
61
|
|
|
59
62
|
@response.flush
|
|
60
63
|
|
|
61
|
-
|
|
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)
|
|
62
70
|
@response = yield @request
|
|
71
|
+
collect_cookies_from_response
|
|
63
72
|
end
|
|
64
73
|
|
|
65
74
|
@response
|
|
@@ -67,6 +76,48 @@ module HTTP
|
|
|
67
76
|
|
|
68
77
|
private
|
|
69
78
|
|
|
79
|
+
# All known cookies. On the original request, this is only the original cookies, but after that,
|
|
80
|
+
# Set-Cookie headers can add, set or delete cookies.
|
|
81
|
+
def cookie_jar
|
|
82
|
+
# it seems that @response.cookies instance is reused between responses, so we have to "clone"
|
|
83
|
+
@cookie_jar ||= HTTP::CookieJar.new
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def collect_cookies_from_request
|
|
87
|
+
request_cookie_header = @request.headers["Cookie"]
|
|
88
|
+
cookies =
|
|
89
|
+
if request_cookie_header
|
|
90
|
+
HTTP::Cookie.cookie_value_to_hash(request_cookie_header)
|
|
91
|
+
else
|
|
92
|
+
{}
|
|
93
|
+
end
|
|
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
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Carry cookies from one response to the next. Carrying cookies to the next response ends up
|
|
101
|
+
# carrying them to the next request as well.
|
|
102
|
+
#
|
|
103
|
+
# Note that this isn't part of the IETF standard, but all major browsers support setting cookies
|
|
104
|
+
# on redirect: https://blog.dubbelboer.com/2012/11/25/302-cookie.html
|
|
105
|
+
def collect_cookies_from_response
|
|
106
|
+
# Overwrite previous cookies
|
|
107
|
+
@response.cookies.each do |cookie|
|
|
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
|
|
119
|
+
end
|
|
120
|
+
|
|
70
121
|
# Check if we reached max amount of redirect hops
|
|
71
122
|
# @return [Boolean]
|
|
72
123
|
def too_many_hops?
|
|
@@ -89,6 +140,7 @@ module HTTP
|
|
|
89
140
|
|
|
90
141
|
if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code)
|
|
91
142
|
raise StateError, "can't follow #{@response.status} redirect" if @strict
|
|
143
|
+
|
|
92
144
|
verb = :get
|
|
93
145
|
end
|
|
94
146
|
|
data/lib/http/request/body.rb
CHANGED
|
@@ -19,6 +19,7 @@ module HTTP
|
|
|
19
19
|
@source.bytesize
|
|
20
20
|
elsif @source.respond_to?(:read)
|
|
21
21
|
raise RequestError, "IO object must respond to #size" unless @source.respond_to?(:size)
|
|
22
|
+
|
|
22
23
|
@source.size
|
|
23
24
|
elsif @source.nil?
|
|
24
25
|
0
|
|
@@ -35,13 +36,43 @@ module HTTP
|
|
|
35
36
|
yield @source
|
|
36
37
|
elsif @source.respond_to?(:read)
|
|
37
38
|
IO.copy_stream(@source, ProcIO.new(block))
|
|
39
|
+
rewind(@source)
|
|
38
40
|
elsif @source.is_a?(Enumerable)
|
|
39
41
|
@source.each(&block)
|
|
40
42
|
end
|
|
43
|
+
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Request bodies are equivalent when they have the same source.
|
|
48
|
+
def ==(other)
|
|
49
|
+
self.class == other.class && self.source == other.source # rubocop:disable Style/RedundantSelf
|
|
41
50
|
end
|
|
42
51
|
|
|
43
52
|
private
|
|
44
53
|
|
|
54
|
+
def rewind(io)
|
|
55
|
+
io.rewind if io.respond_to? :rewind
|
|
56
|
+
rescue Errno::ESPIPE, Errno::EPIPE
|
|
57
|
+
# Pipe IOs respond to `:rewind` but fail when you call it.
|
|
58
|
+
#
|
|
59
|
+
# Calling `IO#rewind` on a pipe, fails with *ESPIPE* on MRI,
|
|
60
|
+
# but *EPIPE* on jRuby.
|
|
61
|
+
#
|
|
62
|
+
# - **ESPIPE** -- "Illegal seek."
|
|
63
|
+
# Invalid seek operation (such as on a pipe).
|
|
64
|
+
#
|
|
65
|
+
# - **EPIPE** -- "Broken pipe."
|
|
66
|
+
# There is no process reading from the other end of a pipe. Every
|
|
67
|
+
# library function that returns this error code also generates
|
|
68
|
+
# a SIGPIPE signal; this signal terminates the program if not handled
|
|
69
|
+
# or blocked. Thus, your program will never actually see EPIPE unless
|
|
70
|
+
# it has handled or blocked SIGPIPE.
|
|
71
|
+
#
|
|
72
|
+
# See: https://www.gnu.org/software/libc/manual/html_node/Error-Codes.html
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
45
76
|
def validate_source_type!
|
|
46
77
|
return if @source.is_a?(String)
|
|
47
78
|
return if @source.respond_to?(:read)
|
data/lib/http/request/writer.rb
CHANGED
|
@@ -47,7 +47,11 @@ module HTTP
|
|
|
47
47
|
# Adds the headers to the header array for the given request body we are working
|
|
48
48
|
# with
|
|
49
49
|
def add_body_type_headers
|
|
50
|
-
return if @headers[Headers::CONTENT_LENGTH] || chunked?
|
|
50
|
+
return if @headers[Headers::CONTENT_LENGTH] || chunked? || (
|
|
51
|
+
@body.source.nil? && %w[GET HEAD DELETE CONNECT].any? do |method|
|
|
52
|
+
@request_header[0].start_with?("#{method} ")
|
|
53
|
+
end
|
|
54
|
+
)
|
|
51
55
|
|
|
52
56
|
@request_header << "#{Headers::CONTENT_LENGTH}: #{@body.size}"
|
|
53
57
|
end
|
|
@@ -57,25 +61,35 @@ module HTTP
|
|
|
57
61
|
def join_headers
|
|
58
62
|
# join the headers array with crlfs, stick two on the end because
|
|
59
63
|
# that ends the request header
|
|
60
|
-
@request_header.join(CRLF) + CRLF * 2
|
|
64
|
+
@request_header.join(CRLF) + (CRLF * 2)
|
|
61
65
|
end
|
|
62
66
|
|
|
67
|
+
# Writes HTTP request data into the socket.
|
|
63
68
|
def send_request
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
#
|
|
67
|
-
|
|
69
|
+
each_chunk { |chunk| write chunk }
|
|
70
|
+
rescue Errno::EPIPE
|
|
71
|
+
# server doesn't need any more data
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Yields chunks of request data that should be sent to the socket.
|
|
76
|
+
#
|
|
77
|
+
# It's important to send the request in a single write call when possible
|
|
78
|
+
# in order to play nicely with Nagle's algorithm. Making two writes in a
|
|
79
|
+
# row triggers a pathological case where Nagle is expecting a third write
|
|
80
|
+
# that never happens.
|
|
81
|
+
def each_chunk
|
|
68
82
|
data = join_headers
|
|
69
83
|
|
|
70
84
|
@body.each do |chunk|
|
|
71
85
|
data << encode_chunk(chunk)
|
|
72
|
-
|
|
86
|
+
yield data
|
|
73
87
|
data.clear
|
|
74
88
|
end
|
|
75
89
|
|
|
76
|
-
|
|
90
|
+
yield data unless data.empty?
|
|
77
91
|
|
|
78
|
-
|
|
92
|
+
yield CHUNKED_END if chunked?
|
|
79
93
|
end
|
|
80
94
|
|
|
81
95
|
# Returns the chunk encoded for to the specified "Transfer-Encoding" header.
|
|
@@ -94,12 +108,18 @@ module HTTP
|
|
|
94
108
|
|
|
95
109
|
private
|
|
96
110
|
|
|
111
|
+
# @raise [SocketWriteError] when unable to write to socket
|
|
97
112
|
def write(data)
|
|
98
113
|
until data.empty?
|
|
99
114
|
length = @socket.write(data)
|
|
100
115
|
break unless data.bytesize > length
|
|
116
|
+
|
|
101
117
|
data = data.byteslice(length..-1)
|
|
102
118
|
end
|
|
119
|
+
rescue Errno::EPIPE
|
|
120
|
+
raise
|
|
121
|
+
rescue IOError, SocketError, SystemCallError => e
|
|
122
|
+
raise SocketWriteError, "error writing to socket: #{e}", e.backtrace
|
|
103
123
|
end
|
|
104
124
|
end
|
|
105
125
|
end
|
data/lib/http/request.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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"
|
|
@@ -15,6 +15,7 @@ module HTTP
|
|
|
15
15
|
class Request
|
|
16
16
|
extend Forwardable
|
|
17
17
|
|
|
18
|
+
include HTTP::Base64
|
|
18
19
|
include HTTP::Headers::Mixin
|
|
19
20
|
|
|
20
21
|
# The method given was not understood
|
|
@@ -46,7 +47,13 @@ module HTTP
|
|
|
46
47
|
:patch,
|
|
47
48
|
|
|
48
49
|
# draft-reschke-webdav-search: WebDAV Search
|
|
49
|
-
:search
|
|
50
|
+
:search,
|
|
51
|
+
|
|
52
|
+
# RFC 4791: Calendaring Extensions to WebDAV -- CalDAV
|
|
53
|
+
:mkcalendar,
|
|
54
|
+
|
|
55
|
+
# Implemented by several caching servers, like Squid, Varnish or Fastly
|
|
56
|
+
:purge
|
|
50
57
|
].freeze
|
|
51
58
|
|
|
52
59
|
# Allowed schemes
|
|
@@ -54,10 +61,10 @@ module HTTP
|
|
|
54
61
|
|
|
55
62
|
# Default ports of supported schemes
|
|
56
63
|
PORTS = {
|
|
57
|
-
:http
|
|
58
|
-
:https
|
|
59
|
-
:ws
|
|
60
|
-
:wss
|
|
64
|
+
:http => 80,
|
|
65
|
+
:https => 443,
|
|
66
|
+
:ws => 80,
|
|
67
|
+
:wss => 443
|
|
61
68
|
}.freeze
|
|
62
69
|
|
|
63
70
|
# Method is given as a lowercase symbol e.g. :get, :post
|
|
@@ -66,6 +73,8 @@ module HTTP
|
|
|
66
73
|
# Scheme is normalized to be a lowercase symbol e.g. :http, :https
|
|
67
74
|
attr_reader :scheme
|
|
68
75
|
|
|
76
|
+
attr_reader :uri_normalizer
|
|
77
|
+
|
|
69
78
|
# "Request URI" as per RFC 2616
|
|
70
79
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
|
|
71
80
|
attr_reader :uri
|
|
@@ -73,25 +82,25 @@ module HTTP
|
|
|
73
82
|
|
|
74
83
|
# @option opts [String] :version
|
|
75
84
|
# @option opts [#to_s] :verb HTTP request method
|
|
85
|
+
# @option opts [#call] :uri_normalizer (HTTP::URI::NORMALIZER)
|
|
76
86
|
# @option opts [HTTP::URI, #to_s] :uri
|
|
77
87
|
# @option opts [Hash] :headers
|
|
78
88
|
# @option opts [Hash] :proxy
|
|
79
89
|
# @option opts [String, Enumerable, IO, nil] :body
|
|
80
90
|
def initialize(opts)
|
|
81
|
-
@verb
|
|
82
|
-
@
|
|
91
|
+
@verb = opts.fetch(:verb).to_s.downcase.to_sym
|
|
92
|
+
@uri_normalizer = opts[:uri_normalizer] || HTTP::URI::NORMALIZER
|
|
93
|
+
|
|
94
|
+
@uri = @uri_normalizer.call(opts.fetch(:uri))
|
|
83
95
|
@scheme = @uri.scheme.to_s.downcase.to_sym if @uri.scheme
|
|
84
96
|
|
|
85
97
|
raise(UnsupportedMethodError, "unknown method: #{verb}") unless METHODS.include?(@verb)
|
|
86
98
|
raise(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme)
|
|
87
99
|
|
|
88
100
|
@proxy = opts[:proxy] || {}
|
|
89
|
-
@body = request_body(opts[:body], opts)
|
|
90
101
|
@version = opts[:version] || "1.1"
|
|
91
|
-
@headers =
|
|
92
|
-
|
|
93
|
-
@headers[Headers::HOST] ||= default_host_header_value
|
|
94
|
-
@headers[Headers::USER_AGENT] ||= USER_AGENT
|
|
102
|
+
@headers = prepare_headers(opts[:headers])
|
|
103
|
+
@body = prepare_body(opts[:body])
|
|
95
104
|
end
|
|
96
105
|
|
|
97
106
|
# Returns new Request with updated uri
|
|
@@ -99,13 +108,28 @@ module HTTP
|
|
|
99
108
|
headers = self.headers.dup
|
|
100
109
|
headers.delete(Headers::HOST)
|
|
101
110
|
|
|
111
|
+
new_body = body.source
|
|
112
|
+
if verb == :get
|
|
113
|
+
# request bodies should not always be resubmitted when following a redirect
|
|
114
|
+
# some servers will close the connection after receiving the request headers
|
|
115
|
+
# which may cause Errno::ECONNRESET: Connection reset by peer
|
|
116
|
+
# see https://github.com/httprb/http/issues/649
|
|
117
|
+
# new_body = Request::Body.new(nil)
|
|
118
|
+
new_body = nil
|
|
119
|
+
# the CONTENT_TYPE header causes problems if set on a get request w/ an empty body
|
|
120
|
+
# the server might assume that there should be content if it is set to multipart
|
|
121
|
+
# rack raises EmptyContentError if this happens
|
|
122
|
+
headers.delete(Headers::CONTENT_TYPE)
|
|
123
|
+
end
|
|
124
|
+
|
|
102
125
|
self.class.new(
|
|
103
|
-
:verb
|
|
104
|
-
:uri
|
|
105
|
-
:headers
|
|
106
|
-
:proxy
|
|
107
|
-
:body
|
|
108
|
-
:version
|
|
126
|
+
:verb => verb,
|
|
127
|
+
:uri => @uri.join(uri),
|
|
128
|
+
:headers => headers,
|
|
129
|
+
:proxy => proxy,
|
|
130
|
+
:body => new_body,
|
|
131
|
+
:version => version,
|
|
132
|
+
:uri_normalizer => uri_normalizer
|
|
109
133
|
)
|
|
110
134
|
end
|
|
111
135
|
|
|
@@ -136,7 +160,7 @@ module HTTP
|
|
|
136
160
|
end
|
|
137
161
|
|
|
138
162
|
def proxy_authorization_header
|
|
139
|
-
digest =
|
|
163
|
+
digest = encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}")
|
|
140
164
|
"Basic #{digest}"
|
|
141
165
|
end
|
|
142
166
|
|
|
@@ -152,7 +176,9 @@ module HTTP
|
|
|
152
176
|
uri.omit(:fragment)
|
|
153
177
|
else
|
|
154
178
|
uri.request_uri
|
|
155
|
-
end
|
|
179
|
+
end.to_s
|
|
180
|
+
|
|
181
|
+
raise RequestError, "Invalid request URI: #{request_uri.inspect}" if request_uri.match?(/\s/)
|
|
156
182
|
|
|
157
183
|
"#{verb.to_s.upcase} #{request_uri} HTTP/#{version}"
|
|
158
184
|
end
|
|
@@ -165,8 +191,8 @@ module HTTP
|
|
|
165
191
|
# Headers to send with proxy connect request
|
|
166
192
|
def proxy_connect_headers
|
|
167
193
|
connect_headers = HTTP::Headers.coerce(
|
|
168
|
-
Headers::HOST
|
|
169
|
-
Headers::USER_AGENT
|
|
194
|
+
Headers::HOST => headers[Headers::HOST],
|
|
195
|
+
Headers::USER_AGENT => headers[Headers::USER_AGENT]
|
|
170
196
|
)
|
|
171
197
|
|
|
172
198
|
connect_headers[Headers::PROXY_AUTHORIZATION] = proxy_authorization_header if using_authenticated_proxy?
|
|
@@ -184,15 +210,20 @@ module HTTP
|
|
|
184
210
|
using_proxy? ? proxy[:proxy_port] : port
|
|
185
211
|
end
|
|
186
212
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
213
|
+
# Human-readable representation of base request info.
|
|
214
|
+
#
|
|
215
|
+
# @example
|
|
216
|
+
#
|
|
217
|
+
# req.inspect
|
|
218
|
+
# # => #<HTTP::Request/1.1 GET https://example.com>
|
|
219
|
+
#
|
|
220
|
+
# @return [String]
|
|
221
|
+
def inspect
|
|
222
|
+
"#<#{self.class}/#{@version} #{verb.to_s.upcase} #{uri}>"
|
|
194
223
|
end
|
|
195
224
|
|
|
225
|
+
private
|
|
226
|
+
|
|
196
227
|
# @!attribute [r] host
|
|
197
228
|
# @return [String]
|
|
198
229
|
def_delegator :@uri, :host
|
|
@@ -205,20 +236,24 @@ module HTTP
|
|
|
205
236
|
|
|
206
237
|
# @return [String] Default host (with port if needed) header value.
|
|
207
238
|
def default_host_header_value
|
|
208
|
-
PORTS[@scheme]
|
|
239
|
+
value = PORTS[@scheme] == port ? host : "#{host}:#{port}"
|
|
240
|
+
|
|
241
|
+
raise RequestError, "Invalid host: #{value.inspect}" if value.match?(/\s/)
|
|
242
|
+
|
|
243
|
+
value
|
|
209
244
|
end
|
|
210
245
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
246
|
+
def prepare_body(body)
|
|
247
|
+
body.is_a?(Request::Body) ? body : Request::Body.new(body)
|
|
248
|
+
end
|
|
214
249
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
250
|
+
def prepare_headers(headers)
|
|
251
|
+
headers = HTTP::Headers.coerce(headers || {})
|
|
252
|
+
|
|
253
|
+
headers[Headers::HOST] ||= default_host_header_value
|
|
254
|
+
headers[Headers::USER_AGENT] ||= USER_AGENT
|
|
255
|
+
|
|
256
|
+
headers
|
|
222
257
|
end
|
|
223
258
|
end
|
|
224
259
|
end
|
data/lib/http/response/body.rb
CHANGED
|
@@ -28,7 +28,8 @@ module HTTP
|
|
|
28
28
|
def readpartial(*args)
|
|
29
29
|
stream!
|
|
30
30
|
chunk = @stream.readpartial(*args)
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
String.new(chunk, :encoding => @encoding) if chunk
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
# Iterate over the body, allowing it to be enumerable
|
|
@@ -46,11 +47,11 @@ module HTTP
|
|
|
46
47
|
|
|
47
48
|
begin
|
|
48
49
|
@streaming = false
|
|
49
|
-
@contents = String.new(""
|
|
50
|
+
@contents = String.new("", :encoding => @encoding)
|
|
50
51
|
|
|
51
52
|
while (chunk = @stream.readpartial)
|
|
52
|
-
@contents <<
|
|
53
|
-
chunk
|
|
53
|
+
@contents << String.new(chunk, :encoding => @encoding)
|
|
54
|
+
chunk = nil # deallocate string
|
|
54
55
|
end
|
|
55
56
|
rescue
|
|
56
57
|
@contents = nil
|
|
@@ -64,6 +65,7 @@ module HTTP
|
|
|
64
65
|
# Assert that the body is actively being streamed
|
|
65
66
|
def stream!
|
|
66
67
|
raise StateError, "body has already been consumed" if @streaming == false
|
|
68
|
+
|
|
67
69
|
@streaming = true
|
|
68
70
|
end
|
|
69
71
|
|