jeff 0.6.4 → 0.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 237d96394f884df5250ed73a02edd178581025bf
4
- data.tar.gz: 6da5c616420819d2fceee08136521521e84395ba
3
+ metadata.gz: 3f7158c532ec3dae4f1536ea70d1f3db88014dcd
4
+ data.tar.gz: 9c85321983f671dbde03310f437bb77e65169a99
5
5
  SHA512:
6
- metadata.gz: ffad5ced35b9a4c30e77497984be97501c167480b9aaae017c5191c44363bfc5b9367bde5df265a21172a6038f9f90a950f118ed32ef893dece199b93e12dd58
7
- data.tar.gz: c07df5ac211256101a75524e4324692ff54a389136b2d618822a48dd69835269916ca168efe6827938e1e9551b93cf1b7ddecd52d22b3b68b1479a770911f99d
6
+ metadata.gz: c25346ce0feb21e3d09e5ec1b155b8a6f3bcb0b3cdb5722d384a0ca1065a670f01d5793676ce674ba18425e63ec51bc87a64c0d46a8536176b0eba8bc044ec27
7
+ data.tar.gz: 993c1b39c3ce3ffc9b9ef200edb568b8603640d0e68c3d88f0ea63a60cd6033dcec285af8654b2db399a4f910d5e61a639d0880dfd3434e80e851ba8ac341123
data/Gemfile CHANGED
@@ -1,6 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
-
4
-
5
- gem 'rake'
6
- gem 'jruby-openssl', :platform => :jruby
3
+ gem 'jruby-openssl', platform: :jruby
data/README.md CHANGED
@@ -1,11 +1,29 @@
1
1
  # Jeff
2
2
 
3
- [Serially or in parallel, Monsieur Jeff will diligently sign your Amazon Web
4
- Services requests.][sign]
3
+ Jeff mixes in client behaviour for Amazon Web Services (AWS) which require
4
+ [Signature version 2 authentication][sig].
5
5
 
6
- :heart::heart::heart:
6
+ ![jeff][jef]
7
7
 
8
- ![jeff][jeff]
8
+ ## Usage
9
9
 
10
- [sign]: http://docs.amazonwebservices.com/general/latest/gr/signature-version-2.html
11
- [jeff]: http://f.cl.ly/items/0i1H1h071z1K0N3V2x06/bezos.png
10
+ A minimal example:
11
+
12
+ ```ruby
13
+ Request = Struct.new(:aws_access_key_id, :aws_secret_access_key) do
14
+ include Jeff
15
+
16
+ def aws_endpoint; 'https://mws.amazonservices.com/Products/2011-10-01'; end
17
+ end
18
+
19
+ req = Request.new('foo', 'bar')
20
+ res = req.get(query: { 'Action' => 'GetServiceStatus' })
21
+
22
+ puts res.body.match(/Status>([^<]+)/)[1]
23
+ ```
24
+
25
+ [Vacuum][vac] provides an example implementation.
26
+
27
+ [sig]: http://docs.amazonwebservices.com/general/latest/gr/signature-version-2.html
28
+ [vac]: https://github.com/hakanensari/vacuum
29
+ [jef]: http://f.cl.ly/items/0a3R3J0k1R2f423k1q2l/jeff.jpg
@@ -6,7 +6,7 @@ Gem::Specification.new do |gem|
6
6
  gem.authors = ['Hakan Ensari']
7
7
  gem.email = ['hakan.ensari@papercavalier.com']
8
8
  gem.description = %q{An Amazon Web Services client}
9
- gem.summary = %q{AWS client}
9
+ gem.summary = %q{An AWS client}
10
10
  gem.homepage = 'https://github.com/papercavalier/jeff'
11
11
 
12
12
  gem.files = `git ls-files`.split($\)
@@ -15,7 +15,9 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ['lib']
16
16
  gem.version = Jeff::VERSION
17
17
 
