jedlik 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1 @@
1
+ gemspec
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'rspec'
2
+
3
+ $:.unshift (File.join (File.dirname __FILE__), 'lib')
4
+ require 'jedlik'
5
+
6
+ RSpec.configure do |config|
7
+ config.color = true
8
+ config.formatter = :documentation
9
+ end
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: []