module AWS module S3 # All authentication is taken care of for you by the AWS::S3 library. None the less, some details of the two types # of authentication and when they are used may be of interest to some. # # === Header based authentication # # Header based authentication is achieved by setting a special Authorization header whose value # is formatted like so: # # "AWS #{access_key_id}:#{encoded_canonical}" # # The access_key_id is the public key that is assigned by Amazon for a given account which you use when # establishing your initial connection. The encoded_canonical is computed according to rules layed out # by Amazon which we will describe presently. # # ==== Generating the encoded canonical string # # The "canonical string", generated by the CanonicalString class, is computed by collecting the current request method, # a set of significant headers of the current request, and the current request path into a string. # That canonical string is then encrypted with the secret_access_key assigned by Amazon. The resulting encrypted canonical # string is then base 64 encoded. # # === Query string based authentication # # When accessing a restricted object from the browser, you can authenticate via the query string, by setting the following parameters: # # "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" # # The QueryString class is responsible for generating the appropriate parameters for authentication via the # query string. # # The access_key_id and encoded_canonical are the same as described in the Header based authentication section. # The expires value dictates for how long the current url is valid (by default, it will expire in 5 minutes). Expiration can be specified # either by an absolute time (expressed in seconds since the epoch), or in relative time (in number of seconds from now). # Details of how to customize the expiration of the url are provided in the documentation for the QueryString class. # # All requests made by this library use header authentication. When a query string authenticated url is needed, # the S3Object#url method will include the appropriate query string parameters. # # === Full authentication specification # # The full specification of the authentication protocol can be found at # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTAuthentication.html class Authentication constant :AMAZON_HEADER_PREFIX, 'x-amz-' # Signature is the abstract super class for the Header and QueryString authentication methods. It does the job # of computing the canonical_string using the CanonicalString class as well as encoding the canonical string. The subclasses # parameterize these computations and arrange them in a string form appropriate to how they are used, in one case a http request # header value, and in the other case key/value query string parameter pairs. class Signature < String #:nodoc: attr_reader :request, :access_key_id, :secret_access_key, :options def initialize(request, access_key_id, secret_access_key, options = {}) super() @request, @access_key_id, @secret_access_key = request, access_key_id, secret_access_key @options = options end private def canonical_string options = @options.slice(*CanonicalString::SIGNIFICANT_PARAMETERS) options[:expires] = expires if expires? CanonicalString.new(request, options) end memoized :canonical_string def encoded_canonical digest = OpenSSL::Digest::Digest.new('sha1') b64_hmac = [OpenSSL::HMAC.digest(digest, secret_access_key, canonical_string)].pack("m").strip url_encode? ? CGI.escape(b64_hmac) : b64_hmac end def url_encode? !@options[:url_encode].nil? end def expires? is_a? QueryString end def date request['date'].to_s.strip.empty? ? Time.now : Time.parse(request['date']) end end # Provides header authentication by computing the value of the Authorization header. More details about the # various authentication schemes can be found in the docs for its containing module, Authentication. class Header < Signature #:nodoc: def initialize(*args) super self << "AWS #{access_key_id}:#{encoded_canonical}" end end # Provides query string authentication by computing the three authorization parameters: AWSAccessKeyId, Expires and Signature. # More details about the various authentication schemes can be found in the docs for its containing module, Authentication. class QueryString < Signature #:nodoc: constant :DEFAULT_EXPIRY, 300 # 5 minutes def initialize(*args) super options[:url_encode] = true self << build end private # Will return one of three values, in the following order of precedence: # # 1) Seconds since the epoch explicitly passed in the +:expires+ option # 2) The current time in seconds since the epoch plus the number of seconds passed in # the +:expires_in+ option # 3) The current time in seconds since the epoch plus the default number of seconds (60 seconds) def expires return options[:expires] if options[:expires] date.to_i + expires_in end def expires_in options.has_key?(:expires_in) ? Integer(options[:expires_in]) : DEFAULT_EXPIRY end # Keep in alphabetical order def build "AWSAccessKeyId=#{access_key_id}&Expires=#{expires}&Signature=#{encoded_canonical}" end end # The CanonicalString is used to generate an encrypted signature, signed with your secrect access key. It is composed of # data related to the given request for which it provides authentication. This data includes the request method, request headers, # and the request path. Both Header and QueryString use it to generate their signature. class CanonicalString < String #:nodoc: class << self def default_headers %w(content-type content-md5) end def interesting_headers ['content-md5', 'content-type', 'date', amazon_header_prefix] end def amazon_header_prefix /^#{AMAZON_HEADER_PREFIX}/io end end attr_reader :request, :headers def initialize(request, options = {}) super() @request = request @headers = {} @options = options # "For non-authenticated or anonymous requests. A NotImplemented error result code will be returned if # an authenticated (signed) request specifies a Host: header other than 's3.amazonaws.com'" # (from http://docs.amazonwebservices.com/AmazonS3/2006-03-01/VirtualHosting.html) request['Host'] = DEFAULT_HOST build end private def build self << "#{request.method}\n" ensure_date_is_valid initialize_headers set_expiry! headers.sort_by {|k, _| k}.each do |key, value| value = value.to_s.strip self << (key =~ self.class.amazon_header_prefix ? "#{key}:#{value}" : value) self << "\n" end self << path end def initialize_headers identify_interesting_headers set_default_headers end def set_expiry! self.headers['date'] = @options[:expires] if @options[:expires] end def ensure_date_is_valid request['Date'] ||= Time.now.httpdate end def identify_interesting_headers request.each do |key, value| key = key.downcase # Can't modify frozen string so no bang if self.class.interesting_headers.any? {|header| header === key} self.headers[key] = value.to_s.strip end end end def set_default_headers self.class.default_headers.each do |header| self.headers[header] ||= '' end end # see http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html#ConstructingTheCanonicalizedResourceElement def path [only_path, extract_significant_parameters].compact.join('?') end SIGNIFICANT_PARAMETERS = [ 'acl', 'location', 'logging', 'notification', 'partNumber', 'policy', 'requestPayment', 'torrent', 'uploadId', 'uploads', 'versionId', 'versioning', 'versions', 'website', 'response-content-type', 'response-content-language', 'response-expires', 'response-cache-control', 'response-content-disposition', 'response-content-encoding' ] def extract_significant_parameters # only the last value for each key, with preference to those still # in the options hash, will be included in the canonicalized # resource parameters = {} # significant parameters may already be in the query string of the # request's path (with values CGI escaped)... if request.path['?'] request.path.split('?', 2).last.split('&').each do |p| key, value = p.split('=', 2) next unless SIGNIFICANT_PARAMETERS.include?(key) parameters[key] = value && CGI.unescape(value) end end # ...or they may be in the options that will eventually make their # way into the query string (with values not yet CGI escaped) @options.each do |key,value| # treat symbols and string equally (as the string) key = key.to_s next unless SIGNIFICANT_PARAMETERS.include?(key) parameters[key] = value end return nil if parameters.empty? parameters.keys.sort.map do |key| if parameters[key] # we specifically don't do CGI escaping on the values going # into the signature string [key, parameters[key]].join('=') else key end end.join('&') end def only_path return request.path unless request.path['?'] request.path.split('?', 2).first end end end end end