elasticity 5.0.1 → 5.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/HISTORY.md +5 -0
- data/lib/elasticity.rb +6 -1
- data/lib/elasticity/aws_request_v2.rb +42 -0
- data/lib/elasticity/aws_request_v4.rb +98 -0
- data/lib/elasticity/aws_session.rb +72 -0
- data/lib/elasticity/aws_utils.rb +65 -0
- data/lib/elasticity/emr.rb +1 -1
- data/lib/elasticity/version.rb +1 -1
- data/spec/lib/elasticity/aws_request_v2_spec.rb +38 -0
- data/spec/lib/elasticity/aws_request_v4_spec.rb +80 -0
- data/spec/lib/elasticity/aws_session_spec.rb +191 -0
- data/spec/lib/elasticity/aws_utils_spec.rb +105 -0
- data/spec/lib/elasticity/emr_spec.rb +26 -26
- metadata +14 -5
- data/lib/elasticity/aws_request.rb +0 -135
- data/spec/lib/elasticity/aws_request_spec.rb +0 -274
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd9e39257ac81bf6cb0a18f38d680bc0e6bdc5f1
|
4
|
+
data.tar.gz: d6f3dd0970a81a164e548e52707b29419f3ce107
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b47d9a75474f3afb40a34c9f0f7bd846fa250e0e15551b3106cd92cd804a710e8c818ac2770b65f9c57e5760d985c5a641db668094dcb9c57f0c2a694866634e
|
7
|
+
data.tar.gz: c6d00853cf474fb771bde33fdb50828ec9923d9918d059b06409fdd6ab7709184bf71db78943df23df1b30a7710504b574ccb3687b0cdf7bbf3d6c961752a8e1
|
data/HISTORY.md
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
## 5.0.2 - April 27, 2015
|
2
|
+
|
3
|
+
- Fix for issue [#83](https://github.com/rslifka/elasticity/issues/83), `elasticity` has now transitioned to the AWS [Signature Version 4 Signing Process](http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html).
|
4
|
+
- Removed the ability to create insecure (HTTP) connections to the EMR endpoints.
|
5
|
+
|
1
6
|
## 5.0.1 - April 12, 2015
|
2
7
|
|
3
8
|
- Bear with me here :) Backmerged into 4.0.4 to add [IAM Service Role support](http://docs.aws.amazon.com/ElasticMapReduce/latest/DeveloperGuide/emr-iam-roles-creatingroles.html) per @alexanderdean. As part of the forward merge, bumping the version to trigger an update.
|
data/lib/elasticity.rb
CHANGED
@@ -5,7 +5,12 @@ require 'rest_client'
|
|
5
5
|
require 'nokogiri'
|
6
6
|
require 'fog'
|
7
7
|
|
8
|
-
require 'elasticity/
|
8
|
+
require 'elasticity/version'
|
9
|
+
|
10
|
+
require 'elasticity/aws_utils'
|
11
|
+
require 'elasticity/aws_session'
|
12
|
+
require 'elasticity/aws_request_v2'
|
13
|
+
require 'elasticity/aws_request_v4'
|
9
14
|
require 'elasticity/emr'
|
10
15
|
|
11
16
|
require 'elasticity/sync_to_s3'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Elasticity
|
2
|
+
|
3
|
+
class AwsRequestV2
|
4
|
+
|
5
|
+
def initialize(aws_session, ruby_service_hash)
|
6
|
+
@aws_session = aws_session
|
7
|
+
@ruby_service_hash = ruby_service_hash
|
8
|
+
end
|
9
|
+
|
10
|
+
def url
|
11
|
+
"https://elasticmapreduce.#{@aws_session.region}.amazonaws.com"
|
12
|
+
end
|
13
|
+
|
14
|
+
def headers
|
15
|
+
{
|
16
|
+
:content_type => 'application/x-www-form-urlencoded; charset=utf-8'
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
# (Used from RightScale's right_aws gem.)
|
21
|
+
# EC2, SQS, SDB and EMR requests must be signed by this guy.
|
22
|
+
# See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
|
23
|
+
# http://developer.amazonwebservices.com/connect/entry.jspa?externalID=1928
|
24
|
+
def payload
|
25
|
+
service_hash = AwsUtils.convert_ruby_to_aws(@ruby_service_hash)
|
26
|
+
service_hash.merge!({
|
27
|
+
'AWSAccessKeyId' => @aws_session.access_key,
|
28
|
+
'Timestamp' => Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.000Z'),
|
29
|
+
'SignatureVersion' => '2',
|
30
|
+
'SignatureMethod' => 'HmacSHA256'
|
31
|
+
})
|
32
|
+
canonical_string = service_hash.keys.sort.map do |key|
|
33
|
+
"#{AwsUtils.aws_escape(key)}=#{AwsUtils.aws_escape(service_hash[key])}"
|
34
|
+
end.join('&')
|
35
|
+
string_to_sign = "POST\n#{@aws_session.host.downcase}\n/\n#{canonical_string}"
|
36
|
+
signature = AwsUtils.aws_escape(Base64.encode64(OpenSSL::HMAC.digest('sha256', @aws_session.secret_key, string_to_sign)).strip)
|
37
|
+
"#{canonical_string}&Signature=#{signature}"
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Elasticity
|
2
|
+
|
3
|
+
# To help ensure correctness, Amazon has provided a step-by-step guide of
|
4
|
+
# query-and-response conversations for various types of API calls.
|
5
|
+
#
|
6
|
+
# http://docs.aws.amazon.com/general/latest/gr/signature-v4-test-suite.html
|
7
|
+
#
|
8
|
+
# We are working with POSTs only, where the body of the POST contains the
|
9
|
+
# service details, so the 'post-x-www-form-urlencoded-parameters' suite is
|
10
|
+
# the most applicable.
|
11
|
+
class AwsRequestV4
|
12
|
+
|
13
|
+
SERVICE_NAME = 'elasticmapreduce'
|
14
|
+
|
15
|
+
def initialize(aws_session, ruby_service_hash)
|
16
|
+
@aws_session = aws_session
|
17
|
+
|
18
|
+
@ruby_service_hash = ruby_service_hash
|
19
|
+
@operation = @ruby_service_hash[:operation]
|
20
|
+
@ruby_service_hash.delete(:operation)
|
21
|
+
|
22
|
+
@timestamp = Time.now.utc
|
23
|
+
end
|
24
|
+
|
25
|
+
def headers
|
26
|
+
{
|
27
|
+
'Authorization' => "AWS4-HMAC-SHA256 Credential=#{@aws_session.access_key}/#{credential_scope}, SignedHeaders=content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target, Signature=#{aws_v4_signature}",
|
28
|
+
'Content-Type' => 'application/x-amz-json-1.1',
|
29
|
+
'Host' => host,
|
30
|
+
'User-Agent' => "elasticity/#{Elasticity::VERSION}",
|
31
|
+
'X-Amz-Content-SHA256' => Digest::SHA256.hexdigest(payload),
|
32
|
+
'X-Amz-Date' => @timestamp.strftime('%Y%m%dT%H%M%SZ'),
|
33
|
+
'X-Amz-Target' => "ElasticMapReduce.#{@operation}",
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def url
|
38
|
+
"https://#{host}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def payload
|
42
|
+
AwsUtils.convert_ruby_to_aws_v4(@ruby_service_hash).to_json
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def host
|
48
|
+
"elasticmapreduce.#{@aws_session.region}.amazonaws.com"
|
49
|
+
end
|
50
|
+
|
51
|
+
def credential_scope
|
52
|
+
"#{@timestamp.strftime('%Y%m%d')}/#{@aws_session.region}/#{SERVICE_NAME}/aws4_request"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Task 1: Create a Canonical Request For Signature Version 4
|
56
|
+
# http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
57
|
+
def canonical_request
|
58
|
+
[
|
59
|
+
'POST',
|
60
|
+
'/',
|
61
|
+
'',
|
62
|
+
'content-type:application/x-amz-json-1.1',
|
63
|
+
"host:#{host}",
|
64
|
+
"user-agent:elasticity/#{Elasticity::VERSION}",
|
65
|
+
"x-amz-content-sha256:#{Digest::SHA256.hexdigest(payload)}",
|
66
|
+
"x-amz-date:#{@timestamp.strftime('%Y%m%dT%H%M%SZ')}",
|
67
|
+
"x-amz-target:ElasticMapReduce.#{@operation}",
|
68
|
+
'',
|
69
|
+
'content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target',
|
70
|
+
Digest::SHA256.hexdigest(payload)
|
71
|
+
].join("\n")
|
72
|
+
end
|
73
|
+
|
74
|
+
# Task 2: Create a String to Sign for Signature Version 4
|
75
|
+
# http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
76
|
+
def string_to_sign
|
77
|
+
[
|
78
|
+
'AWS4-HMAC-SHA256',
|
79
|
+
@timestamp.strftime('%Y%m%dT%H%M%SZ'),
|
80
|
+
credential_scope,
|
81
|
+
Digest::SHA256.hexdigest(canonical_request)
|
82
|
+
].join("\n")
|
83
|
+
end
|
84
|
+
|
85
|
+
# Task 3: Calculate the AWS Signature Version 4
|
86
|
+
# http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
|
87
|
+
def aws_v4_signature
|
88
|
+
date = OpenSSL::HMAC.digest('sha256', 'AWS4' + @aws_session.secret_key, @timestamp.strftime('%Y%m%d'))
|
89
|
+
region = OpenSSL::HMAC.digest('sha256', date, @aws_session.region)
|
90
|
+
service = OpenSSL::HMAC.digest('sha256', region, SERVICE_NAME)
|
91
|
+
signing_key = OpenSSL::HMAC.digest('sha256', service, 'aws4_request')
|
92
|
+
|
93
|
+
OpenSSL::HMAC.hexdigest('sha256', signing_key, string_to_sign)
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Elasticity
|
2
|
+
|
3
|
+
class MissingKeyError < StandardError;
|
4
|
+
end
|
5
|
+
class MissingRegionError < StandardError;
|
6
|
+
end
|
7
|
+
|
8
|
+
class AwsSession
|
9
|
+
|
10
|
+
attr_reader :access_key
|
11
|
+
attr_reader :secret_key
|
12
|
+
attr_reader :host
|
13
|
+
attr_reader :region
|
14
|
+
|
15
|
+
# Supported values for options:
|
16
|
+
# :region - AWS region (e.g. us-west-1)
|
17
|
+
# :secure - true or false, default true.
|
18
|
+
def initialize(access=nil, secret=nil, options={})
|
19
|
+
# There is a cryptic error if this isn't set
|
20
|
+
if options.has_key?(:region) && options[:region] == nil
|
21
|
+
raise MissingRegionError, 'A valid :region is required to connect to EMR'
|
22
|
+
end
|
23
|
+
options[:region] = 'us-east-1' unless options[:region]
|
24
|
+
@region = options[:region]
|
25
|
+
|
26
|
+
@access_key = get_access_key(access)
|
27
|
+
@secret_key = get_secret_key(secret)
|
28
|
+
@host = "elasticmapreduce.#@region.amazonaws.com"
|
29
|
+
end
|
30
|
+
|
31
|
+
def submit(ruby_service_hash)
|
32
|
+
aws_request = AwsRequestV4.new(self, ruby_service_hash)
|
33
|
+
begin
|
34
|
+
RestClient.post(aws_request.url, aws_request.payload, aws_request.headers)
|
35
|
+
rescue RestClient::BadRequest => e
|
36
|
+
raise ArgumentError, AwsSession.parse_error_response(e.http_body)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def ==(other)
|
41
|
+
return false unless other.is_a? AwsSession
|
42
|
+
return false unless @access_key == other.access_key
|
43
|
+
return false unless @secret_key == other.secret_key
|
44
|
+
return false unless @host == other.host
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def get_access_key(access)
|
51
|
+
return access if access
|
52
|
+
return ENV['AWS_ACCESS_KEY_ID'] if ENV['AWS_ACCESS_KEY_ID']
|
53
|
+
raise MissingKeyError, 'Please provide an access key or set AWS_ACCESS_KEY_ID.'
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_secret_key(secret)
|
57
|
+
return secret if secret
|
58
|
+
return ENV['AWS_SECRET_ACCESS_KEY'] if ENV['AWS_SECRET_ACCESS_KEY']
|
59
|
+
raise MissingKeyError, 'Please provide a secret key or set AWS_SECRET_ACCESS_KEY.'
|
60
|
+
end
|
61
|
+
|
62
|
+
# AWS error responses all follow the same form. Extract the message from
|
63
|
+
# the error document.
|
64
|
+
def self.parse_error_response(error_xml)
|
65
|
+
xml_doc = Nokogiri::XML(error_xml)
|
66
|
+
xml_doc.remove_namespaces!
|
67
|
+
xml_doc.xpath('/ErrorResponse/Error/Message').text
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Elasticity
|
2
|
+
|
3
|
+
class AwsUtils
|
4
|
+
|
5
|
+
# Escape a string according to Amazon's rules.
|
6
|
+
# See: http://docs.amazonwebservices.com/AmazonSimpleDB/2007-11-07/DeveloperGuide/index.html?REST_RESTAuth.html
|
7
|
+
def self.aws_escape(param)
|
8
|
+
param.to_s.gsub(/([^a-zA-Z0-9._~-]+)/n) do
|
9
|
+
'%' + $1.unpack('H2' * $1.size).join('%').upcase
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# With the advent of v4 signing, we can skip the complex translation from v2
|
14
|
+
# and ship the JSON over with nearly the same structure.
|
15
|
+
def self.convert_ruby_to_aws_v4(value)
|
16
|
+
case value
|
17
|
+
when Array
|
18
|
+
return value.map{|v| convert_ruby_to_aws_v4(v)}
|
19
|
+
when Hash
|
20
|
+
result = {}
|
21
|
+
value.each do |k,v|
|
22
|
+
result[camelize(k.to_s)] = convert_ruby_to_aws_v4(v)
|
23
|
+
end
|
24
|
+
return result
|
25
|
+
else
|
26
|
+
return value
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Since we use the same structure as AWS, we can generate AWS param names
|
31
|
+
# from the Ruby versions of those names (and the param nesting).
|
32
|
+
def self.convert_ruby_to_aws(params)
|
33
|
+
result = {}
|
34
|
+
params.each do |key, value|
|
35
|
+
case value
|
36
|
+
when Array
|
37
|
+
prefix = "#{camelize(key.to_s)}.member"
|
38
|
+
value.each_with_index do |item, index|
|
39
|
+
if item.is_a?(String)
|
40
|
+
result["#{prefix}.#{index+1}"] = item
|
41
|
+
else
|
42
|
+
convert_ruby_to_aws(item).each do |nested_key, nested_value|
|
43
|
+
result["#{prefix}.#{index+1}.#{nested_key}"] = nested_value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
when Hash
|
48
|
+
prefix = "#{camelize(key.to_s)}"
|
49
|
+
convert_ruby_to_aws(value).each do |nested_key, nested_value|
|
50
|
+
result["#{prefix}.#{nested_key}"] = nested_value
|
51
|
+
end
|
52
|
+
else
|
53
|
+
result[camelize(key.to_s)] = value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
result
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.camelize(word)
|
60
|
+
word.to_s.gsub(/\/(.?)/) { '::' + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
data/lib/elasticity/emr.rb
CHANGED
@@ -5,7 +5,7 @@ module Elasticity
|
|
5
5
|
attr_reader :aws_request
|
6
6
|
|
7
7
|
def initialize(aws_access_key_id=nil, aws_secret_access_key=nil, options = {})
|
8
|
-
@aws_request = Elasticity::
|
8
|
+
@aws_request = Elasticity::AwsSession.new(aws_access_key_id, aws_secret_access_key, options)
|
9
9
|
end
|
10
10
|
|
11
11
|
# Describe a specific jobflow.
|
data/lib/elasticity/version.rb
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
describe Elasticity::AwsRequestV2 do
|
2
|
+
|
3
|
+
before do
|
4
|
+
Timecop.freeze(Time.at(1302461096))
|
5
|
+
end
|
6
|
+
|
7
|
+
after do
|
8
|
+
Timecop.return
|
9
|
+
end
|
10
|
+
|
11
|
+
subject do
|
12
|
+
Elasticity::AwsRequestV2.new(
|
13
|
+
Elasticity::AwsSession.new('access', 'secret'),
|
14
|
+
{:operation => 'RunJobFlow', :name => 'Elasticity Job Flow'}
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#url' do
|
19
|
+
it 'should construct a proper endpoint' do
|
20
|
+
subject.url.should == 'https://elasticmapreduce.us-east-1.amazonaws.com'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#headers' do
|
25
|
+
it 'should create the proper headers' do
|
26
|
+
subject.headers.should == {
|
27
|
+
:content_type => 'application/x-www-form-urlencoded; charset=utf-8'
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#payload' do
|
33
|
+
it 'should payload up the place' do
|
34
|
+
subject.payload.should == 'AWSAccessKeyId=access&Name=Elasticity%20Job%20Flow&Operation=RunJobFlow&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2011-04-10T18%3A44%3A56.000Z&Signature=5x6YilYHOjgM%2F6nalIOf62txOKoLFGBYyIivoHb%2F27k%3D'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
describe Elasticity::AwsRequestV4 do
|
2
|
+
|
3
|
+
before do
|
4
|
+
Timecop.freeze(Time.at(1315611360))
|
5
|
+
end
|
6
|
+
|
7
|
+
after do
|
8
|
+
Timecop.return
|
9
|
+
end
|
10
|
+
|
11
|
+
subject do
|
12
|
+
Elasticity::AwsRequestV4.new(
|
13
|
+
Elasticity::AwsSession.new('access', 'secret'),
|
14
|
+
{:operation => 'DescribeJobFlows', :job_flow_ids => ['TEST_JOBFLOW_ID']}
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#url' do
|
19
|
+
it 'should construct a proper endpoint' do
|
20
|
+
subject.url.should == 'https://elasticmapreduce.us-east-1.amazonaws.com'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#headers' do
|
25
|
+
it 'should create the proper headers' do
|
26
|
+
subject.headers.should == {
|
27
|
+
'Authorization' => "AWS4-HMAC-SHA256 Credential=access/20110909/us-east-1/elasticmapreduce/aws4_request, SignedHeaders=content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target, Signature=#{subject.send(:aws_v4_signature)}",
|
28
|
+
'Content-Type' => 'application/x-amz-json-1.1',
|
29
|
+
'Host' => 'elasticmapreduce.us-east-1.amazonaws.com',
|
30
|
+
'User-Agent' => "elasticity/#{Elasticity::VERSION}",
|
31
|
+
'X-Amz-Content-SHA256' => Digest::SHA256.hexdigest(subject.payload),
|
32
|
+
'X-Amz-Date' => '20110909T233600Z',
|
33
|
+
'X-Amz-Target' => 'ElasticMapReduce.DescribeJobFlows',
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#payload' do
|
39
|
+
it 'should create the proper payload' do
|
40
|
+
subject.payload.should == '{"JobFlowIds":["TEST_JOBFLOW_ID"]}'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '.canonical_request' do
|
45
|
+
it 'should create the proper canonical request' do
|
46
|
+
subject.send(:canonical_request).should == [
|
47
|
+
'POST',
|
48
|
+
'/',
|
49
|
+
'',
|
50
|
+
'content-type:application/x-amz-json-1.1',
|
51
|
+
'host:elasticmapreduce.us-east-1.amazonaws.com',
|
52
|
+
"user-agent:elasticity/#{Elasticity::VERSION}",
|
53
|
+
"x-amz-content-sha256:#{Digest::SHA256.hexdigest(subject.payload)}",
|
54
|
+
'x-amz-date:20110909T233600Z',
|
55
|
+
'x-amz-target:ElasticMapReduce.DescribeJobFlows',
|
56
|
+
'',
|
57
|
+
'content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target',
|
58
|
+
"#{Digest::SHA256.hexdigest(subject.payload)}"
|
59
|
+
].join("\n")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '.string_to_sign' do
|
64
|
+
it 'should create the proper string to sign' do
|
65
|
+
subject.send(:string_to_sign).should == [
|
66
|
+
'AWS4-HMAC-SHA256',
|
67
|
+
'20110909T233600Z',
|
68
|
+
'20110909/us-east-1/elasticmapreduce/aws4_request',
|
69
|
+
"#{Digest::SHA256.hexdigest(subject.send(:canonical_request))}"
|
70
|
+
].join("\n")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '.aws_v4_signature' do
|
75
|
+
it 'should create the proper signature' do
|
76
|
+
subject.send(:aws_v4_signature).should == '3e88b95410e6828f80b4ec476bcf7e23ab8dd380b22ffcb1d5f7e86390346f68'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|