jedlik 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -0
- data/README.mkd +25 -0
- data/lib/jedlik.rb +16 -0
- data/lib/jedlik/connection.rb +75 -0
- data/lib/jedlik/security_token_service.rb +96 -0
- data/lib/jedlik/typhoeus/request.rb +30 -0
- data/spec/jedlik/security_token_service_spec.rb +65 -0
- data/spec/spec_helper.rb +9 -0
- metadata +85 -0
data/Gemfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
gemspec
|
data/README.mkd
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Jedlik
|
2
|
+
======
|
3
|
+
|
4
|
+
Communicate with *Amazon DynamoDB* in Ruby. Raw access to the full API without
|
5
|
+
having to handle temporary credentials or HTTP requests by yourself.
|
6
|
+
|
7
|
+
Does not require the AWS gem.
|
8
|
+
Requires **Typhoeus**.
|
9
|
+
|
10
|
+
Usage
|
11
|
+
-----
|
12
|
+
|
13
|
+
Jedlik maps the DynamoDB API closely. Once the connection object is ready, all
|
14
|
+
requests are done through `#post`. The first argument is the name of the
|
15
|
+
operation, the second is a hash that will be converted to JSON and used as the
|
16
|
+
request body.
|
17
|
+
|
18
|
+
require 'jedlik'
|
19
|
+
|
20
|
+
conn = Jedlik::Connection.new 'DG7I54_KEY_ID', 'wr31PP+hu5d76+SECRET_KEY'
|
21
|
+
|
22
|
+
conn.post :ListTables # => {"TableNames"=>["table1", "table2"]}
|
23
|
+
|
24
|
+
conn.post :GetItem, {:TableName => "table1", :Key => {:S => "foo"}}
|
25
|
+
# => Hash
|
data/lib/jedlik.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'typhoeus'
|
2
|
+
require 'time'
|
3
|
+
require 'base64'
|
4
|
+
require 'openssl'
|
5
|
+
require 'cgi'
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
module Jedlik
|
9
|
+
class ClientError < Exception; end
|
10
|
+
class ServerError < Exception; end
|
11
|
+
|
12
|
+
require 'jedlik/typhoeus/request'
|
13
|
+
|
14
|
+
require 'jedlik/security_token_service'
|
15
|
+
require 'jedlik/connection'
|
16
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Jedlik
|
2
|
+
|
3
|
+
# Establishes a connection to Amazon DynamoDB using credentials.
|
4
|
+
class Connection
|
5
|
+
attr_reader :sts
|
6
|
+
|
7
|
+
DEFAULTS = {
|
8
|
+
:endpoint => 'dynamodb.us-east-1.amazonaws.com',
|
9
|
+
}
|
10
|
+
|
11
|
+
# Acceptable `opts` keys are:
|
12
|
+
#
|
13
|
+
# :endpoint # DynamoDB endpoint to use.
|
14
|
+
# # Default: 'dynamodb.us-east-1.amazonaws.com'
|
15
|
+
#
|
16
|
+
def initialize access_key_id, secret_access_key, opts={}
|
17
|
+
opts = DEFAULTS.merge opts
|
18
|
+
@sts = SecurityTokenService.new access_key_id, secret_access_key
|
19
|
+
@endpoint = opts[:endpoint]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create and send a request to DynamoDB.
|
23
|
+
# Returns a hash extracted from the response body.
|
24
|
+
#
|
25
|
+
# `operation` can be any DynamoDB operation. `data` is a hash that will be
|
26
|
+
# used as the request body (in JSON format). More info available at:
|
27
|
+
#
|
28
|
+
# http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide
|
29
|
+
#
|
30
|
+
def post operation, data={}
|
31
|
+
request = new_request operation, data.to_json
|
32
|
+
request.sign sts
|
33
|
+
hydra.queue request; hydra.run
|
34
|
+
response = request.response
|
35
|
+
|
36
|
+
if status_ok? response
|
37
|
+
JSON.parse response.body
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def hydra
|
44
|
+
Typhoeus::Hydra.hydra
|
45
|
+
end
|
46
|
+
|
47
|
+
def new_request operation, body
|
48
|
+
(Typhoeus::Request.new "https://#{@endpoint}/",
|
49
|
+
:method => :post,
|
50
|
+
:headers => {
|
51
|
+
'host' => @endpoint,
|
52
|
+
'content-type' => "application/x-amz-json-1.0",
|
53
|
+
'x-amz-date' => (Time.now.utc.strftime "%a, %d %b %Y %H:%M:%S GMT"),
|
54
|
+
'x-amz-security-token' => sts.session_token,
|
55
|
+
'x-amz-target' => "DynamoDB_20111205.#{operation}",
|
56
|
+
},
|
57
|
+
:body => body)
|
58
|
+
end
|
59
|
+
|
60
|
+
def status_ok? response
|
61
|
+
case response.code
|
62
|
+
when 200
|
63
|
+
true
|
64
|
+
when 400..499
|
65
|
+
js = JSON.parse response.body
|
66
|
+
(raise ClientError,
|
67
|
+
"#{js['__type'].match(/#(.+)\Z/)[1]}: #{js["message"]}")
|
68
|
+
when 500..599
|
69
|
+
raise ServerError
|
70
|
+
else
|
71
|
+
false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Jedlik
|
2
|
+
|
3
|
+
# SecurityTokenService automatically manages the creation and renewal of
|
4
|
+
# temporary AWS credentials.
|
5
|
+
#
|
6
|
+
# Usage:
|
7
|
+
#
|
8
|
+
# credentials = SecurityTokenService.new "id", "secret key"
|
9
|
+
# credentials.access_key_id # => String
|
10
|
+
# credentials.secret_access_key # => String
|
11
|
+
# credentials.session_token # => String
|
12
|
+
#
|
13
|
+
class SecurityTokenService
|
14
|
+
|
15
|
+
# A SecurityTokenService is initialized for a single AWS user using his
|
16
|
+
# credentials.
|
17
|
+
def initialize access_key_id, secret_access_key
|
18
|
+
@_access_key_id = access_key_id
|
19
|
+
@_secret_access_key = secret_access_key
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get a temporary access key id from STS or from cache.
|
23
|
+
def access_key_id
|
24
|
+
obtain_credentials
|
25
|
+
@access_key_id
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get a temporary secret access key from STS or from cache.
|
29
|
+
def secret_access_key
|
30
|
+
obtain_credentials
|
31
|
+
@secret_access_key
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get a temporary session token from STS or from cache.
|
35
|
+
def session_token
|
36
|
+
obtain_credentials
|
37
|
+
@session_token
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Extract the contents of a given tag.
|
43
|
+
def get_tag tag, string
|
44
|
+
# Considering that the XML string received from STS is sane and always
|
45
|
+
# has the same simple structure, I think a simple regular expression
|
46
|
+
# can do the job (with the benefit of not adding a dependency on
|
47
|
+
# another library just for ONE method). I will switch to Nokogiri if
|
48
|
+
# needed.
|
49
|
+
string.match(/#{tag.to_s}>([^<]*)/)[1]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Obtain temporary credentials, set to expire after 1 hour. If
|
53
|
+
# credentials were previously obtained, no request is made until they
|
54
|
+
# expire.
|
55
|
+
def obtain_credentials
|
56
|
+
if (not @expiration) or (@expiration <= Time.now.utc)
|
57
|
+
body = (Typhoeus::Request.get request_uri).body
|
58
|
+
|
59
|
+
@session_token = (get_tag :SessionToken, body)
|
60
|
+
@secret_access_key = (get_tag :SecretAccessKey, body)
|
61
|
+
@expiration = (Time.parse (get_tag :Expiration, body))
|
62
|
+
@access_key_id = (get_tag :AccessKeyId, body)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Generate the params to be sent to STS.
|
67
|
+
def request_params
|
68
|
+
{
|
69
|
+
:AWSAccessKeyId => @_access_key_id,
|
70
|
+
:Action => 'GetSessionToken',
|
71
|
+
:DurationSeconds => '3600',
|
72
|
+
:SignatureMethod => 'HmacSHA256',
|
73
|
+
:SignatureVersion => '2',
|
74
|
+
:Timestamp => Time.now.utc.iso8601,
|
75
|
+
:Version => '2011-06-15',
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
# Generate the URI that should be requested.
|
80
|
+
def request_uri
|
81
|
+
qs = (request_params).map do |key, val|
|
82
|
+
[(CGI.escape key.to_s), (CGI.escape val)].join '='
|
83
|
+
end.join '&'
|
84
|
+
|
85
|
+
"https://sts.amazonaws.com/?#{qs}&Signature=" +
|
86
|
+
(CGI.escape (sign "GET\nsts.amazonaws.com\n/\n#{qs}"))
|
87
|
+
end
|
88
|
+
|
89
|
+
# Sign (HMAC-SHA256) a string using the secret key given at
|
90
|
+
# initialization.
|
91
|
+
def sign string
|
92
|
+
(Base64.encode64 (OpenSSL::HMAC.digest 'sha256',
|
93
|
+
@_secret_access_key, string)).chomp
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
class Request
|
3
|
+
def sign sts
|
4
|
+
headers.merge!({'x-amzn-authorization' => [
|
5
|
+
"AWS3 AWSAccessKeyId=#{sts.access_key_id}",
|
6
|
+
"Algorithm=HmacSHA256",
|
7
|
+
"Signature=#{digest sts.secret_access_key}"
|
8
|
+
].join(',')})
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def digest secret_key
|
14
|
+
(Base64.encode64 (OpenSSL::HMAC.digest 'sha256',
|
15
|
+
secret_key, (Digest::SHA256.digest string_to_sign))).chomp
|
16
|
+
end
|
17
|
+
|
18
|
+
def string_to_sign
|
19
|
+
["POST\n/\n\nhost:#{@parsed_uri.host}", amz_to_sts, body].join "\n"
|
20
|
+
end
|
21
|
+
|
22
|
+
def amz_to_sts
|
23
|
+
get_amz_headers.sort.map{|key, val| ([key, val].join ':') + "\n"}.join
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_amz_headers
|
27
|
+
headers.select{|key, val| key =~ /\Ax-amz-/}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
VALID_RESPONSE_BODY = "<GetSessionTokenResponse " +
|
4
|
+
"xmlns=\"https://sts.amazonaws.com/doc/2011-06-15/\">
|
5
|
+
<GetSessionTokenResult>
|
6
|
+
<Credentials>
|
7
|
+
<SessionToken>SESSION_TOKEN</SessionToken>
|
8
|
+
<SecretAccessKey>SECRET_ACCESS_KEY</SecretAccessKey>
|
9
|
+
<Expiration>2036-03-19T01:03:22.276Z</Expiration>
|
10
|
+
<AccessKeyId>ACCESS_KEY_ID</AccessKeyId>
|
11
|
+
</Credentials>
|
12
|
+
</GetSessionTokenResult>
|
13
|
+
<ResponseMetadata>
|
14
|
+
<RequestId>f0fa5827-7156-11e1-8f1e-a92b58fdc66e</RequestId>
|
15
|
+
</ResponseMetadata>
|
16
|
+
</GetSessionTokenResponse>
|
17
|
+
"
|
18
|
+
|
19
|
+
module Jedlik
|
20
|
+
describe SecurityTokenService do
|
21
|
+
let(:sts){SecurityTokenService.new "access_key_id", "secret_access_key"}
|
22
|
+
let(:response){(Typhoeus::Response.new body: VALID_RESPONSE_BODY)}
|
23
|
+
|
24
|
+
before{Typhoeus::Request.stub(:get).and_return response}
|
25
|
+
|
26
|
+
shared_examples_for 'cached' do |method|
|
27
|
+
it 'sends a request to Amazon STS at first call' do
|
28
|
+
Typhoeus::Request.should_receive(:get).and_return response
|
29
|
+
sts.send method
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'signs the request'
|
33
|
+
|
34
|
+
it 'caches its value' do
|
35
|
+
Typhoeus::Request.should_receive(:get).and_return response
|
36
|
+
sts.send method
|
37
|
+
sts.send method
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'access_key_id' do
|
42
|
+
it_behaves_like 'cached', :access_key_id
|
43
|
+
|
44
|
+
it 'returns a value' do
|
45
|
+
sts.access_key_id.should == "ACCESS_KEY_ID"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'secret_access_key' do
|
50
|
+
it_behaves_like 'cached', :secret_access_key
|
51
|
+
|
52
|
+
it 'returns a value' do
|
53
|
+
sts.secret_access_key.should == "SECRET_ACCESS_KEY"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe 'session_token' do
|
58
|
+
it_behaves_like 'cached', :session_token
|
59
|
+
|
60
|
+
it 'returns a value' do
|
61
|
+
sts.session_token.should == "SESSION_TOKEN"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jedlik
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Hashmal
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-23 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: typhoeus
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Communicate with Amazon DynamoDB. Raw access to the full API without
|
47
|
+
having to handle temporary credentials or HTTP requests by yourself.
|
48
|
+
email: jeremypinat@gmail.com
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- lib/jedlik/connection.rb
|
54
|
+
- lib/jedlik/security_token_service.rb
|
55
|
+
- lib/jedlik/typhoeus/request.rb
|
56
|
+
- lib/jedlik.rb
|
57
|
+
- Gemfile
|
58
|
+
- README.mkd
|
59
|
+
- spec/jedlik/security_token_service_spec.rb
|
60
|
+
- spec/spec_helper.rb
|
61
|
+
homepage: http://github.com/hashmal/jedlik
|
62
|
+
licenses: []
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubyforge_project:
|
81
|
+
rubygems_version: 1.8.19
|
82
|
+
signing_key:
|
83
|
+
specification_version: 3
|
84
|
+
summary: Communicate with Amazon DynamoDB.
|
85
|
+
test_files: []
|