http 0.8.0.pre4 → 0.8.0.pre5
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/.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
|