module S3 # Class responsible for handling connections to amazon hosts class Connection include Parser attr_accessor :access_key_id, :secret_access_key, :use_ssl, :timeout, :debug, :proxy, :host alias :use_ssl? :use_ssl # Creates new connection object. # # ==== Options # * :access_key_id - Access key id (REQUIRED) # * :secret_access_key - Secret access key (REQUIRED) # * :use_ssl - Use https or http protocol (false by # default) # * :debug - Display debug information on the STDOUT # (false by default) # * :timeout - Timeout to use by the Net::HTTP object # (60 by default) # * :proxy - Hash for Net::HTTP Proxy settings # { :host => "proxy.mydomain.com", :port => "80, :user => "user_a", :password => "secret" } # * :chunk_size - Size of a chunk when streaming # (1048576 (1 MiB) by default) def initialize(options = {}) @access_key_id = options.fetch(:access_key_id) @secret_access_key = options.fetch(:secret_access_key) @host = options.fetch(:host) @use_ssl = options.fetch(:use_ssl, false) @debug = options.fetch(:debug, false) @timeout = options.fetch(:timeout, 60) @proxy = options.fetch(:proxy, nil) @chunk_size = options.fetch(:chunk_size, 1048576) end # Makes request with given HTTP method, sets missing parameters, # adds signature to request header and returns response object # (Net::HTTPResponse) # # ==== Parameters # * method - HTTP Method symbol, can be :get, # :put, :delete # # ==== Options: # * :host - Hostname to connecto to, defaults # to s3.amazonaws.com # * :path - path to send request to (REQUIRED) # * :body - Request body, only meaningful for # :put request # * :params - Parameters to add to query string for # request, can be String or Hash # * :headers - Hash of headers fields to add to request # header # # ==== Returns # Net::HTTPResponse object -- response from the server def request(method, options) host = @host path = options.fetch(:path) body = options.fetch(:body, nil) params = options.fetch(:params, {}) headers = options.fetch(:headers, {}) # Must be done before adding params # Encodes all characters except forward-slash (/) and explicitly legal URL characters path = URI.escape(path, /[^#{URI::REGEXP::PATTERN::UNRESERVED}\/]/) if params params = params.is_a?(String) ? params : self.class.parse_params(params) path << "?#{params}" end request = Request.new(@chunk_size, method.to_s.upcase, !!body, method.to_s.upcase != "HEAD", path) headers = self.class.parse_headers(headers) headers.each do |key, value| request[key] = value end if body if body.respond_to?(:read) request.body_stream = body else request.body = body end request.content_length = body.respond_to?(:lstat) ? body.stat.size : body.size end send_request(host, request) end # Helper function to parser parameters and create single string of # params added to questy string # # ==== Parameters # * params - Hash of parameters # # ==== Returns # String -- containing all parameters joined in one params string, # i.e. param1=val¶m2¶m3=0 def self.parse_params(params) interesting_keys = [:max_keys, :prefix, :marker, :delimiter, :location] result = [] params.each do |key, value| if interesting_keys.include?(key) parsed_key = key.to_s.gsub("_", "-") case value when nil result << parsed_key else result << "#{parsed_key}=#{value}" end end end result.join("&") end # Helper function to change headers from symbols, to in correct # form (i.e. with '-' instead of '_') # # ==== Parameters # * headers - Hash of pairs headername => value, # where value can be Range (for Range header) or any other value # which can be translated to string # # ==== Returns # Hash of headers translated from symbol to string, containing # only interesting headers def self.parse_headers(headers) interesting_keys = [:content_type, :content_length, :cache_control, :x_amz_acl, :x_amz_storage_class, :range, :if_modified_since, :if_unmodified_since, :if_match, :if_none_match, :content_disposition, :content_encoding, :x_amz_copy_source, :x_amz_metadata_directive, :x_amz_copy_source_if_match, :x_amz_copy_source_if_none_match, :x_amz_copy_source_if_unmodified_since, :x_amz_copy_source_if_modified_since] parsed_headers = {} if headers headers.each do |key, value| if interesting_keys.include?(key) parsed_key = key.to_s.gsub("_", "-") parsed_value = value case value when Range parsed_value = "bytes=#{value.first}-#{value.last}" end parsed_headers[parsed_key] = parsed_value end end end parsed_headers end private def port use_ssl ? 443 : 80 end def proxy_settings @proxy.values_at(:host, :port, :user, :password) unless @proxy.nil? || @proxy.empty? end def http(host) http = Net::HTTP.new(host, port, *proxy_settings) http.set_debug_output(STDOUT) if @debug http.use_ssl = @use_ssl http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @use_ssl http.read_timeout = @timeout if @timeout http end def send_request(host, request, skip_authorization = false) response = http(host).start do |http| host = http.address request["Date"] ||= Time.now.httpdate if request.body request["Content-Type"] ||= "application/octet-stream" request["Content-MD5"] = Base64.encode64(Digest::MD5.digest(request.body)).chomp unless request.body.empty? end unless skip_authorization request["Authorization"] = Signature.generate(:host => host, :request => request, :access_key_id => access_key_id, :secret_access_key => secret_access_key) end http.request(request) end if response.code.to_i == 307 if response.body doc = Document.new response.body send_request(doc.elements["Error"].elements["Endpoint"].text, request, true) end else handle_response(response) end end def handle_response(response) case response.code.to_i when 200...300 response when 300...600 if response.body.nil? || response.body.empty? raise Error::ResponseError.new(nil, response) else code, message = parse_error(response.body) raise Error::ResponseError.exception(code).new(message, response) end else raise(ConnectionError.new(response, "Unknown response code: #{response.code}")) end response end end end