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 +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
|