Class: RightScale::CloudApi::AWS::S3::RequestSigner

Inherits:
CloudApi::Routine
  • Object
show all
Defined in:
lib/cloud/aws/s3/routines/request_signer.rb

Overview

S3 Request signer

Defined Under Namespace

Classes: Error

Constant Summary collapse

SUB_RESOURCES =

This guys are used to sign a request

%w{
  acl
  cors
  delete
  lifecycle
  location
  logging
  notification
  policy
  requestPayment
  tagging
  torrent
  uploads
  versionId
  versioning
  versions
  website
}
OVERRIDE_RESPONSE_HEADERS =

Using Query String API Amazon allows to override some of response headers:

response-content-type response-content-language response-expires reponse-cache-control response-content-disposition response-content-encoding

/^response-/
ONE_YEAR_OF_SECONDS =

One year in seconds

365*60*60*24

Instance Method Summary collapse

Instance Method Details

#processvoid

This method returns an undefined value.

Authenticates an S3 request

Examples:

# no example


76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/cloud/aws/s3/routines/request_signer.rb', line 76

def process
  # Extract sub-resource(s).
  # Sub-resources are acl, torrent, versioning, location etc.
  sub_resources = {}
  @data[:request][:params].each do |key, value|
    sub_resources[key] = (value._blank? ? nil : value) if SUB_RESOURCES.include?(key) || key[OVERRIDE_RESPONSE_HEADERS]
  end

  # Extract bucket name and object path
  if @data[:request][:bucket]._blank?
    # This is a very first attempt:
    # 1. extract the bucket name from the path
    # 2. save the bucket into request data vars
    bucket_name, @data[:request][:relative_path] = @data[:request][:relative_path].to_s[/^([^\/]*)\/?(.*)$/] && [$1, $2]
    @data[:request][:bucket] = bucket_name
    # Static path is the path that the original URL has.
    # 1. For Amazon it is always ''.
    # 2. For Euca it is usually a non-blank string.
    static_path = @data[:connection][:uri].path
    # Add trailing '/' to the path unless it is.
    static_path = "#{static_path}/" unless static_path[/\/$/]
    # Store the path: we may need it for signing redirects later.
    @data[:request][:static_path] = static_path
  else
    # This is a retry or a redirect:
    # 1. Extract the bucket name from the request data;
    # 2. Get rid of the path the remote server sent in the location header. We are
    #    re-signing the request and have to build everything from the scratch.
    #    In the crazy case when the new location has path differs from the original one
    #    we are screwed up and we will get "SignatureDoesNotMatch" error. But this does
    #    not seem to be the case for Amazon or Euca.
    bucket_name = @data[:request][:bucket]
    # Revert static path back to the original value.
    static_path = @data[:request][:static_path]
    @data[:connection][:uri].path = static_path
  end

  # Escape that part of the path that may have UTF-8 chars (in S3 Object name for instance).
  # Amazon wants them to be escaped before we sign the request.
  #
  # P.S. Escape AFTER we extract bucket name.
  @data[:request][:relative_path] = Utils::AWS::amz_escape(@data[:request][:relative_path])

  # Calculate a canonical path (bucket part must end with '/')
  bucket_string      = Utils::AWS::is_dns_bucket?(bucket_name) ? "#{bucket_name}/" : bucket_name.to_s
  canonicalized_path = Utils::join_urn(static_path,
                                       bucket_string,
                                       @data[:request][:relative_path],
                                       sub_resources ){ |value| value } # pass this block to avoid escaping: Amazon does not like escaped things in canonicalized_path

  # Make sure headers required for authentication are set
  unless @data[:options][:cloud][:link]
    # Make sure 'content-type' is set.
    # P.S. Ruby 2.1+ sets 'content-type' by default for POST and PUT requests.
    #      So we need to include it into our signature to avoid the error below:
    #      'The request signature we calculated does not match the signature you provided.
    #       Check your key and signing method.'
    @data[:request][:headers].set_if_blank('content-type', 'application/octet-stream')
    # REST Auth:
    unless @data[:request][:body]._blank?
      # Fix body if it is a Hash instance
      if @data[:request][:body].is_a?(Hash)
        @data[:request][:body] = Utils::contentify_body(@data[:request][:body], @data[:request][:headers]['content-type'])
      end
      # Calculate 'content-md5' when possible (some API calls wanna have it set)
      if @data[:request][:body].is_a?(String)
        @data[:request][:headers]['content-md5'] = Base64::encode64(Digest::MD5::digest(@data[:request][:body])).strip
      end
    end
    # Set date
    @data[:request][:headers].set_if_blank('date', Time::now.utc.httpdate)
    # Sign a request
    signature = Utils::AWS::sign_s3_signature( @data[:credentials][:aws_secret_access_key],
                                               @data[:request][:verb],
                                               canonicalized_path,
                                               @data[:request][:headers])
    @data[:request][:headers]['authorization'] = "AWS #{@data[:credentials][:aws_access_key_id]}:#{signature}"
  else
    # @see http://docs.amazonwebservices.com/AmazonS3/latest/dev/RESTAuthentication.html
    #
    # Amazon: ... We assume that when a browser makes the GET request, it won't provide a Content-MD5 or a Content-Type header,
    #  nor will it set any x-amz- headers, so those parts of the StringToSign are left blank. ...
    #
    # Only GET requests!
    raise Error::new("Only GET requests are supported by S3 Query String API") unless @data[:request][:verb] == :get
    # Expires
    expires = Utils::dearrayify(@data[:request][:headers]['expires'].first || (Time.now.utc.to_i + ONE_YEAR_OF_SECONDS))
    expires = expires.to_i unless expires.is_a?(Fixnum)
    # QUERY STRING AUTH: ('expires' and 'x-amz-*' headers are not supported)
    @data[:request][:params]['Expires']  = expires
    @data[:request][:headers]['expires'] = expires # a hack to sign a record
    @data[:request][:headers].dup.each do |header, values|
      @data[:request][:headers].delete(header) unless header.to_s[/(^x-amz-)|(^expires$)/]
    end
    @data[:request][:params]['AWSAccessKeyId'] = @data[:credentials][:aws_access_key_id]
    # Sign a request
    signature = Utils::AWS::sign_s3_signature( @data[:credentials][:aws_secret_access_key],
                                               @data[:request][:verb],
                                               canonicalized_path,
                                               @data[:request][:headers] )
    @data[:request][:params]['Signature'] = signature
    # we dont need this header any more
    @data[:request][:headers].delete('expires')
  end

  # Sub-domain compatible buckets vs incompatible ones
  if !@data[:options][:cloud][:no_subdomains] && Utils::AWS::is_dns_bucket?(bucket_name)
    # DNS compatible bucket name:
    #
    # Figure out if we need to add bucket name into the host name. It is rediculous but
    # sometimes Amazon returns a redirect to a host with the bucket name already mixed in
    # but sometimes without.
    # The only thing we can do so far is to check if the host name starts with the bucket
    # and the name is at least 4th level DNS name.
    #
    # Examples:
    #   * my-bucket.s3-ap-southeast-2.amazonaws.com
    #   * my-bucket.s3.amazonaws.com
    #   * s3.amazonaws.com
    #
    # P.S. This assumtion will not work for any other providers but we will figure it out later
    #      if we support any. The only other provider we support is Eucalyptus but it always
    #      expects that the bucket goes into path and never into the host therefore we are
    #      OK with Euca (Euca is expected to be run with :no_subdomains => true).
    #
    unless @data[:connection][:uri].host[/^#{bucket_name}\..+\.[^.]+\.[^.]+$/]
      # If there was a redirect and it had 'location' header then there is nothing to do with the host
      # otherwise we have to add the bucket to the host.
      # P.S. When Amazon returns a redirect (usually 301) with the new host in the message body
      # the new host does not have the bucket name in it. But if it is 307 and the host is in the location
      # header then that host name already includes the bucket in it. Grrrr....
      @data[:connection][:uri].host = "#{bucket_name}.#{@data[:connection][:uri].host}"
    end
    @data[:request][:path] = Utils::join_urn( @data[:connection][:uri].path,
                                              @data[:request][:relative_path],
                                              @data[:request][:params] )
  else
    # Old incompatible or Eucalyptus
    @data[:request][:path] = Utils::join_urn( @data[:connection][:uri].path,
                                              "#{bucket_name}",
                                              @data[:request][:relative_path],
                                              @data[:request][:params] )
  end

  # Host should be set for REST requests (and should not affect on Query String ones)
  @data[:request][:headers]['host'] = @data[:connection][:uri].host

  # Finalize data
  if @data[:options][:cloud][:link]
    # Amazon supports only some GET requests without body and any headers:
    # Return the link
    uri = @data[:connection][:uri].clone
    uri.path, uri.query = @data[:request][:path].split('?')
    @data[:result] = {
      "verb"    => @data[:request][:verb].to_s,
      "link"    => uri.to_s,
      "headers" => @data[:request][:headers]
    }
    # Query Auth:we should stop here because we just generated a link for the third part usage
    @data[:vars][:system][:done]   = true
  end
end