lib/restclient/payload.rb
require 'tempfile'
require 'securerandom'
require 'stringio'
begin
# Use mime/types/columnar if available, for reduced memory usage
require 'mime/types/columnar'
rescue LoadError
require 'mime/types'
end
module RestClient
module Payload
extend self
def generate(params)
if params.is_a?(RestClient::Payload::Base)
# pass through Payload objects unchanged
params
elsif params.is_a?(String)
Base.new(params)
elsif params.is_a?(Hash)
if params.delete(:multipart) == true || has_file?(params)
Multipart.new(params)
else
UrlEncoded.new(params)
end
elsif params.is_a?(ParamsArray)
if _has_file?(params)
Multipart.new(params)
else
UrlEncoded.new(params)
end
elsif params.respond_to?(:read)
Streamed.new(params)
else
nil
end
end
def has_file?(params)
unless params.is_a?(Hash)
raise ArgumentError.new("Must pass Hash, not #{params.inspect}")
end
_has_file?(params)
end
def _has_file?(obj)
case obj
when Hash, ParamsArray
obj.any? {|_, v| _has_file?(v) }
when Array
obj.any? {|v| _has_file?(v) }
else
obj.respond_to?(:path) && obj.respond_to?(:read)
end
end
class Base
def initialize(params)
build_stream(params)
end
def build_stream(params)
@stream = StringIO.new(params)
@stream.seek(0)
end
def read(*args)
@stream.read(*args)
end
def to_s
result = read
@stream.seek(0)
result
end
def headers
{'Content-Length' => size.to_s}
end
def size
@stream.size
end
alias :length :size
def close
@stream.close unless @stream.closed?
end
def closed?
@stream.closed?
end
def to_s_inspect
to_s.inspect
end
def short_inspect
if size && size > 500
"#{size} byte(s) length"
else
to_s_inspect
end
end
end
class Streamed < Base
def build_stream(params = nil)
@stream = params
end
def size
if @stream.respond_to?(:size)
@stream.size
elsif @stream.is_a?(IO)
@stream.stat.size
end
end
# TODO (breaks compatibility): ought to use mime_for() to autodetect the
# Content-Type for stream objects that have a filename.
alias :length :size
end
class UrlEncoded < Base
def build_stream(params = nil)
@stream = StringIO.new(Utils.encode_query_string(params))
@stream.seek(0)
end
def headers
super.merge({'Content-Type' => 'application/x-www-form-urlencoded'})
end
end
class Multipart < Base
EOL = "\r\n"
def build_stream(params)
b = '--' + boundary
@stream = Tempfile.new('rest-client.multipart.')
@stream.binmode
@stream.write(b + EOL)
case params
when Hash, ParamsArray
x = Utils.flatten_params(params)
else
x = params
end
last_index = x.length - 1
x.each_with_index do |a, index|
k, v = * a
if v.respond_to?(:read) && v.respond_to?(:path)
create_file_field(@stream, k, v)
else
create_regular_field(@stream, k, v)
end
@stream.write(EOL + b)
@stream.write(EOL) unless last_index == index
end
@stream.write('--')
@stream.write(EOL)
@stream.seek(0)
end
def create_regular_field(s, k, v)
s.write("Content-Disposition: form-data; name=\"#{k}\"")
s.write(EOL)
s.write(EOL)
s.write(v)
end
def create_file_field(s, k, v)
begin
s.write("Content-Disposition: form-data;")
s.write(" name=\"#{k}\";") unless (k.nil? || k=='')
s.write(" filename=\"#{v.respond_to?(:original_filename) ? v.original_filename : File.basename(v.path)}\"#{EOL}")
s.write("Content-Type: #{v.respond_to?(:content_type) ? v.content_type : mime_for(v.path)}#{EOL}")
s.write(EOL)
while (data = v.read(8124))
s.write(data)
end
ensure
v.close if v.respond_to?(:close)
end
end
def mime_for(path)
mime = MIME::Types.type_for path
mime.empty? ? 'text/plain' : mime[0].content_type
end
def boundary
return @boundary if defined?(@boundary) && @boundary
# Use the same algorithm used by WebKit: generate 16 random
# alphanumeric characters, replacing `+` `/` with `A` `B` (included in
# the list twice) to round out the set of 64.
s = SecureRandom.base64(12)
s.tr!('+/', 'AB')
@boundary = '----RubyFormBoundary' + s
end
# for Multipart do not escape the keys
#
# Ostensibly multipart keys MAY be percent encoded per RFC 7578, but in
# practice no major browser that I'm aware of uses percent encoding.
#
# Further discussion of multipart encoding:
# https://github.com/rest-client/rest-client/pull/403#issuecomment-156976930
#
def handle_key key
key
end
def headers
super.merge({'Content-Type' => %Q{multipart/form-data; boundary=#{boundary}}})
end
def close
@stream.close!
end
end
end
end