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

Direct Known Subclasses

Link::RequestSigner

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

#compute_body(body, content_type) ⇒ Object

Returns response body

Parameters:

  • body (Object)
  • content_type (String)

Returns:

  • (Object)


223
224
225
226
227
228
# File 'lib/cloud/aws/s3/routines/request_signer.rb', line 223

def compute_body(body, content_type)
  return body if body._blank?
  # Make sure it is a String instance
  return body unless body.is_a?(Hash)
  Utils::contentify_body(body, content_type)
end

#compute_bucket_name_and_object_path(bucket, relative_path) ⇒ Array

Extracts S3 bucket name and escapes relative path

Examples:

subject.compute_bucket_name_and_object_path(nil, 'my-test-bucket/foo/bar/банана.jpg') #=>
  ['my-test-bucket', 'foo%2Fbar%2F%D0%B1%D0%B0%D0%BD%D0%B0%D0%BD%D0%B0.jpg']

Parameters:

  • bucket (String)
  • relative_path (String)

Returns:

  • (Array)
    bucket, escaped_relative_path


177
178
179
180
181
182
183
184
# File 'lib/cloud/aws/s3/routines/request_signer.rb', line 177

def compute_bucket_name_and_object_path(bucket, relative_path)
  return [bucket, relative_path] if bucket
  # This is a very first attempt:
  relative_path.to_s[/^([^\/]*)\/?(.*)$/]
  # Escape 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.
  [ $1, Utils::AWS::amz_escape($2) ]
end

#compute_canonicalized_bucket(bucket) ⇒ String

Returns canonicalized bucket

Examples:

# DNS bucket
compute_canonicalized_bucket('foo-bar') #=> 'foo-bar/'
# non DNS bucket
compute_canonicalized_bucket('foo_bar') #=> 'foo_bar'

Parameters:

  • bucket (String)

Returns:

  • (String)


138
139
140
141
# File 'lib/cloud/aws/s3/routines/request_signer.rb', line 138

def compute_canonicalized_bucket(bucket)
  bucket += '/' if Utils::AWS::is_dns_bucket?(bucket)
  bucket
end

#compute_canonicalized_path(bucket, relative_path, params) ⇒ String

Returns canonicalized path

Examples:

params = { 'Foo' => 1, 'acl' => '2', 'response-content-type' => 'jpg' }
compute_canonicalized_path('foo-bar_bucket', 'a/b/c/d.jpg', params)
  #=> '/foo-bar_bucket/a/b/c/d.jpg?acl=3&response-content-type=jpg'

Parameters:

  • bucket (String)
  • relative_path (String)
  • params (Hash)

Returns:

  • (String)


157
158
159
160
161
162
163
# File 'lib/cloud/aws/s3/routines/request_signer.rb', line 157

def compute_canonicalized_path(bucket, relative_path, params)
  can_bucket = compute_canonicalized_bucket(bucket)
  sub_params = get_subresources(params)
  # We use the block below to avoid escaping: Amazon does not like escaped bucket and '/'
  # in canonicalized path (relative path has been escaped above already)
  Utils::join_urn(can_bucket, relative_path, sub_params) { |value| value }
end

#compute_headers!(headers, body, host) ⇒ Hash

Sets response headers

Parameters:

  • headers (Hash)
  • body (String)
  • host (String)

Returns:

  • (Hash)


239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/cloud/aws/s3/routines/request_signer.rb', line 239

def compute_headers!(headers, body, host)
  # 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.'
  headers.set_if_blank('content-type', 'application/octet-stream')
  headers.set_if_blank('date', Time::now.utc.httpdate)
  headers['content-md5'] = Base64::encode64(Digest::MD5::digest(body)).strip if !body._blank?
  headers['host']        = host
  headers
end

#compute_host(bucket, uri) ⇒ URI

Figure out if we need to add bucket name into the host 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. 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

Parameters:

  • bucket (String)
  • uri (URI)

Returns:

  • (URI)


208
209
210
211
212
213
# File 'lib/cloud/aws/s3/routines/request_signer.rb', line 208

def compute_host(bucket, uri)
  return uri unless Utils::AWS::is_dns_bucket?(bucket)
  return uri if uri.host[/^#{bucket}\..+\.[^.]+\.[^.]+$/]
  uri.host = "#{bucket}.#{uri.host}"
  uri
end

#compute_path(bucket, object, params) ⇒ String

Builds request path

Parameters:

  • bucket (String)
  • object (String)
  • params (Hash)

Returns:

  • (String)


278
279
280
281
282
283
284
# File 'lib/cloud/aws/s3/routines/request_signer.rb', line 278

def compute_path(bucket, object, params)
  data = []
  data << bucket unless Utils::AWS::is_dns_bucket?(bucket)
  data << object
  data << params
  Utils::join_urn(*data)
end

#compute_signature(access_key, secret_key, verb, bucket, object, params, headers) ⇒ String

Computes signature

Parameters:

  • access_key (String)
  • secret_key (String)
  • verb (String)
  • bucket (String)
  • params (Hash)
  • headers (Hash)

Returns:

  • (String)


264
265
266
267
# File 'lib/cloud/aws/s3/routines/request_signer.rb', line 264

def compute_signature(access_key, secret_key, verb, bucket, object, params, headers)
  can_path  = compute_canonicalized_path(bucket, object, params)
  Utils::AWS::sign_s3_signature(secret_key, verb, can_path, headers)
end

#get_subresources(params) ⇒ Hash

Returns a list of sub-resource(s)

Sub-resources are acl, torrent, versioning, location, etc. See SUB_RESOURCES

Returns:

  • (Hash)


114
115
116
117
118
119
120
121
# File 'lib/cloud/aws/s3/routines/request_signer.rb', line 114

def get_subresources(params)
  result = {}
  params.each do |key, value|
    next unless SUB_RESOURCES.include?(key) || key[OVERRIDE_RESPONSE_HEADERS]
    result[key] = (value._blank? ? nil : value)
  end
  result
end

#processvoid

This method returns an undefined value.

Authenticates an S3 request

Examples:

# no example


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
# File 'lib/cloud/aws/s3/routines/request_signer.rb', line 77

def process
  uri        = @data[:connection][:uri]
  access_key = @data[:credentials][:aws_access_key_id]
  secret_key = @data[:credentials][:aws_secret_access_key]
  body       = @data[:request][:body]
  bucket     = @data[:request][:bucket]
  headers    = @data[:request][:headers]
  object     = @data[:request][:relative_path]
  params     = @data[:request][:params]
  verb       = @data[:request][:verb]

  bucket, object = compute_bucket_name_and_object_path(bucket, object)
  body           = compute_body(body, headers['content-type'])
  uri            = compute_host(bucket, uri)

  compute_headers!(headers, body, uri.host)

  # Set Authorization header
  signature = compute_signature(access_key, secret_key, verb, bucket, object, params, headers)
  headers['authorization'] = "AWS #{access_key}:#{signature}"

  @data[:request][:body]           = body
  @data[:request][:bucket]         = bucket
  @data[:request][:headers]        = headers
  @data[:request][:params]         = params
  @data[:request][:path]           = compute_path(bucket, object, params)
  @data[:request][:relative_path]  = object
  @data[:connection][:uri] = uri
end