jedlik 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|