amaze_sns 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +38 -0
- data/lib/amaze/exceptions.rb +34 -0
- data/lib/amaze/helpers.rb +31 -0
- data/lib/amaze/request.rb +94 -0
- data/lib/amaze/subscription.rb +17 -0
- data/lib/amaze/topic.rb +342 -0
- data/lib/amaze_sns.rb +147 -0
- data/spec/amaze_sns_spec.rb +138 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/topic_spec.rb +23 -0
- metadata +157 -0
data/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
Amazon SNS gem
|
2
|
+
==============
|
3
|
+
|
4
|
+
Introduction
|
5
|
+
---------
|
6
|
+
A Ruby gem for use with the Amazon Simple Notification service (http://aws.amazon.com/sns/).
|
7
|
+
|
8
|
+
Usage
|
9
|
+
---------------
|
10
|
+
|
11
|
+
AmazeSNS.skey = <your amazon aws secret key>
|
12
|
+
|
13
|
+
AmazeSNS.akey = <your amazon aws access key>
|
14
|
+
|
15
|
+
AmazeSNS['your_topic_name'] # creates a new Topic Object but not yet published
|
16
|
+
AmazeSNS['your_topic_name'].create # add new topic to local hash and to SNS
|
17
|
+
AmazeSNS['your_topic_name'].delete # removes it from both SNS and local hash
|
18
|
+
|
19
|
+
AmazeSNS.logger = my_logger # set a logger for the response
|
20
|
+
|
21
|
+
Dependencies
|
22
|
+
---------------
|
23
|
+
Require the CrackXML gem for parsing XML responses back from SNS; EventMachine
|
24
|
+
and EM-Http request gem for the requests; and ruby hmac gem for authenticating with Amazon Web Services
|
25
|
+
|
26
|
+
|
27
|
+
Tests
|
28
|
+
---------------
|
29
|
+
The specs are not fully working at the moment as the gem is undergoing a serious revamp.
|
30
|
+
|
31
|
+
The gem itself has been tested on systems running ruby 1.8.6 and ruby 1.8.7
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
Copyright
|
36
|
+
---------
|
37
|
+
|
38
|
+
Copyright (c) 2010 29 Steps UK. See LICENSE for details.
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class AmazeSNSRuntimeError < RuntimeError; end
|
2
|
+
class AmazeSNSError < StandardError; end
|
3
|
+
class AuthenticationError < AmazeSNSRuntimeError; end
|
4
|
+
|
5
|
+
class InvalidOptions < AmazeSNSError
|
6
|
+
def message
|
7
|
+
'Please supply valid options'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class AuthorizationError < AmazeSNSError
|
12
|
+
def message
|
13
|
+
'You do not have permission to access the resource'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class InternalError < AmazeSNSError
|
18
|
+
def message
|
19
|
+
'An internal service error has occured on the Simple Notification Service'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class InvalidParameterError < AmazeSNSError
|
24
|
+
def message
|
25
|
+
'An invalid parameter is in the request'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
class NotFoundError < AmazeSNSError
|
31
|
+
def message
|
32
|
+
'The requested resource is not found'
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
require "hmac"
|
3
|
+
require "hmac-sha2"
|
4
|
+
require "cgi"
|
5
|
+
require "time"
|
6
|
+
require "base64"
|
7
|
+
|
8
|
+
|
9
|
+
# HELPER METHODS
|
10
|
+
def url_encode(string)
|
11
|
+
string = string.to_s
|
12
|
+
# It's kinda like CGI.escape, except CGI.escape is encoding a tilde when
|
13
|
+
# it ought not to be, so we turn it back. Also space NEEDS to be %20 not +.
|
14
|
+
return CGI.escape(string).gsub("%7E", "~").gsub("+", "%20")
|
15
|
+
end
|
16
|
+
|
17
|
+
def canonical_querystring(params)
|
18
|
+
# I hope this built-in sort sorts by byte order, that's what's required.
|
19
|
+
values = params.keys.sort.collect {|key| [url_encode(key), url_encode(params[key])].join("=") }
|
20
|
+
|
21
|
+
return values.join("&")
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def hash_to_query(hash)
|
26
|
+
hash.collect do |key, value|
|
27
|
+
|
28
|
+
url_encode(key) + "=" + url_encode(value)
|
29
|
+
|
30
|
+
end.join("&")
|
31
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
|
2
|
+
require 'crack/xml'
|
3
|
+
|
4
|
+
require File.dirname(__FILE__) + "/helpers"
|
5
|
+
require File.dirname(__FILE__) + "/exceptions"
|
6
|
+
|
7
|
+
require 'em-http'
|
8
|
+
|
9
|
+
|
10
|
+
class Request
|
11
|
+
|
12
|
+
attr_accessor :params, :options, :httpresponse
|
13
|
+
|
14
|
+
def initialize(params, options={})
|
15
|
+
@params = params
|
16
|
+
@options = options
|
17
|
+
end
|
18
|
+
|
19
|
+
def process2
|
20
|
+
query_string = canonical_querystring(@params)
|
21
|
+
string_to_sign = "GET
|
22
|
+
#{AmazeSNS.host}
|
23
|
+
/
|
24
|
+
#{query_string}"
|
25
|
+
|
26
|
+
hmac = HMAC::SHA256.new(AmazeSNS.skey)
|
27
|
+
hmac.update( string_to_sign )
|
28
|
+
signature = Base64.encode64(hmac.digest).chomp
|
29
|
+
|
30
|
+
params['Signature'] = signature
|
31
|
+
querystring2 = params.collect { |key, value| [url_encode(key), url_encode(value)].join("=") }.join('&') # order doesn't matter for the actual request
|
32
|
+
response = HttpClient.get "#{AmazeSNS.host}?#{querystring2}"
|
33
|
+
parsed_response = Crack::XML.parse(response)
|
34
|
+
return parsed_response
|
35
|
+
end
|
36
|
+
|
37
|
+
def process
|
38
|
+
query_string = canonical_querystring(@params)
|
39
|
+
|
40
|
+
string_to_sign = "GET
|
41
|
+
#{AmazeSNS.host}
|
42
|
+
/
|
43
|
+
#{query_string}"
|
44
|
+
|
45
|
+
hmac = HMAC::SHA256.new(AmazeSNS.skey)
|
46
|
+
hmac.update( string_to_sign )
|
47
|
+
signature = Base64.encode64(hmac.digest).chomp
|
48
|
+
|
49
|
+
params['Signature'] = signature
|
50
|
+
querystring2 = params.collect { |key, value| [url_encode(key), url_encode(value)].join("=") }.join('&') # order doesn't matter for the actual request
|
51
|
+
|
52
|
+
unless defined?(EventMachine) && EventMachine.reactor_running?
|
53
|
+
raise AmazeSNSRuntimeError, "In order to use this you must be running inside an eventmachine loop"
|
54
|
+
end
|
55
|
+
|
56
|
+
require 'em-http' unless defined?(EventMachine::HttpRequest)
|
57
|
+
|
58
|
+
@httpresponse ||= http_class.new("http://#{AmazeSNS.host}/?#{querystring2}").send(:get)
|
59
|
+
@httpresponse.callback{ success_callback }
|
60
|
+
@httpresponse.errback{ error_callback }
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def http_class
|
65
|
+
EventMachine::HttpRequest
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
def success_callback
|
70
|
+
case @httpresponse.response_header.status
|
71
|
+
when 403
|
72
|
+
raise AuthorizationError
|
73
|
+
when 500
|
74
|
+
raise InternalError
|
75
|
+
when 400
|
76
|
+
raise InvalidParameterError
|
77
|
+
when 404
|
78
|
+
raise NotFoundError
|
79
|
+
else
|
80
|
+
call_user_success_handler
|
81
|
+
end #end case
|
82
|
+
end
|
83
|
+
|
84
|
+
def call_user_success_handler
|
85
|
+
@options[:on_success].call(httpresponse) if options[:on_success].respond_to?(:call)
|
86
|
+
end
|
87
|
+
|
88
|
+
def error_callback
|
89
|
+
EventMachine.stop
|
90
|
+
raise AmazeSNSRuntimeError.new("A runtime error has occured: status code: #{@httpresponse.response_header.status}")
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Subscription
|
2
|
+
|
3
|
+
attr_accessor :owner, :protocol, :topicarn, :endpoint, :subarn
|
4
|
+
|
5
|
+
def initialize(args)
|
6
|
+
@owner = args["Owner"]
|
7
|
+
@protocol = args["Protocol"]
|
8
|
+
@topicarn = args["TopicArn"]
|
9
|
+
@endpoint = args["Endpoint"]
|
10
|
+
@subarn = args["SubscriptionArn"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"Subscription: Owner - #{@owner} : Protocol - #{@protocol} : TopicARN - #{@topicarn} : EndPoint - #{@endpoint} : SubscriptionARN - #{@subarn}"
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
data/lib/amaze/topic.rb
ADDED
@@ -0,0 +1,342 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/request"
|
2
|
+
require File.dirname(__FILE__) + "/exceptions"
|
3
|
+
require "eventmachine"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
class Topic
|
7
|
+
|
8
|
+
attr_accessor :topic, :arn, :attrs
|
9
|
+
|
10
|
+
def initialize(topic, arn='')
|
11
|
+
@topic = topic
|
12
|
+
@arn = arn
|
13
|
+
@attrs ||= {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_request(params,&blk)
|
17
|
+
req_options={}
|
18
|
+
req_options[:on_success] = blk if blk
|
19
|
+
Request.new(params, req_options).process
|
20
|
+
end
|
21
|
+
|
22
|
+
# for running th EM loop w/o repetitions
|
23
|
+
def reactor(&blk)
|
24
|
+
EM.run do
|
25
|
+
instance_eval(&blk)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def create
|
30
|
+
params = {
|
31
|
+
'Name' => "#{@topic}",
|
32
|
+
'Action' => 'CreateTopic',
|
33
|
+
'SignatureMethod' => 'HmacSHA256',
|
34
|
+
'SignatureVersion' => 2,
|
35
|
+
'Timestamp' => Time.now.iso8601,
|
36
|
+
'AWSAccessKeyId' => AmazeSNS.akey
|
37
|
+
}
|
38
|
+
|
39
|
+
reactor{
|
40
|
+
generate_request(params) do |response|
|
41
|
+
parsed_response = Crack::XML.parse(response.response)
|
42
|
+
@arn = parsed_response["CreateTopicResponse"]["CreateTopicResult"]["TopicArn"]
|
43
|
+
AmazeSNS.topics[@topic.to_s] = self # add to hash
|
44
|
+
AmazeSNS.topics.rehash
|
45
|
+
EM.stop
|
46
|
+
end
|
47
|
+
}
|
48
|
+
@arn
|
49
|
+
end
|
50
|
+
|
51
|
+
# delete topic
|
52
|
+
def delete
|
53
|
+
parsed_response = ''
|
54
|
+
params = {
|
55
|
+
'TopicArn' => "#{arn}",
|
56
|
+
'Action' => 'DeleteTopic',
|
57
|
+
'SignatureMethod' => 'HmacSHA256',
|
58
|
+
'SignatureVersion' => 2,
|
59
|
+
'Timestamp' => Time.now.iso8601,
|
60
|
+
'AWSAccessKeyId' => AmazeSNS.akey
|
61
|
+
}
|
62
|
+
|
63
|
+
reactor{
|
64
|
+
generate_request(params) do |response|
|
65
|
+
parsed_response = Crack::XML.parse(response.response)
|
66
|
+
AmazeSNS.topics.delete("#{@topic}")
|
67
|
+
AmazeSNS.topics.rehash
|
68
|
+
EM.stop
|
69
|
+
end
|
70
|
+
}
|
71
|
+
parsed_response
|
72
|
+
end
|
73
|
+
|
74
|
+
# get attributes for topic from remote sns server
|
75
|
+
# TopicArn -- the topic's ARN
|
76
|
+
# Owner -- the AWS account ID of the topic's owner
|
77
|
+
# Policy -- the JSON serialization of the topic's access control policy
|
78
|
+
# DisplayName -- the human-readable name used in the "From" field for notifications to email and email-json endpoints
|
79
|
+
|
80
|
+
def attrs
|
81
|
+
outcome = nil
|
82
|
+
params = {
|
83
|
+
'TopicArn' => "#{arn}",
|
84
|
+
'Action' => 'GetTopicAttributes',
|
85
|
+
'SignatureMethod' => 'HmacSHA256',
|
86
|
+
'SignatureVersion' => 2,
|
87
|
+
'Timestamp' => Time.now.iso8601,
|
88
|
+
'AWSAccessKeyId' => AmazeSNS.akey
|
89
|
+
}
|
90
|
+
|
91
|
+
reactor{
|
92
|
+
generate_request(params) do |response|
|
93
|
+
parsed_response = Crack::XML.parse(response.response)
|
94
|
+
res = parsed_response['GetTopicAttributesResponse']['GetTopicAttributesResult']['Attributes']["entry"]
|
95
|
+
outcome = make_hash(res) #res["entry"] is an array of hashes - need to turn it into hash with key value
|
96
|
+
EM.stop
|
97
|
+
end
|
98
|
+
}
|
99
|
+
return outcome
|
100
|
+
end
|
101
|
+
|
102
|
+
# The SetTopicAttributes action allows a topic owner to set an attribute of the topic to a new value.
|
103
|
+
# only following attributes can be set:
|
104
|
+
# TopicArn -- the topic's ARN
|
105
|
+
# Owner -- the AWS account ID of the topic's owner
|
106
|
+
# Policy -- the JSON serialization of the topic's access control policy
|
107
|
+
# DisplayName -- the human-readable name used in the "From" field for notifications to email and email-json endpoints
|
108
|
+
|
109
|
+
def set_attrs(opts)
|
110
|
+
outcome = nil
|
111
|
+
params = {
|
112
|
+
'AttributeName' => "#{opts[:name]}",
|
113
|
+
'AttributeValue' => "#{opts[:value]}",
|
114
|
+
'TopicArn' => "#{arn}",
|
115
|
+
'Action' => 'SetTopicAttributes',
|
116
|
+
'SignatureMethod' => 'HmacSHA256',
|
117
|
+
'SignatureVersion' => 2,
|
118
|
+
'Timestamp' => Time.now.iso8601,
|
119
|
+
'AWSAccessKeyId' => AmazeSNS.akey
|
120
|
+
}
|
121
|
+
|
122
|
+
reactor{
|
123
|
+
generate_request(params) do |response|
|
124
|
+
parsed_response = Crack::XML.parse(response.response)
|
125
|
+
outcome = parsed_response['SetTopicAttributesResponse']['ResponseMetadata']['RequestId']
|
126
|
+
EM.stop
|
127
|
+
end
|
128
|
+
}
|
129
|
+
return outcome
|
130
|
+
end
|
131
|
+
|
132
|
+
# subscribe method
|
133
|
+
def subscribe(opts)
|
134
|
+
raise InvalidOptions unless ( !(opts.empty?) && opts.instance_of?(Hash) )
|
135
|
+
res=''
|
136
|
+
params = {
|
137
|
+
'TopicArn' => "#{arn}",
|
138
|
+
'Endpoint' => "#{opts[:endpoint]}",
|
139
|
+
'Protocol' => "#{opts[:protocol]}",
|
140
|
+
'Action' => 'Subscribe',
|
141
|
+
'SignatureMethod' => 'HmacSHA256',
|
142
|
+
'SignatureVersion' => 2,
|
143
|
+
'Timestamp' => Time.now.iso8601,
|
144
|
+
'AWSAccessKeyId' => AmazeSNS.akey
|
145
|
+
}
|
146
|
+
|
147
|
+
reactor{
|
148
|
+
generate_request(params) do |response|
|
149
|
+
parsed_response = Crack::XML.parse(response.response)
|
150
|
+
res = parsed_response['SubscribeResponse']['SubscribeResult']['SubscriptionArn']
|
151
|
+
return res
|
152
|
+
EM.stop
|
153
|
+
end
|
154
|
+
}
|
155
|
+
res
|
156
|
+
end
|
157
|
+
|
158
|
+
def unsubscribe(id)
|
159
|
+
raise InvalidOptions unless ( !(id.empty?) && id.instance_of?(String) )
|
160
|
+
res=''
|
161
|
+
params = {
|
162
|
+
'SubscriptionArn' => "#{id}",
|
163
|
+
'Action' => 'Unsubscribe',
|
164
|
+
'SignatureMethod' => 'HmacSHA256',
|
165
|
+
'SignatureVersion' => 2,
|
166
|
+
'Timestamp' => Time.now.iso8601,
|
167
|
+
'AWSAccessKeyId' => AmazeSNS.akey
|
168
|
+
}
|
169
|
+
|
170
|
+
reactor{
|
171
|
+
generate_request(params) do |response|
|
172
|
+
parsed_response = Crack::XML.parse(response.response)
|
173
|
+
res = parsed_response['UnsubscribeResponse']['ResponseMetadata']['RequestId']
|
174
|
+
return res
|
175
|
+
EM.stop
|
176
|
+
end
|
177
|
+
}
|
178
|
+
res
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
# grabs list of subscriptions for this topic only
|
183
|
+
def subscriptions
|
184
|
+
nh={}
|
185
|
+
params = {
|
186
|
+
'TopicArn' => "#{arn}",
|
187
|
+
'Action' => 'ListSubscriptionsByTopic',
|
188
|
+
'SignatureMethod' => 'HmacSHA256',
|
189
|
+
'SignatureVersion' => 2,
|
190
|
+
'Timestamp' => Time.now.iso8601,
|
191
|
+
'AWSAccessKeyId' => AmazeSNS.akey
|
192
|
+
}
|
193
|
+
|
194
|
+
reactor{
|
195
|
+
generate_request(params) do |response|
|
196
|
+
parsed_response = Crack::XML.parse(response.response)
|
197
|
+
arr = parsed_response['ListSubscriptionsByTopicResponse']['ListSubscriptionsByTopicResult']['Subscriptions']['member'] unless (parsed_response['ListSubscriptionsByTopicResponse']['ListSubscriptionsByTopicResult']['Subscriptions'].nil?)
|
198
|
+
|
199
|
+
if !(arr.nil?) && (arr.instance_of?(Array))
|
200
|
+
#temp fix for now
|
201
|
+
nh = arr.inject({}) do |h,v|
|
202
|
+
key = v["SubscriptionArn"].to_s
|
203
|
+
value = v
|
204
|
+
h[key.to_s] = value
|
205
|
+
h
|
206
|
+
end
|
207
|
+
elsif !(arr.nil?) && (arr.instance_of?(Hash))
|
208
|
+
# to deal with one subscription issue
|
209
|
+
key = arr["SubscriptionArn"]
|
210
|
+
arr.delete("SubscriptionArn")
|
211
|
+
nh[key.to_s] = arr
|
212
|
+
end
|
213
|
+
return nh
|
214
|
+
EM.stop
|
215
|
+
end
|
216
|
+
}
|
217
|
+
nh
|
218
|
+
end
|
219
|
+
|
220
|
+
# The AddPermission action adds a statement to a topic's access control policy, granting access for the
|
221
|
+
# specified AWS accounts to the specified actions.
|
222
|
+
|
223
|
+
def add_permission(opts)
|
224
|
+
raise InvalidOptions unless ( !(opts.empty?) && opts.instance_of?(Hash) )
|
225
|
+
res=''
|
226
|
+
params = {
|
227
|
+
'TopicArn' => "#{arn}",
|
228
|
+
'Label' => "#{opts[:label]}",
|
229
|
+
'ActionName.member.1' => "#{opts[:action_name]}",
|
230
|
+
'AWSAccountId.member.1' => "#{opts[:account_id]}",
|
231
|
+
'Action' => 'AddPermission',
|
232
|
+
'SignatureMethod' => 'HmacSHA256',
|
233
|
+
'SignatureVersion' => 2,
|
234
|
+
'Timestamp' => Time.now.iso8601,
|
235
|
+
'AWSAccessKeyId' => AmazeSNS.akey
|
236
|
+
}
|
237
|
+
|
238
|
+
reactor{
|
239
|
+
generate_request(params) do |response|
|
240
|
+
parsed_response = Crack::XML.parse(response.response)
|
241
|
+
res = parsed_response['AddPermissionResponse']['ResponseMetadata']['RequestId']
|
242
|
+
return res
|
243
|
+
EM.stop
|
244
|
+
end
|
245
|
+
}
|
246
|
+
res
|
247
|
+
end
|
248
|
+
|
249
|
+
# The RemovePermission action removes a statement from a topic's access control policy.
|
250
|
+
def remove_permission(label)
|
251
|
+
raise InvalidOptions unless ( !(label.empty?) && label.instance_of?(String) )
|
252
|
+
res=''
|
253
|
+
params = {
|
254
|
+
'TopicArn' => "#{arn}",
|
255
|
+
'Label' => "#{label}",
|
256
|
+
'Action' => 'RemovePermission',
|
257
|
+
'SignatureMethod' => 'HmacSHA256',
|
258
|
+
'SignatureVersion' => 2,
|
259
|
+
'Timestamp' => Time.now.iso8601,
|
260
|
+
'AWSAccessKeyId' => AmazeSNS.akey
|
261
|
+
}
|
262
|
+
|
263
|
+
reactor{
|
264
|
+
generate_request(params) do |response|
|
265
|
+
parsed_response = Crack::XML.parse(response.response)
|
266
|
+
res = parsed_response['RemovePermissionResponse']['ResponseMetadata']['RequestId']
|
267
|
+
return res
|
268
|
+
EM.stop
|
269
|
+
end
|
270
|
+
}
|
271
|
+
res
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
def publish!(msg, subject='')
|
276
|
+
raise InvalidOptions unless ( !(msg.empty?) && msg.instance_of?(String) )
|
277
|
+
res=''
|
278
|
+
params = {
|
279
|
+
'Subject' => "My First Message",
|
280
|
+
'TopicArn' => "#{arn}",
|
281
|
+
"Message" => "#{msg}",
|
282
|
+
'Action' => 'Publish',
|
283
|
+
'SignatureMethod' => 'HmacSHA256',
|
284
|
+
'SignatureVersion' => 2,
|
285
|
+
'Timestamp' => Time.now.iso8601,
|
286
|
+
'AWSAccessKeyId' => AmazeSNS.akey
|
287
|
+
}
|
288
|
+
|
289
|
+
reactor{
|
290
|
+
generate_request(params) do |response|
|
291
|
+
parsed_response = Crack::XML.parse(response.response)
|
292
|
+
res = parsed_response['PublishResponse']['PublishResult']['MessageId']
|
293
|
+
return res
|
294
|
+
EM.stop
|
295
|
+
end
|
296
|
+
}
|
297
|
+
res
|
298
|
+
end
|
299
|
+
|
300
|
+
def confirm_subscription(token)
|
301
|
+
raise InvalidOptions unless ( !(token.empty?) && token.instance_of?(String) )
|
302
|
+
arr=[]
|
303
|
+
params = {
|
304
|
+
'TopicArn' => "#{arn}",
|
305
|
+
'Token' => "#{token}",
|
306
|
+
'Action' => 'ConfirmSubscription',
|
307
|
+
'SignatureMethod' => 'HmacSHA256',
|
308
|
+
'SignatureVersion' => 2,
|
309
|
+
'Timestamp' => Time.now.iso8601,
|
310
|
+
'AWSAccessKeyId' => AmazeSNS.akey
|
311
|
+
}
|
312
|
+
|
313
|
+
reactor{
|
314
|
+
generate_request(params) do |response|
|
315
|
+
parsed_response = Crack::XML.parse(response.response)
|
316
|
+
resp = parsed_response['ConfirmSubscriptionResponse']['ConfirmSubscriptionResult']['SubscriptionArn']
|
317
|
+
id = parsed_response['ConfirmSubscriptionResponse']['ResponseMetadata']['RequestId']
|
318
|
+
arr = [resp,id]
|
319
|
+
return arr
|
320
|
+
EM.stop
|
321
|
+
end
|
322
|
+
}
|
323
|
+
arr
|
324
|
+
end
|
325
|
+
|
326
|
+
|
327
|
+
|
328
|
+
|
329
|
+
def make_hash(arr)
|
330
|
+
hash = arr.inject({}) do |h, v|
|
331
|
+
(v["key"] == "Policy")? value = JSON.parse(v["value"]) : value = v["value"]
|
332
|
+
key = v["key"].to_s
|
333
|
+
h[key] = value
|
334
|
+
h
|
335
|
+
end
|
336
|
+
|
337
|
+
hash
|
338
|
+
end
|
339
|
+
|
340
|
+
|
341
|
+
|
342
|
+
end
|
data/lib/amaze_sns.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
autoload 'Logger', 'logger'
|
2
|
+
|
3
|
+
|
4
|
+
require File.dirname(__FILE__) + "/amaze/topic"
|
5
|
+
require File.dirname(__FILE__) + "/amaze/subscription"
|
6
|
+
require File.dirname(__FILE__) + "/amaze/helpers"
|
7
|
+
require File.dirname(__FILE__) + "/amaze/request"
|
8
|
+
require File.dirname(__FILE__) + "/amaze/exceptions"
|
9
|
+
require "eventmachine"
|
10
|
+
require 'crack/xml'
|
11
|
+
|
12
|
+
class AmazeSNS
|
13
|
+
|
14
|
+
class CredentialError < ::ArgumentError
|
15
|
+
def message
|
16
|
+
'Please provide your Amazon keys to use the service'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
attr_accessor :host, :logger, :topics, :skey, :akey, :subscriptions
|
22
|
+
#attr_writer :skey, :akey
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
self.host = 'sns.us-east-1.amazonaws.com'
|
27
|
+
self.logger = Logger.new($STDOUT)
|
28
|
+
self.skey = ''
|
29
|
+
self.akey=''
|
30
|
+
self.topics ||= {}
|
31
|
+
self.subscriptions ||= {}
|
32
|
+
|
33
|
+
def self.[](topic)
|
34
|
+
raise CredentialError unless (!(@skey.empty?) && !(@akey.empty?))
|
35
|
+
@topics[topic.to_s] = Topic.new(topic) unless @topics.has_key?(topic)
|
36
|
+
@topics[topic.to_s]
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def self.method_missing(id, *args, &blk)
|
41
|
+
case(id.to_s)
|
42
|
+
when /^list_(.*)/
|
43
|
+
send(:process_query, $1, &blk)
|
44
|
+
when /^refresh_(.*)/
|
45
|
+
send(:process_query, $1, &blk)
|
46
|
+
else
|
47
|
+
#super
|
48
|
+
raise NoMethodError
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.process_query(type, &blk)
|
53
|
+
# p "INSIDE PROCESS query"
|
54
|
+
type = type.capitalize
|
55
|
+
params = {
|
56
|
+
'Action' => "List#{type}",
|
57
|
+
'SignatureMethod' => 'HmacSHA256',
|
58
|
+
'SignatureVersion' => 2,
|
59
|
+
'Timestamp' => Time.now.iso8601,
|
60
|
+
'AWSAccessKeyId' => @akey
|
61
|
+
}
|
62
|
+
|
63
|
+
req_options={}
|
64
|
+
|
65
|
+
if (blk)
|
66
|
+
prc = blk
|
67
|
+
else
|
68
|
+
prc = default_prc
|
69
|
+
end
|
70
|
+
|
71
|
+
req_options[:on_success] = prc
|
72
|
+
#req = Request.new(params, req_options).process
|
73
|
+
|
74
|
+
EM.run{
|
75
|
+
Request.new(params, req_options).process
|
76
|
+
}
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.default_prc
|
81
|
+
prc = Proc.new do |resp|
|
82
|
+
parsed_response = Crack::XML.parse(resp.response)
|
83
|
+
#p "SUB RESPONSE: #{parsed_response.inspect}"
|
84
|
+
self.process_response(parsed_response)
|
85
|
+
EM.stop
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.process_response(resp)
|
90
|
+
kind = (resp.has_key?("ListTopicsResponse"))? "Topics" : "Subscriptions"
|
91
|
+
cla = (resp.has_key?("ListTopicsResponse"))? "Topic" : "Subscription"
|
92
|
+
#p "KIND IS #{kind}"
|
93
|
+
|
94
|
+
result = resp["List#{kind}Response"]["List#{kind}Result"]["#{kind}"]
|
95
|
+
if result.nil?
|
96
|
+
p "NO DATA FOUND"
|
97
|
+
return nil
|
98
|
+
else
|
99
|
+
results = result["member"]
|
100
|
+
end
|
101
|
+
|
102
|
+
@collection = self.send(kind.downcase)
|
103
|
+
|
104
|
+
if !(results.nil?)
|
105
|
+
if (results.instance_of?(Array))
|
106
|
+
results.each do |t|
|
107
|
+
label = t["TopicArn"].split(':').last.to_s
|
108
|
+
unless @collection.has_key?(label)
|
109
|
+
case cla
|
110
|
+
when "Topic"
|
111
|
+
@collection[label] = Topic.new(label,t["TopicArn"])
|
112
|
+
when "Subscription"
|
113
|
+
@collection[label] = Array.new
|
114
|
+
@collection[label] << Subscription.new(t)
|
115
|
+
end
|
116
|
+
|
117
|
+
#@collection[label] = Kernel.const_get("#{cla}").new(t) # t is a hash
|
118
|
+
else
|
119
|
+
case cla
|
120
|
+
when "Topic"
|
121
|
+
@collection[label].arn = t["TopicArn"]
|
122
|
+
when "Subscription"
|
123
|
+
#@collection[label] = t["TopicArn"]
|
124
|
+
sub = Subscription.new(t)
|
125
|
+
@collection[label] << sub unless @collection[label].detect{|x| x.subarn == sub.subarn}
|
126
|
+
end
|
127
|
+
#@collection[label].arn = t["TopicArn"]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
elsif (results.instance_of?(Hash))
|
131
|
+
# lone entry results in a hash so parse it that way ...
|
132
|
+
label = results["TopicArn"].split(':').last.to_s
|
133
|
+
case cla
|
134
|
+
when "Topic"
|
135
|
+
@collection[label] = Topic.new(label, results["TopicArn"])
|
136
|
+
when "Subscription"
|
137
|
+
@collection[label] = Subscription.new(results)
|
138
|
+
end
|
139
|
+
|
140
|
+
#@collection[label] = Kernel.const_get("#{cla}").new(results)
|
141
|
+
end
|
142
|
+
else
|
143
|
+
return nil
|
144
|
+
end # end outer if
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe AmazeSNS do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
EventMachine::MockHttpRequest.reset_counts!
|
7
|
+
EventMachine::MockHttpRequest.reset_registry!
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'in its initial state' do
|
11
|
+
|
12
|
+
it "should return the preconfigured host endpoint" do
|
13
|
+
AmazeSNS.host.should == 'sns.us-east-1.amazonaws.com'
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should return a blank Topics hash" do
|
17
|
+
AmazeSNS.topics.should == {}
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'without the keys' do
|
23
|
+
it 'should raise an ArgumentError if no keys are present' do
|
24
|
+
lambda{
|
25
|
+
AmazeSNS['test']
|
26
|
+
}.should raise_error(AmazeSNS::CredentialError)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'with the keys configured' do
|
31
|
+
|
32
|
+
before do
|
33
|
+
AmazeSNS.akey = '123456'
|
34
|
+
AmazeSNS.skey = '123456'
|
35
|
+
@topic = AmazeSNS['Test']
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should return a new topic object' do
|
39
|
+
@topic.should be_kind_of(Topic)
|
40
|
+
@topic.topic.should == 'Test'
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
describe 'Request#process' do
|
47
|
+
module EventMachine
|
48
|
+
module HttpEncoding
|
49
|
+
def encode_query(path, query, uri_query)
|
50
|
+
encoded_query = if query.kind_of?(Hash)
|
51
|
+
query.sort{|a, b| a.to_s <=> b.to_s}.
|
52
|
+
map { |k, v| encode_param(k, v) }.
|
53
|
+
join('&')
|
54
|
+
else
|
55
|
+
query.to_s
|
56
|
+
end
|
57
|
+
if !uri_query.to_s.empty?
|
58
|
+
encoded_query = [encoded_query, uri_query].reject {|part| part.empty?}.join("&")
|
59
|
+
end
|
60
|
+
return path if encoded_query.to_s.empty?
|
61
|
+
"#{path}?#{encoded_query}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
before :each do
|
67
|
+
EM::HttpRequest = EM::MockHttpRequest
|
68
|
+
EM::HttpRequest.reset_registry!
|
69
|
+
EM::HttpRequest.reset_counts!
|
70
|
+
EM::HttpRequest.pass_through_requests = false
|
71
|
+
|
72
|
+
@params = {
|
73
|
+
'Action' => 'ListTopics',
|
74
|
+
'SignatureMethod' => 'HmacSHA256',
|
75
|
+
'SignatureVersion' => 2,
|
76
|
+
'Timestamp' => Time.now.iso8601,
|
77
|
+
'AWSAccessKeyId' => AmazeSNS.akey
|
78
|
+
}
|
79
|
+
|
80
|
+
@query_string = canonical_querystring(@params)
|
81
|
+
|
82
|
+
string_to_sign = "GET
|
83
|
+
#{AmazeSNS.host}
|
84
|
+
/
|
85
|
+
#{@query_string}"
|
86
|
+
|
87
|
+
hmac = HMAC::SHA256.new(AmazeSNS.skey)
|
88
|
+
hmac.update( string_to_sign )
|
89
|
+
signature = Base64.encode64(hmac.digest).chomp
|
90
|
+
|
91
|
+
@params['Signature'] = signature
|
92
|
+
@querystring2 = @params.collect { |key, value| [url_encode(key), url_encode(value)].join("=") }.join('&')
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should return a deferrable which succeeds in success case" do
|
97
|
+
#Time.stub(:now).and_return(123)
|
98
|
+
|
99
|
+
data = <<-RESPONSE.gsub(/^ +/, '')
|
100
|
+
HTTP/1.1 202 Accepted
|
101
|
+
Content-Type: text/html
|
102
|
+
Content-Length: 13
|
103
|
+
Connection: keep-alive
|
104
|
+
Server: thin 1.2.7 codename No Hup
|
105
|
+
|
106
|
+
202 ACCEPTED
|
107
|
+
RESPONSE
|
108
|
+
|
109
|
+
url = "http://#{AmazeSNS.host}/?#{@querystring2}"
|
110
|
+
|
111
|
+
EM::HttpRequest.register(url, :get, data)
|
112
|
+
|
113
|
+
EM.run {
|
114
|
+
d = AmazeSNS.list_topics
|
115
|
+
d.callback{
|
116
|
+
@raw_resp = Crack::XML.parse(resp.response)
|
117
|
+
AmazeSNS.process_response(@raw_resp)
|
118
|
+
EM.stop
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
|
130
|
+
|
131
|
+
after do
|
132
|
+
AmazeSNS.topics={}
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
end # end outer describe
|
137
|
+
|
138
|
+
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
require 'spec'
|
3
|
+
require 'spec/autorun'
|
4
|
+
require 'request'
|
5
|
+
require 'amaze_sns'
|
6
|
+
require 'eventmachine'
|
7
|
+
require 'em-http'
|
8
|
+
require 'em-http/mock'
|
9
|
+
|
10
|
+
Spec::Runner.configure do |config|
|
11
|
+
# EventMachine.instance_eval do
|
12
|
+
# # Switching out EM's defer since it makes tests just a tad more unreliable
|
13
|
+
# alias :defer_original :defer
|
14
|
+
# def defer
|
15
|
+
# yield
|
16
|
+
# end
|
17
|
+
# end unless EM.respond_to?(:defer_original)
|
18
|
+
#
|
19
|
+
#
|
20
|
+
# def run_in_em_loop
|
21
|
+
# EM.run {
|
22
|
+
# yield
|
23
|
+
# }
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# class Request
|
27
|
+
# self.module_eval do
|
28
|
+
# def http_class
|
29
|
+
# EventMachine::MockHttpRequest
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
|
34
|
+
|
35
|
+
def fake_response
|
36
|
+
<<-HEREDOC
|
37
|
+
<ListTopicsResponse xmlns=\"http://sns.amazonaws.com/doc/2010-03-31/\">\n<ListTopicsResult>\n<Topics>\n<member>\n<TopicArn>arn:aws:sns:us-east-1:365155214602:cars</TopicArn>\n</member>\n<member>\n<TopicArn>arn:aws:sns:us-east-1:365155214602:luckypooz</TopicArn>\n</member>\n<member>\n<TopicArn>arn:aws:sns:us-east-1:365155214602:29steps_products</TopicArn>\n</member>\n</Topics>\n</ListTopicsResult>\n <ResponseMetadata>\n<RequestId>d4a2ff9b-56dc-11df-b6e7-a7864eff589e</RequestId>\n</ResponseMetadata>\n</ListTopicsResponse>
|
38
|
+
HEREDOC
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
end #end configure
|
45
|
+
|
46
|
+
|
47
|
+
|
data/spec/topic_spec.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe Topic do
|
4
|
+
before do
|
5
|
+
@topic = Topic.new('my_test_topic', 'arn:123456')
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'in its initial state' do
|
9
|
+
it 'should be an instance of Topic class' do
|
10
|
+
@topic.should be_kind_of(Topic)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should return the topic name and arn when requested' do
|
14
|
+
@topic.topic.should == 'my_test_topic'
|
15
|
+
@topic.arn.should == 'arn:123456'
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'when creating a topic' do
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: amaze_sns
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 1.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Chee Yeo
|
13
|
+
- 29 Steps UK
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-07-08 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: eventmachine
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
- 12
|
31
|
+
- 9
|
32
|
+
version: 0.12.9
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: em-http-request
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
segments:
|
43
|
+
- 0
|
44
|
+
- 2
|
45
|
+
- 8
|
46
|
+
version: 0.2.8
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: crack
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
- 1
|
59
|
+
- 6
|
60
|
+
version: 0.1.6
|
61
|
+
type: :runtime
|
62
|
+
version_requirements: *id003
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: ruby-hmac
|
65
|
+
prerelease: false
|
66
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
segments:
|
71
|
+
- 0
|
72
|
+
- 4
|
73
|
+
- 0
|
74
|
+
version: 0.4.0
|
75
|
+
type: :runtime
|
76
|
+
version_requirements: *id004
|
77
|
+
- !ruby/object:Gem::Dependency
|
78
|
+
name: json
|
79
|
+
prerelease: false
|
80
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
segments:
|
85
|
+
- 1
|
86
|
+
- 4
|
87
|
+
- 3
|
88
|
+
version: 1.4.3
|
89
|
+
type: :runtime
|
90
|
+
version_requirements: *id005
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: rspec
|
93
|
+
prerelease: false
|
94
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
segments:
|
99
|
+
- 1
|
100
|
+
- 2
|
101
|
+
- 9
|
102
|
+
version: 1.2.9
|
103
|
+
type: :development
|
104
|
+
version_requirements: *id006
|
105
|
+
description: Ruby gem to interface with the Amazon Simple Notification Service
|
106
|
+
email: info@29steps.co.uk
|
107
|
+
executables: []
|
108
|
+
|
109
|
+
extensions: []
|
110
|
+
|
111
|
+
extra_rdoc_files:
|
112
|
+
- README.md
|
113
|
+
files:
|
114
|
+
- lib/amaze/exceptions.rb
|
115
|
+
- lib/amaze/helpers.rb
|
116
|
+
- lib/amaze/request.rb
|
117
|
+
- lib/amaze/subscription.rb
|
118
|
+
- lib/amaze/topic.rb
|
119
|
+
- lib/amaze_sns.rb
|
120
|
+
- README.md
|
121
|
+
has_rdoc: true
|
122
|
+
homepage: http://29steps.co.uk
|
123
|
+
licenses: []
|
124
|
+
|
125
|
+
post_install_message:
|
126
|
+
rdoc_options:
|
127
|
+
- --charset=UTF-8
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
segments:
|
135
|
+
- 1
|
136
|
+
- 8
|
137
|
+
- 6
|
138
|
+
version: 1.8.6
|
139
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
segments:
|
144
|
+
- 0
|
145
|
+
version: "0"
|
146
|
+
requirements: []
|
147
|
+
|
148
|
+
rubyforge_project:
|
149
|
+
rubygems_version: 1.3.6
|
150
|
+
signing_key:
|
151
|
+
specification_version: 3
|
152
|
+
summary: Ruby gem for Amazon Simple Notification Service SNS
|
153
|
+
test_files:
|
154
|
+
- spec/amaze_sns_spec.rb
|
155
|
+
- spec/spec_helper.rb
|
156
|
+
- spec/topic_spec.rb
|
157
|
+
- spec/spec.opts
|