amaze_sns 1.0.1
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/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
|