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 +4 -4
- data/Gemfile +1 -4
- data/README.md +24 -6
- data/jeff.gemspec +4 -2
- data/lib/jeff.rb +110 -131
- data/lib/jeff/version.rb +1 -1
- data/spec/jeff_spec.rb +4 -39
- metadata +34 -7
- data/lib/jeff/secret.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3f7158c532ec3dae4f1536ea70d1f3db88014dcd
|
4
|
+
data.tar.gz: 9c85321983f671dbde03310f437bb77e65169a99
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c25346ce0feb21e3d09e5ec1b155b8a6f3bcb0b3cdb5722d384a0ca1065a670f01d5793676ce674ba18425e63ec51bc87a64c0d46a8536176b0eba8bc044ec27
|
7
|
+
data.tar.gz: 993c1b39c3ce3ffc9b9ef200edb568b8603640d0e68c3d88f0ea63a60cd6033dcec285af8654b2db399a4f910d5e61a639d0880dfd3434e80e851ba8ac341123
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,11 +1,29 @@
|
|
1
1
|
# Jeff
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
Jeff mixes in client behaviour for Amazon Web Services (AWS) which require
|
4
|
+
[Signature version 2 authentication][sig].
|
5
5
|
|
6
|
-
|
6
|
+
![jeff][jef]
|
7
7
|
|
8
|
-
|
8
|
+
## Usage
|
9
9
|
|
10
|
-
|
11
|
-
|
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
|
data/jeff.gemspec
CHANGED
@@ -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.
|
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
|
data/lib/jeff.rb
CHANGED
@@ -1,173 +1,152 @@
|
|
1
|
-
|
2
|
-
|
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
|
-
#
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
#
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
-
#
|
60
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
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
|
-
#
|
75
|
-
|
51
|
+
# Because Ruby's CGI escapes ~, we have to resort to writing our own escape.
|
52
|
+
module Utils
|
53
|
+
UNRESERVED = /([^\w.~-]+)/
|
76
54
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
#
|
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
|
-
#
|
87
|
-
|
88
|
-
|
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
|
-
#
|
92
|
-
#
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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}(
|
104
|
-
|
105
|
-
connection.request(build_options(
|
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(
|
113
|
-
if
|
114
|
-
|
115
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
150
|
-
|
151
|
-
|
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
|
data/lib/jeff/version.rb
CHANGED
data/spec/jeff_spec.rb
CHANGED
@@ -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
|
-
|
53
|
-
|
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
|
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.
|
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-
|
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.
|
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.
|
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.
|
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
|
data/lib/jeff/secret.rb
DELETED
@@ -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
|