# # Author:: Stanislav Vitvitskiy # Author:: Nuo Yan (nuo@chef.io) # Author:: Christopher Walters () # Copyright:: Copyright (c) Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require "uri" unless defined?(URI) require "net/http" unless defined?(Net::HTTP) require "mixlib/authentication/signedheaderauth" require "openssl" unless defined?(OpenSSL) require_relative "dist" class Chef # == Chef::CookbookSiteStreamingUploader # A streaming multipart HTTP upload implementation. Used to upload cookbooks # (in tarball form) to https://supermarket.chef.io # # inspired by http://stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html class CookbookSiteStreamingUploader DefaultHeaders = { "accept" => "application/json", "x-chef-version" => ::Chef::VERSION }.freeze # rubocop:disable Naming/ConstantName class << self def create_build_dir(cookbook) tmp_cookbook_path = Tempfile.new("#{Chef::Dist::SHORT}-#{cookbook.name}-build") tmp_cookbook_path.close tmp_cookbook_dir = tmp_cookbook_path.path File.unlink(tmp_cookbook_dir) FileUtils.mkdir_p(tmp_cookbook_dir) Chef::Log.trace("Staging at #{tmp_cookbook_dir}") checksums_to_on_disk_paths = cookbook.checksums cookbook.each_file do |manifest_record| path_in_cookbook = manifest_record[:path] on_disk_path = checksums_to_on_disk_paths[manifest_record[:checksum]] dest = File.join(tmp_cookbook_dir, cookbook.name.to_s, path_in_cookbook) FileUtils.mkdir_p(File.dirname(dest)) Chef::Log.trace("Staging #{on_disk_path} to #{dest}") FileUtils.cp(on_disk_path, dest) end # First, generate metadata Chef::Log.trace("Generating metadata") kcm = Chef::Knife::CookbookMetadata.new kcm.config[:cookbook_path] = [ tmp_cookbook_dir ] kcm.name_args = [ cookbook.name.to_s ] kcm.run tmp_cookbook_dir end def post(to_url, user_id, secret_key_filename, params = {}, headers = {}) make_request(:post, to_url, user_id, secret_key_filename, params, headers) end def put(to_url, user_id, secret_key_filename, params = {}, headers = {}) make_request(:put, to_url, user_id, secret_key_filename, params, headers) end def make_request(http_verb, to_url, user_id, secret_key_filename, params = {}, headers = {}) boundary = "----RubyMultipartClient" + rand(1000000).to_s + "ZZZZZ" parts = [] content_file = nil secret_key = OpenSSL::PKey::RSA.new(File.read(secret_key_filename)) unless params.nil? || params.empty? params.each do |key, value| if value.is_a?(File) content_file = value filepath = value.path filename = File.basename(filepath) parts << StringPart.new( "--" + boundary + "\r\n" + "Content-Disposition: form-data; name=\"" + key.to_s + "\"; filename=\"" + filename + "\"\r\n" + "Content-Type: application/octet-stream\r\n\r\n") parts << StreamPart.new(value, File.size(filepath)) parts << StringPart.new("\r\n") else parts << StringPart.new( "--" + boundary + "\r\n" + "Content-Disposition: form-data; name=\"" + key.to_s + "\"\r\n\r\n") parts << StringPart.new(value.to_s + "\r\n") end end parts << StringPart.new("--" + boundary + "--\r\n") end body_stream = MultipartStream.new(parts) timestamp = Time.now.utc.iso8601 url = URI.parse(to_url) Chef::Log.logger.debug("Signing: method: #{http_verb}, url: #{url}, file: #{content_file}, User-id: #{user_id}, Timestamp: #{timestamp}") # We use the body for signing the request if the file parameter # wasn't a valid file or wasn't included. Extract the body (with # multi-part delimiters intact) to sign the request. # TODO: tim: 2009-12-28: It'd be nice to remove this special case, and # always hash the entire request body. In the file case it would just be # expanded multipart text - the entire body of the POST. content_body = parts.inject("") { |result, part| result + part.read(0, part.size) } content_file.rewind if content_file # we consumed the file for the above operation, so rewind it. signing_options = { http_method: http_verb, path: url.path, user_id: user_id, timestamp: timestamp } (content_file && signing_options[:file] = content_file) || (signing_options[:body] = (content_body || "")) headers.merge!(Mixlib::Authentication::SignedHeaderAuth.signing_object(signing_options).sign(secret_key)) content_file.rewind if content_file # net/http doesn't like symbols for header keys, so we'll to_s each one just in case headers = DefaultHeaders.merge(Hash[*headers.map { |k, v| [k.to_s, v] }.flatten]) req = case http_verb when :put Net::HTTP::Put.new(url.path, headers) when :post Net::HTTP::Post.new(url.path, headers) end req.content_length = body_stream.size req.content_type = "multipart/form-data; boundary=" + boundary unless parts.empty? req.body_stream = body_stream http = Chef::HTTP::BasicClient.new(url).http_client res = http.request(req) # alias status to code and to_s to body for test purposes # TODO: stop the following madness! class << res alias :to_s :body # BUG this makes the response compatible with what response_steps expects to test headers (response.headers[] -> response[]) def headers # rubocop:disable Lint/NestedMethodDefinition self end def status # rubocop:disable Lint/NestedMethodDefinition code.to_i end end res end end class StreamPart def initialize(stream, size) @stream, @size = stream, size end def size @size end # read the specified amount from the stream def read(offset, how_much) @stream.read(how_much) end end class StringPart def initialize(str) @str = str end def size @str.length end # read the specified amount from the string starting at the offset def read(offset, how_much) @str[offset, how_much] end end class MultipartStream def initialize(parts) @parts = parts @part_no = 0 @part_offset = 0 end def size @parts.inject(0) { |size, part| size + part.size } end def read(how_much, dst_buf = nil) if @part_no >= @parts.size dst_buf.replace("") if dst_buf return dst_buf end how_much_current_part = @parts[@part_no].size - @part_offset how_much_current_part = if how_much_current_part > how_much how_much else how_much_current_part end how_much_next_part = how_much - how_much_current_part current_part = @parts[@part_no].read(@part_offset, how_much_current_part) # recurse into the next part if the current one was not large enough if how_much_next_part > 0 @part_no += 1 @part_offset = 0 next_part = read(how_much_next_part) result = current_part + if next_part next_part else "" end else @part_offset += how_much_current_part result = current_part end dst_buf ? dst_buf.replace(result || "") : result end end end end