sevenwire-http_client 0.1.0

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.
@@ -0,0 +1,30 @@
1
+ require File.dirname(__FILE__) + '/mixin/response'
2
+
3
+ module HttpClient
4
+ # The response from HttpClient on a raw request looks like a string, but is
5
+ # actually one of these. 99% of the time you're making a http call all you
6
+ # care about is the body, but on the occassion you want to fetch the
7
+ # headers you can:
8
+ #
9
+ # HttpClient.get('http://example.com').headers[:content_type]
10
+ #
11
+ # In addition, if you do not use the response as a string, you can access
12
+ # a Tempfile object at res.file, which contains the path to the raw
13
+ # downloaded request body.
14
+ class RawResponse
15
+ include HttpClient::Mixin::Response
16
+
17
+ attr_reader :file
18
+
19
+ def initialize(tempfile, net_http_res)
20
+ @net_http_res = net_http_res
21
+ @file = tempfile
22
+ end
23
+
24
+ def to_s
25
+ @file.open
26
+ @file.read
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,207 @@
1
+ require 'tempfile'
2
+
3
+ module HttpClient
4
+ # This class is used internally by HttpClient to send the request, but you can also
5
+ # access it internally if you'd like to use a method not directly supported by the
6
+ # main API. For example:
7
+ #
8
+ # HttpClient::Request.execute(:method => :head, :url => 'http://example.com')
9
+ #
10
+ class Request
11
+ attr_reader :method, :url, :payload, :headers,
12
+ :cookies, :user, :password, :timeout, :open_timeout,
13
+ :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file,
14
+ :raw_response
15
+
16
+ def self.execute(args)
17
+ new(args).execute
18
+ end
19
+
20
+ def initialize(args)
21
+ @method = args[:method] or raise ArgumentError, "must pass :method"
22
+ @url = args[:url] or raise ArgumentError, "must pass :url"
23
+ @headers = args[:headers] || {}
24
+ @cookies = @headers.delete(:cookies) || args[:cookies] || {}
25
+ @payload = process_payload(args[:payload])
26
+ @user = args[:user]
27
+ @password = args[:password]
28
+ @timeout = args[:timeout]
29
+ @open_timeout = args[:open_timeout]
30
+ @raw_response = args[:raw_response] || false
31
+ @verify_ssl = args[:verify_ssl] || false
32
+ @ssl_client_cert = args[:ssl_client_cert] || nil
33
+ @ssl_client_key = args[:ssl_client_key] || nil
34
+ @ssl_ca_file = args[:ssl_ca_file] || nil
35
+ @tf = nil # If you are a raw request, this is your tempfile
36
+ end
37
+
38
+ def execute
39
+ uri = parse_url_with_auth(url)
40
+ transmit uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload
41
+ end
42
+
43
+ def make_headers(user_headers)
44
+ unless @cookies.empty?
45
+ user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ')
46
+ end
47
+
48
+ default_headers.merge(user_headers).inject({}) do |final, (key, value)|
49
+ final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s
50
+ final
51
+ end
52
+ end
53
+
54
+ def net_http_class
55
+ if HttpClient.proxy
56
+ proxy_uri = URI.parse(HttpClient.proxy)
57
+ Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
58
+ else
59
+ Net::HTTP
60
+ end
61
+ end
62
+
63
+ def net_http_request_class(method)
64
+ Net::HTTP.const_get(method.to_s.capitalize)
65
+ end
66
+
67
+ def parse_url(url)
68
+ url = "http://#{url}" unless url.match(/^http/)
69
+ URI.parse(url)
70
+ end
71
+
72
+ def parse_url_with_auth(url)
73
+ uri = parse_url(url)
74
+ @user = uri.user if uri.user
75
+ @password = uri.password if uri.password
76
+ uri
77
+ end
78
+
79
+ def process_payload(p=nil, parent_key=nil)
80
+ unless p.is_a?(Hash)
81
+ p
82
+ else
83
+ @headers[:content_type] ||= 'application/x-www-form-urlencoded'
84
+ p.keys.map do |k|
85
+ key = parent_key ? "#{parent_key}[#{k}]" : k
86
+ if p[k].is_a? Hash
87
+ process_payload(p[k], key)
88
+ else
89
+ value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
90
+ "#{key}=#{value}"
91
+ end
92
+ end.join("&")
93
+ end
94
+ end
95
+
96
+ def transmit(uri, req, payload)
97
+ setup_credentials(req)
98
+
99
+ net = net_http_class.new(uri.host, uri.port)
100
+ net.use_ssl = uri.is_a?(URI::HTTPS)
101
+ net.verify_mode = OpenSSL::SSL::VERIFY_NONE if @verify_ssl == false
102
+ net.cert = @ssl_client_cert if @ssl_client_cert
103
+ net.key = @ssl_client_key if @ssl_client_key
104
+ net.ca_file = @ssl_ca_file if @ssl_ca_file
105
+ net.read_timeout = @timeout if @timeout
106
+ net.open_timeout = @open_timeout if @open_timeout
107
+
108
+ display_log request_log
109
+
110
+ net.start do |http|
111
+ res = http.request(req, payload) { |http_response| fetch_body(http_response) }
112
+ result = process_result(res)
113
+ display_log response_log(res)
114
+
115
+ if result.kind_of?(String) or @method == :head
116
+ Response.new(result, res)
117
+ elsif @raw_response
118
+ RawResponse.new(@tf, res)
119
+ else
120
+ nil
121
+ end
122
+ end
123
+ rescue EOFError
124
+ raise HttpClient::ServerBrokeConnection
125
+ rescue Timeout::Error
126
+ raise HttpClient::RequestTimeout
127
+ rescue Errno::ECONNREFUSED
128
+ raise HttpClient::ConnectionRefused
129
+ end
130
+
131
+ def setup_credentials(req)
132
+ req.basic_auth(user, password) if user
133
+ end
134
+
135
+ def fetch_body(http_response)
136
+ if @raw_response
137
+ # Taken from Chef, which as in turn...
138
+ # Stolen from http://www.ruby-forum.com/topic/166423
139
+ # Kudos to _why!
140
+ @tf = Tempfile.new("http-client")
141
+ size, total = 0, http_response.header['Content-Length'].to_i
142
+ http_response.read_body do |chunk|
143
+ @tf.write(chunk)
144
+ size += chunk.size
145
+ if size == 0
146
+ display_log("#{@method} #{@url} done (0 length file)")
147
+ elsif total == 0
148
+ display_log("#{@method} #{@url} (zero content length)")
149
+ else
150
+ display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total])
151
+ end
152
+ end
153
+ @tf.close
154
+ @tf
155
+ else
156
+ http_response.read_body
157
+ end
158
+ http_response
159
+ end
160
+
161
+ def process_result(res)
162
+ # We don't decode raw requests
163
+ unless @raw_response
164
+ decode res['content-encoding'], res.body if res.body
165
+ end
166
+ end
167
+
168
+ def decode(content_encoding, body)
169
+ if content_encoding == 'gzip' and not body.empty?
170
+ Zlib::GzipReader.new(StringIO.new(body)).read
171
+ elsif content_encoding == 'deflate'
172
+ Zlib::Inflate.new.inflate(body)
173
+ else
174
+ body
175
+ end
176
+ end
177
+
178
+ def request_log
179
+ out = []
180
+ out << "HttpClient.#{method} #{url.inspect}"
181
+ out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
182
+ out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
183
+ out.join(', ')
184
+ end
185
+
186
+ def response_log(res)
187
+ size = @raw_response ? File.size(@tf.path) : res.body.size
188
+ "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes"
189
+ end
190
+
191
+ def display_log(msg)
192
+ return unless log_to = HttpClient.log
193
+
194
+ if log_to == 'stdout'
195
+ STDOUT.puts msg
196
+ elsif log_to == 'stderr'
197
+ STDERR.puts msg
198
+ else
199
+ File.open(log_to, 'a') { |f| f.puts msg }
200
+ end
201
+ end
202
+
203
+ def default_headers
204
+ { :accept => 'application/xml', :accept_encoding => 'gzip, deflate' }
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,146 @@
1
+ module HttpClient
2
+ # A class that can be instantiated for access to a HTTPful resource,
3
+ # including authentication.
4
+ #
5
+ # Example:
6
+ #
7
+ # resource = HttpClient::Resource.new('http://some/resource')
8
+ # jpg = resource.get(:accept => 'image/jpg')
9
+ #
10
+ # With HTTP basic authentication:
11
+ #
12
+ # resource = HttpClient::Resource.new('http://protected/resource', :user => 'user', :password => 'password')
13
+ # resource.delete
14
+ #
15
+ # With a timeout (seconds):
16
+ #
17
+ # HttpClient::Resource.new('http://slow', :timeout => 10)
18
+ #
19
+ # With an open timeout (seconds):
20
+ #
21
+ # HttpClient::Resource.new('http://behindfirewall', :open_timeout => 10)
22
+ #
23
+ # You can also use resources to share common headers. For headers keys,
24
+ # symbols are converted to strings. Example:
25
+ #
26
+ # resource = HttpClient::Resource.new('http://some/resource', :headers => { :client_version => 1 })
27
+ #
28
+ # This header will be transported as X-Client-Version (notice the X prefix,
29
+ # capitalization and hyphens)
30
+ #
31
+ # Use the [] syntax to allocate subresources:
32
+ #
33
+ # site = HttpClient::Resource.new('http://example.com', :user => 'adam', :password => 'mypasswd')
34
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
35
+ #
36
+ class Resource
37
+ attr_reader :url, :options
38
+
39
+ def initialize(url, options={}, backwards_compatibility=nil)
40
+ @url = url
41
+ if options.class == Hash
42
+ @options = options
43
+ else # compatibility with previous versions
44
+ @options = { :user => options, :password => backwards_compatibility }
45
+ end
46
+ end
47
+
48
+ def get(additional_headers={})
49
+ Request.execute(options.merge(
50
+ :method => :get,
51
+ :url => url,
52
+ :headers => headers.merge(additional_headers)
53
+ ))
54
+ end
55
+
56
+ def post(payload, additional_headers={})
57
+ Request.execute(options.merge(
58
+ :method => :post,
59
+ :url => url,
60
+ :payload => payload,
61
+ :headers => headers.merge(additional_headers)
62
+ ))
63
+ end
64
+
65
+ def put(payload, additional_headers={})
66
+ Request.execute(options.merge(
67
+ :method => :put,
68
+ :url => url,
69
+ :payload => payload,
70
+ :headers => headers.merge(additional_headers)
71
+ ))
72
+ end
73
+
74
+ def delete(additional_headers={})
75
+ Request.execute(options.merge(
76
+ :method => :delete,
77
+ :url => url,
78
+ :headers => headers.merge(additional_headers)
79
+ ))
80
+ end
81
+
82
+ def to_s
83
+ url
84
+ end
85
+
86
+ def user
87
+ options[:user]
88
+ end
89
+
90
+ def password
91
+ options[:password]
92
+ end
93
+
94
+ def headers
95
+ options[:headers] || {}
96
+ end
97
+
98
+ def timeout
99
+ options[:timeout]
100
+ end
101
+
102
+ def open_timeout
103
+ options[:open_timeout]
104
+ end
105
+
106
+ # Construct a subresource, preserving authentication.
107
+ #
108
+ # Example:
109
+ #
110
+ # site = HttpClient::Resource.new('http://example.com', 'adam', 'mypasswd')
111
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
112
+ #
113
+ # This is especially useful if you wish to define your site in one place and
114
+ # call it in multiple locations:
115
+ #
116
+ # def orders
117
+ # HttpClient::Resource.new('http://example.com/orders', 'admin', 'mypasswd')
118
+ # end
119
+ #
120
+ # orders.get # GET http://example.com/orders
121
+ # orders['1'].get # GET http://example.com/orders/1
122
+ # orders['1/items'].delete # DELETE http://example.com/orders/1/items
123
+ #
124
+ # Nest resources as far as you want:
125
+ #
126
+ # site = HttpClient::Resource.new('http://example.com')
127
+ # posts = site['posts']
128
+ # first_post = posts['1']
129
+ # comments = first_post['comments']
130
+ # comments.post 'Hello', :content_type => 'text/plain'
131
+ #
132
+ def [](suburl)
133
+ self.class.new(concat_urls(url, suburl), options)
134
+ end
135
+
136
+ def concat_urls(url, suburl) # :nodoc:
137
+ url = url.to_s
138
+ suburl = suburl.to_s
139
+ if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/'
140
+ url + suburl
141
+ else
142
+ "#{url}/#{suburl}"
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,20 @@
1
+ require File.dirname(__FILE__) + '/mixin/response'
2
+
3
+ module HttpClient
4
+ # The response from HttpClient looks like a string, but is actually one of
5
+ # these. 99% of the time you're making a http call all you care about is
6
+ # the body, but on the occassion you want to fetch the headers you can:
7
+ #
8
+ # HttpClient.get('http://example.com').headers[:content_type]
9
+ #
10
+ class Response < String
11
+
12
+ include HttpClient::Mixin::Response
13
+
14
+ def initialize(string, net_http_res)
15
+ @net_http_res = net_http_res
16
+ super(string || "")
17
+ end
18
+
19
+ end
20
+ end
data/spec/base.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+
4
+ require File.dirname(__FILE__) + '/../lib/http_client'
@@ -0,0 +1,12 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe HttpClient::Exception do
4
+ it "sets the exception message to ErrorMessage" do
5
+ HttpClient::ServerBrokeConnection.new.message.should == 'Server broke connection'
6
+ end
7
+
8
+ it "contains exceptions in HttpClient" do
9
+ HttpClient::ServerBrokeConnection.new.should be_a_kind_of(HttpClient::Exception)
10
+ HttpClient::RequestTimeout.new.should be_a_kind_of(HttpClient::Exception)
11
+ end
12
+ end
@@ -0,0 +1,53 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe HttpClient do
4
+ describe "API" do
5
+ it "GET" do
6
+ HttpClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {})
7
+ HttpClient.get('http://some/resource')
8
+ end
9
+
10
+ it "POST" do
11
+ HttpClient::Request.should_receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'payload', :headers => {})
12
+ HttpClient.post('http://some/resource', 'payload')
13
+ end
14
+
15
+ it "PUT" do
16
+ HttpClient::Request.should_receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'payload', :headers => {})
17
+ HttpClient.put('http://some/resource', 'payload')
18
+ end
19
+
20
+ it "DELETE" do
21
+ HttpClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {})
22
+ HttpClient.delete('http://some/resource')
23
+ end
24
+
25
+ it "HEAD" do
26
+ HttpClient::Request.should_receive(:execute).with(:method => :head, :url => 'http://some/resource', :headers => {})
27
+ HttpClient.head('http://some/resource')
28
+ end
29
+ end
30
+
31
+ describe "logging" do
32
+ after do
33
+ HttpClient.log = nil
34
+ end
35
+
36
+ it "gets the log source from the HTTPCLIENT_LOG environment variable" do
37
+ ENV.stub!(:[]).with('HTTPCLIENT_LOG').and_return('from env')
38
+ HttpClient.log = 'from class method'
39
+ HttpClient.log.should == 'from env'
40
+ end
41
+
42
+ it "sets a destination for log output, used if no environment variable is set" do
43
+ ENV.stub!(:[]).with('HTTPCLIENT_LOG').and_return(nil)
44
+ HttpClient.log = 'from class method'
45
+ HttpClient.log.should == 'from class method'
46
+ end
47
+
48
+ it "returns nil (no logging) if neither are set (default)" do
49
+ ENV.stub!(:[]).with('HTTPCLIENT_LOG').and_return(nil)
50
+ HttpClient.log.should == nil
51
+ end
52
+ end
53
+ end