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 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: []