jeff 0.6.4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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