jeff 0.1.0

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.
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ pkg
@@ -0,0 +1,7 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.3
4
+ - jruby-18mode
5
+ - jruby-19mode
6
+ - rbx-18mode
7
+ - rbx-19mode
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'jruby-openssl', :platform => :jruby
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Hakan Ensari
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,25 @@
1
+ # Jeff
2
+
3
+ Jeff is a minimum-viable client for [Amazon Web Services (AWS) APIs][aws] that
4
+ support [Signature Version 2][sign].
5
+
6
+ ## Usage
7
+
8
+ ```ruby
9
+ client = Jeff.new 'http://some-aws-url.com/'
10
+ client << params
11
+ client << data
12
+
13
+ client.request
14
+ ```
15
+
16
+ Stream responses.
17
+
18
+ ```ruby
19
+ client << ->(chunk, remaining, total) { puts chunk }
20
+
21
+ client.request
22
+ ```
23
+
24
+ [aws]: http://aws.amazon.com/
25
+ [sign]: http://docs.amazonwebservices.com/general/latest/gr/signature-version-2.html
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc 'Run all specs in spec directory'
6
+ RSpec::Core::RakeTask.new(:spec) do |t|
7
+ t.pattern = 'spec/**/*_spec.rb'
8
+ end
9
+
10
+ task :default => [:spec]
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require File.expand_path('../lib/jeff/version.rb', __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.authors = ['Hakan Ensari']
7
+ gem.email = ['hakan.ensari@papercavalier.com']
8
+ gem.description = %q{A minimum-viable Amazon Web Services (AWS) client}
9
+ gem.summary = %q{An AWS client}
10
+ gem.homepage = 'https://github.com/hakanensari/jeff'
11
+
12
+ gem.files = `git ls-files`.split($\)
13
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
14
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
+ gem.name = 'jeff'
16
+ gem.require_paths = ['lib']
17
+ gem.version = Jeff::VERSION
18
+
19
+ gem.add_dependency 'excon', '~> 0.14'
20
+ gem.add_development_dependency 'rake', '~> 0.9'
21
+ gem.add_development_dependency 'rspec', '~> 2.10'
22
+ end
@@ -0,0 +1,19 @@
1
+ require 'base64'
2
+ require 'forwardable'
3
+ require 'time'
4
+
5
+ require 'excon'
6
+
7
+ require 'jeff/version'
8
+ require 'jeff/query_builder'
9
+ require 'jeff/user_agent'
10
+ require 'jeff/client'
11
+ require 'jeff/signature'
12
+
13
+ module Jeff
14
+ class << self
15
+ extend Forwardable
16
+
17
+ def_delegator Client, :new
18
+ end
19
+ end
@@ -0,0 +1,126 @@
1
+ module Jeff
2
+ # A minimum-viable Amazon Web Services (AWS) client.
3
+ class Client
4
+ include UserAgent
5
+
6
+ # Internal: Returns the String request body.
7
+ attr :body
8
+
9
+ # Internal: Returns the Proc chunked request body.
10
+ attr :chunker
11
+
12
+ # Gets/Sets the String AWS access key id.
13
+ attr_accessor :key
14
+
15
+ # Gets/Sets the String AWS secret key.
16
+ attr_accessor :secret
17
+
18
+ # Creates a new client.
19
+ #
20
+ # endpoint - A String AWS endpoint.
21
+ #
22
+ # Examples
23
+ #
24
+ # client = Jeff.new 'http://ecs.amazonaws.com/onca/xml'
25
+ #
26
+ def initialize(endpoint)
27
+ @endpoint = URI endpoint
28
+ @connection = Excon.new endpoint.to_s, :headers => {
29
+ 'User-Agent' => USER_AGENT
30
+ }
31
+
32
+ reset_request_attributes
33
+ end
34
+
35
+ # Updates the request attributes.
36
+ #
37
+ # data - A Hash of parameters or a String request body or a Proc that will
38
+ # deliver chunks of data.
39
+ #
40
+ # Returns self.
41
+ #
42
+ # Examples
43
+ #
44
+ # @client << {
45
+ # 'AssociateTag' => 'tag',
46
+ # 'Service' => 'AWSECommerceService',
47
+ # 'Version' => '2011-08-01'
48
+ # }
49
+ #
50
+ def <<(data)
51
+ case data
52
+ when Hash
53
+ @params.update data
54
+ when String
55
+ @body ||= '' << data
56
+ when Proc
57
+ @chunker = data
58
+ end
59
+
60
+ self
61
+ end
62
+
63
+ # Configures the client.
64
+ #
65
+ # Yields self.
66
+ #
67
+ # Examples
68
+ #
69
+ # client.configure do |c|
70
+ # c.key = 'key'
71
+ # c.secret = 'secret'
72
+ # end
73
+ #
74
+ def configure
75
+ yield self
76
+ end
77
+
78
+ # Returns the Hash request parameters, including required defaults.
79
+ def params
80
+ {
81
+ 'AWSAccessKeyId' => @key,
82
+ 'SignatureVersion' => '2',
83
+ 'SignatureMethod' => 'HmacSHA256',
84
+ 'Timestamp' => Time.now.utc.iso8601
85
+ }.merge @params
86
+ end
87
+
88
+ # Makes an HTTP request.
89
+ #
90
+ # Returns an Excon::Response.
91
+ def request(opts = {}, &blk)
92
+ opts.update :body => @body,
93
+ :method => action,
94
+ :query => query,
95
+ :request_block => @chunker
96
+
97
+ begin
98
+ @connection.request opts, &blk
99
+ ensure
100
+ reset_request_attributes
101
+ end
102
+ end
103
+
104
+ # Returns the String URL.
105
+ def url
106
+ [@endpoint, query].join '?'
107
+ end
108
+
109
+ private
110
+
111
+ def action
112
+ @body || @chunker ? :post : :get
113
+ end
114
+
115
+ def query
116
+ @query_builder ||= QueryBuilder.new @endpoint, @secret
117
+ @query_builder.build action, params
118
+ end
119
+
120
+ def reset_request_attributes
121
+ @body = nil
122
+ @chunker = nil
123
+ @params = {}
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,42 @@
1
+ module Jeff
2
+ class QueryBuilder
3
+ UNRESERVED = /([^\w.~-]+)/
4
+
5
+ def initialize(endpoint, secret)
6
+ @endpoint = endpoint
7
+ @secret = secret
8
+ end
9
+
10
+ def build(mth, params)
11
+ @mth = mth.to_s.upcase
12
+ @query = stringify params
13
+
14
+ "#{@query}&Signature=#{escape signature}"
15
+ end
16
+
17
+ private
18
+
19
+ def escape(val)
20
+ val.to_s.gsub(UNRESERVED) do
21
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
22
+ end
23
+ end
24
+
25
+ def signature
26
+ Signature.new @secret, string_to_sign
27
+ end
28
+
29
+ def string_to_sign
30
+ [
31
+ @mth,
32
+ @endpoint.host,
33
+ @endpoint.path,
34
+ @query
35
+ ].join "\n"
36
+ end
37
+
38
+ def stringify(hsh)
39
+ hsh.map { |k, v| "#{k}=#{ escape v }" }.sort.join '&'
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ module Jeff
2
+ class Signature
3
+ SHA256 = OpenSSL::Digest::SHA256.new
4
+
5
+ def initialize(secret, message)
6
+ @secret = secret
7
+ @message = message
8
+ end
9
+
10
+ def digest
11
+ OpenSSL::HMAC.digest SHA256, @secret, @message
12
+ end
13
+
14
+ def to_s
15
+ Base64.encode64(digest).chomp
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ module Jeff
2
+ module UserAgent
3
+ USER_AGENT = begin
4
+ hostname = `hostname`.chomp
5
+ engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
6
+ language = [engine, RUBY_VERSION, "p#{RUBY_PATCHLEVEL}"].join ' '
7
+
8
+ "Jeff/#{VERSION} (Language=#{language}; Host=#{hostname})"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Jeff
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,87 @@
1
+ require 'spec_helper'
2
+
3
+ module Jeff
4
+ describe Client do
5
+ before do
6
+ @endpoint = 'http://slowapi.com/delay/0'
7
+
8
+ @client = Client.new @endpoint
9
+ @client.configure do |config|
10
+ config.key = 'key'
11
+ config.secret = 'secret'
12
+ end
13
+ end
14
+
15
+ describe '#<<' do
16
+ it 'updates the request parameters' do
17
+ @client << { 'Foo' => 1 }
18
+ @client.params.should include 'Foo'
19
+ end
20
+
21
+ it 'updates the request body' do
22
+ @client << 'foo'
23
+ @client.body.should eql 'foo'
24
+ end
25
+
26
+ it 'updates the request chunked body' do
27
+ chunker = lambda {}
28
+ @client << chunker
29
+ @client.chunker.should eql chunker
30
+ end
31
+ end
32
+
33
+ describe '#params' do
34
+ subject { @client.params }
35
+
36
+ it 'includes a key' do
37
+ should include 'AWSAccessKeyId'
38
+ end
39
+
40
+ it 'includes a signature version' do
41
+ should include 'SignatureVersion'
42
+ end
43
+
44
+ it 'includes a signature method' do
45
+ should include 'SignatureMethod'
46
+ end
47
+
48
+ it 'includes a timestamp' do
49
+ should include 'Timestamp'
50
+ end
51
+ end
52
+
53
+ describe '#url' do
54
+ subject { @client.url }
55
+
56
+ it 'includes the endpoint' do
57
+ should include @endpoint
58
+ end
59
+
60
+ it 'sorts the parameters' do
61
+ @client << { 'Z' => 1, 'A' => 1 }
62
+ should match /\?A=[^&]+/
63
+ end
64
+
65
+ it 'is signed' do
66
+ should match /Signature=[^&]+$/
67
+ end
68
+ end
69
+
70
+ describe '#request' do
71
+ context 'given no body or chunker' do
72
+ it 'makes a GET request' do
73
+ Excon.stub({ :method => :get }, { :body => 'get' })
74
+ @client.request(:mock => true).body.should eql 'get'
75
+ end
76
+ end
77
+
78
+ context 'given a body' do
79
+ it 'makes a POST request' do
80
+ Excon.stub({ :method => :post }, { :body => 'post' })
81
+ @client << 'foo'
82
+ @client.request(:mock => true).body.should eql 'post'
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ module Jeff
4
+ describe UserAgent do
5
+ subject { UserAgent::USER_AGENT }
6
+
7
+ it 'describes the library' do
8
+ should match /Jeff\/[\d\w.]+\s/
9
+ end
10
+
11
+ it 'describes the interpreter' do
12
+ should match /Language=(?:j?ruby|rbx)/
13
+ end
14
+
15
+ it 'describes the host' do
16
+ should match /Host=[\w\d]+/
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe Jeff do
4
+ describe '.new' do
5
+ it 'delegates to Client' do
6
+ Jeff.new('http://foo').should be_a Jeff::Client
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ require 'rspec'
2
+ begin
3
+ require 'pry'
4
+ rescue LoadError
5
+ end
6
+
7
+ require 'jeff'
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jeff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Hakan Ensari
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: excon
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '0.14'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '0.14'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '0.9'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '0.9'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '2.10'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2.10'
62
+ description: A minimum-viable Amazon Web Services (AWS) client
63
+ email:
64
+ - hakan.ensari@papercavalier.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - .travis.yml
71
+ - Gemfile
72
+ - LICENSE
73
+ - README.md
74
+ - Rakefile
75
+ - jeff.gemspec
76
+ - lib/jeff.rb
77
+ - lib/jeff/client.rb
78
+ - lib/jeff/query_builder.rb
79
+ - lib/jeff/signature.rb
80
+ - lib/jeff/user_agent.rb
81
+ - lib/jeff/version.rb
82
+ - spec/jeff/client_spec.rb
83
+ - spec/jeff/user_agent_spec.rb
84
+ - spec/jeff_spec.rb
85
+ - spec/spec_helper.rb
86
+ homepage: https://github.com/hakanensari/jeff
87
+ licenses: []
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ! '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ segments:
99
+ - 0
100
+ hash: 2544026845906331060
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ! '>='
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ segments:
108
+ - 0
109
+ hash: 2544026845906331060
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 1.8.23
113
+ signing_key:
114
+ specification_version: 3
115
+ summary: An AWS client
116
+ test_files:
117
+ - spec/jeff/client_spec.rb
118
+ - spec/jeff/user_agent_spec.rb
119
+ - spec/jeff_spec.rb
120
+ - spec/spec_helper.rb