18
- gem.add_dependency 'excon', '~> 0.23.0'
18
+ gem.add_dependency 'excon', '~> 0.25.0'
19
+ gem.add_development_dependency 'minitest'
20
+ gem.add_development_dependency 'rake'
19
21
 
20
22
  gem.required_ruby_version = '>= 1.9'
21
23
  end
@@ -1,173 +1,152 @@
1
- require 'base64'
2
- require 'digest/md5'
1
+ # Our only external dependency. Excon is currently my preferred HTTP client in
2
+ # Ruby.
3
3
  require 'excon'
4
+
5
+ # Standard library dependencies.
6
+ require 'base64'
7
+ require 'openssl'
4
8
  require 'time'
5
9
 
6
- require 'jeff/secret'
7
10
  require 'jeff/version'
8
11
 
9
- # Mixes in Amazon Web Services (AWS) client behaviour.
12
+ # Jeff mixes in client behaviour for Amazon Web Services (AWS) that require
13
+ # Signature version 2 authentication.
14
+ #
15
+ # It's Jeff, as in Jeff Bezos.
10
16
  module Jeff
11
- MissingEndpoint = Class.new(ArgumentError)
12
- MissingKey = Class.new(ArgumentError)
13
- MissingSecret = Class.new(ArgumentError)
14
-
15
- UNRESERVED = /([^\w.~-]+)/
16
-
17
- # A User-Agent header that identifies the application, its version number,
18
- # and programming language.
19
- #
20
- # Amazon recommends to include one in requests to AWS endpoints.
21
- USER_AGENT = "Jeff/#{VERSION} (Language=Ruby; #{`hostname`.chomp})"
22
-
23
- def self.included(base)
24
- base.extend ClassMethods
25
-
26
- base.headers 'User-Agent' => USER_AGENT
27
-
28
- base.params 'AWSAccessKeyId' => -> { key },
29
- 'SignatureVersion' => '2',
30
- 'SignatureMethod' => 'HmacSHA256',
31
- 'Timestamp' => -> { Time.now.utc.iso8601 }
17
+ # Converts a query value to a sorted query string.
18
+ Query = Struct.new(:values) do
19
+ def to_s
20
+ values.sort.map { |k, v| "#{k}=#{ Utils.escape(v) }" }.join('&')
21
+ end
32
22
  end
33
23
 
34
- # Internal: Build a sorted query.
35
- #
36
- # hsh - A hash of query parameters specific to the request.
37
- #
38
- # Returns a query String.
39
- def build_query(hsh)
40
- params
41
- .merge(hsh)
42
- .sort
43
- .map { |k, v| "#{k}=#{ escape(v) }" }
44
- .join('&')
24
+ # Calculates an MD5sum for file being uploaded.
25
+ Content = Struct.new(:body) do
26
+ def md5
27
+ Base64.encode64(OpenSSL::Digest::MD5.digest(body)).strip
28
+ end
45
29
  end
46
30
 
47
- # Internal: Returns an Excon::Connection.
48
- def connection
49
- @connection ||= Excon.new(endpoint, headers: headers, expects: 200)
50
- end
31
+ # Signs an AWS request.
32
+ Request = Struct.new(:method, :host, :path, :query_string) do
33
+ def sign(aws_secret_access_key)
34
+ Signature.new(aws_secret_access_key).sign(string_to_sign)
35
+ end
51
36
 
52
- # Internal: Gets the String AWS endpoint.
53
- #
54
- # Raises a MissingEndpoint error if endpoint is missing.
55
- def endpoint
56
- @endpoint or raise MissingEndpoint
37
+ def string_to_sign
38
+ [method, host, path, query_string].join("\n")
39
+ end
57
40
  end
58
41
 
59
- # Sets the String AWS endpoint.
60
- attr_writer :endpoint
61
-
62
- # Internal: Returns the Hash default headers.
63
- def headers
64
- self.class.headers
65
- end
42
+ # Calculates an RFC 2104-compliant HMAC signature.
43
+ Signature = Struct.new(:secret) do
44
+ SHA256 = OpenSSL::Digest::SHA256.new
66
45
 
