jeff 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -2,36 +2,19 @@
2
2
 
3
3
  [![travis][stat]][trav]
4
4
 
5
- **Jeff** adds [authentication][sign] behaviour for some [Amazon Web Services (AWS)][aws].
5
+ **Jeff** mixes in [authentication][sign] and other client behaviour for some
6
+ [Amazon Web Services (AWS)][aws].
6
7
 
7
8
  ![jeff][jeff]
8
9
 
9
10
  ## Usage
10
11
 
11
- Mix in.
12
-
13
12
  ```ruby
14
- class Client
13
+ class Service
15
14
  include Jeff
16
15
  end
17
16
  ```
18
17
 
19
- Set endpoint and credentials.
20
-
21
- ```ruby
22
- client = Client.new
23
-
24
- client.endpoint = 'http://example.com/path'
25
- client.key = 'key'
26
- client.secret = 'secret'
27
- ```
28
-
29
- Request.
30
-
31
- ```ruby
32
- client.get query: { 'Foo' => 'Bar' }
33
- ```
34
-
35
18
  [stat]: https://secure.travis-ci.org/papercavalier/jeff.png
36
19
  [trav]: http://travis-ci.org/papercavalier/jeff
37
20
  [aws]: http://aws.amazon.com/
@@ -1,167 +1,7 @@
1
- require 'excon'
2
- require 'time'
1
+ require 'jeff/serviceable'
3
2
 
4
- require 'jeff/secret'
5
- require 'jeff/version'
6
-
7
- # Jeff is a light-weight module that mixes in client behaviour for Amazon Web
8
- # Services (AWS).
9
3
  module Jeff
10
- USER_AGENT = "Jeff/#{VERSION} (Language=Ruby; #{`hostname`.chomp})"
11
-
12
- MissingEndpoint = Class.new ArgumentError
13
- MissingKey = Class.new ArgumentError
14
- MissingSecret = Class.new ArgumentError
15
-
16
- UNRESERVED = /([^\w.~-]+)/
17
-
18
4
  def self.included(base)
19
- base.extend ClassMethods
20
-
21
- base.headers 'User-Agent' => USER_AGENT
22
-
23
- base.params 'AWSAccessKeyId' => -> { key },
24
- 'SignatureVersion' => '2',
25
- 'SignatureMethod' => 'HmacSHA256',
26
- 'Timestamp' => -> { Time.now.utc.iso8601 }
27
- end
28
-
29
- # Internal: Builds a sorted query.
30
- #
31
- # hsh - A hash of query parameters specific to the request.
32
- #
33
- # Returns a query String.
34
- def build_query(hsh)
35
- params
36
- .merge(hsh)
37
- .sort
38
- .map { |k, v| "#{k}=#{ escape v }" }
39
- .join '&'
40
- end
41
-
42
- # Internal: Returns an Excon::Connection.
43
- def connection
44
- @connection ||= Excon.new endpoint, headers: headers, expects: 200
45
- end
46
-
47
- # Internal: Gets the String AWS endpoint.
48
- #
49
- # Raises a MissingEndpoint error if endpoint is missing.
50
- def endpoint
51
- @endpoint or raise MissingEndpoint
52
- end
53
-
54
- # Sets the String AWS endpoint.
55
- attr_writer :endpoint
56
-
57
- # Internal: Returns the Hash default headers.
58
- def headers
59
- self.class.headers
60
- end
61
-
62
- # Internal: Gets the String AWS access key id.
63
- #
64
- # Raises a MissingKey error if key is missing.
65
- def key
66
- @key or raise MissingKey
67
- end
68
-
69
- # Sets the String AWS access key id.
70
- attr_writer :key
71
-
72
- # Internal: Returns the Hash default request parameters.
73
- def params
74
- self.class.params.reduce({}) do |a, (k, v)|
75
- a.update k => (v.respond_to?(:call) ? instance_exec(&v) : v)
76
- end
77
- end
78
-
79
- # Internal: Gets the Jeff::Secret.
80
- #
81
- # Raises a MissingSecret error if secret is missing.
82
- def secret
83
- @secret or raise MissingSecret
84
- end
85
-
86
- # Sets the AWS secret key.
87
- #
88
- # key - A String secret.
89
- #
90
- # Returns a Jeff::Secret.
91
- def secret=(key)
92
- @secret = Secret.new key
93
- end
94
-
95
- # Generate HTTP request verb methods that sign queries and return response
96
- # bodies as IO objects.
97
- Excon::HTTP_VERBS.each do |method|
98
- eval <<-DEF
99
- def #{method}(opts = {})
100
- opts.update method: :#{method}
101
- connection.request sign opts
102
- end
103
- DEF
104
- end
105
-
106
- private
107
-
108
- def sign(opts)
109
- query = build_query opts[:query] || {}
110
-
111
- string_to_sign = [
112
- opts[:method].upcase,
113
- connection_host,
114
- opts[:path] || connection_path,
115
- query
116
- ].join "\n"
117
- signature = secret.sign string_to_sign
118
-
119
- opts.update query: [
120
- query,
121
- "Signature=#{escape signature}"
122
- ].join('&')
123
- end
124
-
125
- def connection_host
126
- [
127
- connection.connection[:host],
128
- connection.connection[:port]
129
- ].join ':'
130
- end
131
-
132
- def connection_path
133
- connection.connection[:path]
134
- end
135
-
136
- def escape(val)
137
- val.to_s.gsub(UNRESERVED) do
138
- '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
139
- end
140
- end
141
-
142
- module ClassMethods
143
- # Gets/Updates the default headers.
144
- #
145
- # hsh - A Hash of headers.
146
- #
147
- # Returns the Hash headers.
148
- def headers(hsh = nil)
149
- @headers ||= {}
150
- @headers.update hsh if hsh
151
-
152
- @headers
153
- end
154
-
155
- # Gets/Updates the default request parameters.
156
- #
157
- # hsh - A Hash of parameters (default: nil).
158
- #
159
- # Returns the Hash parameters.
160
- def params(hsh = nil)
161
- @params ||= {}
162
- @params.update hsh if hsh
163
-
164
- @params
165
- end
5
+ base.send :include, Serviceable
166
6
  end
