auser-rest-client 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ module RestClient
2
+ module Mixin
3
+ module Response
4
+ attr_reader :net_http_res
5
+
6
+ # HTTP status code, always 200 since RestClient throws exceptions for
7
+ # other codes.
8
+ def code
9
+ @code ||= @net_http_res.code.to_i
10
+ end
11
+
12
+ # A hash of the headers, beautified with symbols and underscores.
13
+ # e.g. "Content-type" will become :content_type.
14
+ def headers
15
+ @headers ||= self.class.beautify_headers(@net_http_res.to_hash)
16
+ end
17
+
18
+ # Hash of cookies extracted from response headers
19
+ def cookies
20
+ @cookies ||= (self.headers[:set_cookie] || "").split('; ').inject({}) do |out, raw_c|
21
+ key, val = raw_c.split('=')
22
+ unless %w(expires domain path secure).member?(key)
23
+ out[key] = val
24
+ end
25
+ out
26
+ end
27
+ end
28
+
29
+ def self.included(receiver)
30
+ receiver.extend(RestClient::Mixin::Response::ClassMethods)
31
+ end
32
+
33
+ module ClassMethods
34
+ def beautify_headers(headers)
35
+ headers.inject({}) do |out, (key, value)|
36
+ out[key.gsub(/-/, '_').to_sym] = value.first
37
+ out
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,236 @@
1
+ require 'tempfile'
2
+
3
+ module RestClient
4
+ # This class is used internally by RestClient 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
+ # RestClient::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
+ execute_inner
40
+ rescue Redirect => e
41
+ @url = e.url
42
+ execute
43
+ end
44
+
45
+ def execute_inner
46
+ uri = parse_url_with_auth(url)
47
+ transmit uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload
48
+ end
49
+
50
+ def make_headers(user_headers)
51
+ unless @cookies.empty?
52
+ user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ')
53
+ end
54
+
55
+ default_headers.merge(user_headers).inject({}) do |final, (key, value)|
56
+ final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s
57
+ final
58
+ end
59
+ end
60
+
61
+ def net_http_class
62
+ if RestClient.proxy
63
+ proxy_uri = URI.parse(RestClient.proxy)
64
+ Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
65
+ else
66
+ Net::HTTP
67
+ end
68
+ end
69
+
70
+ def net_http_request_class(method)
71
+ Net::HTTP.const_get(method.to_s.capitalize)
72
+ end
73
+
74
+ def parse_url(url)
75
+ url = "http://#{url}" unless url.match(/^http/)
76
+ URI.parse(url)
77
+ end
78
+
79
+ def parse_url_with_auth(url)
80
+ uri = parse_url(url)
81
+ @user = uri.user if uri.user
82
+ @password = uri.password if uri.password
83
+ uri
84
+ end
85
+
86
+ def process_payload(p=nil, parent_key=nil)
87
+ unless p.is_a?(Hash)
88
+ p
89
+ else
90
+ @headers[:content_type] ||= 'application/x-www-form-urlencoded'
91
+ p.keys.map do |k|
92
+ key = parent_key ? "#{parent_key}[#{k}]" : k
93
+ if p[k].is_a? Hash
94
+ process_payload(p[k], key)
95
+ else
96
+ value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
97
+ "#{key}=#{value}"
98
+ end
99
+ end.join("&")
100
+ end
101
+ end
102
+
103
+ def transmit(uri, req, payload)
104
+ setup_credentials(req)
105
+
106
+ net = net_http_class.new(uri.host, uri.port)
107
+ net.use_ssl = uri.is_a?(URI::HTTPS)
108
+ if @verify_ssl == false
109
+ net.verify_mode = OpenSSL::SSL::VERIFY_NONE
110
+ elsif @verify_ssl.is_a? Integer
111
+ net.verify_mode = @verify_ssl
112
+ end
113
+ net.cert = @ssl_client_cert if @ssl_client_cert
114
+ net.key = @ssl_client_key if @ssl_client_key
115
+ net.ca_file = @ssl_ca_file if @ssl_ca_file
116
+ net.read_timeout = @timeout if @timeout
117
+ net.open_timeout = @open_timeout if @open_timeout
118
+
119
+ display_log request_log
120
+
121
+ net.start do |http|
122
+ res = http.request(req, payload) { |http_response| fetch_body(http_response) }
123
+ result = process_result(res)
124
+ display_log response_log(res)
125
+
126
+ if result.kind_of?(String) or @method == :head
127
+ Response.new(result, res)
128
+ elsif @raw_response
129
+ RawResponse.new(@tf, res)
130
+ else
131
+ nil
132
+ end
133
+ end
134
+ rescue EOFError
135
+ raise RestClient::ServerBrokeConnection
136
+ rescue Timeout::Error
137
+ raise RestClient::RequestTimeout
138
+ end
139
+
140
+ def setup_credentials(req)
141
+ req.basic_auth(user, password) if user
142
+ end
143
+
144
+ def fetch_body(http_response)
145
+ if @raw_response
146
+ # Taken from Chef, which as in turn...
147
+ # Stolen from http://www.ruby-forum.com/topic/166423
148
+ # Kudos to _why!
149
+ @tf = Tempfile.new("rest-client")
150
+ size, total = 0, http_response.header['Content-Length'].to_i
151
+ http_response.read_body do |chunk|
152
+ @tf.write(chunk)
153
+ size += chunk.size
154
+ if size == 0
155
+ display_log("#{@method} #{@url} done (0 length file)")
156
+ elsif total == 0
157
+ display_log("#{@method} #{@url} (zero content length)")
158
+ else
159
+ display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total])
160
+ end
161
+ end
162
+ @tf.close
163
+ @tf
164
+ else
165
+ http_response.read_body
166
+ end
167
+ http_response
168
+ end
169
+
170
+ def process_result(res)
171
+ if res.code =~ /\A2\d{2}\z/
172
+ # We don't decode raw requests
173
+ unless @raw_response
174
+ decode res['content-encoding'], res.body if res.body
175
+ end
176
+ elsif %w(301 302 303).include? res.code
177
+ url = res.header['Location']
178
+
179
+ if url !~ /^http/
180
+ uri = URI.parse(@url)
181
+ uri.path = "/#{url}".squeeze('/')
182
+ url = uri.to_s
183
+ end
184
+
185
+ raise Redirect, url
186
+ elsif res.code == "304"
187
+ raise NotModified, res
188
+ elsif res.code == "401"
189
+ raise Unauthorized, res
190
+ elsif res.code == "404"
191
+ raise ResourceNotFound, res
192
+ else
193
+ raise RequestFailed, res
194
+ end
195
+ end
196
+
197
+ def decode(content_encoding, body)
198
+ if content_encoding == 'gzip' and not body.empty?
199
+ Zlib::GzipReader.new(StringIO.new(body)).read
200
+ elsif content_encoding == 'deflate'
201
+ Zlib::Inflate.new.inflate(body)
202
+ else
203
+ body
204
+ end
205
+ end
206
+
207
+ def request_log
208
+ out = []
209
+ out << "RestClient.#{method} #{url.inspect}"
210
+ out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
211
+ out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
212
+ out.join(', ')
213
+ end
214
+
215
+ def response_log(res)
216
+ size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
217
+ "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes"
218
+ end
219
+
220
+ def display_log(msg)
221
+ return unless log_to = RestClient.log
222
+
223
+ if log_to == 'stdout'
224
+ STDOUT.puts msg
225
+ elsif log_to == 'stderr'
226
+ STDERR.puts msg
227
+ else
228
+ File.open(log_to, 'a') { |f| f.puts msg }
229
+ end
230
+ end
231
+
232
+ def default_headers
233
+ { :accept => 'application/xml', :accept_encoding => 'gzip, deflate' }
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,146 @@
1
+ module RestClient
2
+ # A class that can be instantiated for access to a RESTful resource,
3
+ # including authentication.
4
+ #
5
+ # Example:
6
+ #
7
+ # resource = RestClient::Resource.new('http://some/resource')
8
+ # jpg = resource.get(:accept => 'image/jpg')
9
+ #
10
+ # With HTTP basic authentication:
11
+ #
12
+ # resource = RestClient::Resource.new('http://protected/resource', :user => 'user', :password => 'password')
13
+ # resource.delete
14
+ #
15
+ # With a timeout (seconds):
16
+ #
17
+ # RestClient::Resource.new('http://slow', :timeout => 10)
18
+ #
19
+ # With an open timeout (seconds):
20
+ #
21
+ # RestClient::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 = RestClient::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 = RestClient::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 = RestClient::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
+ # RestClient::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 = RestClient::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 RestClient
4
+ # The response from RestClient looks like a string, but is actually one of
5
+ # these. 99% of the time you're making a rest call all you care about is
6
+ # the body, but on the occassion you want to fetch the headers you can:
7
+ #
8
+ # RestClient.get('http://example.com').headers[:content_type]
9
+ #
10
+ class Response < String
11
+
12
+ include RestClient::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