67
- # Internal: Gets the String AWS access key id.
68
- #
69
- # Raises a MissingKey error if key is missing.
70
- def key
71
- @key or raise MissingKey
46
+ def sign(message)
47
+ Base64.encode64(OpenSSL::HMAC.digest(SHA256, secret, message)).strip
48
+ end
72
49
  end
73
50
 
74
- # Sets the String AWS access key id.
75
- attr_writer :key
51
+ # Because Ruby's CGI escapes ~, we have to resort to writing our own escape.
52
+ module Utils
53
+ UNRESERVED = /([^\w.~-]+)/
76
54
 
77
- # Internal: Returns the Hash default request parameters.
78
- def params
79
- self.class.params.reduce({}) do |a, (k, v)|
80
- a.update k => (v.respond_to?(:call) ? instance_exec(&v) : v)
55
+ def self.escape(val)
56
+ val.to_s.gsub(UNRESERVED) do
57
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
58
+ end
81
59
  end
82
60
  end
83
61
 
84
- # Internal: Gets the Jeff::Secret.
62
+ # Amazon recommends to include a User-Agent header with every request to
63
+ # identify the application, its version number, programming language, and
64
+ # host.
85
65
  #
86
- # Raises a MissingSecret error if secret is missing.
87
- def secret
88
- @secret or raise MissingSecret
66
+ # If not happy, override.
67
+ USER_AGENT = "Jeff/#{VERSION} (Language=Ruby; #{`hostname`.chomp})"
68
+
69
+ def self.included(base)
70
+ base.extend(ClassMethods)
71
+
72
+ # Common parameters required by all AWS requests.
73
+ #
74
+ # Add other common parameters using `Jeff.params` if required in your
75
+ # implementation.
76
+ base.params(
77
+ 'AWSAccessKeyId' => -> { aws_access_key_id },
78
+ 'SignatureVersion' => '2',
79
+ 'SignatureMethod' => 'HmacSHA256',
80
+ 'Timestamp' => -> { Time.now.utc.iso8601 }
81
+ )
89
82
  end
90
83
 
91
- # Sets the AWS secret key.
92
- #
93
- # key - A String secret.
94
- #
95
- # Returns a Jeff::Secret.
96
- def secret=(key)
97
- @secret = Secret.new(key)
84
+ # An HTTP connection. It's reusable, which, as the author of Excon puts it,
85
+ # is more performant!
86
+ def connection
87
+ @connection ||= Excon.new(endpoint,
88
+ headers: { 'User-Agent' => USER_AGENT },
89
+ expects: 200,
90
+ omit_default_port: true
91
+ )
98
92
  end
99
93
 
94
+ # Accessors for required AWS attributes.
95
+ attr_accessor :aws_endpoint, :aws_access_key_id, :aws_secret_access_key
96
+
97
+ # We'll keep these around so we don't break dependent libraries.
98
+ alias endpoint aws_endpoint
99
+ alias endpoint= aws_endpoint=
100
+ alias key aws_access_key_id
101
+ alias key= aws_access_key_id=
102
+ alias secret aws_secret_access_key
103
+ alias secret= aws_secret_access_key=
104
+
100
105
  # Generate HTTP request verb methods.
101
106
  Excon::HTTP_VERBS.each do |method|
102
107
  eval <<-DEF
