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 CHANGED
@@ -3,9 +3,9 @@ rvm:
3
3
  - 1.9.2
4
4
  - 1.9.3
5
5
  - ree
6
- - ruby-head
6
+ # - ruby-head
7
7
  - jruby-18mode
8
8
  - jruby-19mode
9
- - jruby-head
9
+ # - jruby-head
10
10
  - rbx-18mode
11
11
  - rbx-19mode
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
@@ -1,4 +1,6 @@
1
1
  source 'http://rubygems.org'
2
2
 
3
+ gem 'jruby-openssl' if defined? JRUBY_VERSION
4
+
3
5
  # Specify your gem's dependencies in http.gemspec
4
6
  gemspec
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  Http
2
2
  ====
3
- [![Build Status](http://travis-ci.org/tarcieri/http.png)](http://travis-ci.org/tarcieri/http)
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
- A shorter alias exists for HTTP.with_headers:
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.add_dependency 'certified'
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
- # THIS IS ENTIRELY TEMPORARY, I ASSURE YOU
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
- # Transform to canonical HTTP header capitalization
30
- def self.canonicalize_header(header)
31
- header.to_s.split('-').map(&:capitalize).join('-')
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
@@ -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
- if opts.form
20
- body = URI.encode_www_form(opts.form)
21
- headers['Content-Type'] ||= 'application/x-www-form-urlencoded'
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
- request = Request.new method, uri, headers, body
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
- uri = request.uri
35
- http = Net::HTTP.new(uri.host, uri.port)
36
- http.use_ssl = true if uri.is_a? URI::HTTPS
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
- Http::Response.new.tap do |res|
40
- response.each_header do |header, value|
41
- res[header] = value
42
- end
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
- res.status = Integer(response.code)
45
- res.body = response.body
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
- # Before callbacks
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
- protected :response=, :headers=, :form=, :callbacks=
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
- def self.new(default = {})
19
- return default if default.is_a?(Options)
20
- super
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(default = {})
24
- @response = default[:response] || :auto
25
- @headers = default[:headers] || {}
26
- @form = default[:form] || nil
27
- @callbacks = default[:callbacks] || {:request => [], :response => []}
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
- :callbacks => callbacks}
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
- # Create a Net::HTTP request from this request
35
- def to_net_http_request
36
- request_class = Net::HTTP.const_get(@method.to_s.capitalize)
37
- request = request_class.new(@uri.request_uri, @headers)
38
- request.body = @body
39
- request
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
- attr_accessor :status
61
- attr_accessor :headers
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, :status
66
- alias_method :code=, :status=
66
+ alias_method :code, :status
67
+ alias_method :status_code, :status
67
68
 
68
- alias_method :status_code, :status
69
- alias_method :status_code=, :status
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(@body) if mime_type
121
+ return mime_type.parse(body) if mime_type
102
122
  end
103
123
 
104
- @body
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
@@ -1,3 +1,3 @@
1
1
  module Http
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -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
- :callbacks => {:request => ["common"], :response => ["foo", "bar"]}
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
@@ -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
- let(:subject){ Http::Response.new }
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 'are available through a `headers` accessor' do
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
- it 'works on a registered mime-type' do
24
- subject["Content-Type"] = "application/json"
25
- subject.body = ::JSON.dump("hello" => "World")
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
- it 'returns the body on an unregistered mime-type' do
30
- subject["Content-Type"] = "text/plain"
31
- subject.body = "Hello world"
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
- it 'mimics Rack' do
40
- subject.tap do |r|
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
- it 'uses parse_body if known mime-type' do
54
- subject.tap do |r|
55
- r.status = 200
56
- r.headers = {"Content-Type" => "application/json"}
57
- r.body = ::JSON.dump("hello" => "World")
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) { "http://127.0.0.1:#{ExampleService::PORT}/" }
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
@@ -1,2 +1,3 @@
1
1
  require 'http'
2
- require 'support/example_server'
2
+ require 'support/example_server'
3
+ require 'support/proxy_server'
@@ -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.2.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-03-05 00:00:00.000000000 Z
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: &70273361267740 !ruby/object:Gem::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: *70273361267740
35
+ version_requirements: *70356765427240
25
36
  - !ruby/object:Gem::Dependency
26
37
  name: rake
27
- requirement: &70273361267320 !ruby/object:Gem::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: *70273361267320
46
+ version_requirements: *70356765442460
36
47
  - !ruby/object:Gem::Dependency
37
48
  name: rspec
38
- requirement: &70273361266900 !ruby/object:Gem::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: *70273361266900
57
+ version_requirements: *70356765441700
47
58
  - !ruby/object:Gem::Dependency
48
59
  name: json
49
- requirement: &70273361266480 !ruby/object:Gem::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: *70273361266480
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.17
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: