sml-rest-client 1.1.7

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,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,21 @@
1
+ #
2
+ # Replace the request method in Net::HTTP to sniff the body type
3
+ # and set the stream if appropriate
4
+ #
5
+ # Taken from:
6
+ # http://www.missiondata.com/blog/ruby/29/streaming-data-to-s3-with-ruby/
7
+
8
+ module Net
9
+ class HTTP
10
+ alias __request__ request
11
+
12
+ def request(req, body=nil, &block)
13
+ if body != nil && body.respond_to?(:read)
14
+ req.body_stream = body
15
+ return __request__(req, nil, &block)
16
+ else
17
+ return __request__(req, body, &block)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,207 @@
1
+ require "tempfile"
2
+ require "stringio"
3
+
4
+ module RestClient
5
+ module Payload
6
+ extend self
7
+
8
+ def generate(params)
9
+ if params.is_a?(String)
10
+ Base.new(params)
11
+ elsif params
12
+ if params.delete(:multipart) == true || has_file?(params)
13
+ Multipart.new(params)
14
+ else
15
+ UrlEncoded.new(params)
16
+ end
17
+ else
18
+ nil
19
+ end
20
+ end
21
+
22
+ def has_file?(params)
23
+ params.any? do |_, v|
24
+ case v
25
+ when Hash
26
+ has_file?(v)
27
+ else
28
+ v.respond_to?(:path) && v.respond_to?(:read)
29
+ end
30
+ end
31
+ end
32
+
33
+ class Base
34
+ def initialize(params)
35
+ build_stream(params)
36
+ end
37
+
38
+ def build_stream(params)
39
+ @stream = StringIO.new(params)
40
+ @stream.seek(0)
41
+ end
42
+
43
+ def read(bytes=nil)
44
+ @stream.read(bytes)
45
+ end
46
+ alias :to_s :read
47
+
48
+ def escape(v)
49
+ URI.escape(v.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
50
+ end
51
+
52
+ def headers
53
+ { 'Content-Length' => size.to_s }
54
+ end
55
+
56
+ def size
57
+ @stream.size
58
+ end
59
+ alias :length :size
60
+
61
+ def close
62
+ @stream.close
63
+ end
64
+
65
+ def inspect
66
+ to_s.inspect
67
+ @stream.seek(0)
68
+ end
69
+ end
70
+
71
+ class UrlEncoded < Base
72
+ def build_stream(params)
73
+ @stream = StringIO.new(params.map do |k,v|
74
+ "#{escape(k)}=#{escape(v)}"
75
+ end.join("&"))
76
+ @stream.seek(0)
77
+ end
78
+
79
+ def headers
80
+ super.merge({ 'Content-Type' => 'application/x-www-form-urlencoded' })
81
+ end
82
+ end
83
+
84
+ class Multipart < Base
85
+ EOL = "\r\n"
86
+
87
+ def build_stream(params)
88
+ b = "--#{boundary}"
89
+
90
+ @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
91
+ @stream.write(b + EOL)
92
+ x = params.to_a
93
+ last_index = x.length - 1
94
+ x.each_with_index do |a, index|
95
+ k, v = *a
96
+ if v.respond_to?(:read) && v.respond_to?(:path)
97
+ create_file_field(@stream, k,v)
98
+ else
99
+ create_regular_field(@stream, k,v)
100
+ end
101
+ @stream.write(EOL + b)
102
+ @stream.write(EOL) unless last_index == index
103
+ end
104
+ @stream.write('--')
105
+ @stream.write(EOL)
106
+ @stream.seek(0)
107
+ end
108
+
109
+ def create_regular_field(s, k, v)
110
+ s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"")
111
+ s.write(EOL)
112
+ s.write(EOL)
113
+ s.write(v)
114
+ end
115
+
116
+ def create_file_field(s, k, v)
117
+ begin
118
+ s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
119
+ s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : mime_for(v.path)}#{EOL}")
120
+ s.write(EOL)
121
+ while data = v.read(8124)
122
+ s.write(data)
123
+ end
124
+ ensure
125
+ v.close
126
+ end
127
+ end
128
+
129
+ def mime_for(path)
130
+ ext = File.extname(path)[1..-1]
131
+ MIME_TYPES[ext] || 'text/plain'
132
+ end
133
+
134
+ def boundary
135
+ @boundary ||= rand(1_000_000).to_s
136
+ end
137
+
138
+ def headers
139
+ super.merge({'Content-Type' => %Q{multipart/form-data; boundary="#{boundary}"}})
140
+ end
141
+
142
+ def close
143
+ @stream.close
144
+ end
145
+ end
146
+
147
+ # :stopdoc:
148
+ # From WEBrick.
149
+ MIME_TYPES = {
150
+ "ai" => "application/postscript",
151
+ "asc" => "text/plain",
152
+ "avi" => "video/x-msvideo",
153
+ "bin" => "application/octet-stream",
154
+ "bmp" => "image/bmp",
155
+ "class" => "application/octet-stream",
156
+ "cer" => "application/pkix-cert",
157
+ "crl" => "application/pkix-crl",
158
+ "crt" => "application/x-x509-ca-cert",
159
+ "css" => "text/css",
160
+ "dms" => "application/octet-stream",
161
+ "doc" => "application/msword",
162
+ "dvi" => "application/x-dvi",
163
+ "eps" => "application/postscript",
164
+ "etx" => "text/x-setext",
165
+ "exe" => "application/octet-stream",
166
+ "gif" => "image/gif",
167
+ "gz" => "application/x-gzip",
168
+ "htm" => "text/html",
169
+ "html" => "text/html",
170
+ "jpe" => "image/jpeg",
171
+ "jpeg" => "image/jpeg",
172
+ "jpg" => "image/jpeg",
173
+ "js" => "text/javascript",
174
+ "lha" => "application/octet-stream",
175
+ "lzh" => "application/octet-stream",
176
+ "mov" => "video/quicktime",
177
+ "mpe" => "video/mpeg",
178
+ "mpeg" => "video/mpeg",
179
+ "mpg" => "video/mpeg",
180
+ "pbm" => "image/x-portable-bitmap",
181
+ "pdf" => "application/pdf",
182
+ "pgm" => "image/x-portable-graymap",
183
+ "png" => "image/png",
184
+ "pnm" => "image/x-portable-anymap",
185
+ "ppm" => "image/x-portable-pixmap",
186
+ "ppt" => "application/vnd.ms-powerpoint",
187
+ "ps" => "application/postscript",
188
+ "qt" => "video/quicktime",
189
+ "ras" => "image/x-cmu-raster",
190
+ "rb" => "text/plain",
191
+ "rd" => "text/plain",
192
+ "rtf" => "application/rtf",
193
+ "sgm" => "text/sgml",
194
+ "sgml" => "text/sgml",
195
+ "tif" => "image/tiff",
196
+ "tiff" => "image/tiff",
197
+ "txt" => "text/plain",
198
+ "xbm" => "image/x-xbitmap",
199
+ "xls" => "application/vnd.ms-excel",
200
+ "xml" => "text/xml",
201
+ "xpm" => "image/x-xpixmap",
202
+ "xwd" => "image/x-xwindowdump",
203
+ "zip" => "application/zip",
204
+ }
205
+ # :startdoc:
206
+ end
207
+ end
@@ -0,0 +1,30 @@
1
+ require File.dirname(__FILE__) + '/mixin/response'
2
+
3
+ module RestClient
4
+ # The response from RestClient on a raw request looks like a string, but is
5
+ # actually one of these. 99% of the time you're making a rest call all you
6
+ # care about is the body, but on the occassion you want to fetch the
7
+ # headers you can:
8
+ #
9
+ # RestClient.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 RestClient::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,241 @@
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 = Payload.generate(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
+ @method = :get
43
+ @payload = nil
44
+ execute
45
+ end
46
+
47
+ def execute_inner
48
+ uri = parse_url_with_auth(url)
49
+ transmit uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload
50
+ end
51
+
52
+ def make_headers(user_headers)
53
+ unless @cookies.empty?
54
+ user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ')
55
+ end
56
+
57
+ headers = default_headers.merge(user_headers).inject({}) do |final, (key, value)|
58
+ final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s
59
+ final
60
+ end
61
+
62
+ headers.merge!(@payload.headers) if @payload
63
+ headers
64
+ end
65
+
66
+ def net_http_class
67
+ if RestClient.proxy
68
+ proxy_uri = URI.parse(RestClient.proxy)
69
+ Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
70
+ else
71
+ Net::HTTP
72
+ end
73
+ end
74
+
75
+ def net_http_request_class(method)
76
+ Net::HTTP.const_get(method.to_s.capitalize)
77
+ end
78
+
79
+ def parse_url(url)
80
+ url = "http://#{url}" unless url.match(/^http/)
81
+ URI.parse(url)
82
+ end
83
+
84
+ def parse_url_with_auth(url)
85
+ uri = parse_url(url)
86
+ @user = uri.user if uri.user
87
+ @password = uri.password if uri.password
88
+ uri
89
+ end
90
+
91
+ def process_payload(p=nil, parent_key=nil)
92
+ unless p.is_a?(Hash)
93
+ p
94
+ else
95
+ @headers[:content_type] ||= 'application/x-www-form-urlencoded'
96
+ p.keys.map do |k|
97
+ key = parent_key ? "#{parent_key}[#{k}]" : k
98
+ if p[k].is_a? Hash
99
+ process_payload(p[k], key)
100
+ else
101
+ value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
102
+ "#{key}=#{value}"
103
+ end
104
+ end.join("&")
105
+ end
106
+ end
107
+
108
+ def transmit(uri, req, payload)
109
+ setup_credentials(req)
110
+
111
+ net = net_http_class.new(uri.host, uri.port)
112
+ net.use_ssl = uri.is_a?(URI::HTTPS)
113
+ if @verify_ssl == false
114
+ net.verify_mode = OpenSSL::SSL::VERIFY_NONE
115
+ elsif @verify_ssl.is_a? Integer
116
+ net.verify_mode = @verify_ssl
117
+ end
118
+ net.cert = @ssl_client_cert if @ssl_client_cert
119
+ net.key = @ssl_client_key if @ssl_client_key
120
+ net.ca_file = @ssl_ca_file if @ssl_ca_file
121
+ net.read_timeout = @timeout if @timeout
122
+ net.open_timeout = @open_timeout if @open_timeout
123
+
124
+ display_log request_log
125
+
126
+ net.start do |http|
127
+ res = http.request(req, payload) { |http_response| fetch_body(http_response) }
128
+ result = process_result(res)
129
+ display_log response_log(res)
130
+
131
+ if result.kind_of?(String) or @method == :head
132
+ Response.new(result, res)
133
+ elsif @raw_response
134
+ RawResponse.new(@tf, res)
135
+ else
136
+ nil
137
+ end
138
+ end
139
+ rescue EOFError
140
+ raise RestClient::ServerBrokeConnection
141
+ rescue Timeout::Error
142
+ raise RestClient::RequestTimeout
143
+ end
144
+
145
+ def setup_credentials(req)
146
+ req.basic_auth(user, password) if user
147
+ end
148
+
149
+ def fetch_body(http_response)
150
+ if @raw_response
151
+ # Taken from Chef, which as in turn...
152
+ # Stolen from http://www.ruby-forum.com/topic/166423
153
+ # Kudos to _why!
154
+ @tf = Tempfile.new("rest-client")
155
+ size, total = 0, http_response.header['Content-Length'].to_i
156
+ http_response.read_body do |chunk|
157
+ @tf.write(chunk)
158
+ size += chunk.size
159
+ if size == 0
160
+ display_log("#{@method} #{@url} done (0 length file)")
161
+ elsif total == 0
162
+ display_log("#{@method} #{@url} (zero content length)")
163
+ else
164
+ display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total])
165
+ end
166
+ end
167
+ @tf.close
168
+ @tf
169
+ else
170
+ http_response.read_body
171
+ end
172
+ http_response
173
+ end
174
+
175
+ def process_result(res)
176
+ if res.code =~ /\A2\d{2}\z/
177
+ # We don't decode raw requests
178
+ unless @raw_response
179
+ self.class.decode res['content-encoding'], res.body if res.body
180
+ end
181
+ elsif %w(301 302 303).include? res.code
182
+ url = res.header['Location']
183
+
184
+ if url !~ /^http/
185
+ uri = URI.parse(@url)
186
+ uri.path = "/#{url}".squeeze('/')
187
+ url = uri.to_s
188
+ end
189
+
190
+ raise Redirect, url
191
+ elsif res.code == "304"
192
+ raise NotModified, res
193
+ elsif res.code == "401"
194
+ raise Unauthorized, res
195
+ elsif res.code == "404"
196
+ raise ResourceNotFound, res
197
+ else
198
+ raise RequestFailed, res
199
+ end
200
+ end
201
+
202
+ def self.decode(content_encoding, body)
203
+ if content_encoding == 'gzip' and not body.empty?
204
+ Zlib::GzipReader.new(StringIO.new(body)).read
205
+ elsif content_encoding == 'deflate'
206
+ Zlib::Inflate.new.inflate(body)
207
+ else
208
+ body
209
+ end
210
+ end
211
+
212
+ def request_log
213
+ out = []
214
+ out << "RestClient.#{method} #{url.inspect}"
215
+ out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
216
+ out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
217
+ out.join(', ')
218
+ end
219
+
220
+ def response_log(res)
221
+ size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
222
+ "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes"
223
+ end
224
+
225
+ def display_log(msg)
226
+ return unless log_to = RestClient.log
227
+
228
+ if log_to == 'stdout'
229
+ STDOUT.puts msg
230
+ elsif log_to == 'stderr'
231
+ STDERR.puts msg
232
+ else
233
+ File.open(log_to, 'a') { |f| f.puts msg }
234
+ end
235
+ end
236
+
237
+ def default_headers
238
+ { :accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate' }
239
+ end
240
+ end
241
+ end