jeff 0.1.0 → 0.2.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.
- data/.travis.yml +0 -3
- data/Guardfile +5 -0
- data/README.md +57 -11
- data/jeff.gemspec +7 -5
- data/lib/jeff.rb +157 -8
- data/lib/jeff/version.rb +1 -1
- data/spec/jeff_spec.rb +160 -3
- data/spec/spec_helper.rb +1 -4
- metadata +48 -28
- data/lib/jeff/client.rb +0 -126
- data/lib/jeff/query_builder.rb +0 -42
- data/lib/jeff/signature.rb +0 -18
- data/lib/jeff/user_agent.rb +0 -11
- data/spec/jeff/client_spec.rb +0 -87
- data/spec/jeff/user_agent_spec.rb +0 -19
data/.travis.yml
CHANGED
data/Guardfile
ADDED
data/README.md
CHANGED
@@ -1,25 +1,71 @@
|
|
1
1
|
# Jeff
|
2
2
|
|
3
|
-
Jeff is a
|
4
|
-
|
3
|
+
**Jeff** is a light-weight module that mixes in client behaviour for [Amazon
|
4
|
+
Web Services (AWS)][aws]. It wraps the HTTP adapter [Excon][excon] and
|
5
|
+
implements [Signature Version 2][sign].
|
6
|
+
|
7
|
+
![jeff][jeff]
|
5
8
|
|
6
9
|
## Usage
|
7
10
|
|
11
|
+
Here's a hypothetical client.
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
class Client
|
15
|
+
include Jeff
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
Customise default headers and parameters.
|
20
|
+
|
8
21
|
```ruby
|
9
|
-
|
10
|
-
|
11
|
-
|
22
|
+
class Client
|
23
|
+
headers 'User-Agent' => 'Client'
|
24
|
+
params 'Service' => 'SomeService',
|
25
|
+
'Tag' => Proc.new { tag }
|
12
26
|
|
13
|
-
|
27
|
+
attr_accessor :tag
|
28
|
+
end
|
14
29
|
```
|
15
30
|
|
16
|
-
|
31
|
+
Set an AWS endpoint and credentials.
|
17
32
|
|
18
33
|
```ruby
|
19
|
-
client
|
34
|
+
client = Client.new.tap do |config|
|
35
|
+
config.endpoint = 'http://example.com/path'
|
36
|
+
config.key = 'key'
|
37
|
+
config.secret = 'secret'
|
38
|
+
end
|
39
|
+
```
|
40
|
+
You should now be able to access the endpoint.
|
20
41
|
|
21
|
-
|
42
|
+
```ruby
|
43
|
+
client.post query: {},
|
44
|
+
body: 'data'
|
22
45
|
```
|
23
46
|
|
24
|
-
|
25
|
-
|
47
|
+
Make a chunked request.
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
file = File.open 'data'
|
51
|
+
chunker = -> { file.read Excon::CHUNK_SIZE).to_s }
|
52
|
+
|
53
|
+
client.post query: {},
|
54
|
+
request_block: chunker
|
55
|
+
```
|
56
|
+
|
57
|
+
Stream a response.
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
streamer = ->(chunk, remaining, total) { puts chunk }
|
61
|
+
|
62
|
+
client.get query: {},
|
63
|
+
response_block: streamer
|
64
|
+
```
|
65
|
+
|
66
|
+
HTTP connections are persistent.
|
67
|
+
|
68
|
+
[aws]: http://aws.amazon.com/
|
69
|
+
[excon]: https://github.com/geemus/excon
|
70
|
+
[sign]: http://docs.amazonwebservices.com/general/latest/gr/signature-version-2.html
|
71
|
+
[jeff]: http://f.cl.ly/items/0a3R3J0k1R2f423k1q2l/jeff.jpg
|
data/jeff.gemspec
CHANGED
@@ -5,8 +5,8 @@ require File.expand_path('../lib/jeff/version.rb', __FILE__)
|
|
5
5
|
Gem::Specification.new do |gem|
|
6
6
|
gem.authors = ['Hakan Ensari']
|
7
7
|
gem.email = ['hakan.ensari@papercavalier.com']
|
8
|
-
gem.description = %q{
|
9
|
-
gem.summary = %q{
|
8
|
+
gem.description = %q{Minimum-viable Amazon Web Services (AWS) client}
|
9
|
+
gem.summary = %q{AWS client}
|
10
10
|
gem.homepage = 'https://github.com/hakanensari/jeff'
|
11
11
|
|
12
12
|
gem.files = `git ls-files`.split($\)
|
@@ -16,7 +16,9 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.require_paths = ['lib']
|
17
17
|
gem.version = Jeff::VERSION
|
18
18
|
|
19
|
-
gem.add_dependency 'excon', '~> 0.14'
|
20
|
-
gem.add_development_dependency '
|
21
|
-
gem.add_development_dependency '
|
19
|
+
gem.add_dependency 'excon', '~> 0.14.2'
|
20
|
+
gem.add_development_dependency 'guard-rspec'
|
21
|
+
gem.add_development_dependency 'pry'
|
22
|
+
gem.add_development_dependency 'rake'
|
23
|
+
gem.add_development_dependency 'rspec'
|
22
24
|
end
|
data/lib/jeff.rb
CHANGED
@@ -1,19 +1,168 @@
|
|
1
1
|
require 'base64'
|
2
|
-
require 'forwardable'
|
3
2
|
require 'time'
|
4
3
|
|
5
4
|
require 'excon'
|
6
5
|
|
7
6
|
require 'jeff/version'
|
8
|
-
require 'jeff/query_builder'
|
9
|
-
require 'jeff/user_agent'
|
10
|
-
require 'jeff/client'
|
11
|
-
require 'jeff/signature'
|
12
7
|
|
13
8
|
module Jeff
|
14
|
-
|
15
|
-
|
9
|
+
MissingEndpoint = Class.new ArgumentError
|
10
|
+
MissingKey = Class.new ArgumentError
|
11
|
+
MissingSecret = Class.new ArgumentError
|
16
12
|
|
17
|
-
|
13
|
+
SHA256 = OpenSSL::Digest::SHA256.new
|
14
|
+
UNRESERVED = /([^\w.~-]+)/
|
15
|
+
|
16
|
+
def self.included(base)
|
17
|
+
base.extend ClassMethods
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns an Excon::Connection.
|
21
|
+
def connection
|
22
|
+
@connection ||= Excon.new endpoint, :headers => default_headers
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns the Hash default request parameters.
|
26
|
+
def default_params
|
27
|
+
self.class.params.reduce({}) do |a, (k, v)|
|
28
|
+
a.update k => (v.is_a?(Proc) ? instance_eval(&v) : v)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the Hash default headers.
|
33
|
+
def default_headers
|
34
|
+
self.class.headers
|
35
|
+
end
|
36
|
+
|
37
|
+
# Gets the String AWS endpoint.
|
38
|
+
#
|
39
|
+
# Raises a MissingEndpoint error if endpoint is missing.
|
40
|
+
def endpoint
|
41
|
+
@endpoint or raise MissingEndpoint
|
42
|
+
end
|
43
|
+
|
44
|
+
# Sets the String AWS endpoint.
|
45
|
+
attr_writer :endpoint
|
46
|
+
|
47
|
+
# Gets the String AWS access key id.
|
48
|
+
#
|
49
|
+
# Raises a MissingKey error if key is missing.
|
50
|
+
def key
|
51
|
+
@key or raise MissingKey
|
52
|
+
end
|
53
|
+
|
54
|
+
# Sets the String AWS access key id.
|
55
|
+
attr_writer :key
|
56
|
+
|
57
|
+
# Gets the String AWS secret key.
|
58
|
+
#
|
59
|
+
# Raises a MissingSecret error if secret is missing.
|
60
|
+
def secret
|
61
|
+
@secret or raise MissingSecret
|
62
|
+
end
|
63
|
+
|
64
|
+
# Sets the String AWS secret key.
|
65
|
+
attr_writer :secret
|
66
|
+
|
67
|
+
# Generate HTTP request verb methods that sign queries and then delegate
|
68
|
+
# request to Excon.
|
69
|
+
Excon::HTTP_VERBS. each do |method|
|
70
|
+
eval <<-DEF
|
71
|
+
def #{method}(opts = {}, &block)
|
72
|
+
opts.update method: :#{method}
|
73
|
+
request opts, &block
|
74
|
+
end
|
75
|
+
DEF
|
76
|
+
end
|
77
|
+
|
78
|
+
# Internal: Builds a sorted query.
|
79
|
+
#
|
80
|
+
# hsh - A hash of parameters specific to request.
|
81
|
+
#
|
82
|
+
# Returns a query String.
|
83
|
+
def build_query(hsh)
|
84
|
+
default_params
|
85
|
+
.merge(hsh)
|
86
|
+
.map { |k, v| "#{k}=#{ escape v }" }
|
87
|
+
.sort
|
88
|
+
.join '&'
|
89
|
+
end
|
90
|
+
|
91
|
+
# Internal: Signs a message.
|
92
|
+
#
|
93
|
+
# message - A String to sign.
|
94
|
+
#
|
95
|
+
# Returns a String signature.
|
96
|
+
def sign(message)
|
97
|
+
digest = OpenSSL::HMAC.digest SHA256, secret, message
|
98
|
+
Base64.encode64(digest).chomp
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def request(opts, &block)
|
104
|
+
query = build_query opts[:query] || {}
|
105
|
+
string_to_sign = [
|
106
|
+
opts[:method],
|
107
|
+
host,
|
108
|
+
path,
|
109
|
+
query
|
110
|
+
].join "\n"
|
111
|
+
signature = sign string_to_sign
|
112
|
+
opts[:query] = [query, "Signature=#{escape signature}"].join '&'
|
113
|
+
|
114
|
+
connection.request opts, &block
|
115
|
+
end
|
116
|
+
|
117
|
+
def escape(val)
|
118
|
+
val.to_s.gsub(UNRESERVED) do
|
119
|
+
'%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def host
|
124
|
+
@host ||= url.host
|
125
|
+
end
|
126
|
+
|
127
|
+
def path
|
128
|
+
@path ||= url.path
|
129
|
+
end
|
130
|
+
|
131
|
+
def url
|
132
|
+
@url ||= URI endpoint
|
133
|
+
end
|
134
|
+
|
135
|
+
module ClassMethods
|
136
|
+
# Amazon recommends that libraries identify themselves via a User Agent.
|
137
|
+
USER_AGENT = "Jeff/#{VERSION} (Language=Ruby; #{`hostname`.chomp})"
|
138
|
+
|
139
|
+
# Gets/Updates the default headers.
|
140
|
+
#
|
141
|
+
# hsh - A Hash of headers.
|
142
|
+
#
|
143
|
+
# Returns the Hash headers.
|
144
|
+
def headers(hsh = nil)
|
145
|
+
@headers ||= { 'User-Agent' => USER_AGENT }
|
146
|
+
@headers.update hsh if hsh
|
147
|
+
|
148
|
+
@headers
|
149
|
+
end
|
150
|
+
|
151
|
+
# Gets/Updates the default request parameters.
|
152
|
+
#
|
153
|
+
# hsh - A Hash of parameters (default: nil).
|
154
|
+
#
|
155
|
+
# Returns the Hash parameters.
|
156
|
+
def params(hsh = nil)
|
157
|
+
@params ||= {
|
158
|
+
'AWSAccessKeyId' => Proc.new { key },
|
159
|
+
'SignatureVersion' => '2',
|
160
|
+
'SignatureMethod' => 'HmacSHA256',
|
161
|
+
'Timestamp' => Proc.new { Time.now.utc.iso8601 }
|
162
|
+
}
|
163
|
+
@params.update hsh if hsh
|
164
|
+
|
165
|
+
@params
|
166
|
+
end
|
18
167
|
end
|
19
168
|
end
|
data/lib/jeff/version.rb
CHANGED
data/spec/jeff_spec.rb
CHANGED
@@ -1,9 +1,166 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Jeff do
|
4
|
-
|
5
|
-
|
6
|
-
|
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 '#default_params' do
|
65
|
+
subject { client.default_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 'Foo' => 1, 'AA' => 1 }
|
78
|
+
|
79
|
+
it 'should include default parameters' do
|
80
|
+
should match /Timestamp/
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should include request-specific parameters' do
|
84
|
+
should match /Foo/
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should sort parameters' do
|
88
|
+
should match /^AA/
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'given a key and a secret' do
|
94
|
+
before do
|
95
|
+
client.key = 'key'
|
96
|
+
client.secret = 'secret'
|
97
|
+
end
|
98
|
+
|
99
|
+
describe '#sign' do
|
100
|
+
subject { client.sign 'foo' }
|
101
|
+
|
102
|
+
it { should be_a String }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'given an endpoint' do
|
107
|
+
before do
|
108
|
+
client.endpoint = 'http://slowapi.com/delay/0'
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "#connection" do
|
112
|
+
subject { client.connection }
|
113
|
+
let(:headers) { subject.connection[:headers] }
|
114
|
+
|
115
|
+
it { should be_an Excon::Connection }
|
116
|
+
|
117
|
+
it 'should set default headers' do
|
118
|
+
headers.should eq klass.headers
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'should cache itself' do
|
122
|
+
subject.should be client.connection
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'given an endpoint, key, and secret' do
|
128
|
+
before do
|
129
|
+
client.endpoint = 'http://slowapi.com/delay/0'
|
130
|
+
client.key = 'key'
|
131
|
+
client.secret = 'secret'
|
132
|
+
end
|
133
|
+
|
134
|
+
Excon::HTTP_VERBS.each do |method|
|
135
|
+
describe "##{method}" do
|
136
|
+
subject { client.send method, mock: true }
|
137
|
+
|
138
|
+
it "should make a #{method.upcase} request" do
|
139
|
+
Excon.stub({ method: method.to_sym }, { body: method })
|
140
|
+
subject.body.should eql method
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should include default headers' do
|
144
|
+
Excon.stub({ method: method.to_sym }) do |params|
|
145
|
+
{ body: params[:headers] }
|
146
|
+
end
|
147
|
+
subject.body.should have_key 'User-Agent'
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'should include parameters' do
|
151
|
+
Excon.stub({ method: method.to_sym }) do |params|
|
152
|
+
{ body: params[:query] }
|
153
|
+
end
|
154
|
+
subject.body.should match client.build_query({})
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'should append a signature' do
|
158
|
+
Excon.stub({ method: method.to_sym }) do |params|
|
159
|
+
{ body: params[:query] }
|
160
|
+
end
|
161
|
+
subject.body.should match /Signature=[^&]+$/
|
162
|
+
end
|
163
|
+
end
|
7
164
|
end
|
8
165
|
end
|
9
166
|
end
|
data/spec/spec_helper.rb
CHANGED
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
|
+
version: 0.2.0
|
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-07-
|
12
|
+
date: 2012-07-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: excon
|
@@ -18,7 +18,7 @@ dependencies:
|
|
18
18
|
requirements:
|
19
19
|
- - ~>
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version:
|
21
|
+
version: 0.14.2
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -26,40 +26,72 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - ~>
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version:
|
29
|
+
version: 0.14.2
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: guard-rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: pry
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
30
62
|
- !ruby/object:Gem::Dependency
|
31
63
|
name: rake
|
32
64
|
requirement: !ruby/object:Gem::Requirement
|
33
65
|
none: false
|
34
66
|
requirements:
|
35
|
-
- -
|
67
|
+
- - ! '>='
|
36
68
|
- !ruby/object:Gem::Version
|
37
|
-
version: '0
|
69
|
+
version: '0'
|
38
70
|
type: :development
|
39
71
|
prerelease: false
|
40
72
|
version_requirements: !ruby/object:Gem::Requirement
|
41
73
|
none: false
|
42
74
|
requirements:
|
43
|
-
- -
|
75
|
+
- - ! '>='
|
44
76
|
- !ruby/object:Gem::Version
|
45
|
-
version: '0
|
77
|
+
version: '0'
|
46
78
|
- !ruby/object:Gem::Dependency
|
47
79
|
name: rspec
|
48
80
|
requirement: !ruby/object:Gem::Requirement
|
49
81
|
none: false
|
50
82
|
requirements:
|
51
|
-
- -
|
83
|
+
- - ! '>='
|
52
84
|
- !ruby/object:Gem::Version
|
53
|
-
version: '
|
85
|
+
version: '0'
|
54
86
|
type: :development
|
55
87
|
prerelease: false
|
56
88
|
version_requirements: !ruby/object:Gem::Requirement
|
57
89
|
none: false
|
58
90
|
requirements:
|
59
|
-
- -
|
91
|
+
- - ! '>='
|
60
92
|
- !ruby/object:Gem::Version
|
61
|
-
version: '
|
62
|
-
description:
|
93
|
+
version: '0'
|
94
|
+
description: Minimum-viable Amazon Web Services (AWS) client
|
63
95
|
email:
|
64
96
|
- hakan.ensari@papercavalier.com
|
65
97
|
executables: []
|
@@ -69,18 +101,13 @@ files:
|
|
69
101
|
- .gitignore
|
70
102
|
- .travis.yml
|
71
103
|
- Gemfile
|
104
|
+
- Guardfile
|
72
105
|
- LICENSE
|
73
106
|
- README.md
|
74
107
|
- Rakefile
|
75
108
|
- jeff.gemspec
|
76
109
|
- lib/jeff.rb
|
77
|
-
- lib/jeff/client.rb
|
78
|
-
- lib/jeff/query_builder.rb
|
79
|
-
- lib/jeff/signature.rb
|
80
|
-
- lib/jeff/user_agent.rb
|
81
110
|
- lib/jeff/version.rb
|
82
|
-
- spec/jeff/client_spec.rb
|
83
|
-
- spec/jeff/user_agent_spec.rb
|
84
111
|
- spec/jeff_spec.rb
|
85
112
|
- spec/spec_helper.rb
|
86
113
|
homepage: https://github.com/hakanensari/jeff
|
@@ -95,26 +122,19 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
95
122
|
- - ! '>='
|
96
123
|
- !ruby/object:Gem::Version
|
97
124
|
version: '0'
|
98
|
-
segments:
|
99
|
-
- 0
|
100
|
-
hash: 2544026845906331060
|
101
125
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
126
|
none: false
|
103
127
|
requirements:
|
104
128
|
- - ! '>='
|
105
129
|
- !ruby/object:Gem::Version
|
106
130
|
version: '0'
|
107
|
-
segments:
|
108
|
-
- 0
|
109
|
-
hash: 2544026845906331060
|
110
131
|
requirements: []
|
111
132
|
rubyforge_project:
|
112
133
|
rubygems_version: 1.8.23
|
113
134
|
signing_key:
|
114
135
|
specification_version: 3
|
115
|
-
summary:
|
136
|
+
summary: AWS client
|
116
137
|
test_files:
|
117
|
-
- spec/jeff/client_spec.rb
|
118
|
-
- spec/jeff/user_agent_spec.rb
|
119
138
|
- spec/jeff_spec.rb
|
120
139
|
- spec/spec_helper.rb
|
140
|
+
has_rdoc:
|
data/lib/jeff/client.rb
DELETED
@@ -1,126 +0,0 @@
|
|
1
|
-
module Jeff
|
2
|
-
# A minimum-viable Amazon Web Services (AWS) client.
|
3
|
-
class Client
|
4
|
-
include UserAgent
|
5
|
-
|
6
|
-
# Internal: Returns the String request body.
|
7
|
-
attr :body
|
8
|
-
|
9
|
-
# Internal: Returns the Proc chunked request body.
|
10
|
-
attr :chunker
|
11
|
-
|
12
|
-
# Gets/Sets the String AWS access key id.
|
13
|
-
attr_accessor :key
|
14
|
-
|
15
|
-
# Gets/Sets the String AWS secret key.
|
16
|
-
attr_accessor :secret
|
17
|
-
|
18
|
-
# Creates a new client.
|
19
|
-
#
|
20
|
-
# endpoint - A String AWS endpoint.
|
21
|
-
#
|
22
|
-
# Examples
|
23
|
-
#
|
24
|
-
# client = Jeff.new 'http://ecs.amazonaws.com/onca/xml'
|
25
|
-
#
|
26
|
-
def initialize(endpoint)
|
27
|
-
@endpoint = URI endpoint
|
28
|
-
@connection = Excon.new endpoint.to_s, :headers => {
|
29
|
-
'User-Agent' => USER_AGENT
|
30
|
-
}
|
31
|
-
|
32
|
-
reset_request_attributes
|
33
|
-
end
|
34
|
-
|
35
|
-
# Updates the request attributes.
|
36
|
-
#
|
37
|
-
# data - A Hash of parameters or a String request body or a Proc that will
|
38
|
-
# deliver chunks of data.
|
39
|
-
#
|
40
|
-
# Returns self.
|
41
|
-
#
|
42
|
-
# Examples
|
43
|
-
#
|
44
|
-
# @client << {
|
45
|
-
# 'AssociateTag' => 'tag',
|
46
|
-
# 'Service' => 'AWSECommerceService',
|
47
|
-
# 'Version' => '2011-08-01'
|
48
|
-
# }
|
49
|
-
#
|
50
|
-
def <<(data)
|
51
|
-
case data
|
52
|
-
when Hash
|
53
|
-
@params.update data
|
54
|
-
when String
|
55
|
-
@body ||= '' << data
|
56
|
-
when Proc
|
57
|
-
@chunker = data
|
58
|
-
end
|
59
|
-
|
60
|
-
self
|
61
|
-
end
|
62
|
-
|
63
|
-
# Configures the client.
|
64
|
-
#
|
65
|
-
# Yields self.
|
66
|
-
#
|
67
|
-
# Examples
|
68
|
-
#
|
69
|
-
# client.configure do |c|
|
70
|
-
# c.key = 'key'
|
71
|
-
# c.secret = 'secret'
|
72
|
-
# end
|
73
|
-
#
|
74
|
-
def configure
|
75
|
-
yield self
|
76
|
-
end
|
77
|
-
|
78
|
-
# Returns the Hash request parameters, including required defaults.
|
79
|
-
def params
|
80
|
-
{
|
81
|
-
'AWSAccessKeyId' => @key,
|
82
|
-
'SignatureVersion' => '2',
|
83
|
-
'SignatureMethod' => 'HmacSHA256',
|
84
|
-
'Timestamp' => Time.now.utc.iso8601
|
85
|
-
}.merge @params
|
86
|
-
end
|
87
|
-
|
88
|
-
# Makes an HTTP request.
|
89
|
-
#
|
90
|
-
# Returns an Excon::Response.
|
91
|
-
def request(opts = {}, &blk)
|
92
|
-
opts.update :body => @body,
|
93
|
-
:method => action,
|
94
|
-
:query => query,
|
95
|
-
:request_block => @chunker
|
96
|
-
|
97
|
-
begin
|
98
|
-
@connection.request opts, &blk
|
99
|
-
ensure
|
100
|
-
reset_request_attributes
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
# Returns the String URL.
|
105
|
-
def url
|
106
|
-
[@endpoint, query].join '?'
|
107
|
-
end
|
108
|
-
|
109
|
-
private
|
110
|
-
|
111
|
-
def action
|
112
|
-
@body || @chunker ? :post : :get
|
113
|
-
end
|
114
|
-
|
115
|
-
def query
|
116
|
-
@query_builder ||= QueryBuilder.new @endpoint, @secret
|
117
|
-
@query_builder.build action, params
|
118
|
-
end
|
119
|
-
|
120
|
-
def reset_request_attributes
|
121
|
-
@body = nil
|
122
|
-
@chunker = nil
|
123
|
-
@params = {}
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|
data/lib/jeff/query_builder.rb
DELETED
@@ -1,42 +0,0 @@
|
|
1
|
-
module Jeff
|
2
|
-
class QueryBuilder
|
3
|
-
UNRESERVED = /([^\w.~-]+)/
|
4
|
-
|
5
|
-
def initialize(endpoint, secret)
|
6
|
-
@endpoint = endpoint
|
7
|
-
@secret = secret
|
8
|
-
end
|
9
|
-
|
10
|
-
def build(mth, params)
|
11
|
-
@mth = mth.to_s.upcase
|
12
|
-
@query = stringify params
|
13
|
-
|
14
|
-
"#{@query}&Signature=#{escape signature}"
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def escape(val)
|
20
|
-
val.to_s.gsub(UNRESERVED) do
|
21
|
-
'%' + $1.unpack('H2' * $1.bytesize).join('%').upcase
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def signature
|
26
|
-
Signature.new @secret, string_to_sign
|
27
|
-
end
|
28
|
-
|
29
|
-
def string_to_sign
|
30
|
-
[
|
31
|
-
@mth,
|
32
|
-
@endpoint.host,
|
33
|
-
@endpoint.path,
|
34
|
-
@query
|
35
|
-
].join "\n"
|
36
|
-
end
|
37
|
-
|
38
|
-
def stringify(hsh)
|
39
|
-
hsh.map { |k, v| "#{k}=#{ escape v }" }.sort.join '&'
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
data/lib/jeff/signature.rb
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
module Jeff
|
2
|
-
class Signature
|
3
|
-
SHA256 = OpenSSL::Digest::SHA256.new
|
4
|
-
|
5
|
-
def initialize(secret, message)
|
6
|
-
@secret = secret
|
7
|
-
@message = message
|
8
|
-
end
|
9
|
-
|
10
|
-
def digest
|
11
|
-
OpenSSL::HMAC.digest SHA256, @secret, @message
|
12
|
-
end
|
13
|
-
|
14
|
-
def to_s
|
15
|
-
Base64.encode64(digest).chomp
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
data/lib/jeff/user_agent.rb
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
module Jeff
|
2
|
-
module UserAgent
|
3
|
-
USER_AGENT = begin
|
4
|
-
hostname = `hostname`.chomp
|
5
|
-
engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
|
6
|
-
language = [engine, RUBY_VERSION, "p#{RUBY_PATCHLEVEL}"].join ' '
|
7
|
-
|
8
|
-
"Jeff/#{VERSION} (Language=#{language}; Host=#{hostname})"
|
9
|
-
end
|
10
|
-
end
|
11
|
-
end
|
data/spec/jeff/client_spec.rb
DELETED
@@ -1,87 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module Jeff
|
4
|
-
describe Client do
|
5
|
-
before do
|
6
|
-
@endpoint = 'http://slowapi.com/delay/0'
|
7
|
-
|
8
|
-
@client = Client.new @endpoint
|
9
|
-
@client.configure do |config|
|
10
|
-
config.key = 'key'
|
11
|
-
config.secret = 'secret'
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
describe '#<<' do
|
16
|
-
it 'updates the request parameters' do
|
17
|
-
@client << { 'Foo' => 1 }
|
18
|
-
@client.params.should include 'Foo'
|
19
|
-
end
|
20
|
-
|
21
|
-
it 'updates the request body' do
|
22
|
-
@client << 'foo'
|
23
|
-
@client.body.should eql 'foo'
|
24
|
-
end
|
25
|
-
|
26
|
-
it 'updates the request chunked body' do
|
27
|
-
chunker = lambda {}
|
28
|
-
@client << chunker
|
29
|
-
@client.chunker.should eql chunker
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
describe '#params' do
|
34
|
-
subject { @client.params }
|
35
|
-
|
36
|
-
it 'includes a key' do
|
37
|
-
should include 'AWSAccessKeyId'
|
38
|
-
end
|
39
|
-
|
40
|
-
it 'includes a signature version' do
|
41
|
-
should include 'SignatureVersion'
|
42
|
-
end
|
43
|
-
|
44
|
-
it 'includes a signature method' do
|
45
|
-
should include 'SignatureMethod'
|
46
|
-
end
|
47
|
-
|
48
|
-
it 'includes a timestamp' do
|
49
|
-
should include 'Timestamp'
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
describe '#url' do
|
54
|
-
subject { @client.url }
|
55
|
-
|
56
|
-
it 'includes the endpoint' do
|
57
|
-
should include @endpoint
|
58
|
-
end
|
59
|
-
|
60
|
-
it 'sorts the parameters' do
|
61
|
-
@client << { 'Z' => 1, 'A' => 1 }
|
62
|
-
should match /\?A=[^&]+/
|
63
|
-
end
|
64
|
-
|
65
|
-
it 'is signed' do
|
66
|
-
should match /Signature=[^&]+$/
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
describe '#request' do
|
71
|
-
context 'given no body or chunker' do
|
72
|
-
it 'makes a GET request' do
|
73
|
-
Excon.stub({ :method => :get }, { :body => 'get' })
|
74
|
-
@client.request(:mock => true).body.should eql 'get'
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
context 'given a body' do
|
79
|
-
it 'makes a POST request' do
|
80
|
-
Excon.stub({ :method => :post }, { :body => 'post' })
|
81
|
-
@client << 'foo'
|
82
|
-
@client.request(:mock => true).body.should eql 'post'
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
module Jeff
|
4
|
-
describe UserAgent do
|
5
|
-
subject { UserAgent::USER_AGENT }
|
6
|
-
|
7
|
-
it 'describes the library' do
|
8
|
-
should match /Jeff\/[\d\w.]+\s/
|
9
|
-
end
|
10
|
-
|
11
|
-
it 'describes the interpreter' do
|
12
|
-
should match /Language=(?:j?ruby|rbx)/
|
13
|
-
end
|
14
|
-
|
15
|
-
it 'describes the host' do
|
16
|
-
should match /Host=[\w\d]+/
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|