http 0.8.0.pre4 → 0.8.0.pre5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGES.md +1 -1
- data/Gemfile +1 -0
- data/README.md +1 -0
- data/Rakefile +42 -0
- data/lib/http/chainable.rb +1 -1
- data/lib/http/client.rb +17 -10
- data/lib/http/connection.rb +81 -52
- data/lib/http/options.rb +11 -1
- data/lib/http/redirector.rb +60 -32
- data/lib/http/response/status/reasons.rb +13 -7
- data/lib/http/timeout/global.rb +14 -29
- data/lib/http/timeout/null.rb +0 -2
- data/lib/http/timeout/per_operation.rb +16 -68
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +24 -28
- data/spec/lib/http/options/merge_spec.rb +2 -0
- data/spec/lib/http/redirector_spec.rb +338 -57
- data/spec/lib/http/response/status_spec.rb +0 -6
- data/spec/lib/http_spec.rb +2 -2
- data/spec/spec_helper.rb +0 -7
- data/spec/support/dummy_server.rb +13 -24
- data/spec/support/http_handling_shared.rb +19 -45
- data/spec/support/servers/config.rb +0 -4
- data/spec/support/ssl_helper.rb +102 -0
- metadata +4 -4
- data/spec/support/create_certs.rb +0 -57
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cfd83e5db031e7c10a162574f2c700e879b66ea1
|
4
|
+
data.tar.gz: 3bcb6718bb90463c39ac0170482c86fe04f59da9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f1b99fd432ba3b1cb7e0ec555796d86d75029543148d8f1a8f0e7fd3d49f898fef0b0dc2afee46fb7eb38add8aa803266b212cfc5b4941d2aa88ea220c938dae
|
7
|
+
data.tar.gz: 8594e4e915ea23e9646d0b3e335b733bf74be8af488a6fe15ff458c3edc1eb70ac3821abcab551afa4e419e4d301ddc13b1bee2c9623eb293683b694963bb106
|
data/.rubocop.yml
CHANGED
data/CHANGES.md
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
data/Rakefile
CHANGED
@@ -26,4 +26,46 @@ Yardstick::Rake::Verify.new do |verify|
|
|
26
26
|
verify.threshold = 58
|
27
27
|
end
|
28
28
|
|
29
|
+
task :generate_status_codes do
|
30
|
+
require "http"
|
31
|
+
require "nokogiri"
|
32
|
+
|
33
|
+
url = "http://www.iana.org/assignments/http-status-codes/http-status-codes.xml"
|
34
|
+
xml = Nokogiri::XML HTTP.get url
|
35
|
+
arr = xml.xpath("//xmlns:record").reduce [] do |a, e|
|
36
|
+
code = e.xpath("xmlns:value").text.to_s
|
37
|
+
desc = e.xpath("xmlns:description").text.to_s
|
38
|
+
|
39
|
+
next a if "Unassigned" == desc || "(Unused)" == desc
|
40
|
+
|
41
|
+
a << "#{code} => #{desc.inspect}"
|
42
|
+
end
|
43
|
+
|
44
|
+
File.open("./lib/http/response/status/reasons.rb", "w") do |io|
|
45
|
+
io.puts <<-TPL.gsub(/^[ ]{6}/, "")
|
46
|
+
# AUTO-GENERATED FILE, DO NOT CHANGE IT MANUALLY
|
47
|
+
|
48
|
+
require "delegate"
|
49
|
+
|
50
|
+
module HTTP
|
51
|
+
class Response
|
52
|
+
class Status < ::Delegator
|
53
|
+
# Code to Reason map
|
54
|
+
#
|
55
|
+
# @example Usage
|
56
|
+
#
|
57
|
+
# REASONS[400] # => "Bad Request"
|
58
|
+
# REASONS[414] # => "Request-URI Too Long"
|
59
|
+
#
|
60
|
+
# @return [Hash<Fixnum => String>]
|
61
|
+
REASONS = {
|
62
|
+
#{arr.join ",\n "}
|
63
|
+
}.each { |_, v| v.freeze }.freeze
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
TPL
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
29
71
|
task :default => [:spec, :rubocop, :verify_measurements]
|
data/lib/http/chainable.rb
CHANGED
data/lib/http/client.rb
CHANGED
@@ -2,6 +2,7 @@ require "cgi"
|
|
2
2
|
require "uri"
|
3
3
|
require "http/form_data"
|
4
4
|
require "http/options"
|
5
|
+
require "http/connection"
|
5
6
|
require "http/redirector"
|
6
7
|
|
7
8
|
module HTTP
|
@@ -18,6 +19,7 @@ module HTTP
|
|
18
19
|
def initialize(default_options = {})
|
19
20
|
@default_options = HTTP::Options.new(default_options)
|
20
21
|
@connection = nil
|
22
|
+
@state = :clean
|
21
23
|
end
|
22
24
|
|
23
25
|
# Make an HTTP request
|
@@ -38,13 +40,11 @@ module HTTP
|
|
38
40
|
req = HTTP::Request.new(verb, uri, headers, proxy, body)
|
39
41
|
res = perform req, opts
|
40
42
|
|
41
|
-
|
42
|
-
res = Redirector.new(opts.follow).perform req, res do |request|
|
43
|
-
perform request, opts
|
44
|
-
end
|
45
|
-
end
|
43
|
+
return res unless opts.follow
|
46
44
|
|
47
|
-
res
|
45
|
+
Redirector.new(opts.follow).perform req, res do |request|
|
46
|
+
perform request, opts
|
47
|
+
end
|
48
48
|
end
|
49
49
|
|
50
50
|
# Perform a single (no follow) HTTP request
|
@@ -57,19 +57,22 @@ module HTTP
|
|
57
57
|
def make_request(req, options)
|
58
58
|
verify_connection!(req.uri)
|
59
59
|
|
60
|
+
@state = :dirty
|
61
|
+
|
60
62
|
@connection ||= HTTP::Connection.new(req, options)
|
61
63
|
@connection.send_request(req)
|
62
64
|
@connection.read_headers!
|
63
65
|
|
64
66
|
res = Response.new(
|
65
|
-
@connection.
|
66
|
-
@connection.
|
67
|
-
@connection.
|
67
|
+
@connection.status_code,
|
68
|
+
@connection.http_version,
|
69
|
+
@connection.headers,
|
68
70
|
Response::Body.new(@connection),
|
69
71
|
req.uri
|
70
72
|
)
|
71
73
|
|
72
74
|
@connection.finish_response if req.verb == :head
|
75
|
+
@state = :clean
|
73
76
|
|
74
77
|
res
|
75
78
|
|
@@ -84,6 +87,7 @@ module HTTP
|
|
84
87
|
def close
|
85
88
|
@connection.close if @connection
|
86
89
|
@connection = nil
|
90
|
+
@state = :clean
|
87
91
|
end
|
88
92
|
|
89
93
|
private
|
@@ -92,11 +96,14 @@ module HTTP
|
|
92
96
|
def verify_connection!(uri)
|
93
97
|
if default_options.persistent? && base_host(uri) != default_options.persistent
|
94
98
|
fail StateError, "Persistence is enabled for #{default_options.persistent}, but we got #{base_host(uri)}"
|
95
|
-
|
96
99
|
# We re-create the connection object because we want to let prior requests
|
97
100
|
# lazily load the body as long as possible, and this mimics prior functionality.
|
98
101
|
elsif @connection && (!@connection.keep_alive? || @connection.expired?)
|
99
102
|
close
|
103
|
+
# If we get into a bad state (eg, Timeout.timeout ensure being killed)
|
104
|
+
# close the connection to prevent potential for mixed responses.
|
105
|
+
elsif @state == :dirty
|
106
|
+
close
|
100
107
|
end
|
101
108
|
end
|
102
109
|
|
data/lib/http/connection.rb
CHANGED
@@ -1,67 +1,80 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
1
3
|
require "http/response/parser"
|
2
4
|
|
3
5
|
module HTTP
|
4
6
|
# A connection to the HTTP server
|
5
7
|
class Connection
|
6
|
-
|
7
|
-
:pending_request, :pending_response
|
8
|
-
|
8
|
+
extend Forwardable
|
9
9
|
# Attempt to read this much data
|
10
10
|
BUFFER_SIZE = 16_384
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
# HTTP/1.0
|
13
|
+
HTTP_1_0 = "1.0".freeze
|
14
14
|
|
15
|
-
|
15
|
+
# HTTP/1.1
|
16
|
+
HTTP_1_1 = "1.1".freeze
|
17
|
+
|
18
|
+
# @param [HTTP::Request] req
|
19
|
+
# @param [HTTP::Options] options
|
20
|
+
def initialize(req, options)
|
21
|
+
@persistent = options.persistent?
|
22
|
+
@keep_alive_timeout = options[:keep_alive_timeout].to_f
|
23
|
+
@pending_request = false
|
24
|
+
@pending_response = false
|
16
25
|
|
17
26
|
@parser = Response::Parser.new
|
18
27
|
|
19
28
|
@socket = options[:timeout_class].new(options[:timeout_options])
|
20
29
|
@socket.connect(options[:socket_class], req.socket_host, req.socket_port)
|
21
30
|
|
22
|
-
|
23
|
-
req.uri.host,
|
24
|
-
options[:ssl_socket_class],
|
25
|
-
options[:ssl_context]
|
26
|
-
) if req.uri.is_a?(URI::HTTPS) && !req.using_proxy?
|
27
|
-
|
31
|
+
start_tls(req, options)
|
28
32
|
reset_timer
|
29
33
|
end
|
30
34
|
|
35
|
+
# @see (HTTP::Response::Parser#status_code)
|
36
|
+
def_delegator :@parser, :status_code
|
37
|
+
|
38
|
+
# @see (HTTP::Response::Parser#http_version)
|
39
|
+
def_delegator :@parser, :http_version
|
40
|
+
|
41
|
+
# @see (HTTP::Response::Parser#headers)
|
42
|
+
def_delegator :@parser, :headers
|
43
|
+
|
31
44
|
# Send a request to the server
|
32
45
|
#
|
33
46
|
# @param [Request] Request to send to the server
|
34
|
-
# @return [
|
47
|
+
# @return [nil]
|
35
48
|
def send_request(req)
|
36
|
-
if pending_response
|
49
|
+
if @pending_response
|
37
50
|
fail StateError, "Tried to send a request while one is pending already. Make sure you read off the body."
|
38
|
-
elsif pending_request
|
51
|
+
elsif @pending_request
|
39
52
|
fail StateError, "Tried to send a request while a response is pending. Make sure you've fully read the body from the request."
|
40
53
|
end
|
41
54
|
|
42
55
|
@pending_request = true
|
43
56
|
|
44
|
-
req.stream socket
|
57
|
+
req.stream @socket
|
45
58
|
|
46
59
|
@pending_response = true
|
47
|
-
@pending_request
|
60
|
+
@pending_request = false
|
48
61
|
end
|
49
62
|
|
50
63
|
# Read a chunk of the body
|
51
64
|
#
|
52
65
|
# @return [String] data chunk
|
53
|
-
# @return [
|
66
|
+
# @return [nil] when no more data left
|
54
67
|
def readpartial(size = BUFFER_SIZE)
|
55
|
-
return unless pending_response
|
68
|
+
return unless @pending_response
|
56
69
|
|
57
70
|
begin
|
58
71
|
read_more size
|
59
|
-
finished = parser.finished?
|
72
|
+
finished = @parser.finished?
|
60
73
|
rescue EOFError
|
61
74
|
finished = true
|
62
75
|
end
|
63
76
|
|
64
|
-
chunk = parser.chunk
|
77
|
+
chunk = @parser.chunk
|
65
78
|
|
66
79
|
finish_response if finished
|
67
80
|
|
@@ -69,75 +82,91 @@ module HTTP
|
|
69
82
|
end
|
70
83
|
|
71
84
|
# Reads data from socket up until headers are loaded
|
85
|
+
# @return [void]
|
72
86
|
def read_headers!
|
73
|
-
read_more BUFFER_SIZE until parser.headers
|
87
|
+
read_more BUFFER_SIZE until @parser.headers
|
74
88
|
set_keep_alive
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
raise IOError, "problem making HTTP request: #{ex}"
|
89
|
+
rescue IOError, Errno::ECONNRESET, Errno::EPIPE => e
|
90
|
+
return if e.is_a?(EOFError) && @parser.headers
|
91
|
+
raise IOError, "problem making HTTP request: #{e}"
|
79
92
|
end
|
80
93
|
|
81
94
|
# Callback for when we've reached the end of a response
|
95
|
+
# @return [void]
|
82
96
|
def finish_response
|
83
97
|
close unless keep_alive?
|
84
98
|
|
85
|
-
parser.reset
|
99
|
+
@parser.reset
|
86
100
|
reset_timer
|
87
101
|
|
88
|
-
@pending_response =
|
102
|
+
@pending_response = false
|
89
103
|
end
|
90
104
|
|
91
105
|
# Close the connection
|
106
|
+
# @return [void]
|
92
107
|
def close
|
93
|
-
socket.close unless socket.closed?
|
108
|
+
@socket.close unless @socket.closed?
|
94
109
|
|
95
|
-
@pending_response =
|
96
|
-
@pending_request
|
110
|
+
@pending_response = false
|
111
|
+
@pending_request = false
|
97
112
|
end
|
98
113
|
|
99
114
|
# Whether we're keeping the conn alive
|
115
|
+
# @return [Boolean]
|
100
116
|
def keep_alive?
|
101
|
-
!!@keep_alive &&
|
117
|
+
!!@keep_alive && !@socket.closed?
|
102
118
|
end
|
103
119
|
|
104
120
|
# Whether our connection has expired
|
121
|
+
# @return [Boolean]
|
105
122
|
def expired?
|
106
123
|
!@conn_expires_at || @conn_expires_at < Time.now
|
107
124
|
end
|
108
125
|
|
109
|
-
|
110
|
-
|
126
|
+
private
|
127
|
+
|
128
|
+
# Sets up SSL context and starts TLS if needed.
|
129
|
+
# @param (see #initialize)
|
130
|
+
# @return [void]
|
131
|
+
def start_tls(req, options)
|
132
|
+
return unless req.uri.is_a?(URI::HTTPS) && !req.using_proxy?
|
133
|
+
|
134
|
+
ssl_context = options[:ssl_context]
|
135
|
+
|
136
|
+
unless ssl_context
|
137
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
138
|
+
ssl_context.set_params(options[:ssl] || {})
|
139
|
+
end
|
140
|
+
|
141
|
+
@socket.start_tls(req.uri.host, options[:ssl_socket_class], ssl_context)
|
111
142
|
end
|
112
143
|
|
113
|
-
|
144
|
+
# Resets expiration of persistent connection.
|
145
|
+
# @return [void]
|
146
|
+
def reset_timer
|
147
|
+
@conn_expires_at = Time.now + @keep_alive_timeout if @persistent
|
148
|
+
end
|
114
149
|
|
115
150
|
# Store whether the connection should be kept alive.
|
116
151
|
# Once we reset the parser, we lose all of this state.
|
152
|
+
# @return [void]
|
117
153
|
def set_keep_alive
|
118
|
-
return @keep_alive = false unless persistent
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
@keep_alive = parser.headers[
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
@keep_alive = parser.headers["Connection"] != HTTP::Client::CLOSE
|
127
|
-
|
128
|
-
# Anything else we assume doesn't supportit
|
129
|
-
else
|
154
|
+
return @keep_alive = false unless @persistent
|
155
|
+
|
156
|
+
case @parser.http_version
|
157
|
+
when HTTP_1_0 # HTTP/1.0 requires opt in for Keep Alive
|
158
|
+
@keep_alive = @parser.headers[Client::CONNECTION] == HTTP::Client::KEEP_ALIVE
|
159
|
+
when HTTP_1_1 # HTTP/1.1 is opt-out
|
160
|
+
@keep_alive = @parser.headers[Client::CONNECTION] != HTTP::Client::CLOSE
|
161
|
+
else # Anything else we assume doesn't supportit
|
130
162
|
@keep_alive = false
|
131
163
|
end
|
132
164
|
end
|
133
165
|
|
134
|
-
private :set_keep_alive
|
135
|
-
|
136
166
|
# Feeds some more data into parser
|
167
|
+
# @return [void]
|
137
168
|
def read_more(size)
|
138
|
-
parser << socket.readpartial(size) unless parser.finished?
|
169
|
+
@parser << @socket.readpartial(size) unless @parser.finished?
|
139
170
|
end
|
140
|
-
|
141
|
-
private :read_more
|
142
171
|
end
|
143
172
|
end
|
data/lib/http/options.rb
CHANGED
@@ -47,6 +47,7 @@ module HTTP
|
|
47
47
|
:timeout_options => {},
|
48
48
|
:socket_class => self.class.default_socket_class,
|
49
49
|
:ssl_socket_class => self.class.default_ssl_socket_class,
|
50
|
+
:ssl => {},
|
50
51
|
:cache => self.class.default_cache,
|
51
52
|
:keep_alive_timeout => 5,
|
52
53
|
:headers => {}}
|
@@ -65,12 +66,21 @@ module HTTP
|
|
65
66
|
|
66
67
|
%w(
|
67
68
|
proxy params form json body follow response
|
68
|
-
socket_class ssl_socket_class ssl_context
|
69
|
+
socket_class ssl_socket_class ssl_context ssl
|
69
70
|
persistent keep_alive_timeout timeout_class timeout_options
|
70
71
|
).each do |method_name|
|
71
72
|
def_option method_name
|
72
73
|
end
|
73
74
|
|
75
|
+
def follow=(value)
|
76
|
+
@follow = case
|
77
|
+
when !value then nil
|
78
|
+
when true == value then {}
|
79
|
+
when value.respond_to?(:fetch) then value
|
80
|
+
else argument_error! "Unsupported follow options: #{value}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
74
84
|
def_option :cache do |cache_or_cache_options|
|
75
85
|
if cache_or_cache_options.respond_to? :perform
|
76
86
|
cache_or_cache_options
|
data/lib/http/redirector.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "set"
|
2
|
+
|
1
3
|
module HTTP
|
2
4
|
class Redirector
|
3
5
|
# Notifies that we reached max allowed redirect hops
|
@@ -7,60 +9,86 @@ module HTTP
|
|
7
9
|
class EndlessRedirectError < TooManyRedirectsError; end
|
8
10
|
|
9
11
|
# HTTP status codes which indicate redirects
|
10
|
-
REDIRECT_CODES = [300, 301, 302, 303, 307, 308].freeze
|
12
|
+
REDIRECT_CODES = [300, 301, 302, 303, 307, 308].to_set.freeze
|
11
13
|
|
12
|
-
#
|
13
|
-
|
14
|
-
|
15
|
-
@max_hops = options.fetch(:max_hops, 5)
|
16
|
-
@max_hops = false if @max_hops && 1 > @max_hops.to_i
|
17
|
-
end
|
14
|
+
# Codes which which should raise StateError in strict mode if original
|
15
|
+
# request was any of {UNSAFE_VERBS}
|
16
|
+
STRICT_SENSITIVE_CODES = [300, 301, 302].to_set.freeze
|
18
17
|
|
19
|
-
#
|
20
|
-
|
21
|
-
|
22
|
-
follow(&block)
|
23
|
-
end
|
18
|
+
# Insecure http verbs, which should trigger StateError in strict mode
|
19
|
+
# upon {STRICT_SENSITIVE_CODES}
|
20
|
+
UNSAFE_VERBS = [:put, :delete, :post].to_set.freeze
|
24
21
|
|
25
|
-
|
22
|
+
# Verbs which will remain unchanged upon See Other response.
|
23
|
+
SEE_OTHER_ALLOWED_VERBS = [:get, :head].to_set.freeze
|
26
24
|
|
27
|
-
#
|
28
|
-
|
29
|
-
|
30
|
-
|
25
|
+
# @!attribute [r] strict
|
26
|
+
# Returns redirector policy.
|
27
|
+
# @return [Boolean]
|
28
|
+
attr_reader :strict
|
29
|
+
|
30
|
+
# @!attribute [r] max_hops
|
31
|
+
# Returns maximum allowed hops.
|
32
|
+
# @return [Fixnum]
|
33
|
+
attr_reader :max_hops
|
34
|
+
|
35
|
+
# @param [Hash] opts
|
36
|
+
# @option opts [Boolean] :strict (true) redirector hops policy
|
37
|
+
# @option opts [#to_i] :max_hops (5) maximum allowed amount of hops
|
38
|
+
def initialize(options = {})
|
39
|
+
@strict = options.fetch(:strict, true)
|
40
|
+
@max_hops = options.fetch(:max_hops, 5).to_i
|
31
41
|
end
|
32
42
|
|
33
|
-
#
|
34
|
-
def
|
35
|
-
|
36
|
-
|
43
|
+
# Follows redirects until non-redirect response found
|
44
|
+
def perform(request, response)
|
45
|
+
@request = request
|
46
|
+
@response = response
|
47
|
+
@visited = []
|
48
|
+
|
49
|
+
while REDIRECT_CODES.include? @response.status.code
|
50
|
+
@visited << "#{@request.verb} #{@request.uri}"
|
37
51
|
|
38
52
|
fail TooManyRedirectsError if too_many_hops?
|
39
53
|
fail EndlessRedirectError if endless_loop?
|
40
54
|
|
41
|
-
|
42
|
-
fail StateError, "no Location header in redirect" unless uri
|
43
|
-
|
44
|
-
if 303 == @response.code
|
45
|
-
@request = @request.redirect uri, :get
|
46
|
-
else
|
47
|
-
@request = @request.redirect uri
|
48
|
-
end
|
49
|
-
|
55
|
+
@request = redirect_to @response.headers["Location"]
|
50
56
|
@response = yield @request
|
51
57
|
end
|
52
58
|
|
53
59
|
@response
|
54
60
|
end
|
55
61
|
|
62
|
+
private
|
63
|
+
|
56
64
|
# Check if we reached max amount of redirect hops
|
65
|
+
# @return [Boolean]
|
57
66
|
def too_many_hops?
|
58
|
-
@max_hops < @visited.count
|
67
|
+
1 <= @max_hops && @max_hops < @visited.count
|
59
68
|
end
|
60
69
|
|
61
70
|
# Check if we got into an endless loop
|
71
|
+
# @return [Boolean]
|
62
72
|
def endless_loop?
|
63
|
-
2
|
73
|
+
2 <= @visited.count(@visited.last)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Redirect policy for follow
|
77
|
+
# @return [Request]
|
78
|
+
def redirect_to(uri)
|
79
|
+
fail StateError, "no Location header in redirect" unless uri
|
80
|
+
|
81
|
+
verb = @request.verb
|
82
|
+
code = @response.status.code
|
83
|
+
|
84
|
+
if UNSAFE_VERBS.include?(verb) && STRICT_SENSITIVE_CODES.include?(code)
|
85
|
+
fail StateError, "can't follow #{@response.status} redirect" if @strict
|
86
|
+
verb = :get
|
87
|
+
end
|
88
|
+
|
89
|
+
verb = :get if !SEE_OTHER_ALLOWED_VERBS.include?(verb) && 303 == code
|
90
|
+
|
91
|
+
@request.redirect(uri, verb)
|
64
92
|
end
|
65
93
|
end
|
66
94
|
end
|