http 0.2.0 → 0.3.0
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.
- data/.travis.yml +2 -2
- data/CHANGES.md +8 -0
- data/Gemfile +2 -0
- data/README.md +10 -2
- data/http.gemspec +2 -1
- data/lib/http.rb +16 -9
- data/lib/http/chainable.rb +25 -1
- data/lib/http/client.rb +61 -17
- data/lib/http/options.rb +66 -13
- data/lib/http/request.rb +37 -9
- data/lib/http/response.rb +30 -10
- data/lib/http/response_parser.rb +62 -0
- data/lib/http/streaming_body.rb +0 -0
- data/lib/http/version.rb +1 -1
- data/spec/http/options/body_spec.rb +18 -0
- data/spec/http/options/merge_spec.rb +8 -1
- data/spec/http/options/new_spec.rb +5 -0
- data/spec/http/options/proxy_spec.rb +21 -0
- data/spec/http/response_spec.rb +31 -43
- data/spec/http_spec.rb +68 -3
- data/spec/spec_helper.rb +2 -1
- data/spec/support/example_server.rb +24 -2
- data/spec/support/proxy_server.rb +17 -0
- metadata +31 -11
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
@@ -1,8 +1,16 @@
|
|
1
|
+
0.3.0
|
2
|
+
-----
|
3
|
+
* New implementation based on tmm1's http_parser.rb instead of Net::HTTP
|
4
|
+
* Support for following redirects
|
5
|
+
* Support for request body through {:body => ...} option
|
6
|
+
* Http#with_response (through Chainable)
|
7
|
+
|
1
8
|
0.2.0
|
2
9
|
-----
|
3
10
|
* Request and response objects
|
4
11
|
* Callback system
|
5
12
|
* Internal refactoring ensuring true chainability
|
13
|
+
* Use the certified gem to ensure SSL certificate verification
|
6
14
|
|
7
15
|
0.1.0
|
8
16
|
-----
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Http
|
2
2
|
====
|
3
|
-
[![Build Status](
|
3
|
+
[![Build Status](https://secure.travis-ci.org/tarcieri/http.png?branch=master)](http://travis-ci.org/tarcieri/http)
|
4
4
|
|
5
5
|
HTTP should be simple and easy! It should be so straightforward it makes
|
6
6
|
you happy every time you use it.
|
@@ -24,6 +24,11 @@ Making POST requests is simple too. Want to POST a form?
|
|
24
24
|
Http.post "http://example.com/resource", :form => {:foo => "42"}
|
25
25
|
```
|
26
26
|
|
27
|
+
Want to POST with a specific body, JSON for instance?
|
28
|
+
```ruby
|
29
|
+
Http.post "http://example.com/resource", :body => JSON.dump(:foo => "42")
|
30
|
+
```
|
31
|
+
|
27
32
|
It's easy!
|
28
33
|
|
29
34
|
Adding Headers
|
@@ -53,11 +58,14 @@ request and returns a response with Content-Type: application/json. If you
|
|
53
58
|
happen to have a library loaded which defines the JSON constant and implements
|
54
59
|
JSON.parse, the Http library will attempt to parse the JSON response.
|
55
60
|
|
56
|
-
|
61
|
+
Shorter aliases exists for HTTP.with_headers:
|
57
62
|
|
58
63
|
```ruby
|
59
64
|
Http.with(:accept => 'application/json').
|
60
65
|
get("https://github.com/tarcieri/http/commit/HEAD")
|
66
|
+
|
67
|
+
Http[:accept => 'application/json'].
|
68
|
+
get("https://github.com/tarcieri/http/commit/HEAD")
|
61
69
|
```
|
62
70
|
|
63
71
|
Content Negotiation
|
data/http.gemspec
CHANGED
@@ -15,7 +15,8 @@ Gem::Specification.new do |gem|
|
|
15
15
|
gem.require_paths = ["lib"]
|
16
16
|
gem.version = Http::VERSION
|
17
17
|
|
18
|
-
gem.
|
18
|
+
gem.add_runtime_dependency 'http_parser.rb'
|
19
|
+
gem.add_runtime_dependency 'certified'
|
19
20
|
|
20
21
|
gem.add_development_dependency 'rake'
|
21
22
|
gem.add_development_dependency 'rspec'
|
data/lib/http.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'certified'
|
3
|
+
require 'http/parser'
|
1
4
|
require 'http/version'
|
2
5
|
|
3
6
|
require 'http/chainable'
|
@@ -6,14 +9,10 @@ require 'http/mime_type'
|
|
6
9
|
require 'http/options'
|
7
10
|
require 'http/request'
|
8
11
|
require 'http/response'
|
12
|
+
require 'http/response_parser'
|
9
13
|
require 'http/uri_backport' if RUBY_VERSION < "1.9.0"
|
10
14
|
|
11
|
-
#
|
12
|
-
require 'net/https'
|
13
|
-
require 'uri'
|
14
|
-
require 'certified'
|
15
|
-
|
16
|
-
# Http, it can be simple!
|
15
|
+
# HTTP should be easy
|
17
16
|
module Http
|
18
17
|
extend Chainable
|
19
18
|
|
@@ -26,8 +25,16 @@ module Http
|
|
26
25
|
# Matches HTTP header names when in "Canonical-Http-Format"
|
27
26
|
CANONICAL_HEADER = /^[A-Z][a-z]*(-[A-Z][a-z]*)*$/
|
28
27
|
|
29
|
-
#
|
30
|
-
|
31
|
-
|
28
|
+
# CRLF is the universal HTTP delimiter
|
29
|
+
CRLF = "\r\n"
|
30
|
+
|
31
|
+
class << self
|
32
|
+
# Http[:accept => 'text/html'].get(...)
|
33
|
+
alias_method :[], :with_headers
|
34
|
+
|
35
|
+
# Transform to canonical HTTP header capitalization
|
36
|
+
def canonicalize_header(header)
|
37
|
+
header.to_s.split(/[\-_]/).map(&:capitalize).join('-')
|
38
|
+
end
|
32
39
|
end
|
33
40
|
end
|
data/lib/http/chainable.rb
CHANGED
@@ -54,6 +54,31 @@ module Http
|
|
54
54
|
def on(event, &block)
|
55
55
|
branch default_options.with_callback(event, block)
|
56
56
|
end
|
57
|
+
|
58
|
+
# Make a request through an HTTP proxy
|
59
|
+
def via(*proxy)
|
60
|
+
proxy_hash = {}
|
61
|
+
proxy_hash[:proxy_address] = proxy[0] if proxy[0].is_a? String
|
62
|
+
proxy_hash[:proxy_port] = proxy[1] if proxy[1].is_a? Integer
|
63
|
+
proxy_hash[:proxy_username]= proxy[2] if proxy[2].is_a? String
|
64
|
+
proxy_hash[:proxy_password]= proxy[3] if proxy[3].is_a? String
|
65
|
+
|
66
|
+
if proxy_hash.keys.size >=2
|
67
|
+
branch default_options.with_proxy(proxy_hash)
|
68
|
+
else
|
69
|
+
raise ArgumentError, "invalid HTTP proxy: #{proxy_hash}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
alias_method :through, :via
|
73
|
+
|
74
|
+
# Specify the kind of response to return (:auto, :object, :body, :parsed_body)
|
75
|
+
def with_response(response_type)
|
76
|
+
branch default_options.with_response(response_type)
|
77
|
+
end
|
78
|
+
|
79
|
+
def with_follow(follow)
|
80
|
+
branch default_options.with_follow(follow)
|
81
|
+
end
|
57
82
|
|
58
83
|
# Make a request with the given headers
|
59
84
|
def with_headers(headers)
|
@@ -105,6 +130,5 @@ module Http
|
|
105
130
|
def branch(options)
|
106
131
|
Client.new(options)
|
107
132
|
end
|
108
|
-
|
109
133
|
end
|
110
134
|
end
|
data/lib/http/client.rb
CHANGED
@@ -5,45 +5,89 @@ module Http
|
|
5
5
|
class Client
|
6
6
|
include Chainable
|
7
7
|
|
8
|
+
BUFFER_SIZE = 4096 # Input buffer size
|
9
|
+
|
8
10
|
attr_reader :default_options
|
9
11
|
|
10
12
|
def initialize(default_options = {})
|
11
13
|
@default_options = Options.new(default_options)
|
12
14
|
end
|
13
15
|
|
16
|
+
def body(opts,headers)
|
17
|
+
if opts.body
|
18
|
+
body = opts.body
|
19
|
+
elsif opts.form
|
20
|
+
headers['Content-Type'] ||= 'application/x-www-form-urlencoded'
|
21
|
+
body = URI.encode_www_form(opts.form)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
14
25
|
# Make an HTTP request
|
15
26
|
def request(method, uri, options = {})
|
16
27
|
opts = @default_options.merge(options)
|
17
28
|
headers = opts.headers
|
29
|
+
proxy = opts.proxy
|
18
30
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
31
|
+
method_body = body(opts, headers)
|
32
|
+
puts method_body
|
33
|
+
request = Request.new method, uri, headers, proxy, method_body
|
23
34
|
|
24
|
-
|
35
|
+
if opts.follow
|
36
|
+
code = 302
|
37
|
+
while code == 302 or code == 301
|
38
|
+
puts uri
|
39
|
+
method_body = body(opts, headers)
|
40
|
+
request = Request.new method, uri, headers, proxy, method_body
|
41
|
+
response = perform request, opts
|
42
|
+
code = response.code
|
43
|
+
uri = response.headers["Location"]
|
44
|
+
end
|
45
|
+
end
|
25
46
|
|
26
47
|
opts.callbacks[:request].each { |c| c.call(request) }
|
27
|
-
response = perform request
|
48
|
+
response = perform request, opts
|
28
49
|
opts.callbacks[:response].each { |c| c.call(response) }
|
29
50
|
|
30
51
|
format_response method, response, opts.response
|
31
52
|
end
|
32
53
|
|
33
|
-
def perform(request)
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
response = http.request request.to_net_http_request
|
54
|
+
def perform(request, options)
|
55
|
+
parser = Http::Response::Parser.new
|
56
|
+
uri, proxy = request.uri, request.proxy
|
57
|
+
socket = options[:socket_class].open(uri.host, uri.port) # TODO: proxy support
|
38
58
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
59
|
+
if uri.is_a?(URI::HTTPS)
|
60
|
+
socket = options[:ssl_socket_class].open(socket, options[:ssl_context])
|
61
|
+
socket.connect
|
62
|
+
end
|
63
|
+
|
64
|
+
request.stream socket
|
65
|
+
|
66
|
+
begin
|
67
|
+
parser << socket.readpartial(BUFFER_SIZE) until parser.headers
|
68
|
+
rescue IOError, Errno::ECONNRESET, Errno::EPIPE
|
69
|
+
# TODO: handle errors
|
70
|
+
raise "zomg IO troubles: #{$!.message}"
|
71
|
+
end
|
72
|
+
|
73
|
+
response = Http::Response.new(parser.status_code, parser.http_version, parser.headers) do
|
74
|
+
if @body_remaining and @body_remaining > 0
|
75
|
+
chunk = parser.chunk
|
76
|
+
unless chunk
|
77
|
+
parser << socket.readpartial(BUFFER_SIZE)
|
78
|
+
chunk = parser.chunk
|
79
|
+
return unless chunk
|
80
|
+
end
|
81
|
+
|
82
|
+
@body_remaining -= chunk.length
|
83
|
+
@body_remaining = nil if @body_remaining < 1
|
43
84
|
|
44
|
-
|
45
|
-
|
85
|
+
chunk
|
86
|
+
end
|
46
87
|
end
|
88
|
+
|
89
|
+
@body_remaining = Integer(response['Content-Length']) if response['Content-Length']
|
90
|
+
response
|
47
91
|
end
|
48
92
|
|
49
93
|
def format_response(method, response, option)
|
data/lib/http/options.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'openssl'
|
3
|
+
|
1
4
|
module Http
|
2
5
|
class Options
|
3
6
|
|
4
|
-
# How to format the response [:object, :body, :parse_body]
|
7
|
+
# How to format the response [:object, :body, :parse_body]
|
5
8
|
attr_accessor :response
|
6
9
|
|
7
10
|
# Http headers to include in the request
|
@@ -10,28 +13,57 @@ module Http
|
|
10
13
|
# Form data to embed in the request
|
11
14
|
attr_accessor :form
|
12
15
|
|
13
|
-
#
|
16
|
+
# Explicit request body of the request
|
17
|
+
attr_accessor :body
|
18
|
+
|
19
|
+
# Http proxy to route request
|
20
|
+
attr_accessor :proxy
|
21
|
+
|
22
|
+
# Before callbacks
|
14
23
|
attr_accessor :callbacks
|
15
24
|
|
16
|
-
|
25
|
+
# Socket classes
|
26
|
+
attr_accessor :socket_class, :ssl_socket_class
|
27
|
+
|
28
|
+
# SSL context
|
29
|
+
attr_accessor :ssl_context
|
30
|
+
|
31
|
+
# Follow redirects
|
32
|
+
attr_accessor :follow
|
33
|
+
|
34
|
+
protected :response=, :headers=, :proxy=, :form=, :callbacks=, :follow=
|
17
35
|
|
18
|
-
|
19
|
-
|
20
|
-
|
36
|
+
@default_socket_class = TCPSocket
|
37
|
+
@default_ssl_socket_class = OpenSSL::SSL::SSLSocket
|
38
|
+
|
39
|
+
class << self
|
40
|
+
attr_accessor :default_socket_class, :default_ssl_socket_class
|
41
|
+
|
42
|
+
def new(options = {})
|
43
|
+
return options if options.is_a?(Options)
|
44
|
+
super
|
45
|
+
end
|
21
46
|
end
|
22
47
|
|
23
|
-
def initialize(
|
24
|
-
@response =
|
25
|
-
@headers =
|
26
|
-
@
|
27
|
-
@callbacks =
|
48
|
+
def initialize(options = {})
|
49
|
+
@response = options[:response] || :auto
|
50
|
+
@headers = options[:headers] || {}
|
51
|
+
@proxy = options[:proxy] || {}
|
52
|
+
@callbacks = options[:callbacks] || {:request => [], :response => []}
|
53
|
+
@body = options[:body]
|
54
|
+
@form = options[:form]
|
55
|
+
@follow = options[:follow]
|
56
|
+
|
57
|
+
@socket_class = options[:socket_class] || self.class.default_socket_class
|
58
|
+
@ssl_socket_class = options[:ssl_socket_class] || self.class.default_ssl_socket_class
|
59
|
+
@ssl_context = options[:ssl_context]
|
28
60
|
end
|
29
61
|
|
30
62
|
def with_response(response)
|
31
63
|
unless [:auto, :object, :body, :parsed_body].include?(response)
|
32
64
|
argument_error! "invalid response type: #{response}"
|
33
65
|
end
|
34
|
-
dup do |opts|
|
66
|
+
dup do |opts|
|
35
67
|
opts.response = response
|
36
68
|
end
|
37
69
|
end
|
@@ -45,12 +77,30 @@ module Http
|
|
45
77
|
end
|
46
78
|
end
|
47
79
|
|
80
|
+
def with_proxy(proxy_hash)
|
81
|
+
dup do |opts|
|
82
|
+
opts.proxy = proxy_hash
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
48
86
|
def with_form(form)
|
49
87
|
dup do |opts|
|
50
88
|
opts.form = form
|
51
89
|
end
|
52
90
|
end
|
53
91
|
|
92
|
+
def with_body(body)
|
93
|
+
dup do |opts|
|
94
|
+
opts.body = body
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def with_follow(follow)
|
99
|
+
dup do |opts|
|
100
|
+
opts.follow = follow
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
54
104
|
def with_callback(event, callback)
|
55
105
|
unless callback.respond_to?(:call)
|
56
106
|
argument_error! "invalid callback: #{callback}"
|
@@ -89,8 +139,11 @@ module Http
|
|
89
139
|
def to_hash
|
90
140
|
{:response => response,
|
91
141
|
:headers => headers,
|
142
|
+
:proxy => proxy,
|
92
143
|
:form => form,
|
93
|
-
:
|
144
|
+
:body => body,
|
145
|
+
:callbacks => callbacks,
|
146
|
+
:follow => follow}
|
94
147
|
end
|
95
148
|
|
96
149
|
def dup
|
data/lib/http/request.rb
CHANGED
@@ -6,10 +6,10 @@ module Http
|
|
6
6
|
# "Request URI" as per RFC 2616
|
7
7
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
|
8
8
|
attr_reader :uri
|
9
|
-
attr_reader :headers, :body, :version
|
9
|
+
attr_reader :headers, :proxy, :body, :version
|
10
10
|
|
11
11
|
# :nodoc:
|
12
|
-
def initialize(method, uri, headers = {}, body = nil, version = "1.1")
|
12
|
+
def initialize(method, uri, headers = {}, proxy = {}, body = nil, version = "1.1")
|
13
13
|
@method = method.to_s.downcase.to_sym
|
14
14
|
raise UnsupportedMethodError, "unknown method: #{method}" unless METHODS.include? @method
|
15
15
|
|
@@ -23,7 +23,7 @@ module Http
|
|
23
23
|
@headers[key] = value
|
24
24
|
end
|
25
25
|
|
26
|
-
@body, @version = body, version
|
26
|
+
@proxy, @body, @version = proxy, body, version
|
27
27
|
end
|
28
28
|
|
29
29
|
# Obtain the given header
|
@@ -31,12 +31,40 @@ module Http
|
|
31
31
|
@headers[Http.canonicalize_header(header)]
|
32
32
|
end
|
33
33
|
|
34
|
-
#
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
34
|
+
# Stream the request to a socket
|
35
|
+
def stream(socket)
|
36
|
+
request_header = "#{method.to_s.upcase} #{uri.path} HTTP/#{version}#{CRLF}"
|
37
|
+
@headers.each do |field, value|
|
38
|
+
request_header << "#{field}: #{value}#{CRLF}"
|
39
|
+
end
|
40
|
+
|
41
|
+
case body
|
42
|
+
when NilClass
|
43
|
+
socket << request_header << CRLF
|
44
|
+
return
|
45
|
+
when String
|
46
|
+
request_header << "Content-Length: #{body.length}#{CRLF}" unless @headers['Content-Length']
|
47
|
+
request_header << CRLF
|
48
|
+
|
49
|
+
socket << request_header
|
50
|
+
socket << body.to_s
|
51
|
+
when Enumerable
|
52
|
+
if encoding = @headers['Transfer-Encoding']
|
53
|
+
raise ArgumentError, "invalid transfer encoding" unless encoding == "chunked"
|
54
|
+
request_header << CRLF
|
55
|
+
else
|
56
|
+
request_header << "Transfer-Encoding: chunked#{CRLF * 2}"
|
57
|
+
end
|
58
|
+
|
59
|
+
socket << request_header
|
60
|
+
body.each do |chunk|
|
61
|
+
socket << chunk.bytesize.to_s(16) << CRLF
|
62
|
+
socket << chunk
|
63
|
+
end
|
64
|
+
|
65
|
+
socket << "0" << CRLF * 2
|
66
|
+
else raise TypeError, "invalid body type: #{body.class}"
|
67
|
+
end
|
40
68
|
end
|
41
69
|
end
|
42
70
|
end
|
data/lib/http/response.rb
CHANGED
@@ -54,22 +54,25 @@ module Http
|
|
54
54
|
507 => 'Insufficient Storage',
|
55
55
|
510 => 'Not Extended'
|
56
56
|
}
|
57
|
+
STATUS_CODES.freeze
|
57
58
|
|
58
59
|
SYMBOL_TO_STATUS_CODE = Hash[STATUS_CODES.map { |code, msg| [msg.downcase.gsub(/\s|-/, '_').to_sym, code] }]
|
60
|
+
SYMBOL_TO_STATUS_CODE.freeze
|
59
61
|
|
60
|
-
|
61
|
-
|
62
|
-
attr_accessor :body
|
62
|
+
attr_reader :status
|
63
|
+
attr_reader :headers
|
63
64
|
|
64
65
|
# Status aliases! TIMTOWTDI!!! (Want to be idiomatic? Just use status :)
|
65
|
-
alias_method :code,
|
66
|
-
alias_method :
|
66
|
+
alias_method :code, :status
|
67
|
+
alias_method :status_code, :status
|
67
68
|
|
68
|
-
|
69
|
-
|
69
|
+
def initialize(status = nil, version = "1.1", headers = {}, body = nil, &body_proc)
|
70
|
+
@status, @version, @body, @body_proc = status, version, body, body_proc
|
70
71
|
|
71
|
-
def initialize
|
72
72
|
@headers = {}
|
73
|
+
headers.each do |field, value|
|
74
|
+
@headers[Http.canonicalize_header(field)] = value
|
75
|
+
end
|
73
76
|
end
|
74
77
|
|
75
78
|
# Set a header
|
@@ -94,14 +97,31 @@ module Http
|
|
94
97
|
@headers[name] || @headers[Http.canonicalize_header(name)]
|
95
98
|
end
|
96
99
|
|
100
|
+
# Obtain the response body
|
101
|
+
def body
|
102
|
+
@body ||= begin
|
103
|
+
raise "no body available for this response" unless @body_proc
|
104
|
+
|
105
|
+
body = "" unless block_given?
|
106
|
+
while (chunk = @body_proc.call)
|
107
|
+
if block_given?
|
108
|
+
yield chunk
|
109
|
+
else
|
110
|
+
body << chunk
|
111
|
+
end
|
112
|
+
end
|
113
|
+
body unless block_given?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
97
117
|
# Parse the response body according to its content type
|
98
118
|
def parse_body
|
99
119
|
if @headers['Content-Type']
|
100
120
|
mime_type = MimeType[@headers['Content-Type'].split(/;\s*/).first]
|
101
|
-
return mime_type.parse(
|
121
|
+
return mime_type.parse(body) if mime_type
|
102
122
|
end
|
103
123
|
|
104
|
-
|
124
|
+
body
|
105
125
|
end
|
106
126
|
|
107
127
|
# Returns an Array ala Rack: `[status, headers, body]`
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Http
|
2
|
+
class Response
|
3
|
+
class Parser
|
4
|
+
attr_reader :headers
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@parser = Http::Parser.new(self)
|
8
|
+
reset
|
9
|
+
end
|
10
|
+
|
11
|
+
def add(data)
|
12
|
+
@parser << data
|
13
|
+
end
|
14
|
+
alias_method :<<, :add
|
15
|
+
|
16
|
+
def headers?
|
17
|
+
!!@headers
|
18
|
+
end
|
19
|
+
|
20
|
+
def http_version
|
21
|
+
@parser.http_version.join(".")
|
22
|
+
end
|
23
|
+
|
24
|
+
def status_code
|
25
|
+
@parser.status_code
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Http::Parser callbacks
|
30
|
+
#
|
31
|
+
|
32
|
+
def on_headers_complete(headers)
|
33
|
+
@headers = headers
|
34
|
+
end
|
35
|
+
|
36
|
+
def on_body(chunk)
|
37
|
+
if @chunk
|
38
|
+
@chunk << chunk
|
39
|
+
else
|
40
|
+
@chunk = chunk
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def chunk
|
45
|
+
if (chunk = @chunk)
|
46
|
+
@chunk = nil
|
47
|
+
chunk
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def on_message_complete
|
52
|
+
@finished = true
|
53
|
+
end
|
54
|
+
|
55
|
+
def reset
|
56
|
+
@finished = false
|
57
|
+
@headers = nil
|
58
|
+
@chunk = nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
File without changes
|
data/lib/http/version.rb
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Http::Options, "body" do
|
4
|
+
|
5
|
+
let(:opts){ Http::Options.new }
|
6
|
+
|
7
|
+
it 'defaults to nil' do
|
8
|
+
opts.body.should be_nil
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'may be specified with with_body' do
|
12
|
+
opts2 = opts.with_body("foo")
|
13
|
+
opts.body.should be_nil
|
14
|
+
opts2.body.should eq("foo")
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
@@ -19,18 +19,25 @@ describe Http::Options, "merge" do
|
|
19
19
|
foo = Http::Options.new(
|
20
20
|
:response => :body,
|
21
21
|
:form => {:foo => 'foo'},
|
22
|
+
:body => "body-foo",
|
22
23
|
:headers => {:accept => "json", :foo => 'foo'},
|
24
|
+
:proxy => {},
|
23
25
|
:callbacks => {:request => ["common"], :response => ["foo"]})
|
24
26
|
bar = Http::Options.new(
|
25
27
|
:response => :parsed_body,
|
26
28
|
:form => {:bar => 'bar'},
|
29
|
+
:body => "body-bar",
|
27
30
|
:headers => {:accept => "xml", :bar => 'bar'},
|
31
|
+
:proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080},
|
28
32
|
:callbacks => {:request => ["common"], :response => ["bar"]})
|
29
33
|
foo.merge(bar).to_hash.should eq(
|
30
34
|
:response => :parsed_body,
|
31
35
|
:form => {:bar => 'bar'},
|
36
|
+
:body => "body-bar",
|
32
37
|
:headers => {:accept => "xml", :foo => "foo", :bar => 'bar'},
|
33
|
-
:
|
38
|
+
:proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080},
|
39
|
+
:callbacks => {:request => ["common"], :response => ["foo", "bar"]},
|
40
|
+
:follow => nil
|
34
41
|
)
|
35
42
|
end
|
36
43
|
|
@@ -18,6 +18,11 @@ describe Http::Options, "new" do
|
|
18
18
|
opts = Http::Options.new(:headers => {:accept => "json"})
|
19
19
|
opts.headers.should eq(:accept => "json")
|
20
20
|
end
|
21
|
+
|
22
|
+
it 'coerces :proxy correctly' do
|
23
|
+
opts = Http::Options.new(:proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080})
|
24
|
+
opts.proxy.should eq(:proxy_address => "127.0.0.1", :proxy_port => 8080)
|
25
|
+
end
|
21
26
|
|
22
27
|
it 'coerces :form correctly' do
|
23
28
|
opts = Http::Options.new(:form => {:foo => 42})
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Http::Options, "proxy" do
|
4
|
+
|
5
|
+
let(:opts){ Http::Options.new }
|
6
|
+
|
7
|
+
it 'defaults to {}' do
|
8
|
+
opts.proxy.should eq({})
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'may be specified with with_proxy' do
|
12
|
+
opts2 = opts.with_proxy(:proxy_address => "127.0.0.1", :proxy_port => 8080)
|
13
|
+
opts.proxy.should eq({})
|
14
|
+
opts2.proxy.should eq(:proxy_address => "127.0.0.1", :proxy_port => 8080)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'accepts proxy address, port, username, and password' do
|
18
|
+
opts2 = opts.with_proxy(:proxy_address => "127.0.0.1", :proxy_port => 8080, :proxy_username => "username", :proxy_password => "password")
|
19
|
+
opts2.proxy.should eq(:proxy_address => "127.0.0.1", :proxy_port => 8080, :proxy_username => "username", :proxy_password => "password")
|
20
|
+
end
|
21
|
+
end
|
data/spec/http/response_spec.rb
CHANGED
@@ -1,69 +1,57 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Http::Response do
|
4
|
+
describe "headers" do
|
5
|
+
subject { Http::Response.new(200, "1.1", "Content-Type" => "text/plain") }
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
describe "the response headers" do
|
8
|
-
|
9
|
-
it 'are available through Hash-like methods' do
|
10
|
-
subject["Content-Type"] = "text/plain"
|
7
|
+
it "exposes header fields for easy access" do
|
11
8
|
subject["Content-Type"].should eq("text/plain")
|
12
9
|
end
|
13
10
|
|
14
|
-
it
|
15
|
-
subject["Content-Type"] = "text/plain"
|
11
|
+
it "provides a #headers accessor too" do
|
16
12
|
subject.headers.should eq("Content-Type" => "text/plain")
|
17
13
|
end
|
18
|
-
|
19
14
|
end
|
20
15
|
|
21
|
-
describe "parse_body" do
|
16
|
+
describe "#parse_body" do
|
17
|
+
context "on a registered MIME type" do
|
18
|
+
let(:body) { ::JSON.dump("Hello" => "World") }
|
19
|
+
subject { Http::Response.new(200, "1.1", {"Content-Type" => "application/json"}, body) }
|
22
20
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
subject.parse_body.should eq("hello" => "World")
|
21
|
+
it "returns a parsed response body" do
|
22
|
+
subject.parse_body.should eq ::JSON.parse(body)
|
23
|
+
end
|
27
24
|
end
|
28
25
|
|
29
|
-
|
30
|
-
|
31
|
-
subject.
|
32
|
-
subject.parse_body.should eq("Hello world")
|
33
|
-
end
|
26
|
+
context "on an unregistered MIME type" do
|
27
|
+
let(:body) { "Hello world" }
|
28
|
+
subject { Http::Response.new(200, "1.1", {"Content-Type" => "text/plain"}, body) }
|
34
29
|
|
30
|
+
it "returns the raw body as a String" do
|
31
|
+
subject.parse_body.should eq(body)
|
32
|
+
end
|
33
|
+
end
|
35
34
|
end
|
36
35
|
|
37
36
|
describe "to_a" do
|
37
|
+
context "on a registered MIME type" do
|
38
|
+
let(:body) { ::JSON.dump("Hello" => "World") }
|
39
|
+
let(:content_type) { "application/json" }
|
40
|
+
subject { Http::Response.new(200, "1.1", {"Content-Type" => content_type}, body) }
|
38
41
|
|
39
|
-
|
40
|
-
|
41
|
-
r.status = 200
|
42
|
-
r.headers = {"Content-Type" => "text/plain"}
|
43
|
-
r.body = "Hello world"
|
42
|
+
it "retuns a Rack-like array with a parsed response body" do
|
43
|
+
subject.to_a.should eq([200, {"Content-Type" => content_type}, ::JSON.parse(body)])
|
44
44
|
end
|
45
|
-
expected = [
|
46
|
-
200,
|
47
|
-
{"Content-Type" => "text/plain"},
|
48
|
-
"Hello world"
|
49
|
-
]
|
50
|
-
subject.to_a.should eq(expected)
|
51
45
|
end
|
52
46
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
47
|
+
context "on an unregistered MIME type" do
|
48
|
+
let(:body) { "Hello world" }
|
49
|
+
let(:content_type) { "text/plain" }
|
50
|
+
subject { Http::Response.new(200, "1.1", {"Content-Type" => content_type}, body) }
|
51
|
+
|
52
|
+
it "returns a Rack-like array" do
|
53
|
+
subject.to_a.should eq([200, {"Content-Type" => content_type}, body])
|
58
54
|
end
|
59
|
-
expected = [
|
60
|
-
200,
|
61
|
-
{"Content-Type" => "application/json"},
|
62
|
-
{"hello" => "World"}
|
63
|
-
]
|
64
|
-
subject.to_a.should eq(expected)
|
65
55
|
end
|
66
|
-
|
67
56
|
end
|
68
|
-
|
69
57
|
end
|
data/spec/http_spec.rb
CHANGED
@@ -2,7 +2,8 @@ require 'spec_helper'
|
|
2
2
|
require 'json'
|
3
3
|
|
4
4
|
describe Http do
|
5
|
-
let(:test_endpoint)
|
5
|
+
let(:test_endpoint) { "http://127.0.0.1:#{ExampleService::PORT}/" }
|
6
|
+
let(:proxy_endpoint) { "#{test_endpoint}proxy" }
|
6
7
|
|
7
8
|
context "getting resources" do
|
8
9
|
it "should be easy" do
|
@@ -10,6 +11,13 @@ describe Http do
|
|
10
11
|
response.should match(/<!doctype html>/)
|
11
12
|
end
|
12
13
|
|
14
|
+
context "with_response" do
|
15
|
+
it 'allows specifying :object' do
|
16
|
+
res = Http.with_response(:object).get test_endpoint
|
17
|
+
res.should be_a(Http::Response)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
13
21
|
context "with headers" do
|
14
22
|
it "should be easy" do
|
15
23
|
response = Http.accept(:json).get test_endpoint
|
@@ -32,15 +40,72 @@ describe Http do
|
|
32
40
|
response.should be_a Http::Response
|
33
41
|
end
|
34
42
|
end
|
43
|
+
|
44
|
+
it "should not mess with the returned status" do
|
45
|
+
client = Http.with_response(:object)
|
46
|
+
res = client.get test_endpoint
|
47
|
+
res.status.should == 200
|
48
|
+
res = client.get "#{test_endpoint}not-found"
|
49
|
+
res.status.should == 404
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context "with http proxy address and port" do
|
54
|
+
it "should proxy the request" do
|
55
|
+
response = Http.via("127.0.0.1", 8080).get proxy_endpoint
|
56
|
+
response.should match(/Proxy!/)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "with http proxy address, port username and password" do
|
61
|
+
it "should proxy the request" do
|
62
|
+
response = Http.via("127.0.0.1", 8081, "username", "password").get proxy_endpoint
|
63
|
+
response.should match(/Proxy!/)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "with http proxy address, port, with wrong username and password" do
|
68
|
+
it "should proxy the request" do
|
69
|
+
pending "fixing proxy support"
|
70
|
+
|
71
|
+
response = Http.via("127.0.0.1", 8081, "user", "pass").get proxy_endpoint
|
72
|
+
response.should match(/Proxy Authentication Required/)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context "without proxy port" do
|
77
|
+
it "should raise an argument error" do
|
78
|
+
expect { Http.via("127.0.0.1") }.to raise_error ArgumentError
|
79
|
+
end
|
35
80
|
end
|
36
81
|
|
37
82
|
context "posting to resources" do
|
38
|
-
it "should be easy" do
|
39
|
-
response = Http.post test_endpoint, :form => {:example => 'testing'}
|
83
|
+
it "should be easy to post forms" do
|
84
|
+
response = Http.post "#{test_endpoint}form", :form => {:example => 'testing-form'}
|
85
|
+
response.should == "passed :)"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "posting with an explicit body" do
|
90
|
+
it "should be easy to post" do
|
91
|
+
response = Http.post "#{test_endpoint}body", :body => "testing-body"
|
40
92
|
response.should == "passed :)"
|
41
93
|
end
|
42
94
|
end
|
43
95
|
|
96
|
+
context "with redirects" do
|
97
|
+
it "should be easy for 301" do
|
98
|
+
response = Http.with_follow(true).get("#{test_endpoint}redirect-301")
|
99
|
+
response.should match(/<!doctype html>/)
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should be easy for 302" do
|
103
|
+
response = Http.with_follow(true).get("#{test_endpoint}redirect-302")
|
104
|
+
response.should match(/<!doctype html>/)
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
44
109
|
context "head requests" do
|
45
110
|
it "should be easy" do
|
46
111
|
response = Http.head test_endpoint
|
data/spec/spec_helper.rb
CHANGED
@@ -16,6 +16,18 @@ class ExampleService < WEBrick::HTTPServlet::AbstractServlet
|
|
16
16
|
response['Content-Type'] = 'text/html'
|
17
17
|
response.body = "<!doctype html>"
|
18
18
|
end
|
19
|
+
when "/proxy"
|
20
|
+
response.status = 200
|
21
|
+
response.body = "Proxy!"
|
22
|
+
when "/not-found"
|
23
|
+
response.body = "not found"
|
24
|
+
response.status = 404
|
25
|
+
when "/redirect-301"
|
26
|
+
response.status = 301
|
27
|
+
response["Location"] = "http://127.0.0.1:#{PORT}/"
|
28
|
+
when "/redirect-302"
|
29
|
+
response.status = 302
|
30
|
+
response["Location"] = "http://127.0.0.1:#{PORT}/"
|
19
31
|
else
|
20
32
|
response.status = 404
|
21
33
|
end
|
@@ -23,8 +35,16 @@ class ExampleService < WEBrick::HTTPServlet::AbstractServlet
|
|
23
35
|
|
24
36
|
def do_POST(request, response)
|
25
37
|
case request.path
|
26
|
-
when "/"
|
27
|
-
if request.query['example'] == 'testing'
|
38
|
+
when "/form"
|
39
|
+
if request.query['example'] == 'testing-form'
|
40
|
+
response.status = 200
|
41
|
+
response.body = "passed :)"
|
42
|
+
else
|
43
|
+
response.status = 400
|
44
|
+
response.body = "invalid! >:E"
|
45
|
+
end
|
46
|
+
when "/body"
|
47
|
+
if request.body == 'testing-body'
|
28
48
|
response.status = 200
|
29
49
|
response.body = "passed :)"
|
30
50
|
else
|
@@ -54,3 +74,5 @@ t = Thread.new { ExampleServer.start }
|
|
54
74
|
trap("INT") { ExampleServer.shutdown; exit }
|
55
75
|
|
56
76
|
Thread.pass while t.status and t.status != "sleep"
|
77
|
+
|
78
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'webrick/httpproxy'
|
2
|
+
|
3
|
+
ProxyServer = WEBrick::HTTPProxyServer.new(:Port => 8080, :AccessLog => [])
|
4
|
+
|
5
|
+
t = Thread.new { ProxyServer.start }
|
6
|
+
trap("INT") { ProxyServer.shutdown; exit }
|
7
|
+
|
8
|
+
AuthenticatedProxyServer = WEBrick::HTTPProxyServer.new(:Port => 8081,
|
9
|
+
:ProxyAuthProc => Proc.new do | req, res |
|
10
|
+
WEBrick::HTTPAuth.proxy_basic_auth(req, res, 'proxy') do | user, pass |
|
11
|
+
user == 'username' and pass == 'password'
|
12
|
+
end
|
13
|
+
end)
|
14
|
+
|
15
|
+
|
16
|
+
t = Thread.new { AuthenticatedProxyServer.start }
|
17
|
+
trap("INT") { AuthenticatedProxyServer.shutdown; exit }
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,22 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-09-01 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: http_parser.rb
|
16
|
+
requirement: &70356765427900 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70356765427900
|
14
25
|
- !ruby/object:Gem::Dependency
|
15
26
|
name: certified
|
16
|
-
requirement: &
|
27
|
+
requirement: &70356765427240 !ruby/object:Gem::Requirement
|
17
28
|
none: false
|
18
29
|
requirements:
|
19
30
|
- - ! '>='
|
@@ -21,10 +32,10 @@ dependencies:
|
|
21
32
|
version: '0'
|
22
33
|
type: :runtime
|
23
34
|
prerelease: false
|
24
|
-
version_requirements: *
|
35
|
+
version_requirements: *70356765427240
|
25
36
|
- !ruby/object:Gem::Dependency
|
26
37
|
name: rake
|
27
|
-
requirement: &
|
38
|
+
requirement: &70356765442460 !ruby/object:Gem::Requirement
|
28
39
|
none: false
|
29
40
|
requirements:
|
30
41
|
- - ! '>='
|
@@ -32,10 +43,10 @@ dependencies:
|
|
32
43
|
version: '0'
|
33
44
|
type: :development
|
34
45
|
prerelease: false
|
35
|
-
version_requirements: *
|
46
|
+
version_requirements: *70356765442460
|
36
47
|
- !ruby/object:Gem::Dependency
|
37
48
|
name: rspec
|
38
|
-
requirement: &
|
49
|
+
requirement: &70356765441700 !ruby/object:Gem::Requirement
|
39
50
|
none: false
|
40
51
|
requirements:
|
41
52
|
- - ! '>='
|
@@ -43,10 +54,10 @@ dependencies:
|
|
43
54
|
version: '0'
|
44
55
|
type: :development
|
45
56
|
prerelease: false
|
46
|
-
version_requirements: *
|
57
|
+
version_requirements: *70356765441700
|
47
58
|
- !ruby/object:Gem::Dependency
|
48
59
|
name: json
|
49
|
-
requirement: &
|
60
|
+
requirement: &70356765441120 !ruby/object:Gem::Requirement
|
50
61
|
none: false
|
51
62
|
requirements:
|
52
63
|
- - ! '>='
|
@@ -54,7 +65,7 @@ dependencies:
|
|
54
65
|
version: '0'
|
55
66
|
type: :development
|
56
67
|
prerelease: false
|
57
|
-
version_requirements: *
|
68
|
+
version_requirements: *70356765441120
|
58
69
|
description: HTTP so awesome it will lure Catherine Zeta Jones into your unicorn petting
|
59
70
|
zoo
|
60
71
|
email:
|
@@ -81,20 +92,25 @@ files:
|
|
81
92
|
- lib/http/options.rb
|
82
93
|
- lib/http/request.rb
|
83
94
|
- lib/http/response.rb
|
95
|
+
- lib/http/response_parser.rb
|
96
|
+
- lib/http/streaming_body.rb
|
84
97
|
- lib/http/uri_backport.rb
|
85
98
|
- lib/http/version.rb
|
86
99
|
- spec/http/compat/curb_spec.rb
|
100
|
+
- spec/http/options/body_spec.rb
|
87
101
|
- spec/http/options/callbacks_spec.rb
|
88
102
|
- spec/http/options/form_spec.rb
|
89
103
|
- spec/http/options/headers_spec.rb
|
90
104
|
- spec/http/options/merge_spec.rb
|
91
105
|
- spec/http/options/new_spec.rb
|
106
|
+
- spec/http/options/proxy_spec.rb
|
92
107
|
- spec/http/options/response_spec.rb
|
93
108
|
- spec/http/options_spec.rb
|
94
109
|
- spec/http/response_spec.rb
|
95
110
|
- spec/http_spec.rb
|
96
111
|
- spec/spec_helper.rb
|
97
112
|
- spec/support/example_server.rb
|
113
|
+
- spec/support/proxy_server.rb
|
98
114
|
homepage: https://github.com/tarcieri/http
|
99
115
|
licenses: []
|
100
116
|
post_install_message:
|
@@ -115,20 +131,24 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
131
|
version: '0'
|
116
132
|
requirements: []
|
117
133
|
rubyforge_project:
|
118
|
-
rubygems_version: 1.8.
|
134
|
+
rubygems_version: 1.8.10
|
119
135
|
signing_key:
|
120
136
|
specification_version: 3
|
121
137
|
summary: HTTP should be easy
|
122
138
|
test_files:
|
123
139
|
- spec/http/compat/curb_spec.rb
|
140
|
+
- spec/http/options/body_spec.rb
|
124
141
|
- spec/http/options/callbacks_spec.rb
|
125
142
|
- spec/http/options/form_spec.rb
|
126
143
|
- spec/http/options/headers_spec.rb
|
127
144
|
- spec/http/options/merge_spec.rb
|
128
145
|
- spec/http/options/new_spec.rb
|
146
|
+
- spec/http/options/proxy_spec.rb
|
129
147
|
- spec/http/options/response_spec.rb
|
130
148
|
- spec/http/options_spec.rb
|
131
149
|
- spec/http/response_spec.rb
|
132
150
|
- spec/http_spec.rb
|
133
151
|
- spec/spec_helper.rb
|
134
152
|
- spec/support/example_server.rb
|
153
|
+
- spec/support/proxy_server.rb
|
154
|
+
has_rdoc:
|