rubizon 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +22 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +119 -0
- data/Rakefile +60 -0
- data/VERSION +1 -0
- data/lib/rubizon/abstract_sig2_product.rb +126 -0
- data/lib/rubizon/exceptions.rb +11 -0
- data/lib/rubizon/product/product_advertising.rb +35 -0
- data/lib/rubizon/product/sns.rb +71 -0
- data/lib/rubizon/request.rb +149 -0
- data/lib/rubizon/security_credentials.rb +25 -0
- data/lib/rubizon.rb +2 -0
- data/rubizon.gemspec +91 -0
- data/test/helper.rb +23 -0
- data/test/test_abstract_sig2_product.rb +75 -0
- data/test/test_request.rb +87 -0
- data/test/test_security_credentials.rb +34 -0
- data/test/test_signature_sample.rb +66 -0
- data/test/test_sns.rb +38 -0
- metadata +184 -0
data/.document
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
gem "ruby-hmac", "~> 0.4.0", :require => "ruby_hmac"
|
4
|
+
|
5
|
+
# Add dependencies to develop your gem here.
|
6
|
+
# Include everything needed to run rake, tests, features, etc.
|
7
|
+
group :development do
|
8
|
+
gem "shoulda", ">= 0"
|
9
|
+
gem "bundler", "~> 1.0.0"
|
10
|
+
gem "jeweler", "~> 1.5.1"
|
11
|
+
gem "rcov", ">= 0"
|
12
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
git (1.2.5)
|
5
|
+
jeweler (1.5.1)
|
6
|
+
bundler (~> 1.0.0)
|
7
|
+
git (>= 1.2.5)
|
8
|
+
rake
|
9
|
+
rake (0.8.7)
|
10
|
+
rcov (0.9.9)
|
11
|
+
ruby-hmac (0.4.0)
|
12
|
+
shoulda (2.11.3)
|
13
|
+
|
14
|
+
PLATFORMS
|
15
|
+
x86-mswin32
|
16
|
+
|
17
|
+
DEPENDENCIES
|
18
|
+
bundler (~> 1.0.0)
|
19
|
+
jeweler (~> 1.5.1)
|
20
|
+
rcov
|
21
|
+
ruby-hmac (~> 0.4.0)
|
22
|
+
shoulda
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Randy McLaughlin
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
= rubizon
|
2
|
+
|
3
|
+
A Ruby interface to Amazon Web Services. Rubizon separates creating a
|
4
|
+
properly-formed, signed URL for making an AWS request from the transport
|
5
|
+
mechanism used. The same logic can thus be used to access AWS using
|
6
|
+
Net::HTTP, EventMachine::Protocols::HttpClient or some other transport.
|
7
|
+
|
8
|
+
In its initial implementation, Rubizon simply builds and signs URLs. Further
|
9
|
+
development may include adapters to various transport mechanisms and
|
10
|
+
interpretation of results. On the other hand, it may turn out to be best
|
11
|
+
kept merely as a URL generator working in concert with other libraries that
|
12
|
+
provide transport and result interpretation.
|
13
|
+
|
14
|
+
===Class structure
|
15
|
+
Rubizon is comprised of a few foundation classes, described below, as well as
|
16
|
+
classes for each of the AWS services it supports.
|
17
|
+
|
18
|
+
*SecurityCredentials encapsulates an AWS Access Key ID and the corresponding
|
19
|
+
Secret Access Key. It allows querying the access key and signing an
|
20
|
+
arbitrary key, but does not support quering the access key. Only a single
|
21
|
+
instance of SecurityCredentials need be created for each key pair.
|
22
|
+
|
23
|
+
*AbstractSig2Product is intended to provided a foundation for building
|
24
|
+
requests to any service that supports signature version 2. The
|
25
|
+
SimpleNotificationService (require 'product/sns') class is a concrete
|
26
|
+
subclass supporting SNS. Similar classes should be able to similarly
|
27
|
+
subclass AbstractSig2Product in order to support other AWS services.
|
28
|
+
Only a single instance of any product's class should be required to
|
29
|
+
serve any number of requests using the same credentials, host and scheme.
|
30
|
+
|
31
|
+
*Request encapsulates one request, the code to sign it, formulate a URL and
|
32
|
+
to access the URL and its component parts. A product's class will create
|
33
|
+
a Request every time a URL is to be generated, provide it with the proper
|
34
|
+
values to perform a requested action and then return the request object.
|
35
|
+
The URL and its components can then be queried from the request object.
|
36
|
+
|
37
|
+
Since the ultimate intent of Rubizon is to support requests to many of the AWS
|
38
|
+
services, even though a given application may only need access to a small number
|
39
|
+
of the supported services, the main rubizon.rb file only requires the core
|
40
|
+
code. An interface for each AWS product are maintained in the rubizon/product
|
41
|
+
directory and will need to be specifically required into any code needing its
|
42
|
+
services.
|
43
|
+
|
44
|
+
===An example of publishing a message via SNS:
|
45
|
+
<tt>
|
46
|
+
require 'rubizon'
|
47
|
+
require 'rubizon/product/sns'
|
48
|
+
require 'net/http'
|
49
|
+
require 'URI'
|
50
|
+
credentials= Rubizon::SecurityCredentials.new '00000000000000000000','1234567890'
|
51
|
+
sns= Rubizon::SimpleNotificationService.new credentials,'sns.us-east-1.amazonaws.com'
|
52
|
+
topic= sns.topic 'arn:aws:sns:us-east-1:123123123123:sample-notifications'
|
53
|
+
req=topic.publish 'this is a hello world message','hello world'
|
54
|
+
Net::HTTP.get_print URI.parse(req.url)
|
55
|
+
</tt>
|
56
|
+
|
57
|
+
===Supported AWS services
|
58
|
+
The initial implementation also simply scratches the author's itch: the need
|
59
|
+
to send a message to SNS. The design should lend itself to a broader
|
60
|
+
range of requests and services, but these can be added as needed. The critical
|
61
|
+
feature is being able to sign a request and the code to do so should be
|
62
|
+
applicable to requests for any service that supports signature version 2 requests,
|
63
|
+
including the following services:
|
64
|
+
*EC2
|
65
|
+
*Elastic MapReduce
|
66
|
+
*Auto Scaling
|
67
|
+
*SimpleDB
|
68
|
+
*RDS
|
69
|
+
*Identity and Access Management
|
70
|
+
*SQS
|
71
|
+
*SNS
|
72
|
+
*CloudWatch
|
73
|
+
*Virtual Private Cloud
|
74
|
+
*Elastic Load Balancing
|
75
|
+
*FPS
|
76
|
+
*AWS Import/Export
|
77
|
+
|
78
|
+
There are other AWS services that expect requests to be signed differently.
|
79
|
+
I'm not sure if there is a definitive reference to what "signature version 1"
|
80
|
+
is but there is definite similarity. I suspect that an AbstractSig1Product
|
81
|
+
class could be created using the AbstractSig2Product model that would cover
|
82
|
+
80% of the remaining services. Here are some notes from a brief exploration
|
83
|
+
of the API docs:
|
84
|
+
*http://docs.amazonwebservices.com/AmazonCloudFront/latest/DeveloperGuide/index.html?RESTAuthentication.html
|
85
|
+
Cloud Front authorizes only the timestamp, using SHA1 only, and places in an
|
86
|
+
"Authorization" header
|
87
|
+
|
88
|
+
*Route 53 uses X-Amzn-Authorization header, similar to CloudFront's
|
89
|
+
Authorization header
|
90
|
+
|
91
|
+
*S3 also uses an Authorization header
|
92
|
+
|
93
|
+
*DevPay uses signature version 1
|
94
|
+
|
95
|
+
*Alexa Web Information Service uses something like signature version 1
|
96
|
+
(key id, timestamp and signature)
|
97
|
+
|
98
|
+
*Mechanical Turk uses something like signature version 1
|
99
|
+
|
100
|
+
It also appears that the services that support signature version 2 rely almost
|
101
|
+
exclusively on HTTP GETs, rather than POSTs, PUTs and DELETEs. Rubizon, as
|
102
|
+
currently written, only supports GETs. The Request class could likely be extended
|
103
|
+
to support the other REST verbs if there is enough interest and need.
|
104
|
+
|
105
|
+
== Contributing to rubizon
|
106
|
+
|
107
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
108
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
109
|
+
* Fork the project
|
110
|
+
* Start a feature/bugfix branch
|
111
|
+
* Commit and push until you are happy with your contribution
|
112
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
113
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
114
|
+
|
115
|
+
== Copyright
|
116
|
+
|
117
|
+
Copyright (c) 2010 Randy McLaughlin. See LICENSE.txt for
|
118
|
+
further details.
|
119
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
15
|
+
gem.name = "rubizon"
|
16
|
+
gem.homepage = "http://github.com/randymized/rubizon"
|
17
|
+
gem.license = "MIT"
|
18
|
+
gem.summary = %Q{A Ruby interface to Amazon Web Services}
|
19
|
+
gem.description = %Q{A Ruby interface to Amazon Web Services. Rubizon separates creating a
|
20
|
+
properly-formed, signed URL for making an AWS request from the transport
|
21
|
+
mechanism used.
|
22
|
+
|
23
|
+
In its initial implementation, Rubizon simply builds and signs URLs. Further
|
24
|
+
development may include adapters to various transport mechanisms and
|
25
|
+
interpretation of results.
|
26
|
+
}
|
27
|
+
gem.email = "ot40ddj02@sneakemail.com"
|
28
|
+
gem.authors = ["Randy McLaughlin"]
|
29
|
+
# Include your dependencies below. Runtime dependencies are required when using your gem,
|
30
|
+
# and development dependencies are only needed for development (ie running rake tasks, tests, etc)
|
31
|
+
gem.add_dependency 'ruby-hmac', '~> 0.4.0'
|
32
|
+
# gem.add_development_dependency 'rspec', '> 1.2.3'
|
33
|
+
end
|
34
|
+
Jeweler::RubygemsDotOrgTasks.new
|
35
|
+
|
36
|
+
require 'rake/testtask'
|
37
|
+
Rake::TestTask.new(:test) do |test|
|
38
|
+
test.libs << 'lib' << 'test'
|
39
|
+
test.pattern = 'test/**/test_*.rb'
|
40
|
+
test.verbose = true
|
41
|
+
end
|
42
|
+
|
43
|
+
require 'rcov/rcovtask'
|
44
|
+
Rcov::RcovTask.new do |test|
|
45
|
+
test.libs << 'test'
|
46
|
+
test.pattern = 'test/**/test_*.rb'
|
47
|
+
test.verbose = true
|
48
|
+
end
|
49
|
+
|
50
|
+
task :default => :test
|
51
|
+
|
52
|
+
require 'rake/rdoctask'
|
53
|
+
Rake::RDocTask.new do |rdoc|
|
54
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
55
|
+
|
56
|
+
rdoc.rdoc_dir = 'rdoc'
|
57
|
+
rdoc.title = "rubizon #{version}"
|
58
|
+
rdoc.rdoc_files.include('README*')
|
59
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
60
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require File.dirname(__FILE__) + '/request'
|
3
|
+
module Rubizon
|
4
|
+
# An abstract representation of an AWS product whose REST API uses
|
5
|
+
# signature version 2. This class provides a foundation for
|
6
|
+
# classes that represent specific AWS products.
|
7
|
+
class AbstractSig2Product
|
8
|
+
# Initialization
|
9
|
+
#
|
10
|
+
# specs - A Hash containing specifications for the product.
|
11
|
+
# :scheme - (optional) Default scheme is https.
|
12
|
+
# May be set to "http".
|
13
|
+
# :host - (conditionally required) If an ARN is not
|
14
|
+
# specified or if the host cannot be properly
|
15
|
+
# deduced from the ARN, :host must be specified.
|
16
|
+
# If specified, this will override any host name
|
17
|
+
# that might be deduced from the ARN.
|
18
|
+
# :ARN - (optional) An ARN may be specified instead of a
|
19
|
+
# host. The host can be deduced from the ARN.
|
20
|
+
# :path - (optional) Default path is '/'. May be set to
|
21
|
+
# a path that applies to all requests for the
|
22
|
+
# product. Additional path elements may be appended
|
23
|
+
# if needed by an operation or subject of that
|
24
|
+
# operation
|
25
|
+
# :URL - (optional) A URL may be specified instead of the
|
26
|
+
# individual scheme, host and path elements. If
|
27
|
+
# a URL is specified, it will override the individual
|
28
|
+
# elements.
|
29
|
+
# :_omit - (optional) An array containing a list of elements
|
30
|
+
# that are not to be included in the query string.
|
31
|
+
# This, for example, can be used in the Product
|
32
|
+
# Advertising API to suppress the SignatureMethod
|
33
|
+
# and SignatureVersion parameter/value pairs which
|
34
|
+
# result from the signing process but are not
|
35
|
+
# supported in that API
|
36
|
+
# (other) - (optional) Other key/value pairs may be specified.
|
37
|
+
# These will be included in any query string
|
38
|
+
# generated for this product
|
39
|
+
def initialize(specs={})
|
40
|
+
@scheme= (specs.delete(:scheme) || specs.delete('scheme') || 'https').to_s
|
41
|
+
@arn= specs.delete(:ARN) || specs.delete('ARN') || specs.delete(:arn) || specs.delete('arn')
|
42
|
+
@host= specs.delete(:host) || specs.delete('host')
|
43
|
+
@url= specs.delete(:URL) || specs.delete('URL') || specs.delete(:url) || specs.delete('url')
|
44
|
+
@path= specs.delete(:path) || specs.delete('path') || '/'
|
45
|
+
if (@url)
|
46
|
+
require 'uri'
|
47
|
+
url = URI.parse(@url)
|
48
|
+
@scheme= url.scheme
|
49
|
+
@host= url.host
|
50
|
+
@path= url.path
|
51
|
+
end
|
52
|
+
if @arn && !@host
|
53
|
+
@host= self.class.host_from_ARN(@arn)
|
54
|
+
end
|
55
|
+
if !@host
|
56
|
+
raise InvalidParameterError, 'No host was specified and one could not be deduced from arn specifications'
|
57
|
+
end
|
58
|
+
@query_elements= specs
|
59
|
+
@query_elements['SignatureMethod']= 'HmacSHA256'
|
60
|
+
@query_elements['SignatureVersion']= 2
|
61
|
+
@query_elements['arn']= @arn if @arn
|
62
|
+
end
|
63
|
+
|
64
|
+
# Default method for calculating the name of the host associated with a given
|
65
|
+
# Amazon Resource Name (ARN). This may vary from product to product, so
|
66
|
+
# any product with a different mapping from ARN to hostname should override
|
67
|
+
# this method.
|
68
|
+
#
|
69
|
+
# arn - A String containing an Amazon Resource Name (ARN).
|
70
|
+
#
|
71
|
+
# Returns the name of the host hosting the named resource.
|
72
|
+
def self.host_from_ARN(arn)
|
73
|
+
elems= arn.split(':',5)
|
74
|
+
"#{elems[2]}.#{elems[3]}.amazonaws.com"
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns the URL scheme, such as http or https
|
78
|
+
attr_reader :scheme
|
79
|
+
|
80
|
+
# Returns the ARN (Amazon Resource Name) served by this object.
|
81
|
+
# Returns nil if an ARN is not defined.
|
82
|
+
attr_reader :arn
|
83
|
+
|
84
|
+
# Returns the host (domain and subdomains) of the product served by this
|
85
|
+
# object.
|
86
|
+
attr_reader :host
|
87
|
+
|
88
|
+
# Returns the path part of the URL of the product served by this object,
|
89
|
+
# typically '/'.
|
90
|
+
attr_reader :path
|
91
|
+
|
92
|
+
# Returns a Hash containing elements to be included in any query string
|
93
|
+
# generated for this product. To this will be added elements identifying
|
94
|
+
# the specific action, the subject of the action, the access key, and
|
95
|
+
# elements related to signing the request.
|
96
|
+
attr_reader :query_elements
|
97
|
+
|
98
|
+
# Returns the product's endpoint. The endpoint is that part of a URL that
|
99
|
+
# includes the scheme, host, and path, but not the query string.
|
100
|
+
# An action may extend the product's path, but otherwise would retain the
|
101
|
+
# rest of the endpoint.
|
102
|
+
def endpoint
|
103
|
+
"#{@scheme}://#{@host}#{@path}"
|
104
|
+
end
|
105
|
+
|
106
|
+
# Create a Request object that can be used to formulate a single request
|
107
|
+
# for this product.
|
108
|
+
#
|
109
|
+
# Returns an instance of Request
|
110
|
+
def create_request(credentials)
|
111
|
+
Request.new(credentials,@scheme,@host,@path,@query_elements)
|
112
|
+
end
|
113
|
+
|
114
|
+
protected
|
115
|
+
# In most cases, an action method needs only to invoke this method.
|
116
|
+
# It will create a Request object, add query elements for the action and
|
117
|
+
# its subject and return the request object. The URL, query string,
|
118
|
+
# hostname and other information needed to get (or post, put or delete)
|
119
|
+
# a resource from AWS can then be obtained via Request methods.
|
120
|
+
def basic_action(action_elements,subject_elements=nil)
|
121
|
+
req= create_request(@credentials)
|
122
|
+
req.add_query_elements(action_elements)
|
123
|
+
req.add_query_elements(subject_elements) if subject_elements
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Rubizon
|
2
|
+
class RubizonError < StandardError; end
|
3
|
+
class UnsupportedSignatureMethodError < StandardError; end
|
4
|
+
class UnsupportedSignatureVersionError < StandardError; end
|
5
|
+
class InvalidParameterError < RubizonError
|
6
|
+
attr_reader :message
|
7
|
+
def initialize(message='An invalid parameter is in the request')
|
8
|
+
@message= message
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require File.dirname(__FILE__) + "/../abstract_sig2_product"
|
3
|
+
module Rubizon
|
4
|
+
# Define a class that generates requests for operations on the Product
|
5
|
+
# Advertising API
|
6
|
+
#
|
7
|
+
class ProductAdvertisingProduct < AbstractSig2Product
|
8
|
+
# Initialize the product interface.
|
9
|
+
#
|
10
|
+
# credentials - A SecurityCredentials object that encapsulates the
|
11
|
+
# access and secret ids to be used for this product.
|
12
|
+
# scheme - (optional - default: http) May set to 'https' if supported.
|
13
|
+
def initialize(credentials,scheme='http')
|
14
|
+
super(
|
15
|
+
:scheme=>scheme,
|
16
|
+
:host=>'webservices.amazon.com',
|
17
|
+
:path=>'/onca/xml',
|
18
|
+
'_omit' => ['SignatureMethod','SignatureVersion'],
|
19
|
+
'Service'=>'AWSECommerceService'
|
20
|
+
)
|
21
|
+
@credentials= credentials
|
22
|
+
end
|
23
|
+
|
24
|
+
# Create a request for an item lookup. The URL to use may be obtained
|
25
|
+
# from the request.
|
26
|
+
#
|
27
|
+
# params - Parameters for the specific request as key/value pairs.
|
28
|
+
def item_lookup_request(subject_elements={})
|
29
|
+
basic_action(
|
30
|
+
@item_lookup_elements||= {'Operation'=>'ItemLookup'},
|
31
|
+
subject_elements
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require File.dirname(__FILE__) + "/../abstract_sig2_product"
|
3
|
+
module Rubizon
|
4
|
+
# Define a class that generates requests for operations on a topic of the
|
5
|
+
# Simple Notification Service (SNS).
|
6
|
+
#
|
7
|
+
class SimpleNotificationService < AbstractSig2Product
|
8
|
+
# Initialize the SNS interface. Each instance supports requests to one
|
9
|
+
# topic.
|
10
|
+
#
|
11
|
+
# credentials - A SecurityCredentials object that encapsulates the
|
12
|
+
# access and secret ids to be used for this product.
|
13
|
+
# arn - The topic served by this object
|
14
|
+
# scheme - (optional - default: http) May set to 'https' if supported.
|
15
|
+
def initialize(credentials,host,scheme='http')
|
16
|
+
super(
|
17
|
+
:scheme=>scheme,
|
18
|
+
:host=>host
|
19
|
+
)
|
20
|
+
@credentials= credentials
|
21
|
+
end
|
22
|
+
|
23
|
+
# Create a Request object that can be used to formulate a single request
|
24
|
+
# for this product.
|
25
|
+
#
|
26
|
+
# Returns an instance of Request
|
27
|
+
def create_request
|
28
|
+
super(@credentials)
|
29
|
+
end
|
30
|
+
|
31
|
+
class Topic
|
32
|
+
# Specify the topic.
|
33
|
+
#
|
34
|
+
# sns - An instance of the SimpleNotificationService class
|
35
|
+
# arn - Specify the topic's ARN
|
36
|
+
def initialize(sns,arn)
|
37
|
+
@sns= sns
|
38
|
+
@arn= arn
|
39
|
+
end
|
40
|
+
|
41
|
+
# Publish a message to the topic.
|
42
|
+
#
|
43
|
+
# message - The message you want to send to the topic.
|
44
|
+
# subject - Optional parameter to be used as the "Subject" line of when the message is delivered to e-mail endpoints.
|
45
|
+
#
|
46
|
+
# Returns the Request object. The url, and its elements may be obtained
|
47
|
+
# from the returned request object.
|
48
|
+
def publish(message,subject=nil)
|
49
|
+
request= create_request
|
50
|
+
request.add_query_elements('Action'=>'Publish','Message'=>message)
|
51
|
+
request.add_query_elements('Subject'=>subject) if subject
|
52
|
+
request
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
def create_request
|
57
|
+
@sns.create_request.add_query_elements('TopicArn'=>@arn)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Define a topic.
|
62
|
+
#
|
63
|
+
# arn - The ARN of the topic
|
64
|
+
#
|
65
|
+
# Returns a SimpleNotificationService::Topic object, which includes methods
|
66
|
+
# for making requests of and for the topic.
|
67
|
+
def topic(arn)
|
68
|
+
Topic.new(self,arn)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
module Rubizon
|
3
|
+
# Represents a request to be made to AWS.
|
4
|
+
#
|
5
|
+
# Starts with access credentials and specifications for the product,
|
6
|
+
# such as its ARN, elements of its URL and query elements.
|
7
|
+
# To this is added specifications for the specific action to be performed.
|
8
|
+
#
|
9
|
+
# Request builds a URL for the action and signs the request. It then
|
10
|
+
# makes the entire URL and various components of it available for whatever
|
11
|
+
# transport mechanism is to be used.
|
12
|
+
#
|
13
|
+
# A new instance of Request is to be created for each request sent to AWS
|
14
|
+
class Request
|
15
|
+
# Initialize the request.
|
16
|
+
#
|
17
|
+
# credentials - A Rubizon::SecurityCredentials object that
|
18
|
+
# encapsulates an AWS key pair.
|
19
|
+
# It will be used to sign the request.
|
20
|
+
# scheme - The scheme to be used: 'http' or 'https'
|
21
|
+
# host - The name of the HTTP host to serve this request
|
22
|
+
# path - The URL path, typically '/'.
|
23
|
+
# query_elements - A hash of key/value pairs to be included in the
|
24
|
+
# query string.
|
25
|
+
# _omit - If an element with a key of '_omit' is included,
|
26
|
+
# the value must be an array containing names of keys
|
27
|
+
# to be omitted from the query_elements array before
|
28
|
+
# the request is signed and the query string formed.
|
29
|
+
# This allows allowing SignatureMethod or
|
30
|
+
# SignatureVersion to be specified, even if they are
|
31
|
+
# not to be included in the query string.
|
32
|
+
# SignatureVersion - The signature version to be used, such
|
33
|
+
# as 1 or 2. This should be a numeric value. If not
|
34
|
+
# present, signature version 1 is implied.
|
35
|
+
# SignatureMethod - The signature methods defined for version
|
36
|
+
# two are HmacSHA256 and HmacSHA1.
|
37
|
+
def initialize(credentials,scheme,host,path,query_elements={})
|
38
|
+
@credentials= credentials
|
39
|
+
@scheme= scheme
|
40
|
+
@host= host
|
41
|
+
@path= path
|
42
|
+
@query_elements= query_elements.dup
|
43
|
+
end
|
44
|
+
|
45
|
+
# Append additional elements to the currently defined path.
|
46
|
+
def append_to_path(path)
|
47
|
+
@path+= path
|
48
|
+
end
|
49
|
+
|
50
|
+
# Replace the currently defined path with the one given.
|
51
|
+
def path=(path)
|
52
|
+
@path= path
|
53
|
+
end
|
54
|
+
|
55
|
+
# Add key/value pairs to the query_elements. Typically, these additional
|
56
|
+
# elements will be added to specify the action to be taken or subject of
|
57
|
+
# that action and parameters of that action or subject.
|
58
|
+
#
|
59
|
+
# query_elements - A Hash containing key/value pairs to be added to the
|
60
|
+
# query string.
|
61
|
+
#
|
62
|
+
# returns self
|
63
|
+
def add_query_elements(query_elements)
|
64
|
+
@query_elements.merge! query_elements
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns the URL scheme, such as http or https
|
69
|
+
attr_reader :scheme
|
70
|
+
|
71
|
+
# Returns the host (domain and subdomains) of the product served by this
|
72
|
+
# object.
|
73
|
+
attr_reader :host
|
74
|
+
|
75
|
+
# Returns the path part of the URL of the product served by this object,
|
76
|
+
# typically '/'.
|
77
|
+
attr_reader :path
|
78
|
+
|
79
|
+
# Returns the product's endpoint. The endpoint is that part of a URL that
|
80
|
+
# includes the scheme, host, and path, but not the query string.
|
81
|
+
# An action may extend the product's path, but otherwise would retain the
|
82
|
+
# rest of the endpoint.
|
83
|
+
def endpoint
|
84
|
+
@endpoint||= "#{@scheme}://#{@host}#{@path}"
|
85
|
+
end
|
86
|
+
|
87
|
+
# Create a query string from a hash and sign it.
|
88
|
+
# The signature algorithm will be determined from the query elements,
|
89
|
+
# such as SignatureVersion
|
90
|
+
#
|
91
|
+
# The query string, once created, is immutable.
|
92
|
+
def query_string
|
93
|
+
return @query_string ||=
|
94
|
+
if @query_elements['SignatureVersion'].to_i == 2
|
95
|
+
query_string_sig2
|
96
|
+
else
|
97
|
+
raise UnsupportedSignatureVersionError, 'Only signature version 2 requests are supported at this time'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns the full URL
|
102
|
+
#
|
103
|
+
# The query string portion of the URL, once created, is immutable.
|
104
|
+
def url
|
105
|
+
endpoint+'?'+query_string
|
106
|
+
end
|
107
|
+
|
108
|
+
# An artifact of the signature version 2 signing process:
|
109
|
+
# The query string portion of the string to sign.
|
110
|
+
# This is of possible debugging value.
|
111
|
+
attr_reader :canonical_querystring
|
112
|
+
|
113
|
+
# An artifact of the signature version 2 signing process:
|
114
|
+
# The string that is used to calculate the signature.
|
115
|
+
# This is of possible debugging value.
|
116
|
+
attr_reader :string_to_sign
|
117
|
+
|
118
|
+
protected
|
119
|
+
# Create a query string and sign it using the signature version 2 algorithm.
|
120
|
+
def query_string_sig2
|
121
|
+
@query_elements['Timestamp']= Time::at(Time.now).utc.strftime("%Y-%m-%dT%H:%M:%S.000Z") unless @query_elements['Timestamp']
|
122
|
+
@query_elements['AWSAccessKeyId']= @credentials.accessID
|
123
|
+
signature_method= @query_elements['SignatureMethod']
|
124
|
+
if @query_elements['_omit']
|
125
|
+
@query_elements['_omit'].each do |k|
|
126
|
+
@query_elements.delete k
|
127
|
+
end
|
128
|
+
@query_elements.delete '_omit'
|
129
|
+
end
|
130
|
+
values = @query_elements.keys.sort.collect {|key| [url_encode(key), url_encode(@query_elements[key])].join("=") }
|
131
|
+
@canonical_querystring= values.join("&")
|
132
|
+
@string_to_sign = <<"____".rstrip
|
133
|
+
GET
|
134
|
+
#{URI::parse(endpoint).host}
|
135
|
+
#{URI::parse(endpoint).path}
|
136
|
+
#{@canonical_querystring}
|
137
|
+
____
|
138
|
+
signature= @credentials.sign(signature_method,@string_to_sign)
|
139
|
+
@query_elements['Signature'] = signature
|
140
|
+
@query_elements.collect { |key, value| [url_encode(key), url_encode(value)].join("=") }.join('&') # order doesn't matter for the actual request
|
141
|
+
end
|
142
|
+
def url_encode(string)
|
143
|
+
string = string.to_s
|
144
|
+
# It's kind of like CGI.escape, except CGI.escape is encoding a tilde when
|
145
|
+
# it ought not to be, so we turn it back. Also space NEEDS to be %20 not +.
|
146
|
+
return CGI.escape(string).gsub("%7E", "~").gsub("+", "%20")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'hmac-sha2'
|
2
|
+
require 'base64'
|
3
|
+
require File.dirname(__FILE__) + '/exceptions'
|
4
|
+
module Rubizon
|
5
|
+
class SecurityCredentials
|
6
|
+
attr_reader :accessID
|
7
|
+
def initialize(accessID, secretID)
|
8
|
+
@accessID= accessID
|
9
|
+
@secretID= secretID
|
10
|
+
end
|
11
|
+
def sign256(string_to_sign)
|
12
|
+
@hmac256||= HMAC::SHA256.new(@secretID)
|
13
|
+
@hmac256.update(string_to_sign)
|
14
|
+
Base64.encode64(@hmac256.digest).chomp
|
15
|
+
end
|
16
|
+
def sign(signature_method,string_to_sign)
|
17
|
+
case signature_method
|
18
|
+
when 'HmacSHA256'
|
19
|
+
sign256(string_to_sign)
|
20
|
+
else
|
21
|
+
raise UnsupportedSignatureMethodError, "The #{signature_method} signature method is not supported"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/rubizon.rb
ADDED
data/rubizon.gemspec
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{rubizon}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Randy McLaughlin"]
|
12
|
+
s.date = %q{2011-01-03}
|
13
|
+
s.description = %q{A Ruby interface to Amazon Web Services. Rubizon separates creating a
|
14
|
+
properly-formed, signed URL for making an AWS request from the transport
|
15
|
+
mechanism used.
|
16
|
+
|
17
|
+
In its initial implementation, Rubizon simply builds and signs URLs. Further
|
18
|
+
development may include adapters to various transport mechanisms and
|
19
|
+
interpretation of results.
|
20
|
+
}
|
21
|
+
s.email = %q{ot40ddj02@sneakemail.com}
|
22
|
+
s.extra_rdoc_files = [
|
23
|
+
"LICENSE.txt",
|
24
|
+
"README.rdoc"
|
25
|
+
]
|
26
|
+
s.files = [
|
27
|
+
".document",
|
28
|
+
"Gemfile",
|
29
|
+
"Gemfile.lock",
|
30
|
+
"LICENSE.txt",
|
31
|
+
"README.rdoc",
|
32
|
+
"Rakefile",
|
33
|
+
"VERSION",
|
34
|
+
"lib/rubizon.rb",
|
35
|
+
"lib/rubizon/abstract_sig2_product.rb",
|
36
|
+
"lib/rubizon/exceptions.rb",
|
37
|
+
"lib/rubizon/product/product_advertising.rb",
|
38
|
+
"lib/rubizon/product/sns.rb",
|
39
|
+
"lib/rubizon/request.rb",
|
40
|
+
"lib/rubizon/security_credentials.rb",
|
41
|
+
"rubizon.gemspec",
|
42
|
+
"test/helper.rb",
|
43
|
+
"test/test_abstract_sig2_product.rb",
|
44
|
+
"test/test_request.rb",
|
45
|
+
"test/test_security_credentials.rb",
|
46
|
+
"test/test_signature_sample.rb",
|
47
|
+
"test/test_sns.rb"
|
48
|
+
]
|
49
|
+
s.homepage = %q{http://github.com/randymized/rubizon}
|
50
|
+
s.licenses = ["MIT"]
|
51
|
+
s.require_paths = ["lib"]
|
52
|
+
s.rubygems_version = %q{1.3.7}
|
53
|
+
s.summary = %q{A Ruby interface to Amazon Web Services}
|
54
|
+
s.test_files = [
|
55
|
+
"test/helper.rb",
|
56
|
+
"test/test_abstract_sig2_product.rb",
|
57
|
+
"test/test_request.rb",
|
58
|
+
"test/test_security_credentials.rb",
|
59
|
+
"test/test_signature_sample.rb",
|
60
|
+
"test/test_sns.rb"
|
61
|
+
]
|
62
|
+
|
63
|
+
if s.respond_to? :specification_version then
|
64
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
65
|
+
s.specification_version = 3
|
66
|
+
|
67
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
68
|
+
s.add_runtime_dependency(%q<ruby-hmac>, ["~> 0.4.0"])
|
69
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
70
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
71
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
|
72
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
73
|
+
s.add_runtime_dependency(%q<ruby-hmac>, ["~> 0.4.0"])
|
74
|
+
else
|
75
|
+
s.add_dependency(%q<ruby-hmac>, ["~> 0.4.0"])
|
76
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
77
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
78
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
|
79
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
80
|
+
s.add_dependency(%q<ruby-hmac>, ["~> 0.4.0"])
|
81
|
+
end
|
82
|
+
else
|
83
|
+
s.add_dependency(%q<ruby-hmac>, ["~> 0.4.0"])
|
84
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
85
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
86
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
|
87
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
88
|
+
s.add_dependency(%q<ruby-hmac>, ["~> 0.4.0"])
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'test/unit'
|
11
|
+
require 'shoulda'
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
15
|
+
require 'rubizon'
|
16
|
+
|
17
|
+
#access and secret ids are from the example REST requests at
|
18
|
+
#http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html?rest-signature.html
|
19
|
+
AWSAccessKeyId='00000000000000000000'
|
20
|
+
SecretAccessKeyId= '1234567890'
|
21
|
+
|
22
|
+
class Test::Unit::TestCase
|
23
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestAbstractSig2Product < Test::Unit::TestCase
|
4
|
+
context "An AbstractSig2Product instance" do
|
5
|
+
setup do
|
6
|
+
@region= 'us-east-1'
|
7
|
+
@product= 'sns'
|
8
|
+
@arn= "arn:aws:#{@product}:#{@region}:123456789:My-Topic" #arn:aws:sns:us-east-1:123456789:My-Topic
|
9
|
+
end
|
10
|
+
should "calculate the hostname if only an ARN is specified" do
|
11
|
+
# If the ARN is: arn:aws:sns:us-east-1:123456789:My-Topic
|
12
|
+
# the host would be: sns.us-east-1.amazonaws.com
|
13
|
+
# The host may be specified specifically if it cannot be deduced from the ARN in this way
|
14
|
+
prod= Rubizon::AbstractSig2Product.new(:arn=>@arn)
|
15
|
+
assert_equal "#{@product}.#{@region}.amazonaws.com", prod.host
|
16
|
+
end
|
17
|
+
should "return any specific host name, even if a different one might be calculated from a specified ARN" do
|
18
|
+
prod= Rubizon::AbstractSig2Product.new(:arn=>@arn, :host=>'foo.com')
|
19
|
+
assert_equal 'foo.com', prod.host
|
20
|
+
end
|
21
|
+
should "determine the host if given a URL" do
|
22
|
+
prod= Rubizon::AbstractSig2Product.new(:url=>'http://foo.com/')
|
23
|
+
assert_equal 'foo.com', prod.host
|
24
|
+
end
|
25
|
+
should "determine the scheme if given a URL" do
|
26
|
+
prod= Rubizon::AbstractSig2Product.new(:url=>'ftp://foo.com/')
|
27
|
+
assert_equal 'ftp', prod.scheme
|
28
|
+
end
|
29
|
+
should "determine the path if given a URL" do
|
30
|
+
prod= Rubizon::AbstractSig2Product.new(:url=>'http://foo.com/bar')
|
31
|
+
assert_equal '/bar', prod.path
|
32
|
+
end
|
33
|
+
should "calculate the URL if only an ARN is specified" do
|
34
|
+
# If the ARN is: arn:aws:sns:us-east-1:123456789:My-Topic
|
35
|
+
# the URL would be: https://sns.us-east-1.amazonaws.com/
|
36
|
+
# The host may be specified specifically if it cannot be deduced from the ARN in this way
|
37
|
+
prod= Rubizon::AbstractSig2Product.new(:arn=>@arn)
|
38
|
+
assert_equal "https://#{@product}.#{@region}.amazonaws.com/", prod.endpoint
|
39
|
+
end
|
40
|
+
should "calculate the URL if only an ARN and a scheme is specified" do
|
41
|
+
prod= Rubizon::AbstractSig2Product.new(:arn=>@arn,:scheme=>:http)
|
42
|
+
assert_equal "http://#{@product}.#{@region}.amazonaws.com/", prod.endpoint
|
43
|
+
end
|
44
|
+
should "calculate the URL if only host is specified" do
|
45
|
+
prod= Rubizon::AbstractSig2Product.new(:host=>'example.com')
|
46
|
+
assert_equal "https://example.com/", prod.endpoint
|
47
|
+
end
|
48
|
+
should "prefer a specified host over one built from ARN" do
|
49
|
+
prod= Rubizon::AbstractSig2Product.new(:host=>'foo.com',:scheme=>'http',:arn=>@arn)
|
50
|
+
assert_equal 'http://foo.com/', prod.endpoint
|
51
|
+
end
|
52
|
+
should "return any specified path" do
|
53
|
+
prod= Rubizon::AbstractSig2Product.new(:host=>'x.com',:path=>'abc/xyz')
|
54
|
+
assert_equal 'abc/xyz', prod.path
|
55
|
+
end
|
56
|
+
should "append any specified path to a URL generated from an ARN" do
|
57
|
+
prod= Rubizon::AbstractSig2Product.new(:arn=>@arn, :path=>'/abc/xyz')
|
58
|
+
assert_equal "https://#{@product}.#{@region}.amazonaws.com/abc/xyz", prod.endpoint
|
59
|
+
end
|
60
|
+
should "return query elements not related to specifying the endpoint in the query_elements hash" do
|
61
|
+
prod= Rubizon::AbstractSig2Product.new(
|
62
|
+
'arn'=>@arn,
|
63
|
+
'scheme'=>'http',
|
64
|
+
'host'=>'foo.com',
|
65
|
+
'path'=>'/abc/xyz',
|
66
|
+
'foo'=>'bar'
|
67
|
+
)
|
68
|
+
assert_equal 4, prod.query_elements.size
|
69
|
+
assert_equal 'bar', prod.query_elements['foo']
|
70
|
+
assert_equal 'HmacSHA256', prod.query_elements['SignatureMethod']
|
71
|
+
assert_equal 2, prod.query_elements['SignatureVersion']
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'rubizon/abstract_sig2_product' # this would normally be required by the specific product's implementation file
|
3
|
+
|
4
|
+
class TestRequest < Test::Unit::TestCase
|
5
|
+
@@credentials= Rubizon::SecurityCredentials.new('00000000000000000000','1234567890')
|
6
|
+
@@eCommerceServiceProduct= Rubizon::AbstractSig2Product.new(
|
7
|
+
:scheme=>'http',
|
8
|
+
:host=>'webservices.amazon.com',
|
9
|
+
:path=>'/onca/xml',
|
10
|
+
'_omit' => ['SignatureMethod','SignatureVersion']
|
11
|
+
)
|
12
|
+
@@eCommerceServiceRequestElements= {
|
13
|
+
'Service'=>'AWSECommerceService',
|
14
|
+
'Operation'=>'ItemLookup',
|
15
|
+
'ItemId'=>'0679722769',
|
16
|
+
'ResponseGroup'=>'ItemAttributes,Offers,Images,Reviews',
|
17
|
+
'Version'=>'2009-01-06',
|
18
|
+
'Timestamp'=>'2009-01-01T12:00:00Z'
|
19
|
+
}
|
20
|
+
@@expectedSignature= 'Nace%2BU3Az4OhN7tISqgs1vdLBHBEijWcBeCqL5xN9xg%3D'
|
21
|
+
context "A Request instance" do
|
22
|
+
setup do
|
23
|
+
@region= 'us-east-1'
|
24
|
+
@product= 'sns'
|
25
|
+
@arn= "arn:aws:#{@product}:#{@region}:123456789:My-Topic" #arn:aws:sns:us-east-1:123456789:My-Topic
|
26
|
+
@host= "#{@product}.#{@region}.amazonaws.com"
|
27
|
+
end
|
28
|
+
should "report the product's host" do
|
29
|
+
prod= Rubizon::AbstractSig2Product.new(
|
30
|
+
:arn=>@arn
|
31
|
+
)
|
32
|
+
req= prod.create_request(@@credentials)
|
33
|
+
assert_equal @host, req.host
|
34
|
+
end
|
35
|
+
should "report the product's scheme" do
|
36
|
+
prod= Rubizon::AbstractSig2Product.new(
|
37
|
+
:arn=>@arn
|
38
|
+
)
|
39
|
+
req= prod.create_request(@@credentials)
|
40
|
+
assert_equal 'https', req.scheme
|
41
|
+
end
|
42
|
+
should "report the product's path" do
|
43
|
+
prod= Rubizon::AbstractSig2Product.new(
|
44
|
+
:arn=>@arn
|
45
|
+
)
|
46
|
+
req= prod.create_request(@@credentials)
|
47
|
+
assert_equal '/', req.path
|
48
|
+
end
|
49
|
+
should "report the product's endpoint" do
|
50
|
+
prod= Rubizon::AbstractSig2Product.new(
|
51
|
+
:arn=>@arn
|
52
|
+
)
|
53
|
+
req= prod.create_request(@@credentials)
|
54
|
+
assert_equal 'https://sns.us-east-1.amazonaws.com/', req.endpoint
|
55
|
+
end
|
56
|
+
should "Create a URL that contains the expected host name for the sample request" do
|
57
|
+
req= @@eCommerceServiceProduct.create_request(@@credentials)
|
58
|
+
req.add_query_elements @@eCommerceServiceRequestElements
|
59
|
+
uri = URI.parse(req.url);
|
60
|
+
assert_equal @@eCommerceServiceProduct.host, uri.host
|
61
|
+
end
|
62
|
+
should "Create a URL that contains the expected path for the sample request" do
|
63
|
+
req= @@eCommerceServiceProduct.create_request(@@credentials)
|
64
|
+
req.add_query_elements @@eCommerceServiceRequestElements
|
65
|
+
uri = URI.parse(req.url);
|
66
|
+
assert_equal @@eCommerceServiceProduct.path, uri.path
|
67
|
+
end
|
68
|
+
should "Create a URL that contains the expected scheme for the sample request" do
|
69
|
+
req= @@eCommerceServiceProduct.create_request(@@credentials)
|
70
|
+
req.add_query_elements @@eCommerceServiceRequestElements
|
71
|
+
uri = URI.parse(req.url);
|
72
|
+
assert_equal @@eCommerceServiceProduct.scheme, uri.scheme
|
73
|
+
end
|
74
|
+
should "Create a URL that contains the expected query string for the sample request" do
|
75
|
+
req= @@eCommerceServiceProduct.create_request(@@credentials)
|
76
|
+
req.add_query_elements @@eCommerceServiceRequestElements
|
77
|
+
uri = URI.parse(req.url);
|
78
|
+
query= CGI::parse(uri.query)
|
79
|
+
@@eCommerceServiceRequestElements.each do |k,v|
|
80
|
+
assert_equal query.delete(k).first, v
|
81
|
+
end
|
82
|
+
assert_equal query.delete('AWSAccessKeyId').first, @@credentials.accessID
|
83
|
+
assert_equal CGI::escape(query.delete('Signature').first), @@expectedSignature
|
84
|
+
assert query.empty? #there's nothing left! All elements are accounted for
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestSecurityCredentials < Test::Unit::TestCase
|
4
|
+
context "A SecurityCredentials instance" do
|
5
|
+
setup do
|
6
|
+
@id= Rubizon::SecurityCredentials.new(AWSAccessKeyId,SecretAccessKeyId)
|
7
|
+
@arbitrary_string= 'x'
|
8
|
+
@expected_signature= '6KClHg2k6AiXNRwaLa7sC7LIxP4NkUieZheem0eHnBI='
|
9
|
+
end
|
10
|
+
should "report the access key ID" do
|
11
|
+
assert_equal AWSAccessKeyId, @id.accessID
|
12
|
+
end
|
13
|
+
should "not allow accessing the secret access key ID" do
|
14
|
+
assert !@id.respond_to?(:secretID)
|
15
|
+
assert !@id.respond_to?(:secretKeyID)
|
16
|
+
assert !@id.respond_to?(:secretAccessKeyID)
|
17
|
+
assert !@id.respond_to?(:awsSecretAccessKeyID)
|
18
|
+
end
|
19
|
+
should "sign an arbitrary string using the secretID" do
|
20
|
+
assert_equal @expected_signature, @id.sign256(@arbitrary_string)
|
21
|
+
end
|
22
|
+
should "produce a different signature if initialized with a different access key" do
|
23
|
+
assert_not_equal @expected_signature, Rubizon::SecurityCredentials.new(AWSAccessKeyId,SecretAccessKeyId+'x').sign256(@arbitrary_string)
|
24
|
+
end
|
25
|
+
should "sign an arbitrary string based upon a signature method of HmacSHA256" do
|
26
|
+
assert_equal @expected_signature, @id.sign('HmacSHA256',@arbitrary_string)
|
27
|
+
end
|
28
|
+
should "raise an exception if an unsupported signature method is requested" do
|
29
|
+
assert_raise(Rubizon::UnsupportedSignatureMethodError) do
|
30
|
+
@id.sign('foo',@arbitrary_string)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'rubizon/product/product_advertising'
|
3
|
+
|
4
|
+
# This is a special test based upon a signature generation sample at
|
5
|
+
# http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html?rest-signature.html
|
6
|
+
# That is the only sample I know of where an actual signature, based upon
|
7
|
+
# simulated security credentials and request parameters is documented.
|
8
|
+
|
9
|
+
# This also serves as a test of the integration of security credentials,
|
10
|
+
# Actions, Products and Requests in order to generate a properly formed URL.
|
11
|
+
|
12
|
+
class TestSignatureSample < Test::Unit::TestCase
|
13
|
+
@@credentials= Rubizon::SecurityCredentials.new('00000000000000000000','1234567890')
|
14
|
+
@@eCommerceServiceProduct= Rubizon::ProductAdvertisingProduct.new(@@credentials)
|
15
|
+
@@eCommerceServiceRequestSubject= {
|
16
|
+
'ItemId'=>'0679722769',
|
17
|
+
'ResponseGroup'=>'ItemAttributes,Offers,Images,Reviews',
|
18
|
+
'Version'=>'2009-01-06',
|
19
|
+
'Timestamp'=>'2009-01-01T12:00:00Z'
|
20
|
+
}
|
21
|
+
@@expectedSignature= 'Nace%2BU3Az4OhN7tISqgs1vdLBHBEijWcBeCqL5xN9xg%3D'
|
22
|
+
context "The signature generation example" do
|
23
|
+
should "calculate the signature (version 2) expected in the example" do
|
24
|
+
req= @@eCommerceServiceProduct.item_lookup_request(@@eCommerceServiceRequestSubject)
|
25
|
+
q= req.query_string
|
26
|
+
assert_equal <<____.rstrip, req.canonical_querystring
|
27
|
+
AWSAccessKeyId=00000000000000000000&ItemId=0679722769&Operation=ItemLookup&ResponseGroup=ItemAttributes%2COffers%2CImages%2CReviews&Service=AWSECommerceService&Timestamp=2009-01-01T12%3A00%3A00Z&Version=2009-01-06
|
28
|
+
____
|
29
|
+
assert_equal <<____.rstrip, req.string_to_sign
|
30
|
+
GET
|
31
|
+
webservices.amazon.com
|
32
|
+
/onca/xml
|
33
|
+
AWSAccessKeyId=00000000000000000000&ItemId=0679722769&Operation=ItemLookup&ResponseGroup=ItemAttributes%2COffers%2CImages%2CReviews&Service=AWSECommerceService&Timestamp=2009-01-01T12%3A00%3A00Z&Version=2009-01-06
|
34
|
+
____
|
35
|
+
assert_equal @@expectedSignature, CGI::escape(CGI::parse(q)['Signature'].first)
|
36
|
+
end
|
37
|
+
should "Create a URL that contains the expected host name for the sample request" do
|
38
|
+
req= @@eCommerceServiceProduct.item_lookup_request(@@eCommerceServiceRequestSubject)
|
39
|
+
uri = URI.parse(req.url);
|
40
|
+
assert_equal @@eCommerceServiceProduct.host, uri.host
|
41
|
+
end
|
42
|
+
should "Create a URL that contains the expected path for the sample request" do
|
43
|
+
req= @@eCommerceServiceProduct.item_lookup_request(@@eCommerceServiceRequestSubject)
|
44
|
+
uri = URI.parse(req.url);
|
45
|
+
assert_equal @@eCommerceServiceProduct.path, uri.path
|
46
|
+
end
|
47
|
+
should "Create a URL that contains the expected scheme for the sample request" do
|
48
|
+
req= @@eCommerceServiceProduct.item_lookup_request(@@eCommerceServiceRequestSubject)
|
49
|
+
uri = URI.parse(req.url);
|
50
|
+
assert_equal @@eCommerceServiceProduct.scheme, uri.scheme
|
51
|
+
end
|
52
|
+
should "Create a URL that contains the expected query string for the sample request" do
|
53
|
+
req= @@eCommerceServiceProduct.item_lookup_request(@@eCommerceServiceRequestSubject)
|
54
|
+
uri = URI.parse(req.url);
|
55
|
+
query= CGI::parse(uri.query)
|
56
|
+
@@eCommerceServiceRequestSubject.each do |k,v|
|
57
|
+
assert_equal query.delete(k).first, v
|
58
|
+
end
|
59
|
+
assert_equal 'ItemLookup', CGI::escape(query.delete('Operation').first)
|
60
|
+
assert_equal 'AWSECommerceService', CGI::escape(query.delete('Service').first)
|
61
|
+
assert_equal @@credentials.accessID, query.delete('AWSAccessKeyId').first
|
62
|
+
assert_equal @@expectedSignature, CGI::escape(query.delete('Signature').first)
|
63
|
+
assert query.empty? #there's nothing left! All elements are accounted for
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/test/test_sns.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'rubizon/product/sns'
|
3
|
+
|
4
|
+
class TestSNS < Test::Unit::TestCase
|
5
|
+
@@access_key= '00000000000000000000'
|
6
|
+
@@credentials= Rubizon::SecurityCredentials.new(@@access_key,'1234567890')
|
7
|
+
@@arn= 'arn:aws:sns:us-east-1:123456789:My-Topic'
|
8
|
+
@@host= 'sns.us-east-1.amazonaws.com'
|
9
|
+
@@snsProduct= Rubizon::SimpleNotificationService.new(@@credentials,@@host)
|
10
|
+
context "A SimpleNotificationService instance" do
|
11
|
+
should "formulate a url that will publish a message to a topic" do
|
12
|
+
message= 'hello world'
|
13
|
+
req= @@snsProduct.topic(@@arn).publish(message)
|
14
|
+
assert_equal "http://#{@@host}/", req.endpoint
|
15
|
+
q= CGI::parse(req.query_string)
|
16
|
+
assert_equal '2', q['SignatureVersion'].first
|
17
|
+
assert_equal 'HmacSHA256', q['SignatureMethod'].first
|
18
|
+
assert q['Signature'].first.is_a?(String)
|
19
|
+
assert_equal 44, CGI::unescape(q['Signature'].first).length
|
20
|
+
assert_equal @@access_key, q['AWSAccessKeyId'].first
|
21
|
+
assert q.has_key?('Timestamp')
|
22
|
+
assert_equal 'Publish', q['Action'].first
|
23
|
+
assert_equal @@arn, CGI::unescape(q['TopicArn'].first)
|
24
|
+
assert_equal message, CGI::unescape(q['Message'].first)
|
25
|
+
assert !q.has_key?('Subject')
|
26
|
+
end
|
27
|
+
should "formulate a url that will publish a message and a subject to a topic" do
|
28
|
+
message= 'world'
|
29
|
+
subject= 'An important word'
|
30
|
+
req= @@snsProduct.topic(@@arn).publish(message,subject)
|
31
|
+
q= CGI::parse(req.query_string)
|
32
|
+
assert_equal 'Publish', q['Action'].first
|
33
|
+
assert_equal @@arn, CGI::unescape(q['TopicArn'].first)
|
34
|
+
assert_equal message, CGI::unescape(q['Message'].first)
|
35
|
+
assert_equal subject, CGI::unescape(q['Subject'].first)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rubizon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Randy McLaughlin
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-01-03 00:00:00 -06:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: ruby-hmac
|
22
|
+
type: :runtime
|
23
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
- 4
|
31
|
+
- 0
|
32
|
+
version: 0.4.0
|
33
|
+
prerelease: false
|
34
|
+
requirement: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: shoulda
|
37
|
+
type: :development
|
38
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
prerelease: false
|
47
|
+
requirement: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: bundler
|
50
|
+
type: :development
|
51
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ~>
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
segments:
|
57
|
+
- 1
|
58
|
+
- 0
|
59
|
+
- 0
|
60
|
+
version: 1.0.0
|
61
|
+
prerelease: false
|
62
|
+
requirement: *id003
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: jeweler
|
65
|
+
type: :development
|
66
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ~>
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
segments:
|
72
|
+
- 1
|
73
|
+
- 5
|
74
|
+
- 1
|
75
|
+
version: 1.5.1
|
76
|
+
prerelease: false
|
77
|
+
requirement: *id004
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rcov
|
80
|
+
type: :development
|
81
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
prerelease: false
|
90
|
+
requirement: *id005
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: ruby-hmac
|
93
|
+
type: :runtime
|
94
|
+
version_requirements: &id006 !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ~>
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
segments:
|
100
|
+
- 0
|
101
|
+
- 4
|
102
|
+
- 0
|
103
|
+
version: 0.4.0
|
104
|
+
prerelease: false
|
105
|
+
requirement: *id006
|
106
|
+
description: |
|
107
|
+
A Ruby interface to Amazon Web Services. Rubizon separates creating a
|
108
|
+
properly-formed, signed URL for making an AWS request from the transport
|
109
|
+
mechanism used.
|
110
|
+
|
111
|
+
In its initial implementation, Rubizon simply builds and signs URLs. Further
|
112
|
+
development may include adapters to various transport mechanisms and
|
113
|
+
interpretation of results.
|
114
|
+
|
115
|
+
email: ot40ddj02@sneakemail.com
|
116
|
+
executables: []
|
117
|
+
|
118
|
+
extensions: []
|
119
|
+
|
120
|
+
extra_rdoc_files:
|
121
|
+
- LICENSE.txt
|
122
|
+
- README.rdoc
|
123
|
+
files:
|
124
|
+
- .document
|
125
|
+
- Gemfile
|
126
|
+
- Gemfile.lock
|
127
|
+
- LICENSE.txt
|
128
|
+
- README.rdoc
|
129
|
+
- Rakefile
|
130
|
+
- VERSION
|
131
|
+
- lib/rubizon.rb
|
132
|
+
- lib/rubizon/abstract_sig2_product.rb
|
133
|
+
- lib/rubizon/exceptions.rb
|
134
|
+
- lib/rubizon/product/product_advertising.rb
|
135
|
+
- lib/rubizon/product/sns.rb
|
136
|
+
- lib/rubizon/request.rb
|
137
|
+
- lib/rubizon/security_credentials.rb
|
138
|
+
- rubizon.gemspec
|
139
|
+
- test/helper.rb
|
140
|
+
- test/test_abstract_sig2_product.rb
|
141
|
+
- test/test_request.rb
|
142
|
+
- test/test_security_credentials.rb
|
143
|
+
- test/test_signature_sample.rb
|
144
|
+
- test/test_sns.rb
|
145
|
+
has_rdoc: true
|
146
|
+
homepage: http://github.com/randymized/rubizon
|
147
|
+
licenses:
|
148
|
+
- MIT
|
149
|
+
post_install_message:
|
150
|
+
rdoc_options: []
|
151
|
+
|
152
|
+
require_paths:
|
153
|
+
- lib
|
154
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
155
|
+
none: false
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
hash: 3
|
160
|
+
segments:
|
161
|
+
- 0
|
162
|
+
version: "0"
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
164
|
+
none: false
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
segments:
|
169
|
+
- 0
|
170
|
+
version: "0"
|
171
|
+
requirements: []
|
172
|
+
|
173
|
+
rubyforge_project:
|
174
|
+
rubygems_version: 1.3.7
|
175
|
+
signing_key:
|
176
|
+
specification_version: 3
|
177
|
+
summary: A Ruby interface to Amazon Web Services
|
178
|
+
test_files:
|
179
|
+
- test/helper.rb
|
180
|
+
- test/test_abstract_sig2_product.rb
|
181
|
+
- test/test_request.rb
|
182
|
+
- test/test_security_credentials.rb
|
183
|
+
- test/test_signature_sample.rb
|
184
|
+
- test/test_sns.rb
|