right_aws_api 0.2.0 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/HISTORY +9 -0
- data/lib/cloud/aws/base/helpers/utils.rb +216 -13
- data/lib/cloud/aws/base/manager.rb +1 -1
- data/lib/cloud/aws/base/routines/request_signer.rb +13 -29
- data/lib/cloud/aws/s3/link/routines/request_signer.rb +12 -44
- data/lib/cloud/aws/s3/manager.rb +13 -0
- data/lib/cloud/aws/s3/routines/request_signer.rb +38 -107
- data/lib/cloud/aws/s3/wrappers/default.rb +37 -35
- data/lib/cloud/aws/sqs/manager.rb +2 -2
- data/lib/cloud/aws/sqs/routines/request_signer.rb +62 -0
- data/lib/right_aws_api_version.rb +1 -1
- data/right_aws_api.gemspec +2 -1
- data/spec/cloud/aws/base/helpers/utils_spec.rb +298 -0
- data/spec/cloud/aws/s3/routines/request_signer_spec.rb +3 -119
- metadata +23 -8
- data/spec/cloud/aws/s3/link/routines/request_signer_spec.rb +0 -53
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 164b81b2c4e7845571d6616ce59b22a3baa7788a
|
4
|
+
data.tar.gz: e7cbd22f3857d650a026b29699e22cc9718d1821
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4b970d6a9794c29953c16e63018f7a97522476f66a7357ea5be7e0bd6d3d110b880615d86531a45a70b7ebe41d96d3bf18697074d2a7fc5844b1fc16caf51089
|
7
|
+
data.tar.gz: d0846816165f048ebfa43d026d144c03c35158f4006c750a7d0c14117a1f50d4031a3d46422b34321935d182e074f121bc426e84be043b3ba76371ed03b59ffa
|
data/HISTORY
CHANGED
@@ -1,2 +1,11 @@
|
|
1
|
+
== 2015-01-16
|
2
|
+
- v0.2.2
|
3
|
+
Added an ability to not use DNS like buckets in AWS S3
|
4
|
+
Some minor bugs were fixed in singature V4
|
5
|
+
|
6
|
+
== 2014-10-30
|
7
|
+
- v0.2.1
|
8
|
+
Added support for AWS v4 signature and make it the default
|
9
|
+
|
1
10
|
== 2013-06-28
|
2
11
|
- pre-release candidate created
|
@@ -171,7 +171,7 @@ module RightScale
|
|
171
171
|
#
|
172
172
|
# @example
|
173
173
|
# sign_s3_signature('secret', :get, 'xxx/yyy/zzz/object', {'header'=>'value'}) #=>
|
174
|
-
# "i85igH0sftHD/cGZcLiBKcYEuks="
|
174
|
+
# "i85igH0sftHD/cGZcLiBKcYEuks="
|
175
175
|
#
|
176
176
|
# @see http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
|
177
177
|
#
|
@@ -196,7 +196,210 @@ module RightScale
|
|
196
196
|
sign(aws_secret_access_key, string_to_sign)
|
197
197
|
end
|
198
198
|
|
199
|
-
|
199
|
+
|
200
|
+
def self.sign_v4_get_service_and_region(host)
|
201
|
+
result =
|
202
|
+
case
|
203
|
+
when host[ /^iam\.amazonaws\.com$/i ] then ['iam', 'us-east-1']
|
204
|
+
when host[ /^(.*\.)?s3\.amazonaws\.com$/i ] then ['s3', 'us-east-1']
|
205
|
+
when host[ /^(.*\.)?s3-external-1\.amazonaws\.com$/i ] then ['s3', 'us-east-1']
|
206
|
+
when host[ /s3-website-([^.]+)\.amazonaws\.com$/i ] then ['s3', $1]
|
207
|
+
when host[ /^(.*\.)?s3-([^.]+).amazonaws\.com$/i ] then ['s3', $2]
|
208
|
+
when host[ /^(.*\.)?s3\.([^.]+)\.amazonaws\.com$/i ] then ['s3', $2]
|
209
|
+
else host[ /^([^.]+)\.([^.]+)\.amazonaws\.com$/i ] && [$1, $2]
|
210
|
+
end
|
211
|
+
fail(ArgumentError, "Cannot extract service name from %s host" % host.inspect) if !result || result[0].to_s.empty?
|
212
|
+
fail(ArgumentError, "Cannot extract region name from %s host" % host.inspect) if result[1].to_s.empty?
|
213
|
+
result
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
# Signs and Authenticates REST Requests
|
218
|
+
#
|
219
|
+
# @param [String] aws_secret_access_key
|
220
|
+
# @param [String] aws_access_key
|
221
|
+
# @param [String] host
|
222
|
+
# @param [Hash] request
|
223
|
+
#
|
224
|
+
# @return [String]
|
225
|
+
#
|
226
|
+
# @see http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
|
227
|
+
#
|
228
|
+
def self.sign_v4_signature(aws_access_key, aws_secret_access_key, host, request, method=:headers)
|
229
|
+
now = Time.now.utc
|
230
|
+
current_date = now.strftime("%Y%m%d")
|
231
|
+
current_time = now.strftime("%Y%m%dT%H%M%SZ")
|
232
|
+
host = host.downcase
|
233
|
+
service, region = sign_v4_get_service_and_region(host)
|
234
|
+
creds_scope = "%s/%s/%s/aws4_request" % [current_date, region, service]
|
235
|
+
algorithm = "AWS4-HMAC-SHA256"
|
236
|
+
|
237
|
+
# Verb
|
238
|
+
canonical_verb = sign_v4_get_canonical_verb(request[:verb])
|
239
|
+
|
240
|
+
# Path
|
241
|
+
request[:path] ||= '/'
|
242
|
+
canonical_path = sign_v4_get_canonical_path(request[:path])
|
243
|
+
|
244
|
+
# Headers (Auth)
|
245
|
+
request[:headers].delete('Authorization')
|
246
|
+
if method == :headers
|
247
|
+
canonical_payload = sign_v4_headers(request, host, current_time)
|
248
|
+
end
|
249
|
+
# Headers (Standard)
|
250
|
+
request[:headers]['Host'] = host
|
251
|
+
_headers = {}
|
252
|
+
request[:headers].each do |key, value|
|
253
|
+
_headers[key.to_s.downcase] = value.is_a?(Array) ? value.join(',') : value
|
254
|
+
end
|
255
|
+
canonical_headers = sign_v4_get_canonical_headers(_headers)
|
256
|
+
signed_headers = sign_v4_get_signed_headers(_headers)
|
257
|
+
|
258
|
+
# Params (Auth)
|
259
|
+
if method != :headers
|
260
|
+
canonical_payload = sign_v4_query_params(
|
261
|
+
request,
|
262
|
+
algorithm,
|
263
|
+
current_time,
|
264
|
+
signed_headers,
|
265
|
+
aws_access_key,
|
266
|
+
creds_scope
|
267
|
+
)
|
268
|
+
end
|
269
|
+
# Params (Standard)
|
270
|
+
canonical_query_string = Utils::params_to_urn(request[:params]){ |value| amz_escape(value) }
|
271
|
+
|
272
|
+
# Canonical String
|
273
|
+
canonical_string = sign_v4_get_canonical_string(
|
274
|
+
canonical_verb,
|
275
|
+
canonical_path,
|
276
|
+
canonical_query_string,
|
277
|
+
canonical_headers,
|
278
|
+
signed_headers,
|
279
|
+
canonical_payload
|
280
|
+
)
|
281
|
+
|
282
|
+
# StringToSign
|
283
|
+
string_to_sign = sign_v4_get_string_to_sign(algorithm, current_time, creds_scope, canonical_string)
|
284
|
+
|
285
|
+
# Signature
|
286
|
+
signature = sign_v4_get_signature_key(aws_secret_access_key, string_to_sign, current_date, region, service)
|
287
|
+
|
288
|
+
request[:path] += "?%s" % canonical_query_string unless canonical_query_string.empty?
|
289
|
+
|
290
|
+
if method == :headers
|
291
|
+
# Authorization Header
|
292
|
+
authorization_header = "%s Credential=%s/%s, SignedHeaders=%s, Signature=%s" %
|
293
|
+
[algorithm, aws_access_key, creds_scope, signed_headers, signature]
|
294
|
+
request[:headers]['Authorization'] = authorization_header
|
295
|
+
else
|
296
|
+
request[:path] += "&X-Amz-Signature=%s" % signature
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
|
301
|
+
def self.sign_v4_get_canonical_verb(verb)
|
302
|
+
verb.to_s.upcase
|
303
|
+
end
|
304
|
+
|
305
|
+
|
306
|
+
def self.sign_v4_get_canonical_path(path)
|
307
|
+
path
|
308
|
+
end
|
309
|
+
|
310
|
+
|
311
|
+
def self.sign_v4_query_params(request, algorithm, current_time, signed_headers, aws_access_key, creds_scope)
|
312
|
+
expires_at = request[:params]['X-Amz-Expires'] || 3600
|
313
|
+
expires_at = expires_at.to_i if expires_at.is_a?(Time)
|
314
|
+
|
315
|
+
request[:params]['X-Amz-Date'] = current_time
|
316
|
+
request[:params]['X-Amz-Expires'] = expires_at
|
317
|
+
request[:params]['X-Amz-Algorithm'] = algorithm
|
318
|
+
request[:params]['X-Amz-SignedHeaders'] = signed_headers
|
319
|
+
request[:params]['X-Amz-Credential'] = "%s/%s" % [aws_access_key, creds_scope]
|
320
|
+
|
321
|
+
'UNSIGNED-PAYLOAD'
|
322
|
+
end
|
323
|
+
|
324
|
+
|
325
|
+
def self.sign_v4_get_canonical_headers(headers)
|
326
|
+
headers.sort.map{ |key, value| "#{key}:#{value}" }.join("\n")
|
327
|
+
end
|
328
|
+
|
329
|
+
|
330
|
+
def self.sign_v4_get_signed_headers(headers)
|
331
|
+
headers.keys.sort.join(';')
|
332
|
+
end
|
333
|
+
|
334
|
+
|
335
|
+
def self.sign_v4_headers(request, host, current_time)
|
336
|
+
expires_at = request[:headers]['X-Amz-Expires'].first || 3600
|
337
|
+
expires_at = expires_at.to_i if expires_at.is_a?(Time)
|
338
|
+
|
339
|
+
if request[:body].is_a?(IO)
|
340
|
+
canonical_payload = ''
|
341
|
+
request[:headers].set_if_blank('X-Amz-Content-Sha256', 'UNSIGNED-PAYLOAD')
|
342
|
+
else
|
343
|
+
request[:body] = request[:body].to_s
|
344
|
+
canonical_payload = hex_encode(Digest::SHA256.digest(request[:body]))
|
345
|
+
content_type = 'application/x-www-form-urlencoded; charset=utf-8'
|
346
|
+
content_md5 = Base64::encode64(Digest::MD5::digest(request[:body])).strip
|
347
|
+
request[:headers].set_if_blank('Content-Length', request[:body].bytesize)
|
348
|
+
request[:headers].set_if_blank('Content-Type', content_type)
|
349
|
+
request[:headers].set_if_blank('Content-Md5', content_md5)
|
350
|
+
request[:headers].set_if_blank('X-Amz-Content-Sha256', canonical_payload)
|
351
|
+
end
|
352
|
+
request[:headers]['X-Amz-Date'] = current_time
|
353
|
+
request[:headers]['X-Amz-Expires'] = expires_at
|
354
|
+
|
355
|
+
canonical_payload
|
356
|
+
end
|
357
|
+
|
358
|
+
|
359
|
+
# Signature V4: Returns Canonical String
|
360
|
+
#
|
361
|
+
# @return [String]
|
362
|
+
#
|
363
|
+
def self.sign_v4_get_canonical_string(verb, path, query_string, headers, signed_headers, payload)
|
364
|
+
verb + "\n" +
|
365
|
+
path + "\n" +
|
366
|
+
query_string + "\n" +
|
367
|
+
headers + "\n\n" +
|
368
|
+
signed_headers + "\n" +
|
369
|
+
payload
|
370
|
+
end
|
371
|
+
|
372
|
+
|
373
|
+
# Signature V4: A string to sign value
|
374
|
+
#
|
375
|
+
# @return [String]
|
376
|
+
#
|
377
|
+
def self.sign_v4_get_string_to_sign(algorithm, current_time, creds_scope, canonical_string)
|
378
|
+
algorithm + "\n" +
|
379
|
+
current_time + "\n" +
|
380
|
+
creds_scope + "\n" +
|
381
|
+
hex_encode(Digest::SHA256.digest(canonical_string)).downcase
|
382
|
+
end
|
383
|
+
|
384
|
+
|
385
|
+
#Helpers from AWS documentation http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html
|
386
|
+
def self.sign_v4_get_signature_key(key, string_to_sign, date, region, service, digest = @@digest256)
|
387
|
+
k_date = OpenSSL::HMAC.digest(digest, "AWS4" + key, date)
|
388
|
+
k_region = OpenSSL::HMAC.digest(digest, k_date, region)
|
389
|
+
k_service = OpenSSL::HMAC.digest(digest, k_region, service)
|
390
|
+
k_signing = OpenSSL::HMAC.digest(digest, k_service, "aws4_request")
|
391
|
+
hex_encode OpenSSL::HMAC.digest(digest, k_signing, string_to_sign)
|
392
|
+
end
|
393
|
+
|
394
|
+
|
395
|
+
def self.hex_encode(bindata)
|
396
|
+
result=""
|
397
|
+
data=bindata.unpack("C*")
|
398
|
+
data.each {|b| result+= "%02x" % b}
|
399
|
+
result
|
400
|
+
end
|
401
|
+
|
402
|
+
|
200
403
|
# Parametrizes data to the format that Amazon EC2 (and compatible APIs) loves
|
201
404
|
#
|
202
405
|
# @param [Hash] data
|
@@ -207,7 +410,7 @@ module RightScale
|
|
207
410
|
# # Where hash is:
|
208
411
|
# { Name.?.Mask => Value | [ Values ],
|
209
412
|
# NamePrefix.? => [{ SubNameA.1 => ValueA.1, SubNameB.1 => ValueB.1 }, # any simple parameter
|
210
|
-
# ...,
|
413
|
+
# ...,
|
211
414
|
# { SubNameN.X => ValueN.X, SubNameM.X => ValueN.X }] # see BlockDeviceMapping case
|
212
415
|
#
|
213
416
|
# @example
|
@@ -237,15 +440,15 @@ module RightScale
|
|
237
440
|
# 'MaxCount' => 2,
|
238
441
|
# 'KeyName' => 'my-key',
|
239
442
|
# 'SecurityGroupId' => ['sg-01234567', 'sg-12345670', 'sg-23456701'],
|
240
|
-
# 'BlockDeviceMapping' => [
|
241
|
-
# { 'DeviceName' => '/dev/sda1',
|
242
|
-
# 'Ebs.SnapshotId' => 'snap-01234567',
|
443
|
+
# 'BlockDeviceMapping' => [
|
444
|
+
# { 'DeviceName' => '/dev/sda1',
|
445
|
+
# 'Ebs.SnapshotId' => 'snap-01234567',
|
243
446
|
# 'Ebs.VolumeSize' => 20,
|
244
447
|
# 'Ebs.DeleteOnTermination' => true },
|
245
|
-
# { 'DeviceName' => '/dev/sdb1',
|
246
|
-
# 'Ebs.SnapshotId' => 'snap-12345670',
|
448
|
+
# { 'DeviceName' => '/dev/sdb1',
|
449
|
+
# 'Ebs.SnapshotId' => 'snap-12345670',
|
247
450
|
# 'Ebs.VolumeSize' => 10,
|
248
|
-
# 'Ebs.DeleteOnTermination' => false } ] ) #=>
|
451
|
+
# 'Ebs.DeleteOnTermination' => false } ] ) #=>
|
249
452
|
# {
|
250
453
|
# "BlockDeviceMapping.1.DeviceName" => "/dev/sda1",
|
251
454
|
# "BlockDeviceMapping.1.Ebs.DeleteOnTermination" => true,
|
@@ -262,7 +465,7 @@ module RightScale
|
|
262
465
|
# "SecurityGroupId.1" => "sg-01234567",
|
263
466
|
# "SecurityGroupId.2" => "sg-12345670",
|
264
467
|
# "SecurityGroupId.3" => "sg-23456701"
|
265
|
-
# }
|
468
|
+
# }
|
266
469
|
#
|
267
470
|
# @example
|
268
471
|
# parametrize( 'DomainName' => 'kdclient',
|
@@ -276,7 +479,7 @@ module RightScale
|
|
276
479
|
# { 'ItemName' => 'diana',
|
277
480
|
# 'Attribute' => [ { 'Name' => 'sex', 'Value' => 'female' },
|
278
481
|
# { 'Name' => 'weight', 'Value' => '120'},
|
279
|
-
# { 'Name' => 'age', 'Value' => '25'} ] } ] ) #=>
|
482
|
+
# { 'Name' => 'age', 'Value' => '25'} ] } ] ) #=>
|
280
483
|
# { "DomainName" => "kdclient",
|
281
484
|
# "Item.1.ItemName" => "konstantin",
|
282
485
|
# "Item.1.Attribute.1.Name" => "sex",
|
@@ -321,8 +524,8 @@ module RightScale
|
|
321
524
|
end
|
322
525
|
result
|
323
526
|
end
|
324
|
-
|
527
|
+
|
325
528
|
end
|
326
529
|
end
|
327
530
|
end
|
328
|
-
end
|
531
|
+
end
|
@@ -116,7 +116,7 @@ module RightScale
|
|
116
116
|
opts[:headers] = params.delete(:headers) || {}
|
117
117
|
opts[:options] = params.delete(:options) || {}
|
118
118
|
opts[:params] = parametrize(params)
|
119
|
-
process_api_request(:
|
119
|
+
process_api_request(:post, '', opts, &block)
|
120
120
|
end
|
121
121
|
|
122
122
|
|
@@ -43,38 +43,22 @@ module RightScale
|
|
43
43
|
# no example
|
44
44
|
#
|
45
45
|
def process
|
46
|
-
# Make sure all the required params are set
|
47
|
-
@data[:request][:params]['AWSAccessKeyId'] = @data[:credentials][:aws_access_key_id]
|
48
|
-
@data[:request][:params]['Version'] ||= @data[:options][:api_version]
|
49
46
|
# Compile a final request path
|
50
47
|
@data[:request][:path] = Utils::join_urn(@data[:connection][:uri].path, @data[:request][:relative_path])
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
signed_path = sign_proc.call(@data)
|
64
|
-
end
|
65
|
-
# Set new path or body and content-type
|
66
|
-
case @data[:request][:verb]
|
67
|
-
when :get
|
68
|
-
@data[:request][:path] << "?#{signed_path}"
|
69
|
-
when :post
|
70
|
-
@data[:request][:body] = signed_path
|
71
|
-
@data[:request][:headers]['content-type'] = 'application/x-www-form-urlencoded; charset=utf-8'
|
72
|
-
else
|
73
|
-
fail Error::new("Unsupported HTTP verb: #{@data[:request][:verb]}")
|
74
|
-
end
|
48
|
+
|
49
|
+
# Swap query params and body
|
50
|
+
@data[:request][:params]['Version'] ||= @data[:options][:api_version]
|
51
|
+
@data[:request][:body] = Utils::params_to_urn(@data[:request][:params]){ |value| Utils::AWS::amz_escape(value) }
|
52
|
+
@data[:request][:params] = {}
|
53
|
+
|
54
|
+
Utils::AWS::sign_v4_signature(
|
55
|
+
@data[:credentials][:aws_access_key_id],
|
56
|
+
@data[:credentials][:aws_secret_access_key],
|
57
|
+
@data[:connection][:uri].host,
|
58
|
+
@data[:request]
|
59
|
+
)
|
75
60
|
end
|
76
61
|
end
|
77
|
-
|
78
62
|
end
|
79
63
|
end
|
80
|
-
end
|
64
|
+
end
|
@@ -43,25 +43,24 @@ module RightScale
|
|
43
43
|
fail Error::new("Body must be blank") unless @data[:request][:body]._blank?
|
44
44
|
fail Error::new("Headers must be blank") unless @data[:request][:headers]._blank?
|
45
45
|
|
46
|
-
uri
|
47
|
-
|
48
|
-
|
49
|
-
bucket = @data[:request][:bucket]
|
50
|
-
object = @data[:request][:relative_path]
|
51
|
-
params = @data[:request][:params]
|
52
|
-
verb = @data[:request][:verb]
|
53
|
-
|
46
|
+
uri = @data[:connection][:uri]
|
47
|
+
bucket = @data[:request][:bucket]
|
48
|
+
object = @data[:request][:relative_path]
|
54
49
|
bucket, object = compute_bucket_name_and_object_path(bucket, object)
|
55
50
|
uri = compute_host(bucket, uri)
|
56
51
|
|
57
|
-
|
52
|
+
@data[:request][:path] = compute_path(bucket, object)
|
58
53
|
|
59
|
-
|
60
|
-
|
61
|
-
|
54
|
+
Utils::AWS::sign_v4_signature(
|
55
|
+
@data[:credentials][:aws_access_key_id],
|
56
|
+
@data[:credentials][:aws_secret_access_key],
|
57
|
+
@data[:connection][:uri].host,
|
58
|
+
@data[:request],
|
59
|
+
:query_params
|
60
|
+
)
|
62
61
|
|
63
62
|
# Compute href
|
64
|
-
path =
|
63
|
+
path = @data[:request][:path]
|
65
64
|
uri.path, uri.query = path.split('?')
|
66
65
|
@data[:result] = uri.to_s
|
67
66
|
|
@@ -69,37 +68,6 @@ module RightScale
|
|
69
68
|
@data[:vars][:system][:done] = true
|
70
69
|
end
|
71
70
|
|
72
|
-
|
73
|
-
# Sets response params
|
74
|
-
#
|
75
|
-
# @param [Hash] params
|
76
|
-
#
|
77
|
-
# @return [Hash]
|
78
|
-
#
|
79
|
-
def compute_params!(params, access_key)
|
80
|
-
# Expires
|
81
|
-
expires = params['Expires']
|
82
|
-
expires ||= Time.now.utc.to_i + ONE_YEAR_OF_SECONDS
|
83
|
-
expires = expires.to_i unless expires.is_a?(Fixnum)
|
84
|
-
params['Expires'] = expires
|
85
|
-
params['AWSAccessKeyId'] = access_key
|
86
|
-
params
|
87
|
-
end
|
88
|
-
|
89
|
-
|
90
|
-
# Computes signature
|
91
|
-
#
|
92
|
-
# @param [String] secret_key
|
93
|
-
# @param [String] verb
|
94
|
-
# @param [String] bucket
|
95
|
-
# @param [Hash] params
|
96
|
-
#
|
97
|
-
# @return [String]
|
98
|
-
#
|
99
|
-
def compute_signature(secret_key, verb, bucket, object, params)
|
100
|
-
can_path = compute_canonicalized_path(bucket, object, params)
|
101
|
-
Utils::AWS::sign_s3_signature(secret_key, verb, can_path, { 'expires' => params['Expires'] })
|
102
|
-
end
|
103
71
|
end
|
104
72
|
|
105
73
|
end
|
data/lib/cloud/aws/s3/manager.rb
CHANGED
@@ -293,6 +293,19 @@ module RightScale
|
|
293
293
|
# 'https://s3.amazonaws.com/?AWSAccessKeyId=AK...TA&Expires=1436651780&
|
294
294
|
# Signature=XK...53s%3D'
|
295
295
|
#
|
296
|
+
# @example
|
297
|
+
# link = RightScale::CloudApi::AWS::S3::Link::Manager::new(key, secret, endpoint)
|
298
|
+
# link.GetObject('Bucket' => 'foo', 'Object' => 'bar') #=>
|
299
|
+
# 'https://foo.s3.amazonaws.com/bar?AWSAccessKeyId=AK...TA&Expires=1436557118&
|
300
|
+
# Signature=hg...%3D&response-content-type=image%2Fpeg'
|
301
|
+
#
|
302
|
+
# @example
|
303
|
+
# # Do not use DNS-like bucket hosts but put buckets into path
|
304
|
+
# link = RightScale::CloudApi::AWS::S3::Link::Manager::new(key, secret, endpoint, :no_dns_buckets => true)
|
305
|
+
# link.GetObject('Bucket' => 'foo', 'Object' => 'bar') #=>
|
306
|
+
# 'https://s3.amazonaws.com/foo/bar?AWSAccessKeyId=AK...TA&Expires=1436557118&
|
307
|
+
# Signature=hg...%3D&response-content-type=image%2Fpeg'
|
308
|
+
#
|
296
309
|
# @see ApiManager
|
297
310
|
# @see Wrapper::DEFAULT.extended Wrapper::DEFAULT.extended (click [View source])
|
298
311
|
# @see http://docs.aws.amazon.com/AmazonS3/latest/API/APIRest.html
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
1
2
|
#--
|
2
3
|
# Copyright (c) 2013 RightScale, Inc.
|
3
4
|
#
|
@@ -75,91 +76,28 @@ module RightScale
|
|
75
76
|
# # no example
|
76
77
|
#
|
77
78
|
def process
|
78
|
-
uri
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
bucket = @data[:request][:bucket]
|
83
|
-
headers = @data[:request][:headers]
|
84
|
-
object = @data[:request][:relative_path]
|
85
|
-
params = @data[:request][:params]
|
86
|
-
verb = @data[:request][:verb]
|
87
|
-
|
79
|
+
uri = @data[:connection][:uri]
|
80
|
+
body = @data[:request][:body]
|
81
|
+
bucket = @data[:request][:bucket]
|
82
|
+
object = @data[:request][:relative_path]
|
88
83
|
bucket, object = compute_bucket_name_and_object_path(bucket, object)
|
89
|
-
body = compute_body(body, headers['content-type'])
|
84
|
+
body = compute_body(body, @data[:request][:headers]['content-type'])
|
90
85
|
uri = compute_host(bucket, uri)
|
91
86
|
|
92
|
-
compute_headers!(headers, body, uri.host)
|
93
|
-
|
94
|
-
# Set Authorization header
|
95
|
-
signature = compute_signature(access_key, secret_key, verb, bucket, object, params, headers)
|
96
|
-
headers['authorization'] = "AWS #{access_key}:#{signature}"
|
97
|
-
|
98
|
-
@data[:request][:body] = body
|
99
|
-
@data[:request][:bucket] = bucket
|
100
|
-
@data[:request][:headers] = headers
|
101
|
-
@data[:request][:params] = params
|
102
|
-
@data[:request][:path] = compute_path(bucket, object, params)
|
103
|
-
@data[:request][:relative_path] = object
|
104
|
-
@data[:connection][:uri] = uri
|
105
|
-
end
|
106
|
-
|
107
|
-
|
108
|
-
# Returns a list of sub-resource(s)
|
109
|
-
#
|
110
|
-
# Sub-resources are acl, torrent, versioning, location, etc. See SUB_RESOURCES
|
111
|
-
#
|
112
|
-
# @return [Hash]
|
113
|
-
#
|
114
|
-
def get_subresources(params)
|
115
|
-
result = {}
|
116
|
-
params.each do |key, value|
|
117
|
-
next unless SUB_RESOURCES.include?(key) || key[OVERRIDE_RESPONSE_HEADERS]
|
118
|
-
result[key] = (value._blank? ? nil : value)
|
119
|
-
end
|
120
|
-
result
|
121
|
-
end
|
87
|
+
compute_headers!(@data[:request][:headers], body, uri.host)
|
122
88
|
|
89
|
+
@data[:connection][:uri] = uri
|
90
|
+
@data[:request][:bucket] = bucket
|
91
|
+
@data[:request][:relative_path] = object
|
92
|
+
@data[:request][:body] = body
|
93
|
+
@data[:request][:path] = compute_path(bucket, object)
|
123
94
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
# @example
|
131
|
-
# # DNS bucket
|
132
|
-
# compute_canonicalized_bucket('foo-bar') #=> 'foo-bar/'
|
133
|
-
#
|
134
|
-
# @example
|
135
|
-
# # non DNS bucket
|
136
|
-
# compute_canonicalized_bucket('foo_bar') #=> 'foo_bar'
|
137
|
-
#
|
138
|
-
def compute_canonicalized_bucket(bucket)
|
139
|
-
bucket += '/' if Utils::AWS::is_dns_bucket?(bucket)
|
140
|
-
bucket
|
141
|
-
end
|
142
|
-
|
143
|
-
|
144
|
-
# Returns canonicalized path
|
145
|
-
#
|
146
|
-
# @param [String] bucket
|
147
|
-
# @param [String] relative_path
|
148
|
-
# @param [Hash] params
|
149
|
-
#
|
150
|
-
# @return [String]
|
151
|
-
#
|
152
|
-
# @example
|
153
|
-
# params = { 'Foo' => 1, 'acl' => '2', 'response-content-type' => 'jpg' }
|
154
|
-
# compute_canonicalized_path('foo-bar_bucket', 'a/b/c/d.jpg', params)
|
155
|
-
# #=> '/foo-bar_bucket/a/b/c/d.jpg?acl=3&response-content-type=jpg'
|
156
|
-
#
|
157
|
-
def compute_canonicalized_path(bucket, relative_path, params)
|
158
|
-
can_bucket = compute_canonicalized_bucket(bucket)
|
159
|
-
sub_params = get_subresources(params)
|
160
|
-
# We use the block below to avoid escaping: Amazon does not like escaped bucket and '/'
|
161
|
-
# in canonicalized path (relative path has been escaped above already)
|
162
|
-
Utils::join_urn(can_bucket, relative_path, sub_params) { |value| value }
|
95
|
+
Utils::AWS::sign_v4_signature(
|
96
|
+
@data[:credentials][:aws_access_key_id],
|
97
|
+
@data[:credentials][:aws_secret_access_key],
|
98
|
+
@data[:connection][:uri].host,
|
99
|
+
@data[:request]
|
100
|
+
)
|
163
101
|
end
|
164
102
|
|
165
103
|
|
@@ -180,7 +118,10 @@ module RightScale
|
|
180
118
|
relative_path.to_s[/^([^\/]*)\/?(.*)$/]
|
181
119
|
# Escape part of the path that may have UTF-8 chars (in S3 Object name for instance).
|
182
120
|
# Amazon wants them to be escaped before we sign the request.
|
183
|
-
|
121
|
+
# P.S. but do not escape "/" (signature v4 does not like this)
|
122
|
+
#
|
123
|
+
object = $2.to_s.split('/').map{|i| Utils::AWS::amz_escape(i)}.join('/')
|
124
|
+
[ $1, object ]
|
184
125
|
end
|
185
126
|
|
186
127
|
|
@@ -206,7 +147,7 @@ module RightScale
|
|
206
147
|
# @return [URI]
|
207
148
|
#
|
208
149
|
def compute_host(bucket, uri)
|
209
|
-
return uri unless
|
150
|
+
return uri unless is_dns_bucket?(bucket)
|
210
151
|
return uri if uri.host[/^#{bucket}\..+\.[^.]+\.[^.]+$/]
|
211
152
|
uri.host = "#{bucket}.#{uri.host}"
|
212
153
|
uri
|
@@ -221,7 +162,7 @@ module RightScale
|
|
221
162
|
# @return [Object]
|
222
163
|
#
|
223
164
|
def compute_body(body, content_type)
|
224
|
-
return body if body.
|
165
|
+
return body if body.nil?
|
225
166
|
# Make sure it is a String instance
|
226
167
|
return body unless body.is_a?(Hash)
|
227
168
|
Utils::contentify_body(body, content_type)
|
@@ -243,30 +184,10 @@ module RightScale
|
|
243
184
|
# 'The request signature we calculated does not match the signature you provided.
|
244
185
|
# Check your key and signing method.'
|
245
186
|
headers.set_if_blank('content-type', 'application/octet-stream')
|
246
|
-
headers.set_if_blank('date', Time::now.utc.httpdate)
|
247
|
-
headers['content-md5'] = Base64::encode64(Digest::MD5::digest(body)).strip if !body._blank?
|
248
|
-
headers['host'] = host
|
249
187
|
headers
|
250
188
|
end
|
251
189
|
|
252
190
|
|
253
|
-
# Computes signature
|
254
|
-
#
|
255
|
-
# @param [String] access_key
|
256
|
-
# @param [String] secret_key
|
257
|
-
# @param [String] verb
|
258
|
-
# @param [String] bucket
|
259
|
-
# @param [Hash] params
|
260
|
-
# @param [Hash] headers
|
261
|
-
#
|
262
|
-
# @return [String]
|
263
|
-
#
|
264
|
-
def compute_signature(access_key, secret_key, verb, bucket, object, params, headers)
|
265
|
-
can_path = compute_canonicalized_path(bucket, object, params)
|
266
|
-
Utils::AWS::sign_s3_signature(secret_key, verb, can_path, headers)
|
267
|
-
end
|
268
|
-
|
269
|
-
|
270
191
|
# Builds request path
|
271
192
|
#
|
272
193
|
# @param [String] bucket
|
@@ -275,16 +196,26 @@ module RightScale
|
|
275
196
|
#
|
276
197
|
# @return [String]
|
277
198
|
#
|
278
|
-
def compute_path(bucket, object
|
199
|
+
def compute_path(bucket, object)
|
279
200
|
data = []
|
280
|
-
data << bucket unless
|
201
|
+
data << bucket unless is_dns_bucket?(bucket)
|
281
202
|
data << object
|
282
|
-
data << params
|
283
203
|
Utils::join_urn(*data)
|
284
204
|
end
|
285
205
|
|
206
|
+
# Returns +true+ if DNS compatible buckets are enabled (default) and the
|
207
|
+
# given bucket is DNS compatible
|
208
|
+
#
|
209
|
+
# @param [String] bucket
|
210
|
+
# @return [Boolean]
|
211
|
+
#
|
212
|
+
def is_dns_bucket?(bucket)
|
213
|
+
return false if @data[:options][:cloud][:no_dns_buckets]
|
214
|
+
Utils::AWS::is_dns_bucket?(bucket)
|
215
|
+
end
|
216
|
+
|
286
217
|
end
|
287
218
|
end
|
288
219
|
end
|
289
220
|
end
|
290
|
-
end
|
221
|
+
end
|