heroic-sns 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ODJhMzlkZTE4ODA5ZWE2ODdmMGNmOTM0M2IxYTFkNWYwMzBlMjVjYw==
5
+ data.tar.gz: !binary |-
6
+ MzEyN2NhN2IyMDMzMjQ0NGZjZGJjNzRlOWZiYWNjNzIxMzI2OGRjNA==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ OTNmZTE4MGI0YTVjZWM4MTYxNjdmNGFlM2I1OGI1YzZiMmUyM2FhNzdmNzk5
10
+ YmE4MThiMjNmYmMxZGFjMDBkZDg0N2VhNDVhYzlhMzBjMDU1MjUyOTg3ZGU5
11
+ NzI1MGIzNTExNzM2MjZiYjU1ZmI2YTYyODNiZjM2OGU3NzcwZmM=
12
+ data.tar.gz: !binary |-
13
+ YjJmNmJhZjg1YzFkZDhjODM2YmQ2ZDQ2MWNjZmM5MDRkYjk5ZjYyYmNiY2E2
14
+ NmQ5ZTczZmE5OTMwYWQyMzhmZWQwZTAwNTNhZGVmOTZlYjIxMjRkODdlMjlm
15
+ NTUxMmI5ODc0ZWY2YzE0MzEwMmQ5YTA0ODNmYTIzMThjM2IwMjY=
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2013 Heroic Software Inc. All Rights Reserved.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,133 @@
1
+ # Heroic SNS Endpoint
2
+
3
+ This gem contains a lightweight Rack middleware for AWS Simple Notification
4
+ Service (SNS) endpoints.
5
+
6
+ SNS messages to your web application are intercepted, parsed, verified, and then
7
+ passed along to your Rack application in the `sns.message` environment key.
8
+
9
+ If something goes wrong, the error will be passed along in the `sns.error`
10
+ environment key. `Endpoint` does not log any messages itself.
11
+
12
+ This gem *does not* depend on [aws-sdk][]. In fact, the only dependency it has
13
+ beyond the standard libraries is [rack][]. This is intentional, since AWS
14
+ credentials are not required to receive SNS notifications.
15
+
16
+ [aws-sdk]: https://github.com/aws/aws-sdk-ruby
17
+ [rack]: http://rack.github.io/
18
+
19
+ ## Overview
20
+
21
+ 1. Install in your middleware stack
22
+ 2. Get SNS messages from `env['sns.message']`
23
+ 3. Get errors from `env['sns.error']`
24
+
25
+ ## How to use it
26
+
27
+ Simply add the following to your `config.ru` file:
28
+
29
+ use Heroic::SNS::Endpoint, :topics => /:aws-ses-bounces$/
30
+
31
+ For Rails, you could also install it in `/config/initializers/sns_endpoint.rb`:
32
+
33
+ Rails.application.config.middleware.use Heroic::SNS::Endpoint, :topic => ...
34
+
35
+ The Endpoint class takes an options hash as an argument, and understands these
36
+ options:
37
+
38
+ `:topic` is required, and provides a filter that defines what SNS topics are
39
+ handled by this endpoint. A message is considered either "on-topic" or
40
+ "off-topic". You can supply any of the following:
41
+
42
+ - a single topic ARN as a `String`
43
+ - a list of topic ARNs as an `Array` of `String`
44
+ - a `RegExp` which matches on-topic ARNs
45
+ - a `Proc` which accepts an ARN as an argument and returns `true` or `false` for
46
+ on-topic and off-topic ARNs, respectively.
47
+
48
+ The key `topics:` also works.
49
+
50
+ `:auto_confirm` affects how on-topic subscription confirmations are handled.
51
+
52
+ - If `true`, they are confirmed by retrieving the URL in the `SubscribeURL` field
53
+ of the SNS message, and your app is not notified.
54
+ - If `false`, they are ignored; your app is also not notified.
55
+ - If `nil`, there is no special handling and the message is passed along to your
56
+ app.
57
+
58
+ The default is `true`.
59
+
60
+ `:auto_resubscribe` affects how on-topic unsubscribe confirmations are handled.
61
+
62
+ - If `true`, they topic is automatically re-subscribed by retrieving the URL in
63
+ the `SubscribeURL` field of the SNS message, and your app is not notified.
64
+ - If `false`, they are ignored and your app is also not notified.
65
+ - If `nil`, there is no special handling and the message is passed along to your
66
+ app.
67
+
68
+ The default is `false`.
69
+
70
+ If you are a control-freak and want no special handling whatsoever, use these
71
+ options:
72
+
73
+ use Heroic::SNS::Endpoint, :topics => Proc.new { true }, :auto_confirm => nil, :auto_resubscribe => nil
74
+
75
+ Then the object will simply parse and verify SNS messages it finds and pass them
76
+ along to your app, taking no action.
77
+
78
+ Once the middleware is set up, any notifications will be made available in your
79
+ Rack environment under the `sns.message` key. If you are using Rails, your
80
+ controller would have a method like this:
81
+
82
+ skip_before_filter :verify_authenticity_token, :only => [:handle_notification]
83
+
84
+ def handle_notification
85
+ if message = request.env['sns.message']
86
+ # message is an instance of Heroic::SNS::Message
87
+ payload = JSON.parse(message.body)
88
+ do_something_awesome(payload)
89
+ elsif error = request.env['sns.error']
90
+ raise error # let the warning be logged
91
+ end
92
+ head :ok
93
+ end
94
+
95
+ You must skip the authenticity token verification to allow Amazon to POST to the
96
+ controller action. Be careful not to disable it for more actions than you need.
97
+ Be sure to disable any authentication checks for that action, too.
98
+
99
+ ## How off-topic notifications are handled
100
+
101
+ As a security measure, `Endpoint` requires you to set up a topic filter. Any
102
+ notifications that do not match this filter are not passed along to your
103
+ application.
104
+
105
+ All off-topic messages are ignored with one exception: if the message is a
106
+ regular notification (meaning your app has an active subscription) *and* the
107
+ message can be verified as authentic (by checking its signature), `Endpoint`
108
+ will cancel the subscription by visiting the URL in the `UnsubscribeURL` field
109
+ of the message.
110
+
111
+ If you would rather make decision about on-topic and off-topic notifications in
112
+ your own code, simply pass `Proc.new { true }` as the topic filter, and all
113
+ messages will be treated as on topic. Be aware that it is dangerous to leave
114
+ `:auto_confirm` enabled with a permissive topic filter, as this will allow
115
+ anyone to subscribe your web app to any SNS notification.
116
+
117
+ ## Receiving multiple notification topics
118
+
119
+ If you are receiving multiple notifications at multiple endpoint URLs, you should
120
+ only include one instance of the Endpoint in your middleware stack, and ensure
121
+ that its topic filter allows all the notifications you are interested in to pass
122
+ through.
123
+
124
+ `Endpoint` does not interact with the URL path at all; if you want your
125
+ subscriptions to go to different URLs, simply set them up that way.
126
+
127
+ ## Questions?
128
+
129
+ You can send me an email at <ben@benzado.com> or Twitter [@benzado][]
130
+
131
+ [@benzado]: https://twitter.com/benzado
132
+
133
+ [![Build Status](https://travis-ci.org/benzado/heroic-sns.png?branch=master)](https://travis-ci.org/benzado/heroic-sns)
@@ -0,0 +1,20 @@
1
+ require 'heroic/sns/message'
2
+ require 'heroic/sns/endpoint'
3
+
4
+ module Heroic
5
+ module SNS
6
+
7
+ class Error < ::StandardError
8
+
9
+ # The message that triggered this error, if available.
10
+ attr_reader :sns_message
11
+
12
+ def initialize(error_message, sns_message = nil)
13
+ super(error_message)
14
+ @sns_message = sns_message
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,136 @@
1
+ require 'openssl'
2
+ require 'open-uri'
3
+
4
+ # Heroic::SNS::Endpoint is Rack middleware which intercepts messages from
5
+ # Amazon's Simple Notification Service (SNS). It makes the parsed and verified
6
+ # message available to your application in the Rack environment under the
7
+ # 'sns.message' key. If an error occurred during message handling, the error
8
+ # is available in the Rack environment in the 'sns.error' key.
9
+
10
+ # Endpoint is to be initialized with a hash of options. It understands three
11
+ # different options:
12
+ # - :topic (or :topics) specifies a filter that defines what SNS topics are
13
+ # handled by this endpoint ("on-topic"). You can supply any of the following:
14
+ # - a topic ARN as a String
15
+ # - a list of topic ARNs as an Array of Strings
16
+ # - a regular expression matching on-topic ARNs
17
+ # - a Proc which takes a topic ARN as a String and returns true or false.
18
+ # You must specify a topic filter. Use Proc.new { true } if you insist on
19
+ # indiscriminately accepting all notifications.
20
+ # - :auto_confirm determines how SubscriptionConfirmation messages are handled.
21
+ # If true, the subscription is confirmed and your app is not notified.
22
+ # If false, the subscription is ignored and your app is not notified.
23
+ # If nil, the message is passed along to your app.
24
+
25
+ # You can install this in your config.ru:
26
+ # use Heroic::SNS::Endpoint, :topics => /whatever/
27
+
28
+ # For Rails, you can also install it in /config/initializers/sns_endpoint.rb:
29
+ # Rails.application.config.middleware.use Heroic::SNS::Endpoint, :topic => ...
30
+
31
+ module Heroic
32
+ module SNS
33
+
34
+ SUBSCRIPTION_ARN_HTTP_HEADER = 'HTTP_X_AMZ_SNS_SUBSCRIPTION_ARN'
35
+
36
+ class Endpoint
37
+
38
+ DEFAULT_OPTIONS = { :auto_confirm => true, :auto_resubscribe => false }
39
+
40
+ def initialize(app, opt = {})
41
+ @app = app
42
+ options = DEFAULT_OPTIONS.merge(opt)
43
+ @auto_confirm = options[:auto_confirm]
44
+ @auto_resubscribe = options[:auto_resubscribe]
45
+ if 1 < [:topic, :topics].count { |k| options.has_key?(k) }
46
+ raise ArgumentError.new("supply zero or one of :topic, :topics")
47
+ end
48
+ @topic_filter = begin
49
+ case a = options[:topic] || options[:topics]
50
+ when String then Proc.new { |t| a == t }
51
+ when Regexp then Proc.new { |t| a.match(t) }
52
+ when Proc then a
53
+ when Array
54
+ unless a.all? { |e| e.is_a? String }
55
+ raise ArgumentError.new("topic array must be strings")
56
+ end
57
+ Proc.new { |t| a.include?(t) }
58
+ when nil
59
+ raise ArgumentError.new("must specify a topic filter!")
60
+ else
61
+ raise ArgumentError.new("can't use topic filter of type #{a.class}")
62
+ end
63
+ end
64
+ end
65
+
66
+ def call(env)
67
+ if topic_arn = env['HTTP_X_AMZ_SNS_TOPIC_ARN']
68
+ if @topic_filter.call(topic_arn)
69
+ call_on_topic(env)
70
+ else
71
+ call_off_topic(env)
72
+ end
73
+ else
74
+ @app.call(env)
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ OK_RESPONSE = [200, {'Content-Type' => 'text/plain'}, []]
81
+
82
+ # Confirms that values specified in HTTP headers match those in the message
83
+ # itself.
84
+ def check_headers!(message, env)
85
+ h = env.values_at 'HTTP_X_AMZ_SNS_MESSAGE_TYPE', 'HTTP_X_AMZ_SNS_MESSAGE_ID', 'HTTP_X_AMZ_SNS_TOPIC_ARN'
86
+ m = message.type, message.id, message.topic_arn
87
+ raise Error.new("message does not match HTTP headers", message) unless h == m
88
+ end
89
+
90
+ # Behavior for "on-topic" messages. Notifications are always passed along
91
+ # to the app. Confirmations are passed along only if their respective
92
+ # option is nil. If true, the subscription is confirmed; if false, it is
93
+ # simply ignored.
94
+ def call_on_topic(env)
95
+ begin
96
+ message = Message.new(env['rack.input'].read)
97
+ check_headers!(message, env)
98
+ message.verify!
99
+ case message.type
100
+ when 'SubscriptionConfirmation'
101
+ open(message.subscribe_url) if @auto_confirm
102
+ return OK_RESPONSE unless @auto_confirm.nil?
103
+ when 'UnsubscribeConfirmation'
104
+ open(message.subscribe_url) if @auto_resubscribe
105
+ return OK_RESPONSE unless @auto_resubscribe.nil?
106
+ end
107
+ env['sns.message'] = message
108
+ rescue OpenURI::HTTPError => e
109
+ env['sns.error'] = Error.new("unable to subscribe: #{e.message}; URL: #{message.subscribe_url}", message)
110
+ rescue Error => e
111
+ env['sns.error'] = e
112
+ end
113
+ @app.call(env)
114
+ end
115
+
116
+ # Default behavior for "off-topic" messages. Subscription and unsubscribe
117
+ # confirmations are simply ignored. Notifications, however, indicate that
118
+ # we are subscribed to a topic we don't know how to deal with. In this
119
+ # case, we automatically unsubscribe (if the message is authentic).
120
+ def call_off_topic(env)
121
+ if env['HTTP_X_AMZ_SNS_MESSAGE_TYPE'] == 'Notification'
122
+ begin
123
+ message = Message.new(env['rack.input'].read)
124
+ message.verify!
125
+ open(message.unsubscribe_url)
126
+ rescue => e
127
+ raise Error.new("error handling off-topic notification: #{e.message}", message)
128
+ end
129
+ end
130
+ OK_RESPONSE
131
+ end
132
+
133
+ end
134
+
135
+ end
136
+ end
@@ -0,0 +1,149 @@
1
+ require 'json'
2
+ require 'base64'
3
+
4
+ module Heroic
5
+ module SNS
6
+
7
+ MAXIMUM_ALLOWED_AGE = 3600 # reject messages older than one hour
8
+
9
+ CERTIFICATE_CACHE = Hash.new do |cache, cert_url|
10
+ begin
11
+ cert_data = open(cert_url)
12
+ cache[cert_url] = OpenSSL::X509::Certificate.new(cert_data.read)
13
+ rescue OpenURI::HTTPError => e
14
+ raise Error.new("unable to retrieve signing certificate: #{e.message}; URL: #{cert_url}")
15
+ rescue OpenSSL::X509::CertificateError => e
16
+ raise Error.new("unable to parse signing certificate: #{e.message}; URL: #{cert_url}")
17
+ end
18
+ end
19
+
20
+ # Encapsulates an SNS message.
21
+ # See: http://docs.aws.amazon.com/sns/latest/gsg/json-formats.html
22
+ class Message
23
+
24
+ def initialize(json)
25
+ @msg = ::JSON.parse(json)
26
+ rescue JSON::ParserError => e
27
+ raise Error.new("failed to parse message as JSON: #{e.message}")
28
+ end
29
+
30
+ def type
31
+ @msg['Type']
32
+ end
33
+
34
+ def topic_arn
35
+ @msg['TopicArn']
36
+ end
37
+
38
+ def id
39
+ @msg['MessageId']
40
+ end
41
+
42
+ # The timestamp as a Time object.
43
+ def timestamp
44
+ Time.xmlschema(@msg['Timestamp'])
45
+ end
46
+
47
+ def signature_version
48
+ @msg['SignatureVersion']
49
+ end
50
+
51
+ def signing_cert_url
52
+ @msg['SigningCertURL']
53
+ end
54
+
55
+ # The message signature data, Base-64 decoded.
56
+ def signature
57
+ Base64::decode64(@msg['Signature'])
58
+ end
59
+
60
+ # The message may not have a subject.
61
+ def subject
62
+ @msg['Subject']
63
+ end
64
+
65
+ def body
66
+ @msg['Message']
67
+ end
68
+
69
+ def subscribe_url
70
+ @msg['SubscribeURL']
71
+ end
72
+
73
+ def unsubscribe_url
74
+ @msg['UnsubscribeURL']
75
+ end
76
+
77
+ # The token is used to confirm subscriptions via the SNS API. If you visit
78
+ # the :subscribe_url, you can ignore this field.
79
+ def token
80
+ @msg['Token']
81
+ end
82
+
83
+ def ==(other_message)
84
+ @msg == other_message.instance_variable_get(:@msg)
85
+ end
86
+
87
+ def hash
88
+ @msg.hash
89
+ end
90
+
91
+ def to_s
92
+ string = "<SNSMessage:\n"
93
+ @msg.each do |k,v|
94
+ string << sprintf(" %s: %s\n", k, v.inspect)
95
+ end
96
+ string << ">"
97
+ end
98
+
99
+ def to_json
100
+ @msg.to_json
101
+ end
102
+
103
+ # Verifies the message signature. Raises an exception if it is not valid.
104
+ # See: http://docs.aws.amazon.com/sns/latest/gsg/SendMessageToHttp.verify.signature.html
105
+ def verify!
106
+ age = Time.now - timestamp
107
+ raise Errow.new("timestamp is in the future", self) if age < 0
108
+ raise Error.new("timestamp is too old", self) if age > MAXIMUM_ALLOWED_AGE
109
+ if signature_version != '1'
110
+ raise Error.new("unknown signature version: #{signature_version}", self)
111
+ end
112
+ if signing_cert_url !~ %r[^https://.*amazonaws\.com/]
113
+ raise Error.new("signing certificate is not from amazonaws.com", self)
114
+ end
115
+ text = string_to_sign # will warn of invalid Type
116
+ cert = CERTIFICATE_CACHE[signing_cert_url]
117
+ digest = OpenSSL::Digest::SHA1.new
118
+ unless cert.public_key.verify(digest, signature, text)
119
+ raise Error.new("message signature is invalid", self)
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ CANONICAL_NOTIFICATION_KEYS = %w[Message MessageId Subject Timestamp TopicArn Type].freeze
126
+
127
+ CANONICAL_SUBSCRIPTION_KEYS = %w[Message MessageId SubscribeURL Timestamp Token TopicArn Type].freeze
128
+
129
+ CANONICAL_KEYS_FOR_TYPE = {
130
+ 'Notification' => CANONICAL_NOTIFICATION_KEYS,
131
+ 'SubscriptionConfirmation' => CANONICAL_SUBSCRIPTION_KEYS,
132
+ 'UnsubscribeConfirmation' => CANONICAL_SUBSCRIPTION_KEYS
133
+ }.freeze
134
+
135
+ def string_to_sign
136
+ keys = CANONICAL_KEYS_FOR_TYPE[self.type]
137
+ raise Error.new("unrecognized message type: #{self.type}", self) unless keys
138
+ string = String.new
139
+ keys.each do |key|
140
+ if @msg.has_key?(key) # in case message has no Subject
141
+ string << key << "\n" << @msg[key] << "\n"
142
+ end
143
+ end
144
+ return string
145
+ end
146
+
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,34 @@
1
+ # Generate a self-signed certificate
2
+ # 1. openssl genrsa -out sns.key 1024
3
+ # 2. openssl req -new -key sns.key -out sns.csr
4
+ # 3. openssl x509 -req -days 3652 -in sns.csr -signkey sns.key -out sns.crt
5
+
6
+ module Heroic
7
+ module SNS
8
+
9
+ TEST_CERT_URL = 'https://sns.test.amazonaws.com/self-signed.pem'
10
+ TEST_CERT_KEY = OpenSSL::PKey::RSA.new(File.read('test/fixtures/sns.key'))
11
+
12
+ CERTIFICATE_CACHE[TEST_CERT_URL] = begin
13
+ # Insert the certificate in the cache so that these tests aren't dependent
14
+ # on network access (or the fact that the certificate is fake).
15
+ cert_data = File.read('test/fixtures/sns.crt')
16
+ OpenSSL::X509::Certificate.new(cert_data)
17
+ end
18
+
19
+ class Message
20
+
21
+ def update_timestamp!(t = Time.now)
22
+ @msg['Timestamp'] = t.utc.xmlschema(3)
23
+ end
24
+
25
+ def sign!
26
+ @msg['SigningCertURL'] = TEST_CERT_URL
27
+ signature = TEST_CERT_KEY.sign(OpenSSL::Digest::SHA1.new, string_to_sign)
28
+ @msg['Signature'] = Base64::encode64(signature)
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,47 @@
1
+ require 'test/unit'
2
+ require 'heroic/sns'
3
+ require 'helper'
4
+
5
+ NULL_APP = Proc.new { |env| [0, {}, []] }
6
+
7
+ class EndpointTest < Test::Unit::TestCase
8
+
9
+ def sns(name)
10
+ json = File.read("test/fixtures/#{name}.json")
11
+ @msg = Heroic::SNS::Message.new(json)
12
+ @msg.update_timestamp!
13
+ @msg.sign!
14
+ @env = {
15
+ 'HTTP_X_AMZ_SNS_MESSAGE_TYPE' => @msg.type,
16
+ 'HTTP_X_AMZ_SNS_MESSAGE_ID' => @msg.id,
17
+ 'HTTP_X_AMZ_SNS_TOPIC_ARN' => @msg.topic_arn,
18
+ 'HTTP_X_AMZ_SNS_SUBSCRIPTION_ARN' => "#{@msg.topic_arn}:af0d2f29-f4c3-4df2-b7e2-5a096fc772f6",
19
+ 'rack.input' => StringIO.new(@msg.to_json)
20
+ }
21
+ end
22
+
23
+ def test_no_topic
24
+ assert_raises ArgumentError do
25
+ Heroic::SNS::Endpoint.new
26
+ end
27
+ end
28
+
29
+ def test_receive_message
30
+ result = [0, {}, []]
31
+ test = Proc.new do |env|
32
+ assert_equal env['sns.message'], @msg
33
+ result
34
+ end
35
+ sns('notification')
36
+ app = Heroic::SNS::Endpoint.new test, :topic => @msg.topic_arn
37
+ assert_equal result, app.call(@env)
38
+ end
39
+
40
+ def test_ignore_message
41
+ test = Proc.new { |env| raise "should be unreachable!" }
42
+ sns('notification')
43
+ app = Heroic::SNS::Endpoint.new test, :topic => "different-topic"
44
+ assert_nothing_raised { app.call(@env) }
45
+ end
46
+
47
+ end
@@ -0,0 +1,63 @@
1
+ require 'test/unit'
2
+ require 'heroic/sns'
3
+ require 'helper'
4
+
5
+ class MessageTest < Test::Unit::TestCase
6
+
7
+ def sns(name, timestamp = Time.now)
8
+ json = File.read("test/fixtures/#{name}.json")
9
+ msg = Heroic::SNS::Message.new(json)
10
+ msg.update_timestamp!(timestamp)
11
+ msg.sign!
12
+ return msg
13
+ end
14
+
15
+ def test_invalid_json
16
+ assert_raises Heroic::SNS::Error do
17
+ Heroic::SNS::Message.new("INV4L!D JS0N")
18
+ end
19
+ end
20
+
21
+ def test_notification
22
+ msg = sns("notification")
23
+ assert_equal 'Notification', msg.type
24
+ assert_equal 'arn:aws:sns:us-east-1:777594007835:racktest', msg.topic_arn
25
+ assert_equal 128, msg.signature.length
26
+ assert_nothing_raised { msg.verify! }
27
+ end
28
+
29
+ def test_subscription
30
+ msg = sns("subscription")
31
+ assert_equal 'SubscriptionConfirmation', msg.type
32
+ assert_equal 'arn:aws:sns:us-east-1:777594007835:racktest', msg.topic_arn
33
+ assert_equal 128, msg.signature.length
34
+ assert_nothing_raised { msg.verify! }
35
+ end
36
+
37
+ def test_unsubscribe
38
+ msg = sns("unsubscribe")
39
+ assert_equal 'UnsubscribeConfirmation', msg.type
40
+ assert_equal 'arn:aws:sns:us-east-1:777594007835:racktest', msg.topic_arn
41
+ assert_equal 128, msg.signature.length
42
+ assert_nothing_raised { msg.verify! }
43
+ end
44
+
45
+ def test_tampered
46
+ json = sns("notification").to_json
47
+ msg = Heroic::SNS::Message.new(json.gsub(/booyakasha/, 'cowabunga'))
48
+ assert_equal 'cowabunga!', msg.body
49
+ assert_raises Heroic::SNS::Error do
50
+ msg.verify!
51
+ end
52
+ end
53
+
54
+ def test_expired
55
+ t = Time.utc(1984, 5)
56
+ msg = sns("notification", t)
57
+ assert_equal t, msg.timestamp
58
+ assert_raises Heroic::SNS::Error do
59
+ msg.verify!
60
+ end
61
+ end
62
+
63
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: heroic-sns
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Benjamin Ragheb
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-05-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.4'
27
+ description: ! 'Lightweight Rack middleware for AWS Simple Notification Service (SNS)
28
+ endpoints.
29
+
30
+ SNS messages are intercepted, parsed, verified, and passed along to the Rack
31
+
32
+ application in the ''sns.message'' environment key. The gem is designed to be
33
+
34
+ lightweight and does not depend on any other gem besides ''rack'' (specifically,
35
+
36
+ the ''aws-sdk'' gem is not required).
37
+
38
+ '
39
+ email: ben@benzado.com
40
+ executables: []
41
+ extensions: []
42
+ extra_rdoc_files: []
43
+ files:
44
+ - README.md
45
+ - LICENSE
46
+ - lib/heroic/sns/endpoint.rb
47
+ - lib/heroic/sns/message.rb
48
+ - lib/heroic/sns.rb
49
+ - test/helper.rb
50
+ - test/test_endpoint.rb
51
+ - test/test_message.rb
52
+ homepage: https://github.com/benzado/heroic-sns
53
+ licenses:
54
+ - Apache
55
+ metadata: {}
56
+ post_install_message: Thanks for installing!
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: 1.8.7
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 2.0.3
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: Lightweight Rack middleware for AWS SNS endpoints
76
+ test_files: []
77
+ has_rdoc: