http 0.5.1 → 0.6.0.pre
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of http might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.gitignore +3 -3
- data/.rspec +3 -2
- data/.rubocop.yml +101 -0
- data/.travis.yml +19 -8
- data/Gemfile +24 -6
- data/LICENSE.txt +1 -1
- data/README.md +144 -29
- data/Rakefile +23 -1
- data/examples/parallel_requests_with_celluloid.rb +2 -2
- data/http.gemspec +14 -14
- data/lib/http.rb +5 -4
- data/lib/http/authorization_header.rb +37 -0
- data/lib/http/authorization_header/basic_auth.rb +24 -0
- data/lib/http/authorization_header/bearer_token.rb +29 -0
- data/lib/http/backports.rb +2 -0
- data/lib/http/backports/base64.rb +6 -0
- data/lib/http/{uri_backport.rb → backports/uri.rb} +10 -10
- data/lib/http/chainable.rb +24 -25
- data/lib/http/client.rb +97 -67
- data/lib/http/content_type.rb +27 -0
- data/lib/http/errors.rb +13 -0
- data/lib/http/headers.rb +154 -0
- data/lib/http/headers/mixin.rb +11 -0
- data/lib/http/mime_type.rb +61 -36
- data/lib/http/mime_type/adapter.rb +24 -0
- data/lib/http/mime_type/json.rb +23 -0
- data/lib/http/options.rb +21 -48
- data/lib/http/redirector.rb +12 -7
- data/lib/http/request.rb +82 -33
- data/lib/http/request/writer.rb +79 -0
- data/lib/http/response.rb +39 -68
- data/lib/http/response/body.rb +62 -0
- data/lib/http/{response_parser.rb → response/parser.rb} +3 -1
- data/lib/http/version.rb +1 -1
- data/logo.png +0 -0
- data/spec/http/authorization_header/basic_auth_spec.rb +29 -0
- data/spec/http/authorization_header/bearer_token_spec.rb +36 -0
- data/spec/http/authorization_header_spec.rb +41 -0
- data/spec/http/backports/base64_spec.rb +13 -0
- data/spec/http/client_spec.rb +181 -0
- data/spec/http/content_type_spec.rb +47 -0
- data/spec/http/headers/mixin_spec.rb +36 -0
- data/spec/http/headers_spec.rb +417 -0
- data/spec/http/options/body_spec.rb +6 -7
- data/spec/http/options/form_spec.rb +4 -5
- data/spec/http/options/headers_spec.rb +9 -17
- data/spec/http/options/json_spec.rb +17 -0
- data/spec/http/options/merge_spec.rb +18 -19
- data/spec/http/options/new_spec.rb +5 -19
- data/spec/http/options/proxy_spec.rb +6 -6
- data/spec/http/options_spec.rb +3 -9
- data/spec/http/redirector_spec.rb +100 -0
- data/spec/http/request/writer_spec.rb +25 -0
- data/spec/http/request_spec.rb +54 -14
- data/spec/http/response/body_spec.rb +24 -0
- data/spec/http/response_spec.rb +61 -32
- data/spec/http_spec.rb +77 -86
- data/spec/spec_helper.rb +25 -2
- data/spec/support/example_server.rb +58 -49
- data/spec/support/proxy_server.rb +27 -11
- metadata +60 -55
- data/lib/http/header.rb +0 -11
- data/lib/http/mime_types/json.rb +0 -19
- data/lib/http/request_stream.rb +0 -77
- data/spec/http/options/callbacks_spec.rb +0 -62
- data/spec/http/options/response_spec.rb +0 -24
- data/spec/http/request_stream_spec.rb +0 -25
data/lib/http/redirector.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module HTTP
|
2
2
|
class Redirector
|
3
3
|
# Notifies that we reached max allowed redirect hops
|
4
|
-
class TooManyRedirectsError <
|
4
|
+
class TooManyRedirectsError < ResponseError; end
|
5
5
|
|
6
6
|
# Notifies that following redirects got into an endless loop
|
7
7
|
class EndlessRedirectError < TooManyRedirectsError; end
|
@@ -10,8 +10,10 @@ module HTTP
|
|
10
10
|
REDIRECT_CODES = [300, 301, 302, 303, 307, 308].freeze
|
11
11
|
|
12
12
|
# :nodoc:
|
13
|
-
def initialize(
|
14
|
-
|
13
|
+
def initialize(options = nil)
|
14
|
+
options = {:max_hops => 5} unless options.respond_to?(:fetch)
|
15
|
+
@max_hops = options.fetch(:max_hops, 5)
|
16
|
+
@max_hops = false if @max_hops && 1 > @max_hops.to_i
|
15
17
|
end
|
16
18
|
|
17
19
|
# Follows redirects until non-redirect response found
|
@@ -39,7 +41,12 @@ module HTTP
|
|
39
41
|
uri = @response.headers['Location']
|
40
42
|
fail StateError, 'no Location header in redirect' unless uri
|
41
43
|
|
42
|
-
|
44
|
+
if 303 == @response.code
|
45
|
+
@request = @request.redirect uri, :get
|
46
|
+
else
|
47
|
+
@request = @request.redirect uri
|
48
|
+
end
|
49
|
+
|
43
50
|
@response = yield @request
|
44
51
|
end
|
45
52
|
|
@@ -48,13 +55,11 @@ module HTTP
|
|
48
55
|
|
49
56
|
# Check if we reached max amount of redirect hops
|
50
57
|
def too_many_hops?
|
51
|
-
|
52
|
-
@max_redirects.to_i < @visited.count
|
58
|
+
@max_hops < @visited.count if @max_hops
|
53
59
|
end
|
54
60
|
|
55
61
|
# Check if we got into an endless loop
|
56
62
|
def endless_loop?
|
57
|
-
# pretty naive condition
|
58
63
|
2 < @visited.count(@visited.last)
|
59
64
|
end
|
60
65
|
end
|
data/lib/http/request.rb
CHANGED
@@ -1,13 +1,18 @@
|
|
1
|
-
require 'http/
|
2
|
-
require 'http/
|
1
|
+
require 'http/headers'
|
2
|
+
require 'http/request/writer'
|
3
|
+
require 'http/version'
|
3
4
|
require 'uri'
|
5
|
+
require 'base64'
|
4
6
|
|
5
7
|
module HTTP
|
6
8
|
class Request
|
7
|
-
include HTTP::
|
9
|
+
include HTTP::Headers::Mixin
|
8
10
|
|
9
11
|
# The method given was not understood
|
10
|
-
class UnsupportedMethodError <
|
12
|
+
class UnsupportedMethodError < RequestError; end
|
13
|
+
|
14
|
+
# The scheme of given URI was not understood
|
15
|
+
class UnsupportedSchemeError < RequestError; end
|
11
16
|
|
12
17
|
# RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
|
13
18
|
METHODS = [:options, :get, :head, :post, :put, :delete, :trace, :connect]
|
@@ -27,53 +32,97 @@ module HTTP
|
|
27
32
|
# draft-reschke-webdav-search: WebDAV Search
|
28
33
|
METHODS.concat [:search]
|
29
34
|
|
35
|
+
# Allowed schemes
|
36
|
+
SCHEMES = [:http, :https, :ws, :wss]
|
37
|
+
|
30
38
|
# Method is given as a lowercase symbol e.g. :get, :post
|
31
|
-
attr_reader :
|
39
|
+
attr_reader :verb
|
40
|
+
|
41
|
+
# Scheme is normalized to be a lowercase symbol e.g. :http, :https
|
42
|
+
attr_reader :scheme
|
43
|
+
|
44
|
+
# The following alias may be removed in three minor versions (0.8.0) or one
|
45
|
+
# major version (1.0.0)
|
46
|
+
alias_method :__method__, :method
|
47
|
+
|
48
|
+
# The following method may be removed in two minor versions (0.7.0) or one
|
49
|
+
# major version (1.0.0)
|
50
|
+
def method(*args)
|
51
|
+
warn "#{Kernel.caller.first}: [DEPRECATION] HTTP::Request#method is deprecated. Use #verb instead. For Object#method, use #__method__."
|
52
|
+
@verb
|
53
|
+
end
|
32
54
|
|
33
55
|
# "Request URI" as per RFC 2616
|
34
56
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
|
35
57
|
attr_reader :uri
|
36
|
-
attr_reader :
|
58
|
+
attr_reader :proxy, :body, :version
|
37
59
|
|
38
60
|
# :nodoc:
|
39
|
-
def initialize(
|
40
|
-
@
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
headers.each do |name, value|
|
47
|
-
name = name.to_s
|
48
|
-
key = name[CANONICAL_HEADER]
|
49
|
-
key ||= canonicalize_header(name)
|
50
|
-
@headers[key] = value
|
51
|
-
end
|
52
|
-
@headers["Host"] ||= @uri.host
|
61
|
+
def initialize(verb, uri, headers = {}, proxy = {}, body = nil, version = '1.1') # rubocop:disable ParameterLists
|
62
|
+
@verb = verb.to_s.downcase.to_sym
|
63
|
+
@uri = uri.is_a?(URI) ? uri : URI(uri.to_s)
|
64
|
+
@scheme = @uri.scheme.to_s.downcase.to_sym if @uri.scheme
|
65
|
+
|
66
|
+
fail(UnsupportedMethodError, "unknown method: #{verb}") unless METHODS.include?(@verb)
|
67
|
+
fail(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme)
|
53
68
|
|
54
69
|
@proxy, @body, @version = proxy, body, version
|
70
|
+
|
71
|
+
@headers = HTTP::Headers.coerce(headers || {})
|
72
|
+
|
73
|
+
@headers['Host'] ||= @uri.host
|
74
|
+
@headers['User-Agent'] ||= "RubyHTTPGem/#{HTTP::VERSION}"
|
55
75
|
end
|
56
76
|
|
57
77
|
# Returns new Request with updated uri
|
58
|
-
def redirect(uri)
|
78
|
+
def redirect(uri, verb = @verb)
|
59
79
|
uri = @uri.merge uri.to_s
|
60
|
-
req = self.class.new(
|
61
|
-
req
|
80
|
+
req = self.class.new(verb, uri, headers, proxy, body, version)
|
81
|
+
req['Host'] = req.uri.host
|
62
82
|
req
|
63
83
|
end
|
64
84
|
|
65
|
-
# Obtain the given header
|
66
|
-
def [](header)
|
67
|
-
@headers[canonicalize_header(header)]
|
68
|
-
end
|
69
|
-
|
70
85
|
# Stream the request to a socket
|
71
86
|
def stream(socket)
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
87
|
+
include_proxy_authorization_header if using_authenticated_proxy?
|
88
|
+
Request::Writer.new(socket, body, headers, request_header).stream
|
89
|
+
end
|
90
|
+
|
91
|
+
# Is this request using a proxy?
|
92
|
+
def using_proxy?
|
93
|
+
proxy && proxy.keys.size >= 2
|
94
|
+
end
|
95
|
+
|
96
|
+
# Is this request using an authenticated proxy?
|
97
|
+
def using_authenticated_proxy?
|
98
|
+
proxy && proxy.keys.size == 4
|
99
|
+
end
|
100
|
+
|
101
|
+
# Compute and add the Proxy-Authorization header
|
102
|
+
def include_proxy_authorization_header
|
103
|
+
digest = Base64.encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}").chomp
|
104
|
+
headers['Proxy-Authorization'] = "Basic #{digest}"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Compute HTTP request header for direct or proxy request
|
108
|
+
def request_header
|
109
|
+
if using_proxy?
|
110
|
+
"#{verb.to_s.upcase} #{uri} HTTP/#{version}"
|
111
|
+
else
|
112
|
+
path = uri.query && !uri.query.empty? ? "#{uri.path}?#{uri.query}" : uri.path
|
113
|
+
path = '/' if path.empty?
|
114
|
+
"#{verb.to_s.upcase} #{path} HTTP/#{version}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Host for tcp socket
|
119
|
+
def socket_host
|
120
|
+
using_proxy? ? proxy[:proxy_address] : uri.host
|
121
|
+
end
|
122
|
+
|
123
|
+
# Port for tcp socket
|
124
|
+
def socket_port
|
125
|
+
using_proxy? ? proxy[:proxy_port] : uri.port
|
77
126
|
end
|
78
127
|
end
|
79
128
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module HTTP
|
2
|
+
class Request
|
3
|
+
class Writer
|
4
|
+
# CRLF is the universal HTTP delimiter
|
5
|
+
CRLF = "\r\n"
|
6
|
+
|
7
|
+
def initialize(socket, body, headers, headerstart) # rubocop:disable ParameterLists
|
8
|
+
@body = body
|
9
|
+
fail(RequestError, 'body of wrong type') unless valid_body_type
|
10
|
+
@socket = socket
|
11
|
+
@headers = headers
|
12
|
+
@request_header = [headerstart]
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid_body_type
|
16
|
+
valid_types = [String, NilClass, Enumerable]
|
17
|
+
checks = valid_types.map { |type| @body.is_a?(type) }
|
18
|
+
checks.any?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Adds headers to the request header from the headers array
|
22
|
+
def add_headers
|
23
|
+
@headers.each do |field, value|
|
24
|
+
@request_header << "#{field}: #{value}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Stream the request to a socket
|
29
|
+
def stream
|
30
|
+
send_request_header
|
31
|
+
send_request_body
|
32
|
+
end
|
33
|
+
|
34
|
+
# Adds the headers to the header array for the given request body we are working
|
35
|
+
# with
|
36
|
+
def add_body_type_headers
|
37
|
+
if @body.is_a?(String) && !@headers['Content-Length']
|
38
|
+
@request_header << "Content-Length: #{@body.length}"
|
39
|
+
elsif @body.is_a?(Enumerable)
|
40
|
+
encoding = @headers['Transfer-Encoding']
|
41
|
+
if encoding == 'chunked'
|
42
|
+
@request_header << 'Transfer-Encoding: chunked'
|
43
|
+
else
|
44
|
+
fail(RequestError, 'invalid transfer encoding')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Joins the headers specified in the request into a correctly formatted
|
50
|
+
# http request header string
|
51
|
+
def join_headers
|
52
|
+
# join the headers array with crlfs, stick two on the end because
|
53
|
+
# that ends the request header
|
54
|
+
@request_header.join(CRLF) + (CRLF) * 2
|
55
|
+
end
|
56
|
+
|
57
|
+
def send_request_header
|
58
|
+
add_headers
|
59
|
+
add_body_type_headers
|
60
|
+
header = join_headers
|
61
|
+
|
62
|
+
@socket << header
|
63
|
+
end
|
64
|
+
|
65
|
+
def send_request_body
|
66
|
+
if @body.is_a?(String)
|
67
|
+
@socket << @body
|
68
|
+
elsif @body.is_a?(Enumerable)
|
69
|
+
@body.each do |chunk|
|
70
|
+
@socket << chunk.bytesize.to_s(16) << CRLF
|
71
|
+
@socket << chunk
|
72
|
+
end
|
73
|
+
|
74
|
+
@socket << '0' << CRLF * 2
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/http/response.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require 'delegate'
|
2
|
-
require 'http/
|
2
|
+
require 'http/headers'
|
3
|
+
require 'http/content_type'
|
4
|
+
require 'http/mime_type'
|
3
5
|
|
4
6
|
module HTTP
|
5
7
|
class Response
|
6
|
-
include HTTP::
|
8
|
+
include HTTP::Headers::Mixin
|
7
9
|
|
8
10
|
STATUS_CODES = {
|
9
11
|
100 => 'Continue',
|
@@ -65,96 +67,65 @@ module HTTP
|
|
65
67
|
SYMBOL_TO_STATUS_CODE.freeze
|
66
68
|
|
67
69
|
attr_reader :status
|
68
|
-
attr_reader :
|
70
|
+
attr_reader :body
|
71
|
+
attr_reader :uri
|
69
72
|
|
70
73
|
# Status aliases! TIMTOWTDI!!! (Want to be idiomatic? Just use status :)
|
71
74
|
alias_method :code, :status
|
72
75
|
alias_method :status_code, :status
|
73
76
|
|
74
|
-
def initialize(status
|
75
|
-
@status, @version, @body, @
|
76
|
-
|
77
|
-
@headers = {}
|
78
|
-
headers.each do |field, value|
|
79
|
-
@headers[canonicalize_header(field)] = value
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
# Set a header
|
84
|
-
def []=(name, value)
|
85
|
-
# If we have a canonical header, we're done
|
86
|
-
key = name[CANONICAL_HEADER]
|
87
|
-
|
88
|
-
# Convert to canonical capitalization
|
89
|
-
key ||= canonicalize_header(name)
|
90
|
-
|
91
|
-
# Check if the header has already been set and group
|
92
|
-
old_value = @headers[key]
|
93
|
-
if old_value
|
94
|
-
@headers[key] = [old_value].flatten << key
|
95
|
-
else
|
96
|
-
@headers[key] = value
|
97
|
-
end
|
77
|
+
def initialize(status, version, headers, body, uri = nil) # rubocop:disable ParameterLists
|
78
|
+
@status, @version, @body, @uri = status, version, body, uri
|
79
|
+
@headers = HTTP::Headers.coerce(headers || {})
|
98
80
|
end
|
99
81
|
|
100
82
|
# Obtain the 'Reason-Phrase' for the response
|
101
83
|
def reason
|
102
|
-
# FIXME: should get the real reason phrase from the parser
|
103
84
|
STATUS_CODES[@status]
|
104
85
|
end
|
105
86
|
|
106
|
-
#
|
107
|
-
def
|
108
|
-
|
87
|
+
# Returns an Array ala Rack: `[status, headers, body]`
|
88
|
+
def to_a
|
89
|
+
[status, headers.to_h, body.to_s]
|
109
90
|
end
|
110
91
|
|
111
|
-
#
|
112
|
-
def
|
113
|
-
|
114
|
-
|
92
|
+
# Return the response body as a string
|
93
|
+
def to_s
|
94
|
+
body.to_s
|
95
|
+
end
|
96
|
+
alias_method :to_str, :to_s
|
115
97
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
else
|
121
|
-
body << chunk
|
122
|
-
end
|
123
|
-
end
|
124
|
-
body unless block_given?
|
125
|
-
end
|
98
|
+
# Parsed Content-Type header
|
99
|
+
# @return [HTTP::ContentType]
|
100
|
+
def content_type
|
101
|
+
@content_type ||= ContentType.parse headers['Content-Type']
|
126
102
|
end
|
127
103
|
|
128
|
-
#
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
end
|
104
|
+
# MIME type of response (if any)
|
105
|
+
# @return [String, nil]
|
106
|
+
def mime_type
|
107
|
+
@mime_type ||= content_type.mime_type
|
108
|
+
end
|
134
109
|
|
135
|
-
|
110
|
+
# Charset of response (if any)
|
111
|
+
# @return [String, nil]
|
112
|
+
def charset
|
113
|
+
@charset ||= content_type.charset
|
136
114
|
end
|
137
115
|
|
138
|
-
#
|
139
|
-
|
140
|
-
|
116
|
+
# Parse response body with corresponding MIME type adapter.
|
117
|
+
#
|
118
|
+
# @param [#to_s] as Parse as given MIME type
|
119
|
+
# instead of the one determined from headers
|
120
|
+
# @raise [Error] if adapter not found
|
121
|
+
# @return [Object]
|
122
|
+
def parse(as = nil)
|
123
|
+
MimeType[as || mime_type].decode to_s
|
141
124
|
end
|
142
125
|
|
143
126
|
# Inspect a response
|
144
127
|
def inspect
|
145
|
-
"#<#{self.class}/#{@version} #{status} #{reason}
|
146
|
-
end
|
147
|
-
|
148
|
-
class BodyDelegator < ::Delegator
|
149
|
-
attr_reader :response
|
150
|
-
|
151
|
-
def initialize(response, body = response.body)
|
152
|
-
super(body)
|
153
|
-
@response, @body = response, body
|
154
|
-
end
|
155
|
-
|
156
|
-
def __getobj__; @body; end
|
157
|
-
def __setobj__(obj); @body = obj; end
|
128
|
+
"#<#{self.class}/#{@version} #{status} #{reason} headers=#{headers.inspect}>"
|
158
129
|
end
|
159
130
|
end
|
160
131
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
class Response
|
5
|
+
# A streamable response body, also easily converted into a string
|
6
|
+
class Body
|
7
|
+
extend Forwardable
|
8
|
+
include Enumerable
|
9
|
+
def_delegator :to_s, :empty?
|
10
|
+
|
11
|
+
def initialize(client)
|
12
|
+
@client = client
|
13
|
+
@streaming = nil
|
14
|
+
@contents = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
# Read up to length bytes, but return any data that's available
|
18
|
+
def readpartial(length = nil)
|
19
|
+
stream!
|
20
|
+
@client.readpartial(length)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Iterate over the body, allowing it to be enumerable
|
24
|
+
def each
|
25
|
+
while (chunk = readpartial)
|
26
|
+
yield chunk
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Eagerly consume the entire body as a string
|
31
|
+
def to_s
|
32
|
+
return @contents if @contents
|
33
|
+
fail StateError, 'body is being streamed' unless @streaming.nil?
|
34
|
+
|
35
|
+
begin
|
36
|
+
@streaming = false
|
37
|
+
@contents = ''
|
38
|
+
while (chunk = @client.readpartial)
|
39
|
+
@contents << chunk
|
40
|
+
end
|
41
|
+
rescue
|
42
|
+
@contents = nil
|
43
|
+
raise
|
44
|
+
end
|
45
|
+
|
46
|
+
@contents
|
47
|
+
end
|
48
|
+
alias_method :to_str, :to_s
|
49
|
+
|
50
|
+
# Assert that the body is actively being streamed
|
51
|
+
def stream!
|
52
|
+
fail StateError, 'body has already been consumed' if @streaming == false
|
53
|
+
@streaming = true
|
54
|
+
end
|
55
|
+
|
56
|
+
# Easier to interpret string inspect
|
57
|
+
def inspect
|
58
|
+
"#<#{self.class}:#{object_id.to_s(16)} @streaming=#{!!@streaming}>"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|