167
7
  end
@@ -1,4 +1,5 @@
1
1
  require 'base64'
2
+ require 'openssl'
2
3
 
3
4
  module Jeff
4
5
  class Secret
@@ -0,0 +1,184 @@
1
+ require 'base64'
2
+ require 'digest/md5'
3
+ require 'excon'
4
+ require 'time'
5
+
6
+ require 'jeff/secret'
7
+ require 'jeff/version'
8
+
9
+ module Jeff
10
+ # Mixes in Amazon Web Services (AWS) client behaviour.
11
+ module Serviceable
12
+ MissingEndpoint = Class.new ArgumentError
13
+ MissingKey = Class.new ArgumentError
14
+ MissingSecret = Class.new ArgumentError
15
+
16
+ UNRESERVED = /([^\w.~-]+)/
17
+
18
+ # A User-Agent header that identifies the application, its version number,
19
+ # and programming language.
20
+ #
21
+ # Amazon recommends to include one in requests to AWS endpoints.
22
+ USER_AGENT = "Jeff/#{VERSION} (Language=Ruby; #{`hostname`.chomp})"
23
+
24
+ def self.included(base)
25
+ base.extend ClassMethods
26
+
27
+ base.headers 'User-Agent' => USER_AGENT
28
+
29
+ base.params 'AWSAccessKeyId' => -> { key },
30
+ 'SignatureVersion' => '2',
31
+ 'SignatureMethod' => 'HmacSHA256',
32
+ 'Timestamp' => -> { Time.now.utc.iso8601 }
33
+ end
34
+
35
+ # Internal: Builds a sorted query.
36
+ #
37
+ # hsh - A hash of query parameters specific to the request.
38
+ #
39
+ # Returns a query String.
40
+ def build_query(hsh)
41
+ params
42
+ .merge(hsh)
43
+ .sort
44
+ .map { |k, v| "#{k}=#{ escape v }" }
45
+ .join '&'
46
+ end
47
+
48
+ # Internal: Returns an Excon::Connection.
49
+ def connection
50
+ @connection ||= Excon.new endpoint,
51
+ headers: headers,
52
+ expects: 200
53
+ end
54
+
55
+ # Internal: Gets the String AWS endpoint.
56
+ #
57
+ # Raises a MissingEndpoint error if endpoint is missing.
58
+ def endpoint
59
+ @endpoint or raise MissingEndpoint
60
+ end
61
+
62
+ # Sets the String AWS endpoint.
63
+ attr_writer :endpoint
64
+
65
+ # Internal: Returns the Hash default headers.
66
+ def headers
67
+ self.class.headers
68
+ end
69
+
70
+ # Internal: Gets the String AWS access key id.
71
+ #
72
+ # Raises a MissingKey error if key is missing.
73
+ def key
74
+ @key or raise MissingKey
75
+ end
76
+
77
+ # Sets the String AWS access key id.
78
+ attr_writer :key
79
+
80
+ # Internal: Returns the Hash default request parameters.
81
+ def params
82
+ self.class.params.reduce({}) do |a, (k, v)|
83
+ a.update k => (v.respond_to?(:call) ? instance_exec(&v) : v)
84
+ end
85
+ end
86
+
87
+ # Internal: Gets the Jeff::Secret.
88
+ #
89
+ # Raises a MissingSecret error if secret is missing.
90
+ def secret
91
+ @secret or raise MissingSecret
92
+ end
93
+
94
+ # Sets the AWS secret key.
95
+ #
96
+ # key - A String secret.
97
+ #
98
+ # Returns a Jeff::Secret.
99
+ def secret=(key)
100
+ @secret = Secret.new key
101
+ end
102
+
103
+ # Generate HTTP request verb methods.
104
+ Excon::HTTP_VERBS.each do |method|
105
+ eval <<-DEF
106
+ def #{method}(opts = {})
107
+ opts.update method: :#{method}
108
+ if opts[:body]
109
+ opts[:headers] ||= {}
110
+ opts[:headers].update 'Content-MD5' => calculate_md5(opts[:body])
111
+ end
112
+
113
+ connection.request sign opts
114
+ end
115
+ DEF
116
+ end
117
+
118
+ private
119
+
120
+ def calculate_md5(body)
121
+ Base64.encode64(Digest::MD5.digest(body)).strip
122
+ end
123
+
124
+ def connection_host
125
+ [
126
+ connection.connection[:host],
127
+ connection.connection[:port]
128
+ ].join ':'
129
+ end
130
+
131
+ def connection_path
132
+ connection.connection[:path]
133
+ end
134
+
135
+ def escape(val)
136
+ val.to_s.gsub(UNRESERVED) do
137
+ '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
138
+ end
139
+ end
140
+
141
+ def sign(opts)
142
+ query = build_query opts[:query] || {}
143
+
144
+ string_to_sign = [
145
+ opts[:method].upcase,
146
+ connection_host,
147
+ opts[:path] || connection_path,
148
+ query
149
+ ].join "\n"
150
+ signature = secret.sign string_to_sign
151
+
152
+ opts.update query: [
153
+ query,
154
+ "Signature=#{escape signature}"
155
+ ].join('&')
156
+ end
157
+
158
+ module ClassMethods
159
+ # Gets/Updates the default headers.
160
+ #
161
+ # hsh - A Hash of headers.
162
+ #
163
+ # Returns the Hash headers.
164
+ def headers(hsh = nil)
165
+ @headers ||= {}
166
+ @headers.update hsh if hsh
167
+
168
+ @headers
169
+ end
170
+
171
+ # Gets/Updates the default request parameters.
172
+ #
173
+ # hsh - A Hash of parameters (default: nil).
174
+ #
175
+ # Returns the Hash parameters.
176
+ def params(hsh = nil)
177
+ @params ||= {}
178
+ @params.update hsh if hsh
179
+
180
+ @params
181
+ end
182
+ end
183
+ end
184
+ end
@@ -1,3 +1,3 @@
1
1
  module Jeff
