Skip to content
This repository was archived by the owner on Apr 26, 2023. It is now read-only.

Commit c5a5dba

Browse files
committed
Use aws4 gem to sign requests
1 parent c808afc commit c5a5dba

File tree

7 files changed

+72
-129
lines changed

7 files changed

+72
-129
lines changed

Gemfile

-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
source "https://rubygems.org"
2-
gem "rake"
32
gemspec

Rakefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
require 'bundler/gem_tasks'
22
require "rspec/core/rake_task"
33
RSpec::Core::RakeTask.new(:spec)
4-
task :default => :spec
4+
task :default => :spec

dynamodb.gemspec

+2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ Gem::Specification.new do |s|
1313
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
1414
s.homepage = 'http://github.com/groupme/dynamodb'
1515

16+
s.add_runtime_dependency 'aws4', '0.0.1'
1617
s.add_runtime_dependency 'multi_json', '1.3.7'
1718

19+
s.add_development_dependency 'rake'
1820
s.add_development_dependency 'rspec', '2.8.0'
1921
s.add_development_dependency 'webmock', '1.8.11'
2022
end

lib/dynamodb.rb

-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ class ClientError < BaseError; end
99
class ServerError < BaseError; end
1010
class AuthenticationError < BaseError; end
1111

12-
Credentials = Struct.new(:access_key_id, :secret_access_key)
13-
1412
require 'dynamodb/version'
1513
require 'dynamodb/connection'
1614
require 'dynamodb/http_handler'

lib/dynamodb/connection.rb

+21-13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require "aws4/signer"
2+
13
module DynamoDB
24
# Establishes a connection to Amazon DynamoDB using credentials.
35
class Connection
@@ -14,42 +16,48 @@ def http_handler=(new_http_handler)
1416
# Create a connection
1517
# uri: # default 'https://dynamodb.us-east-1.amazonaws.com/'
1618
# timeout: # default 5 seconds
17-
# api_version: # default
19+
# api_version: # default
1820
#
1921
def initialize(opts = {})
20-
if opts[:access_key_id] && opts[:secret_access_key]
21-
@credentials = DynamoDB::Credentials.new(opts[:access_key_id], opts[:secret_access_key])
22-
else
22+
if !(opts[:access_key_id] && opts[:secret_access_key])
2323
raise ArgumentError.new("access_key_id and secret_access_key are required")
2424
end
2525

26-
@uri = URI(opts[:uri] || "https://dynamodb.us-east-1.amazonaws.com/")
2726
set_timeout(opts[:timeout]) if opts[:timeout]
28-
27+
@uri = URI(opts[:uri] || "https://dynamodb.us-east-1.amazonaws.com/")
28+
region = @uri.host.split(".", 4)[1] || "us-east-1"
2929
@api_version = opts[:api_version] || "DynamoDB_20111205"
30+
@signer = AWS4::Signer.new(
31+
access_key: opts[:access_key_id],
32+
secret_key: opts[:secret_access_key],
33+
region: region
34+
)
3035
end
3136

3237
# Create and send a request to DynamoDB
3338
#
3439
# This returns either a SuccessResponse or a FailureResponse.
3540
#
36-
# `operation` can be any DynamoDB operation. `data` is a hash that will be
41+
# `operation` can be any DynamoDB operation. `body` is a hash that will be
3742
# used as the request body (in JSON format). More info available at:
3843
# http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide
3944
#
40-
def post(operation, data={})
45+
def post(operation, body={})
4146
request = DynamoDB::Request.new(
42-
uri: @uri,
43-
credentials: @credentials,
44-
api_version: @api_version,
45-
operation: operation,
46-
data: data
47+
signer: @signer,
48+
uri: @uri,
49+
operation: version(operation),
50+
body: body
4751
)
4852
http_handler.handle(request)
4953
end
5054

5155
private
5256

57+
def version(op)
58+
"#{@api_version}.#{op}"
59+
end
60+
5361
def http_handler
5462
self.class.http_handler
5563
end

lib/dynamodb/request.rb

+26-91
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,42 @@
1-
require "base64"
2-
require "openssl"
3-
41
module DynamoDB
52
class Request
6-
class << self
7-
def digest(signing_string, key)
8-
Base64.encode64(
9-
OpenSSL::HMAC.digest('sha256', key, Digest::SHA256.digest(signing_string))
10-
).strip
11-
end
12-
end
3+
RFC1123 = "%a, %d %b %Y %H:%M:%S GMT"
4+
ISO8601 = "%Y%m%dT%H%M%SZ"
135

14-
attr_reader :uri, :datetime, :credentials, :region, :data, :service, :operation, :api_version
6+
attr_reader :uri, :operation, :body, :signer
157

168
def initialize(args = {})
17-
@uri = args[:uri]
18-
@credentials = args[:credentials]
19-
@operation = args[:operation]
20-
@data = args[:data]
21-
@api_version = args[:api_version]
22-
@region = args[:region] || "us-east-1"
23-
@datetime = Time.now.utc.strftime("%Y%m%dT%H%M%SZ")
24-
@service = "dynamodb"
25-
end
26-
27-
def our_headers
28-
{
29-
"user-agent" => "groupme/dynamodb",
30-
"host" => uri.host,
31-
"content-type" => "application/x-amz-json-1.0",
32-
"content-length" => body.size,
33-
"x-amz-date" => datetime,
34-
"x-amz-target" => "#{api_version}.#{operation}",
35-
"x-amz-content-sha256" => hexdigest(body || '')
36-
}
9+
@uri = args[:uri]
10+
@operation = args[:operation]
11+
@body = MultiJson.dump(args[:body])
12+
@signer = args[:signer]
3713
end
3814

