rest-client-multipart 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,48 @@
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
+ # The raw headers.
19
+ def raw_headers
20
+ @raw_headers ||= @net_http_res.to_hash
21
+ end
22
+
23
+ # Hash of cookies extracted from response headers
24
+ def cookies
25
+ @cookies ||= (self.headers[:set_cookie] || "").split('; ').inject({}) do |out, raw_c|
26
+ key, val = raw_c.split('=')
27
+ unless %w(expires domain path secure).member?(key)
28
+ out[key] = val
29
+ end
30
+ out
31
+ end
32
+ end
33
+
34
+ def self.included(receiver)
35
+ receiver.extend(RestClient::Mixin::Response::ClassMethods)
36
+ end
37
+
38
+ module ClassMethods
39
+ def beautify_headers(headers)
40
+ headers.inject({}) do |out, (key, value)|
41
+ out[key.gsub(/-/, '_').downcase.to_sym] = value.first
42
+ out
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ 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,171 @@
1
+ require "tempfile"
2
+ require "stringio"
3
+ require "mime/types"
4
+
5
+ module RestClient
6
+ module Payload
7
+ extend self
8
+
9
+ def generate(params)
10
+ if params.is_a?(String)
11
+ Base.new(params)
12
+ elsif params
13
+ if params.delete(:multipart) == true || has_file?(params)
14
+ Multipart.new(params)
15
+ else
16
+ UrlEncoded.new(params)
17
+ end
18
+ else
19
+ nil
20
+ end
21
+ end
22
+
23
+ def has_file?(params)
24
+ params.any? do |_, v|
25
+ case v
26
+ when Hash
27
+ has_file?(v)
28
+ else
29
+ v.respond_to?(:path) && v.respond_to?(:read)
30
+ end
31
+ end
32
+ end
33
+
34
+ class Base
35
+ def initialize(params)
36
+ build_stream(params)
37
+ end
38
+
39
+ def build_stream(params)
40
+ @stream = StringIO.new(params)
41
+ @stream.seek(0)
42
+ end
43
+
44
+ def read(bytes=nil)
45
+ @stream.read(bytes)
46
+ end
47
+ alias :to_s :read
48
+
49
+ def escape(v)
50
+ URI.escape(v.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
51
+ end
52
+
53
+ # Flatten parameters by converting hashes of hashes to flat hashes
54
+ # {keys1 => {keys2 => value}} will be transformed into {keys1[key2] => value}
55
+ def flatten_params(params, parent_key = nil)
56
+ result = {}
57
+ params.keys.map do |key|
58
+ calculated_key = parent_key ? "#{parent_key}[#{escape key}]" : escape(key)
59
+ value = params[key]
60
+ if value.is_a? Hash
61
+ result.merge!(flatten_params(value, calculated_key))
62
+ else
63
+ result[calculated_key] = value
64
+ end
65
+ end
66
+ result
67
+ end
68
+
69
+ def headers
70
+ { 'Content-Length' => size.to_s }
71
+ end
72
+
73
+ def size
74
+ @stream.size
75
+ end
76
+ alias :length :size
77
+
78
+ def close
79
+ @stream.close
80
+ end
81
+
82
+ def inspect
83
+ result = to_s.inspect
84
+ @stream.seek(0)
85
+ result
86
+ end
87
+ end
88
+
89
+ class UrlEncoded < Base
90
+ def build_stream(params = nil)
91
+ @stream = StringIO.new(flatten_params(params).map do |k,v|
92
+ "#{k}=#{escape(v)}"
93
+ end.join("&"))
94
+ @stream.seek(0)
95
+ end
96
+
97
+ def headers
98
+ super.merge({ 'Content-Type' => 'application/x-www-form-urlencoded' })
99
+ end
100
+ end
101
+
102
+ class Multipart < Base
103
+ EOL = "\r\n"
104
+
105
+ def build_stream(params)
106
+ b = "--#{boundary}"
107
+
108
+ @stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
109
+ @stream.write(b + EOL)
110
+
111
+ if params.is_a? Hash
112
+ x = flatten_params(params).to_a
113
+ else
114
+ x = params.to_a
115
+ end
116
+
117
+ last_index = x.length - 1
118
+ x.each_with_index do |a, index|
119
+ k, v = *a
120
+ if v.respond_to?(:read) && v.respond_to?(:path)
121
+ create_file_field(@stream, k,v)
122
+ else
123
+ create_regular_field(@stream, k,v)
124
+ end
125
+ @stream.write(EOL + b)
126
+ @stream.write(EOL) unless last_index == index
127
+ end
128
+ @stream.write('--')
129
+ @stream.write(EOL)
130
+ @stream.seek(0)
131
+ end
132
+
133
+ def create_regular_field(s, k, v)
134
+ s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"")
135
+ s.write(EOL)
136
+ s.write(EOL)
137
+ s.write(v)
138
+ end
139
+
140
+ def create_file_field(s, k, v)
141
+ begin
142
+ s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
143
+ s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : mime_for(v.path)}#{EOL}")
144
+ s.write(EOL)
145
+ while data = v.read(8124)
146
+ s.write(data)
147
+ end
148
+ ensure
149
+ v.close
150
+ end
151
+ end
152
+
153
+ def mime_for(path)
154
+ mime = MIME::Types.type_for path
155
+ mime.empty? ? 'text/plain' : mime[0].content_type
156
+ end
157
+
158
+ def boundary
159
+ @boundary ||= rand(1_000_000).to_s
160
+ end
161
+
162
+ def headers
163
+ super.merge({'Content-Type' => %Q{multipart/form-data; boundary="#{boundary}"}})
164
+ end
165
+
166
+ def close
167
+ @stream.close
168
+ end
169
+ end
170
+ end
171
+ 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,252 @@
1
+ require 'tempfile'
2
+
3
+ module RestClient
4
+ # This class is used internally by RestClient to send the request, but you can also
5
+ # call it directly if you'd like to use a method not supported by the
6
+ # main API. For example:
7
+ #
8
+ # RestClient::Request.execute(:method => :head, :url => 'http://example.com')
9
+ #
10
+ # Mandatory parameters:
11
+ # * :method
12
+ # * :url
13
+ # Optional parameters (have a look at ssl and/or uri for some explanations):
14
+ # * :headers a hash containing the request headers
15
+ # * :cookies will replace possible cookies in the :headers
16
+ # * :user and :password for basic auth, will be replaced by a user/password available in the :url
17
+ # * :raw_response return a low-level RawResponse instead of a Response
18
+ # * :verify_ssl enable ssl verification, possible values are constants from OpenSSL::SSL
19
+ # * :timeout and :open_timeout
20
+ # * :ssl_client_cert, :ssl_client_key, :ssl_ca_file
21
+ class Request
22
+ attr_reader :method, :url, :payload, :headers,
23
+ :cookies, :user, :password, :timeout, :open_timeout,
24
+ :verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file,
25
+ :raw_response
26
+
27
+ def self.execute(args)
28
+ new(args).execute
29
+ end
30
+
31
+ def initialize(args)
32
+ @method = args[:method] or raise ArgumentError, "must pass :method"
33
+ @url = args[:url] or raise ArgumentError, "must pass :url"
34
+ @headers = args[:headers] || {}
35
+ @cookies = @headers.delete(:cookies) || args[:cookies] || {}
36
+ @payload = Payload.generate(args[:payload])
37
+ @user = args[:user]
38
+ @password = args[:password]
39
+ @timeout = args[:timeout]
40
+ @open_timeout = args[:open_timeout]
41
+ @raw_response = args[:raw_response] || false
42
+ @verify_ssl = args[:verify_ssl] || false
43
+ @ssl_client_cert = args[:ssl_client_cert] || nil
44
+ @ssl_client_key = args[:ssl_client_key] || nil
45
+ @ssl_ca_file = args[:ssl_ca_file] || nil
46
+ @tf = nil # If you are a raw request, this is your tempfile
47
+ end
48
+
49
+ def execute
50
+ execute_inner
51
+ rescue Redirect => e
52
+ @url = e.url
53
+ @method = :get
54
+ @payload = nil
55
+ execute
56
+ end
57
+
58
+ def execute_inner
59
+ uri = parse_url_with_auth(url)
60
+ transmit uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload
61
+ end
62
+
63
+ def make_headers(user_headers)
64
+ unless @cookies.empty?
65
+ user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ')
66
+ end
67
+
68
+ headers = default_headers.merge(user_headers).inject({}) do |final, (key, value)|
69
+ final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s
70
+ final
71
+ end
72
+
73
+ headers.merge!(@payload.headers) if @payload
74
+ headers
75
+ end
76
+
77
+ def net_http_class
78
+ if RestClient.proxy
79
+ proxy_uri = URI.parse(RestClient.proxy)
80
+ Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
81
+ else
82
+ Net::HTTP
83
+ end
84
+ end
85
+
86
+ def net_http_request_class(method)
87
+ Net::HTTP.const_get(method.to_s.capitalize)
88
+ end
89
+
90
+ def parse_url(url)
91
+ url = "http://#{url}" unless url.match(/^http/)
92
+ URI.parse(url)
93
+ end
94
+
95
+ def parse_url_with_auth(url)
96
+ uri = parse_url(url)
97
+ @user = uri.user if uri.user
98
+ @password = uri.password if uri.password
99
+ uri
100
+ end
101
+
102
+ def process_payload(p=nil, parent_key=nil)
103
+ unless p.is_a?(Hash)
104
+ p
105
+ else
106
+ @headers[:content_type] ||= 'application/x-www-form-urlencoded'
107
+ p.keys.map do |k|
108
+ key = parent_key ? "#{parent_key}[#{k}]" : k
109
+ if p[k].is_a? Hash
110
+ process_payload(p[k], key)
111
+ else
112
+ value = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
113
+ "#{key}=#{value}"
114
+ end
115
+ end.join("&")
116
+ end
117
+ end
118
+
119
+ def transmit(uri, req, payload)
120
+ setup_credentials(req)
121
+
122
+ net = net_http_class.new(uri.host, uri.port)
123
+ net.use_ssl = uri.is_a?(URI::HTTPS)
124
+ if @verify_ssl == false
125
+ net.verify_mode = OpenSSL::SSL::VERIFY_NONE
126
+ elsif @verify_ssl.is_a? Integer
127
+ net.verify_mode = @verify_ssl
128
+ end
129
+ net.cert = @ssl_client_cert if @ssl_client_cert
130
+ net.key = @ssl_client_key if @ssl_client_key
131
+ net.ca_file = @ssl_ca_file if @ssl_ca_file
132
+ net.read_timeout = @timeout if @timeout
133
+ net.open_timeout = @open_timeout if @open_timeout
134
+
135
+ display_log request_log
136
+
137
+ net.start do |http|
138
+ res = http.request(req, payload) { |http_response| fetch_body(http_response) }
139
+ result = process_result(res)
140
+ display_log response_log(res)
141
+
142
+ if result.kind_of?(String) or @method == :head
143
+ Response.new(result, res)
144
+ elsif @raw_response
145
+ RawResponse.new(@tf, res)
146
+ else
147
+ Response.new(nil, res)
148
+ end
149
+ end
150
+ rescue EOFError
151
+ raise RestClient::ServerBrokeConnection
152
+ rescue Timeout::Error
153
+ raise RestClient::RequestTimeout
154
+ end
155
+
156
+ def setup_credentials(req)
157
+ req.basic_auth(user, password) if user
158
+ end
159
+
160
+ def fetch_body(http_response)
161
+ if @raw_response
162
+ # Taken from Chef, which as in turn...
163
+ # Stolen from http://www.ruby-forum.com/topic/166423
164
+ # Kudos to _why!
165
+ @tf = Tempfile.new("rest-client")
166
+ size, total = 0, http_response.header['Content-Length'].to_i
167
+ http_response.read_body do |chunk|
168
+ @tf.write(chunk)
169
+ size += chunk.size
170
+ if size == 0
171
+ display_log("#{@method} #{@url} done (0 length file)")
172
+ elsif total == 0
173
+ display_log("#{@method} #{@url} (zero content length)")
174
+ else
175
+ display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total])
176
+ end
177
+ end
178
+ @tf.close
179
+ @tf
180
+ else
181
+ http_response.read_body
182
+ end
183
+ http_response
184
+ end
185
+
186
+ def process_result(res)
187
+ if res.code =~ /\A2\d{2}\z/
188
+ # We don't decode raw requests
189
+ unless @raw_response
190
+ self.class.decode res['content-encoding'], res.body if res.body
191
+ end
192
+ elsif %w(301 302 303).include? res.code
193
+ url = res.header['Location']
194
+
195
+ if url !~ /^http/
196
+ uri = URI.parse(@url)
197
+ uri.path = "/#{url}".squeeze('/')
198
+ url = uri.to_s
199
+ end
200
+
201
+ raise Redirect, url
202
+ elsif res.code == "304"
203
+ raise NotModified, res
204
+ elsif res.code == "401"
205
+ raise Unauthorized, res
206
+ elsif res.code == "404"
207
+ raise ResourceNotFound, res
208
+ else
209
+ raise RequestFailed, res
210
+ end
211
+ end
212
+
213
+ def self.decode(content_encoding, body)
214
+ if content_encoding == 'gzip' and not body.empty?
215
+ Zlib::GzipReader.new(StringIO.new(body)).read
216
+ elsif content_encoding == 'deflate'
217
+ Zlib::Inflate.new.inflate(body)
218
+ else
219
+ body
220
+ end
221
+ end
222
+
223
+ def request_log
224
+ out = []
225
+ out << "RestClient.#{method} #{url.inspect}"
226
+ out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
227
+ out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
228
+ out.join(', ')
229
+ end
230
+
231
+ def response_log(res)
232
+ size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
233
+ "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes"
234
+ end
235
+
236
+ def display_log(msg)
237
+ return unless log_to = RestClient.log
238
+
239
+ if log_to == 'stdout'
240
+ STDOUT.puts msg
241
+ elsif log_to == 'stderr'
242
+ STDERR.puts msg
243
+ else
244
+ File.open(log_to, 'a') { |f| f.puts msg }
245
+ end
246
+ end
247
+
248
+ def default_headers
249
+ { :accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate' }
250
+ end
251
+ end
252
+ end