2
- VERSION = '0.4.2'
2
+ VERSION = '0.4.3'
3
3
  end
@@ -0,0 +1,169 @@
1
+ require 'spec_helper'
2
+
3
+ module Jeff
4
+ describe Serviceable do
5
+ let(:klass) { Class.new { include Jeff } }
6
+ let(:client) { klass.new }
7
+
8
+ describe '.headers' do
9
+ subject { klass.headers }
10
+
11
+ it { should have_key 'User-Agent' }
12
+
13
+ it 'should be configurable' do
14
+ klass.instance_eval do
15
+ headers 'Foo' => 'bar'
16
+ end
17
+
18
+ should have_key 'Foo'
19
+ end
20
+ end
21
+
22
+ describe '.params' do
23
+ subject { klass.params }
24
+
25
+ it { should have_key 'AWSAccessKeyId' }
26
+
27
+ it { should have_key 'SignatureMethod' }
28
+
29
+ it { should have_key 'SignatureVersion' }
30
+
31
+ it { should have_key 'Timestamp' }
32
+
33
+ it 'should be configurable' do
34
+ klass.instance_eval do
35
+ params 'Foo' => 'bar'
36
+ end
37
+
38
+ should have_key 'Foo'
39
+ end
40
+ end
41
+
42
+ describe '#endpoint' do
43
+ it 'should require a value' do
44
+ expect { client.endpoint }.to raise_error Serviceable::MissingEndpoint
45
+ end
46
+ end
47
+
48
+ describe '#key' do
49
+ it 'should require a value' do
50
+ expect { client.key }.to raise_error Serviceable::MissingKey
51
+ end
52
+ end
53
+
54
+ describe '#secret' do
55
+ it 'should require a value' do
56
+ expect { client.secret }.to raise_error Serviceable::MissingSecret
57
+ end
58
+ end
59
+
60
+ context 'given a key' do
61
+ before do
62
+ client.key = 'key'
63
+ end
64
+
65
+ describe '#params' do
66
+ subject { client.params }
67
+
68
+ it 'should include the key' do
69
+ subject['AWSAccessKeyId'].should eql client.key
70
+ end
71
+
72
+ it 'should generate a timestamp' do
73
+ subject['Timestamp'].should be_a String
74
+ end
75
+ end
76
+
77
+ describe '#build_query' do
78
+ subject { client.build_query 'A10' => 1, 'A1' => 1 }
79
+
80
+ it 'should include default parameters' do
81
+ should match(/Timestamp/)
82
+ end
83
+
84
+ it 'should sort lexicographically' do
85
+ should match(/^A1=1&A10=/)
86
+ end
87
+ end
88
+ end
89
+
90
+ context 'given an endpoint' do
91
+ before do
92
+ client.endpoint = 'http://slowapi.com/delay/0'
93
+ end
94
+
95
+ describe "#connection" do
96
+ subject { client.connection }
97
+ let(:headers) { subject.connection[:headers] }
98
+
99
+ it { should be_an Excon::Connection }
100
+
101
+ it 'should set default headers' do
102
+ headers.should eq klass.headers
103
+ end
104
+
105
+ it 'should cache itself' do
106
+ subject.should be client.connection
107
+ end
108
+ end
109
+ end
110
+
111
+ context 'given an endpoint, key, and secret' do
112
+ before do
113
+ client.endpoint = 'http://slowapi.com/delay/0'
114
+ client.key = 'key'
115
+ client.secret = 'secret'
116
+ end
117
+
118
+ Excon::HTTP_VERBS.each do |method|
119
+ describe "##{method}" do
120
+ subject { client.send(method, mock: true).body }
121
+
122
+ before do
123
+ Excon.stub({ method: method.to_sym }) do
124
+ { body: method, status: 200 }
125
+ end
126
+ end
127
+
128
+ after { Excon.stubs.clear }
129
+
130
+ it "should make a #{method.upcase} request" do
131
+ should eql method
132
+ end
133
+ end
134
+ end
135
+
136
+ context 'given a request body' do
137
+ subject { client.get(mock: true, body: 'foo').body }
138
+
139
+ before do
140
+ Excon.stub({ method: :get }) do |params|
141
+ { status: 200, body: params[:headers]['Content-MD5'] }
142
+ end
143
+ end
144
+
145
+ after { Excon.stubs.clear }
146
+
147
+ it "should add an Content-MD5 header" do
148
+ should_not be_empty
149
+ end
150
+ end
151
+
152
+ context 'given an HTTP status error' do
153
+ before do
154
+ Excon.stub({ method: :get }) do
155
+ { status: 503 }
156
+ end
157
+ end
158
+
159
+ after { Excon.stubs.clear }
160
+
161
+ it "should raise an error" do
162
+ expect {
163
+ client.get mock: true
164
+ }.to raise_error Excon::Errors::HTTPStatusError
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jeff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-29 00:00:00.000000000 Z
12
+ date: 2012-09-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: excon
@@ -75,9 +75,10 @@ files:
75
75
  - jeff.gemspec
