rest-client-multipart 1.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,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