3915
def headers
40-
@headers ||= our_headers.merge("authorization" => authorization)
41-
end
42-
43-
def body
44-
@body ||= MultiJson.dump(data)
45-
end
46-
47-
def authorization
48-
parts = []
49-
parts << "AWS4-HMAC-SHA256 Credential=#{credentials.access_key_id}/#{credential_string}"
50-
parts << "SignedHeaders=#{our_headers.keys.sort.join(";")}"
51-
parts << "Signature=#{signature}"
52-
parts.join(', ')
53-
end
54-
55-
def signature
56-
k_secret = credentials.secret_access_key
57-
k_date = hmac("AWS4" + k_secret, datetime[0,8])
58-
k_region = hmac(k_date, region)
59-
k_service = hmac(k_region, service)
60-
k_credentials = hmac(k_service, 'aws4_request')
61-
hexhmac(k_credentials, string_to_sign)
62-
end
63-
64-
def string_to_sign
65-
parts = []
66-
parts << 'AWS4-HMAC-SHA256'
67-
parts << datetime
68-
parts << credential_string
69-
parts << hexdigest(canonical_request)
70-
parts.join("\n")
71-
end
72-
73-
def credential_string
74-
parts = []
75-
parts << datetime[0,8]
76-
parts << region
77-
parts << service
78-
parts << 'aws4_request'
79-
parts.join("/")
80-
end
81-
82-
def canonical_request
83-
parts = []
84-
parts << "POST"
85-
parts << uri.path
86-
parts << uri.query
87-
parts << our_headers.sort.map {|k, v| [k,v].join(':')}.join("\n") + "\n"
88-
parts << "content-length;content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target"
89-
parts << our_headers['x-amz-content-sha256']
90-
parts.join("\n")
16+
@headers ||= signed_headers
17+
end
18+
19+
private
20+
21+
def signed_headers
22+
date = Time.now.utc
23+
h = {
24+
"Date" => date.strftime(RFC1123),
25+
"User-Agent" => "groupme/dynamodb",
26+
"Host" => uri.host,
27+
"Content-Type" => "application/x-amz-json-1.0",
28+
"Content-Length" => body.size.to_s,
29+
"X-AMZ-Date" => date.strftime(ISO8601),
30+
"X-AMZ-Target" => operation,
31+
"X-AMZ-Content-SHA256" => hexdigest(body || '')
32+
}
33+
signer.sign("POST", uri, h, body)
9134
end
9235

93-
def hexdigest value
36+
def hexdigest(value)
9437
digest = Digest::SHA256.new
9538
digest.update(value)
9639
digest.hexdigest
9740
end
98-
99-
def hmac key, value
100-
OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha256'), key, value)
101-
end
102-
103-
def hexhmac key, value
104-
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha256'), key, value)
105-
end
10641
end
10742
end

spec/dynamodb/request_spec.rb

+22-21
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
11
require "spec_helper"
22

33
describe DynamoDB::Request do
4-
let(:uri) { URI("https://dynamodb.us-east-1.amazonaws.com/") }
5-
let(:credentials) { DynamoDB::Credentials.new("access_key_id", "secret_access_key") }
6-
let(:data) { {} }
7-
let(:request) {
8-
DynamoDB::Request.new(
9-
uri: uri,
10-
api_version: "DynamoDB_20111205",
11-
credentials: credentials,
12-
data: data,
13-
operation: "ListTables",
14-
)
15-
}
16-
174
it "signs the request" do
185
Time.stub(now: Time.parse("20130508T201304Z"))
196

7+
uri = URI("https://dynamodb.us-east-1.amazonaws.com/")
8+
signer = AWS4::Signer.new(
9+
access_key: "access_key_id",
10+
secret_key: "secret_access_key",
11+
region: "us-east-1"
12+
)
13+
request = DynamoDB::Request.new(
14+
uri: uri,
15+
signer: signer,
16+
body: {},
17+
operation: "DynamoDB_20111205.ListTables",
18+
)
19+
2020
request.headers.should == {
21-
"content-type"=>"application/x-amz-json-1.0",
22-
"x-amz-target"=>"DynamoDB_20111205.ListTables",
23-
"content-length"=>2,
24-
"user-agent"=>"groupme/dynamodb",
25-
"host"=>"dynamodb.us-east-1.amazonaws.com",
26-
"x-amz-date"=>"20130508T201304Z",
27-
"x-amz-content-sha256"=>"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
28-
"authorization"=>"AWS4-HMAC-SHA256 Credential=access_key_id/20130508/us-east-1/dynamodb/aws4_request, SignedHeaders=content-length;content-type;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target, Signature=52fab5c720b35ce247f8388c5c8f66b300074ae88b666985ee26ca70d923be1d"
21+
"Content-Type"=>"application/x-amz-json-1.0",
22+
"Content-Length"=>"2",
23+
"Date"=>"Wed, 08 May 2013 20:13:04 GMT",
24+
"User-Agent"=>"groupme/dynamodb",
25+
"Host"=>"dynamodb.us-east-1.amazonaws.com",
26+
"X-AMZ-Target"=>"DynamoDB_20111205.ListTables",
27+
"X-AMZ-Date"=>"20130508T201304Z",
28+
"X-AMZ-Content-SHA256"=>"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
29+
"Authorization"=>"AWS4-HMAC-SHA256 Credential=access_key_id/20130508/us-east-1/dynamodb/aws4_request, SignedHeaders=content-length;content-type;date;host;user-agent;x-amz-content-sha256;x-amz-date;x-amz-target, Signature=9e551627237673b4deaa2c22fd3b3777d6d0705facd35b56b289e357c4995c46"
2930
}
3031
end
3132
end

0 commit comments

Comments
 (0)