103
- def #{method}(opts = {})
104
- opts.update(:method => :#{method}, :omit_default_port => true)
105
- connection.request(build_options(opts))
108
+ def #{method}(options = {})
109
+ options.store(:method, :#{method})
110
+ connection.request(build_options(options))
106
111
  end
107
112
  DEF
108
113
  end
109
114
 
110
115
  private
111
116
 
112
- def build_options(opts)
113
- if opts[:body]
114
- opts[:headers] ||= {}
115
- opts[:headers].update('Content-MD5' => calculate_md5(opts[:body]))
117
+ def build_options(options)
118
+ # Add a Content-MD5 header if we're uploading a file.
119
+ if options.has_key?(:body)
120
+ md5 = Content.new(options[:body]).md5
121
+ (options[:headers] ||= {}).store('Content-MD5', md5)
116
122
  end
117
123
 
118
- sign(opts)
119
- end
120
-
121
- def calculate_md5(body)
122
- Base64.encode64(Digest::MD5.digest(body)).strip
123
- end
124
-
125
- def escape(val)
126
- val.to_s.gsub(UNRESERVED) do
127
- '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
128
- end
129
- end
130
-
131
- def sign(opts)
132
- query = build_query(opts[:query] || {})
133
-
134
- string_to_sign = [
135
- opts[:method].upcase,
136
- connection.data[:host],
137
- opts[:path] || connection.data[:path],
138
- query
139
- ].join("\n")
140
- signature = secret.sign(string_to_sign)
141
-
142
- opts.update(query: [
143
- query,
144
- "Signature=#{escape(signature)}"
145
- ].join('&'))
124
+ # Build the query string.
125
+ values = self.class.params
126
+ .reduce({}) { |a, (k, v)|
127
+ a.update(k => (v.respond_to?(:call) ? instance_exec(&v) : v))
128
+ }
129
+ .merge(options.fetch(:query, {}))
130
+ query_string = Query.new(values).to_s
131
+
132
+ # Generate a signature.
133
+ signature = Request
134
+ .new(
135
+ options[:method].upcase,
136
+ connection.data[:host],
137
+ options[:path] || connection.data[:path],
138
+ query_string
139
+ )
140
+ .sign(aws_secret_access_key)
141
+
142
+ # Return options after appending an escaped signature to query.
143
+ options.update(query: "#{query_string}&Signature=#{Utils.escape(signature)}")
146
144
  end
147
145
 
148
146
  module ClassMethods
149
- # Gets/Updates the default headers.
150
- #
151
- # hsh - A Hash of headers.
152
- #
153
- # Returns the Hash headers.
154
- def headers(hsh = nil)
155
- @headers ||= {}
156
- @headers.update(hsh) if hsh
157
-
158
- @headers
159
- end
160
-
161
- # Gets/Updates the default request parameters.
162
- #
163
- # hsh - A Hash of parameters (default: nil).
164
- #
165
- # Returns the Hash parameters.
166
- def params(hsh = nil)
167
- @params ||= {}
168
- @params.update(hsh) if hsh
169
-
170
- @params
147
+ # Gets and optionally updates the default request parameters.
148
+ def params(hsh = {})
149
+ (@params ||= {}).update(hsh)
171
150
  end
172
151
  end
173
152
  end
@@ -1,3 +1,3 @@
1
1
  module Jeff
2
- VERSION = '0.6.4'
2
+ VERSION = '0.7.0'
3
3
  end
@@ -11,18 +11,6 @@ describe Jeff do
11
11
  end
12
12
  end
13
13
 
14
- it 'has a User-Agent request header' do
15
- assert @klass.headers.has_key?('User-Agent')
16
- end
17
-
18
- it 'configures request headers' do
19
- @klass.instance_eval do
20
- headers 'Foo' => 'bar'
21
- end
22
-
23
- assert @klass.headers.has_key?('Foo')
24
- end
25
-
26
14
  it 'has the required request query parameters' do
27
15
  %w(AWSAccessKeyId SignatureMethod SignatureVersion Timestamp)
28
16
  .each { |key| assert @klass.params.has_key?(key) }
@@ -32,48 +20,28 @@ describe Jeff do
32
20
  @klass.instance_eval do
33
21
  params 'Foo' => 'bar'
34
22
  end
35
-
36
23
  assert @klass.params.has_key?('Foo')
37
24
  end
38
25
 
39
- it 'requires an endpoint' do
40
- proc { @klass.new.endpoint }.must_raise Jeff::MissingEndpoint
41
- end
42
-
43
- it 'requires a key' do
44
- proc { @klass.new.key }.must_raise Jeff::MissingKey
45
- end
46
-
47
- it 'requires a secret' do
48
- proc { @klass.new.secret }.must_raise Jeff::MissingSecret
49
- end
50
-
51
26
  it 'sorts the request query parameters of the client lexicographically' do
52
- client = @klass.new
53
- client.key = 'foo'
54
- query = client.build_query 'A10' => 1, 'A1' => 1
55
-
56
- query.must_match(/^A1=1&A10=.*Timestamp/)
27
+ query = Jeff::Query.new('A10' => 1, 'A1' => 1)
28
+ query.to_s.must_equal('A1=1&A10=1')
57
29
  end
58
30
 
59
- it 'sets the request headers of the client connection' do
31
+ it 'sets a User-Agent header for the client connection' do
60
32
  client = @klass.new
61
33
  client.endpoint = 'http://example.com/'
62
-
63
- client.connection.data[:headers].must_equal @klass.headers
34
+ client.connection.data[:headers]['User-Agent'].wont_be_nil
64
35
  end
65
36
 
66
37
  Excon::HTTP_VERBS.each do |method|
67
38
  it "makes a #{method.upcase} request" do
68
39
  Excon.stub({ }, { status: 200 })
69
-
70
40
  client = @klass.new
71
41
  client.endpoint = 'http://example.com/'
72
42
  client.key = 'foo'
73
43
  client.secret = 'bar'
74
-
75
44
  client.send(method).status.must_equal 200
76
-
77
45
  Excon.stubs.clear
78
46
  end
79
47
  end
@@ -82,14 +50,11 @@ describe Jeff do
82
50
  Excon.stub({ }) do |params|
83
51
  { body: params[:headers]['Content-MD5'] }
84
52
  end
85
-
86
53
  client = @klass.new
87
54
  client.endpoint = 'http://example.com/'
88
55
  client.key = 'foo'
89
56
  client.secret = 'bar'
90
-
91
57
  client.post(body: 'foo').body.wont_be_empty
92
-
93
58
  Excon.stubs.clear
94
59
  end
95
60
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jeff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.4
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hakan Ensari
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-06-11 00:00:00.000000000 Z
11
+ date: 2013-08-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: excon
@@ -16,14 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - ~>
18
18
  - !ruby/object:Gem::Version
19
- version: 0.23.0
19
+ version: 0.25.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ~>
25
25
  - !ruby/object:Gem::Version
26
- version: 0.23.0
26
+ version: 0.25.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  description: An Amazon Web Services client
28
56
  email:
29
57
  - hakan.ensari@papercavalier.com
@@ -39,7 +67,6 @@ files:
39
67
  - Rakefile
40
68
  - jeff.gemspec
41
69
  - lib/jeff.rb
42
- - lib/jeff/secret.rb
43
70
  - lib/jeff/version.rb
44
71
  - spec/jeff_spec.rb
45
72
  homepage: https://github.com/papercavalier/jeff
@@ -61,9 +88,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
88
  version: '0'
62
89
  requirements: []
63
90
  rubyforge_project:
64
- rubygems_version: 2.0.2
91
+ rubygems_version: 2.0.5
65
92
  signing_key:
66
93
  specification_version: 4
67
- summary: AWS client
94
+ summary: An AWS client
68
95
  test_files:
69
96
  - spec/jeff_spec.rb
@@ -1,17 +0,0 @@
1
- require 'base64'
2
- require 'openssl'
3
-
4
- module Jeff
5
- class Secret
6
- SHA256 = OpenSSL::Digest::SHA256.new
7
-
8
- def initialize(key)
9
- @key = key
10
- end
11
-
12
- def sign(message)
13
- digest = OpenSSL::HMAC.digest(SHA256, @key, message)
14
- Base64.encode64(digest).chomp
15
- end
16
- end
17
- end