lib/chef-api/connection.rb in chef-api-0.4.0 vs lib/chef-api/connection.rb in chef-api-0.4.1
- old
+ new
@@ -200,29 +200,39 @@
add_request_headers(request)
# Setup PATCH/POST/PUT
if [:patch, :post, :put].include?(verb)
if data.respond_to?(:read)
+ log.info "Detected file/io presence"
request.body_stream = data
elsif data.is_a?(Hash)
# If any of the values in the hash are File-like, assume this is a
# multi-part post
if data.values.any? { |value| value.respond_to?(:read) }
+ log.info "Detected multipart body"
+
multipart = Multipart::Body.new(data)
+
+ log.debug "Content-Type: #{multipart.content_type}"
+ log.debug "Content-Length: #{multipart.content_length}"
+
request.content_length = multipart.content_length
request.content_type = multipart.content_type
+
request.body_stream = multipart.stream
else
+ log.info "Detected form data"
request.form_data = data
end
else
+ log.info "Detected regular body"
request.body = data
end
end
# Sign the request
- add_signing_headers(verb, uri, request, parsed_key)
+ add_signing_headers(verb, uri.path, request)
# Create the HTTP connection object - since the proxy information defaults
# to +nil+, we can just pass it to the initializer method instead of doing
# crazy strange conditionals.
connection = Net::HTTP.new(uri.host, uri.port,
@@ -350,53 +360,10 @@
end
private
#
- # Parse the given private key. Users can specify the private key as:
- #
- # - the path to the key on disk
- # - the raw string key
- # - an +OpenSSL::PKey::RSA object+
- #
- # Any other implementations are not supported and will likely explode.
- #
- # @todo
- # Handle errors when the file cannot be read due to insufficient
- # permissions
- #
- # @return [OpenSSL::PKey::RSA]
- # the RSA private key as an OpenSSL object
- #
- def parsed_key
- return @parsed_key if @parsed_key
-
- log.info "Parsing private key..."
-
- if key.nil?
- log.warn "No private key given!"
- raise 'No private key given!'
- end
-
- if key.is_a?(OpenSSL::PKey::RSA)
- log.debug "Detected private key is an OpenSSL Ruby object"
- @parsed_key = key
- end
-
- if key =~ /(.+)\.pem$/ || File.exists?(File.expand_path(key))
- log.debug "Detected private key is the path to a file"
- contents = File.read(File.expand_path(key))
- @parsed_key = OpenSSL::PKey::RSA.new(contents)
- else
- log.debug "Detected private key was the literal string key"
- @parsed_key = OpenSSL::PKey::RSA.new(key)
- end
-
- @parsed_key
- end
-
- #
# Parse the response object and manipulate the result based on the given
# +Content-Type+ header. For now, this method only parses JSON, but it
# could be expanded in the future to accept other content types.
#
# @param [HTTP::Message] response
@@ -431,11 +398,11 @@
case response['Content-Type']
when /json/
log.debug "Detected error response as JSON"
log.debug "Parsing error response as JSON"
- message = Array(JSON.parse(response.body)['error']).join(', ')
+ message = JSON.parse(response.body)
else
log.debug "Detected response as text/plain"
message = response.body
end
@@ -483,194 +450,36 @@
request[key] = value
end
end
#
- # Use mixlib-auth to create a signed header auth.
+ # Create a signed header authentication that can be consumed by
+ # +Mixlib::Authentication+.
#
+ # @param [Symbol] verb
+ # the HTTP verb (e.g. +:get+)
+ # @param [String] path
+ # the requested URI path (e.g. +/resources/foo+)
# @param [Net::HTTP::Request] request
#
- def add_signing_headers(verb, uri, request, key)
+ def add_signing_headers(verb, path, request)
log.info "Adding signed header authentication..."
- unless defined?(Mixlib::Authentication::SignedHeaderAuth)
- require 'mixlib/authentication/signedheaderauth'
- end
+ authentication = Authentication.from_options(
+ user: client,
+ key: key,
+ verb: verb,
+ path: path,
+ body: request.body || request.body_stream,
+ )
- headers = Mixlib::Authentication::SignedHeaderAuth.signing_object(
- :http_method => verb,
- :body => request.body || '',
- :host => "#{uri.host}:#{uri.port}",
- :path => uri.path,
- :timestamp => Time.now.utc.iso8601,
- :user_id => client,
- :file => '',
- ).sign(key)
-
- headers.each do |key, value|
+ authentication.headers.each do |key, value|
log.debug "#{key}: #{value}"
request[key] = value
end
- end
- end
- require 'cgi'
- require 'mime/types'
-
- module Multipart
- BOUNDARY = '------ChefAPIMultipartBoundary'.freeze
-
- class Body
- def initialize(params = {})
- params.each do |key, value|
- if value.respond_to?(:read)
- parts << FilePart.new(key, value)
- else
- parts << ParamPart.new(key, value)
- end
- end
-
- parts << EndingPart.new
- end
-
- def stream
- MultiIO.new(*parts.map(&:io))
- end
-
- def content_type
- "multipart/form-data; boundary=#{BOUNDARY}"
- end
-
- def content_length
- parts.map(&:size).inject(:+)
- end
-
- private
-
- def parts
- @parts ||= []
- end
- end
-
- class MultiIO
- def initialize(*ios)
- @ios = ios
- @index = 0
- end
-
- # Read from IOs in order until `length` bytes have been received.
- def read(length = nil, outbuf = nil)
- got_result = false
- outbuf = outbuf ? outbuf.replace('') : ''
-
- while io = current_io
- if result = io.read(length)
- got_result ||= !result.nil?
- result.force_encoding('BINARY') if result.respond_to?(:force_encoding)
- outbuf << result
- length -= result.length if length
- break if length == 0
- end
- advance_io
- end
-
- (!got_result && length) ? nil : outbuf
- end
-
- def rewind
- @ios.each { |io| io.rewind }
- @index = 0
- end
-
- private
-
- def current_io
- @ios[@index]
- end
-
- def advance_io
- @index += 1
- end
- end
-
- #
- # A generic key => value part.
- #
- class ParamPart
- def initialize(name, value)
- @part = build(name, value)
- end
-
- def io
- @io ||= StringIO.new(@part)
- end
-
- def size
- @part.bytesize
- end
-
- private
-
- def build(name, value)
- part = %|--#{BOUNDARY}\r\n|
- part << %|Content-Disposition: form-data; name="#{CGI.escape(name)}"\r\n\r\n|
- part << %|#{value}\r\n|
- part
- end
- end
-
- #
- # A File part
- #
- class FilePart
- def initialize(name, file)
- @file = file
- @head = build(name, file)
- @foot = "\r\n"
- end
-
- def io
- @io ||= MultiIO.new(
- StringIO.new(@head),
- @file,
- StringIO.new(@foot)
- )
- end
-
- def size
- @head.bytesize + @file.size + @foot.bytesize
- end
-
- private
-
- def build(name, file)
- filename = File.basename(file.path)
- mime_type = MIME::Types.type_for(filename)[0] || MIME::Types['application/octet-stream'][0]
-
- part = %|--#{BOUNDARY}\r\n|
- part << %|Content-Disposition: form-data; name="#{CGI.escape(name)}"; filename="#{filename}"\r\n|
- part << %|Content-Length: #{file.size}\r\n|
- part << %|Content-Type: #{mime_type.simplified}|
- part << %|Content-Transfer-Encoding: binary\r\n|
- part << %|\r\n|
- part
- end
- end
-
- #
- # The end of the entire request
- #
- class EndingPart
- def initialize
- @part = "--#{BOUNDARY}--\r\n\r\n"
- end
-
- def io
- @io ||= StringIO.new(@part)
- end
-
- def size
- @part.bytesize
+ if request.body_stream
+ request.body_stream.rewind
end
end
end
end