76
76
  - lib/jeff.rb
77
77
  - lib/jeff/secret.rb
78
+ - lib/jeff/serviceable.rb
78
79
  - lib/jeff/version.rb
79
80
  - spec/jeff/secret_spec.rb
80
- - spec/jeff_spec.rb
81
+ - spec/jeff/serviceable_spec.rb
81
82
  - spec/spec_helper.rb
82
83
  homepage: https://github.com/papercavalier/jeff
83
84
  licenses: []
@@ -105,6 +106,6 @@ specification_version: 3
105
106
  summary: AWS client
106
107
  test_files:
107
108
  - spec/jeff/secret_spec.rb
108
- - spec/jeff_spec.rb
109
+ - spec/jeff/serviceable_spec.rb
109
110
  - spec/spec_helper.rb
110
111
  has_rdoc:
@@ -1,153 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Jeff do
4
- let(:klass) { Class.new { include Jeff } }
5
- let(:client) { klass.new }
6
-
7
- describe '.headers' do
8
- subject { klass.headers }
9
-
10
- it { should have_key 'User-Agent' }
11
-
12
- it 'should be configurable' do
13
- klass.instance_eval do
14
- headers 'Foo' => 'bar'
15
- end
16
-
17
- should have_key 'Foo'
18
- end
19
- end
20
-
21
- describe '.params' do
22
- subject { klass.params }
23
-
24
- it { should have_key 'AWSAccessKeyId' }
25
-
26
- it { should have_key 'SignatureMethod' }
27
-
28
- it { should have_key 'SignatureVersion' }
29
-
30
- it { should have_key 'Timestamp' }
31
-
32
- it 'should be configurable' do
33
- klass.instance_eval do
34
- params 'Foo' => 'bar'
35
- end
36
-
37
- should have_key 'Foo'
38
- end
39
- end
40
-
41
- describe '#endpoint' do
42
- it 'should require a value' do
43
- expect { client.endpoint }.to raise_error Jeff::MissingEndpoint
44
- end
45
- end
46
-
47
- describe '#key' do
48
- it 'should require a value' do
49
- expect { client.key }.to raise_error Jeff::MissingKey
50
- end
51
- end
52
-
53
- describe '#secret' do
54
- it 'should require a value' do
55
- expect { client.secret }.to raise_error Jeff::MissingSecret
56
- end
57
- end
58
-
59
- context 'given a key' do
60
- before do
61
- client.key = 'key'
62
- end
63
-
64
- describe '#params' do
65
- subject { client.params }
66
-
67
- it 'should include the key' do
68
- subject['AWSAccessKeyId'].should eql client.key
69
- end
70
-
71
- it 'should generate a timestamp' do
72
- subject['Timestamp'].should be_a String
73
- end
74
- end
75
-
76
- describe '#build_query' do
77
- subject { client.build_query 'A10' => 1, 'A1' => 1 }
78
-
79
- it 'should include default parameters' do
80
- should match /Timestamp/
81
- end
82
-
83
- it 'should sort lexicographically' do
84
- should match /^A1=1&A10=/
85
- end
86
- end
87
- end
88
-
89
- context 'given an endpoint' do
90
- before do
91
- client.endpoint = 'http://slowapi.com/delay/0'
92
- end
93
-
94
- describe "#connection" do
95
- subject { client.connection }
96
- let(:headers) { subject.connection[:headers] }
97
-
98
- it { should be_an Excon::Connection }
99
-
100
- it 'should set default headers' do
101
- headers.should eq klass.headers
102
- end
103
-
104
- it 'should cache itself' do
105
- subject.should be client.connection
106
- end
107
- end
108
- end
109
-
110
- context 'given an endpoint, key, and secret' do
111
- before do
112
- client.endpoint = 'http://slowapi.com/delay/0'
113
- client.key = 'key'
114
- client.secret = 'secret'
115
- end
116
-
117
- Excon::HTTP_VERBS.each do |method|
118
- describe "##{method}" do
119
- subject do
120
- client.send(method, mock: true).body
121
- end
122
-
123
- before do
124
- Excon.stub({ method: method.to_sym }) do |params|
125
- { body: method, status: 200 }
126
- end
127
- end
128
-
129
- after { Excon.stubs.clear }
130
-
131
- it "should make a #{method.upcase} request" do
132
- should eql method
133
- end
134
- end
135
- end
136
-
137
- context 'given an HTTP status error' do
138
- before do
139
- Excon.stub({ method: :get }) do
140
- { status: 503 }
141
- end
142
- end
143
-
144
- after { Excon.stubs.clear }
145
-
146
- it "should raise an error" do
147
- expect {
148
- client.get mock: true
149
- }.to raise_error Excon::Errors::HTTPStatusError
150
- end
151
- end
152
- end
153
- end