heroic-sns 1.1.0 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +6 -14
- data/.gitignore +1 -0
- data/CHANGELOG.md +21 -0
- data/README.md +79 -60
- data/Rakefile +6 -0
- data/heroic-sns.gemspec +2 -1
- data/lib/heroic/lru_cache.rb +37 -28
- data/lib/heroic/sns/endpoint.rb +43 -27
- data/lib/heroic/sns/message.rb +20 -6
- data/lib/heroic/sns/version.rb +1 -1
- data/test/test_lru_cache.rb +41 -42
- metadata +24 -18
- data/CHANGELOG +0 -13
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
metadata.gz: !binary |-
|
9
|
-
ZTkxZjczZTVjODdjMjBlMTM0OWU2YjFmYzI4NzM1YzZmMWYzZWFlZjQ2YzMx
|
10
|
-
ODM1ODk3ZmNlMDRmYjE4NmI3MWE4ZTVmM2RmNTgwZTc2YjMwMjEzZjRhNjEy
|
11
|
-
NTVhMDZkZjlmN2IwYjkxNTdjM2M1Mzc2ODFjMzU3ZjhhZGRjZTg=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
ZTY3YzJmZWRkZTE3OGE4ODRkNjgxNjMwMmYwYzRmZGQ4ODQ1MzE1YjMzZTcw
|
14
|
-
ZmYzMTJkYzhhM2Y5MmQ2YTQ5ZGRlYmUwMWMwYTc5YWUwM2RhMGFiOGJkZmVh
|
15
|
-
OWUyODNjYWE3Njg3Y2MyMzQ1ODI2N2Q0MGE4MmU5MTU0ODIzMDU=
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 11d6f41c3874ec698df32ac516c3d103f74bfd7b
|
4
|
+
data.tar.gz: 9dbf1f1690e3ec3888a52c661c4755c6d5c1a05e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b0e8fe315fd65c499060616c8780586c427b1ed3ff3a829df29437fe17128d764cdc502f1eed9f450c7e09c068b80e65bf9fddce08253ab2ba1d6b4dc3d1062f
|
7
|
+
data.tar.gz: 20d5a1e66117f7c30e15d4107d93d6f84d8439dad5fd58919f46eb2e7bf0209067e47c048e3e7c040dbe365dfe8e18ddb4e33f16e6eb1898bb211b20b9028eac
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
### 1.1.1 (August 9, 2013)
|
2
|
+
|
3
|
+
* Ease up json gem dependency - [@speedmanly]
|
4
|
+
* Documentation cleaned up and tests improved.
|
5
|
+
|
6
|
+
### 1.1.0 (June 20, 2013)
|
7
|
+
|
8
|
+
* Rework Cert logic - [@sbeckeriv]
|
9
|
+
|
10
|
+
### 1.0.1 (May 17, 2013)
|
11
|
+
|
12
|
+
* Gem housekeeping, based on [advice by @dblock](http://code.dblock.org/your-first-ruby-gem)
|
13
|
+
|
14
|
+
### 1.0.0 (May 5, 2013)
|
15
|
+
|
16
|
+
* Initial public release - [@benzado]
|
17
|
+
|
18
|
+
[@benzado]: http://github.com/benzado
|
19
|
+
[@sbeckeriv]: http://github.com/sbeckeriv
|
20
|
+
[@dblock]: http://github.com/dblock
|
21
|
+
[@speedmanly]: http://github.com/speedmanly
|
data/README.md
CHANGED
@@ -3,21 +3,26 @@
|
|
3
3
|
Heroic::SNS provides secure, lightweight Rack middleware for AWS Simple
|
4
4
|
Notification Service (SNS) endpoints.
|
5
5
|
|
6
|
-
Any SNS messages POSTed by Amazon to your web application are
|
7
|
-
parsed, verified, and then passed along via the
|
6
|
+
Any SNS messages POSTed by Amazon to your web application are
|
7
|
+
intercepted, parsed, verified, and then passed along via the
|
8
|
+
`sns.message` environment key.
|
8
9
|
|
9
|
-
If something goes wrong, the error will be passed along via the
|
10
|
-
environment key. `Heroic::SNS::Endpoint` does not log any
|
10
|
+
If something goes wrong, the error will be passed along via the
|
11
|
+
`sns.error` environment key. `Heroic::SNS::Endpoint` does not log any
|
12
|
+
messages itself.
|
11
13
|
|
12
|
-
**Heroic::SNS aims to be secure.** All message signatures are verified
|
13
|
-
forgeries) and stale messages are rejected (to avoid replay
|
14
|
+
**Heroic::SNS aims to be secure.** All message signatures are verified
|
15
|
+
(to avoid forgeries) and stale messages are rejected (to avoid replay
|
16
|
+
attacks).
|
14
17
|
|
15
|
-
**Heroic::SNS aims to be lightweight.** Beside Ruby standard libraries
|
16
|
-
no dependencies
|
17
|
-
|
18
|
+
**Heroic::SNS aims to be lightweight.** Beside Ruby standard libraries
|
19
|
+
there are no dependencies besides [json][] and [rack][]. Specifically,
|
20
|
+
Heroic::SNS *does not* depend on [aws-sdk][]. They will be friendly to
|
21
|
+
each other, however, if you include both in a project.
|
18
22
|
|
19
|
-
[
|
23
|
+
[json]: https://rubygems.org/gems/json
|
20
24
|
[rack]: http://rack.github.io/
|
25
|
+
[aws-sdk]: https://github.com/aws/aws-sdk-ruby
|
21
26
|
|
22
27
|
## Overview
|
23
28
|
|
@@ -29,8 +34,8 @@ in a project.
|
|
29
34
|
|
30
35
|
## How to use it
|
31
36
|
|
32
|
-
Once you have installed the gem, simply add the following to your
|
33
|
-
file:
|
37
|
+
Once you have installed the gem, simply add the following to your
|
38
|
+
`config.ru` file:
|
34
39
|
|
35
40
|
use Heroic::SNS::Endpoint, :topics => /:aws-ses-bounces$/
|
36
41
|
|
@@ -38,52 +43,64 @@ On Rails, you could also install it in `/config/initializers/sns_endpoint.rb`:
|
|
38
43
|
|
39
44
|
Rails.application.config.middleware.use Heroic::SNS::Endpoint, :topic => ...
|
40
45
|
|
41
|
-
The Endpoint class takes an options hash as an argument, and understands
|
42
|
-
options:
|
46
|
+
The Endpoint class takes an options hash as an argument, and understands
|
47
|
+
these options:
|
48
|
+
|
49
|
+
### :topic
|
43
50
|
|
44
|
-
`:topic` is required, and provides a filter that defines what SNS topics
|
45
|
-
handled by this endpoint. **A message is considered either
|
46
|
-
"off-topic".** You can supply any of the following:
|
51
|
+
`:topic` is required, and provides a filter that defines what SNS topics
|
52
|
+
are handled by this endpoint. **A message is considered either
|
53
|
+
"on-topic" or "off-topic".** You can supply any of the following:
|
47
54
|
|
48
55
|
- a `String` containing a single topic ARN
|
56
|
+
|
49
57
|
- an `Array` of `String` representing a list of topic ARNs
|
58
|
+
|
50
59
|
- a `RegExp` which matches on-topic ARNs
|
51
|
-
|
52
|
-
|
60
|
+
|
61
|
+
- a `Proc` which accepts an ARN as an argument and returns `true` or
|
62
|
+
`false` for on-topic and off-topic ARNs, respectively.
|
53
63
|
|
54
64
|
The key `:topics` is also supported.
|
55
65
|
|
66
|
+
### :auto_confirm
|
67
|
+
|
56
68
|
`:auto_confirm` affects how on-topic subscription confirmations are handled.
|
57
69
|
|
58
|
-
- If `true`, they are confirmed by retrieving the URL in the
|
59
|
-
field of the SNS message, and your app is
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
70
|
+
- If `true`, they are confirmed by retrieving the URL in the
|
71
|
+
`SubscribeURL` field of the SNS message, and your app is NOT notified.
|
72
|
+
This is the default.
|
73
|
+
|
74
|
+
- If `false`, they are ignored; your app is NOT notified.
|
75
|
+
|
76
|
+
- If `nil`, there is no special handling and the message is passed along
|
77
|
+
to your app.
|
78
|
+
|
79
|
+
### :auto_resubscribe
|
65
80
|
|
66
81
|
`:auto_resubscribe` affects how on-topic unsubscribe confirmations are handled.
|
67
82
|
|
68
|
-
- If `
|
69
|
-
|
70
|
-
|
71
|
-
- If `
|
72
|
-
app
|
83
|
+
- If `false`, they are ignored and your app is NOT notified. This is the
|
84
|
+
default.
|
85
|
+
|
86
|
+
- If `true`, they topic is automatically re-subscribed by retrieving the
|
87
|
+
URL in the `SubscribeURL` field of the SNS message, and your app is
|
88
|
+
NOT notified.
|
73
89
|
|
74
|
-
|
90
|
+
- If `nil`, there is no special handling and the message is passed along
|
91
|
+
to your app.
|
75
92
|
|
76
|
-
If you are a control-freak and want no special handling whatsoever, use
|
77
|
-
options:
|
93
|
+
If you are a control-freak and want no special handling whatsoever, use
|
94
|
+
these options:
|
78
95
|
|
79
96
|
use Heroic::SNS::Endpoint, :topics => Proc.new { true }, :auto_confirm => nil, :auto_resubscribe => nil
|
80
97
|
|
81
|
-
Then the object will simply parse and verify SNS messages it finds and
|
82
|
-
along to your app, taking no action.
|
98
|
+
Then the object will simply parse and verify SNS messages it finds and
|
99
|
+
pass them along to your app, taking no action.
|
83
100
|
|
84
|
-
Once the middleware is set up, any notifications will be made available
|
85
|
-
Rack environment under the `sns.message` key. If you are using
|
86
|
-
controller would have a method like this:
|
101
|
+
Once the middleware is set up, any notifications will be made available
|
102
|
+
in your Rack environment under the `sns.message` key. If you are using
|
103
|
+
Rails, your controller would have a method like this:
|
87
104
|
|
88
105
|
skip_before_filter :verify_authenticity_token, :only => [:handle_notification]
|
89
106
|
|
@@ -98,43 +115,45 @@ controller would have a method like this:
|
|
98
115
|
head :ok
|
99
116
|
end
|
100
117
|
|
101
|
-
You must skip the authenticity token verification to allow Amazon to
|
102
|
-
controller action. Be careful not to disable it for more
|
103
|
-
Be sure to disable any authentication checks for
|
118
|
+
You must skip the authenticity token verification to allow Amazon to
|
119
|
+
POST to the controller action. Be careful not to disable it for more
|
120
|
+
actions than you need. Be sure to disable any authentication checks for
|
121
|
+
that action, too.
|
104
122
|
|
105
123
|
## Multiple endpoint URLs
|
106
124
|
|
107
|
-
If you are receiving multiple notifications at multiple endpoint URLs,
|
108
|
-
should only include one instance of the Endpoint in your middleware
|
109
|
-
ensure that its topic filter allows all the notifications you
|
110
|
-
to pass through.
|
125
|
+
If you are receiving multiple notifications at multiple endpoint URLs,
|
126
|
+
you should only include one instance of the Endpoint in your middleware
|
127
|
+
stack, and ensure that its topic filter allows all the notifications you
|
128
|
+
are interested in to pass through.
|
111
129
|
|
112
130
|
`Endpoint` does not interact with the URL path at all; if you want your
|
113
131
|
subscriptions to go to different URLs, simply set them up that way.
|
114
132
|
|
115
133
|
## Off-topic notifications
|
116
134
|
|
117
|
-
As a security measure, `Endpoint` requires you to set up a topic filter.
|
118
|
-
notifications that do not match this filter are not passed along to
|
119
|
-
application.
|
135
|
+
As a security measure, `Endpoint` requires you to set up a topic filter.
|
136
|
+
Any notifications that do not match this filter are not passed along to
|
137
|
+
your application.
|
120
138
|
|
121
|
-
All off-topic messages are ignored with one exception: if the message is
|
122
|
-
regular notification (meaning your app has an active subscription)
|
123
|
-
message can be verified as authentic (by checking its
|
124
|
-
will cancel the subscription by visiting the URL
|
125
|
-
of the message.
|
139
|
+
All off-topic messages are ignored with one exception: if the message is
|
140
|
+
a regular notification (meaning your app has an active subscription)
|
141
|
+
*and* the message can be verified as authentic (by checking its
|
142
|
+
signature), `Endpoint` will cancel the subscription by visiting the URL
|
143
|
+
in the `UnsubscribeURL` field of the message.
|
126
144
|
|
127
|
-
If you would rather make decision about on-topic and off-topic
|
128
|
-
your own code, simply pass `Proc.new { true }` as the
|
129
|
-
messages will be treated as on topic. Be aware
|
130
|
-
`:auto_confirm` enabled with a permissive
|
131
|
-
anyone to subscribe your web app to any
|
145
|
+
If you would rather make decision about on-topic and off-topic
|
146
|
+
notifications in your own code, simply pass `Proc.new { true }` as the
|
147
|
+
topic filter, and all messages will be treated as on topic. Be aware
|
148
|
+
that it is dangerous to leave `:auto_confirm` enabled with a permissive
|
149
|
+
topic filter, as this will allow anyone to subscribe your web app to any
|
150
|
+
SNS notification.
|
132
151
|
|
133
152
|
## Contributing
|
134
153
|
|
135
154
|
* Fork the project.
|
136
155
|
* Make your feature addition or bug fix and include tests.
|
137
|
-
* Update `CHANGELOG`.
|
156
|
+
* Update `CHANGELOG.md`.
|
138
157
|
* Send a pull request.
|
139
158
|
|
140
159
|
## Copyright and License
|
data/Rakefile
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'bundler/gem_tasks'
|
3
3
|
require 'rake/testtask'
|
4
|
+
require 'rdoc/task'
|
4
5
|
|
5
6
|
Bundler.setup(:default, :development)
|
6
7
|
|
@@ -8,6 +9,11 @@ Rake::TestTask.new do |t|
|
|
8
9
|
t.libs << 'test'
|
9
10
|
end
|
10
11
|
|
12
|
+
RDoc::Task.new do |rdoc|
|
13
|
+
rdoc.main = "README.md"
|
14
|
+
rdoc.rdoc_files.include("README.md", "CHANGELOG.md", "lib")
|
15
|
+
end
|
16
|
+
|
11
17
|
task :demo do
|
12
18
|
sh 'rackup -Ilib demo/config.ru'
|
13
19
|
end
|
data/heroic-sns.gemspec
CHANGED
@@ -26,6 +26,7 @@ EOD
|
|
26
26
|
|
27
27
|
s.platform = Gem::Platform::RUBY
|
28
28
|
s.required_ruby_version = '>= 1.8.7'
|
29
|
+
s.add_development_dependency 'rdoc', '~> 4.0'
|
29
30
|
s.add_runtime_dependency 'rack', '~> 1.4'
|
30
|
-
s.add_runtime_dependency 'json', '~> 1.7
|
31
|
+
s.add_runtime_dependency 'json', '~> 1.7'
|
31
32
|
end
|
data/lib/heroic/lru_cache.rb
CHANGED
@@ -3,12 +3,12 @@ module Heroic
|
|
3
3
|
# This LRU Cache is a generic key-value store that is designed to be safe for
|
4
4
|
# concurrent access. It uses a doubly-linked-list to identify which item was
|
5
5
|
# least recently retrieved, and a Hash for fast retrieval by key.
|
6
|
-
|
6
|
+
#
|
7
7
|
# To support concurrent access, it uses two levels of locks: a cache-level lock
|
8
8
|
# is used to locate or create the desired node and move it to the front of the
|
9
9
|
# LRU list. A node-level lock is used to synchronize access to the node's
|
10
10
|
# value.
|
11
|
-
|
11
|
+
#
|
12
12
|
# If a thread is busy generating a value to be stored in the cache, other
|
13
13
|
# threads will still be able to read and write to other keys with no conflict.
|
14
14
|
# However, if a second thread tries to read the value that the first thread is
|
@@ -16,24 +16,6 @@ module Heroic
|
|
16
16
|
|
17
17
|
class LRUCache
|
18
18
|
|
19
|
-
class Node
|
20
|
-
attr_reader :key
|
21
|
-
attr_accessor :left, :right
|
22
|
-
def initialize(key)
|
23
|
-
@key = key
|
24
|
-
@lock = Mutex.new
|
25
|
-
end
|
26
|
-
def read
|
27
|
-
@lock.synchronize { @value ||= yield(@key) }
|
28
|
-
end
|
29
|
-
def write(value)
|
30
|
-
@lock.synchronize { @value = value }
|
31
|
-
end
|
32
|
-
def to_s
|
33
|
-
sprintf '<Node:%x(%s)>', self.object_id, @key.inspect
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
19
|
# If you yield a block to the constructor, it will be called on every cache
|
38
20
|
# miss to generate the needed value. This is optional but recommended, as
|
39
21
|
# the block will run while holding a lock on the cache node associated with
|
@@ -53,25 +35,34 @@ module Heroic
|
|
53
35
|
@rightmost = nil
|
54
36
|
end
|
55
37
|
|
38
|
+
# Retrieve a value from the cache for a given key. If the value is in the
|
39
|
+
# cache, the method will return immediately. If the value is not in the
|
40
|
+
# cache and a block was provided to the constructor, it will be invoked to
|
41
|
+
# generate the value and insert it into the cache. If another thread is in
|
42
|
+
# the process of generating the same value, the current thread will wait for
|
43
|
+
# it to complete.
|
44
|
+
#
|
45
|
+
# If the cache is full, this method may cause the least recently used item
|
46
|
+
# to be evicted from the cache.
|
47
|
+
|
56
48
|
def get(key)
|
57
49
|
node = node_for_key(key)
|
58
50
|
node.read(&@block)
|
59
51
|
end
|
60
52
|
|
53
|
+
# Inserts a value into the cache, assigning it to the given key. (Instead of
|
54
|
+
# directly inserting values into the cache, it is recommended that you supply
|
55
|
+
# a block to the constructor to generate values on demand.)
|
56
|
+
|
61
57
|
def put(key, value)
|
62
58
|
node = node_for_key(key)
|
63
59
|
node.write(value)
|
64
60
|
end
|
65
61
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
@leftmost = nil
|
70
|
-
@rightmost = nil
|
71
|
-
end
|
72
|
-
end
|
62
|
+
# Verify the data structures used to maintain the cache. If a problem is
|
63
|
+
# detected, an exception is raised. This method is intended for testing and
|
64
|
+
# debugging only.
|
73
65
|
|
74
|
-
# Verify the list structure. Intended for testing and debugging only.
|
75
66
|
def verify!
|
76
67
|
@lock.synchronize do
|
77
68
|
left_to_right = Array.new
|
@@ -111,6 +102,24 @@ module Heroic
|
|
111
102
|
|
112
103
|
private
|
113
104
|
|
105
|
+
class Node # :nodoc:
|
106
|
+
attr_reader :key
|
107
|
+
attr_accessor :left, :right
|
108
|
+
def initialize(key)
|
109
|
+
@key = key
|
110
|
+
@lock = Mutex.new
|
111
|
+
end
|
112
|
+
def read
|
113
|
+
@lock.synchronize { @value ||= yield(@key) }
|
114
|
+
end
|
115
|
+
def write(value)
|
116
|
+
@lock.synchronize { @value = value }
|
117
|
+
end
|
118
|
+
def to_s
|
119
|
+
sprintf '<Node:%x(%s)>', self.object_id, @key.inspect
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
114
123
|
def node_for_key(key)
|
115
124
|
@lock.synchronize do
|
116
125
|
node = @store[key]
|
data/lib/heroic/sns/endpoint.rb
CHANGED
@@ -1,38 +1,54 @@
|
|
1
1
|
require 'openssl'
|
2
2
|
require 'open-uri'
|
3
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
4
|
module Heroic
|
32
5
|
module SNS
|
33
6
|
|
34
7
|
SUBSCRIPTION_ARN_HTTP_HEADER = 'HTTP_X_AMZ_SNS_SUBSCRIPTION_ARN'
|
35
8
|
|
9
|
+
=begin rdoc
|
10
|
+
|
11
|
+
Heroic::SNS::Endpoint is Rack middleware which intercepts messages from
|
12
|
+
Amazon's Simple Notification Service (SNS). It makes the parsed and
|
13
|
+
verified message available to your application in the Rack environment
|
14
|
+
under the 'sns.message' key. If an error occurred during message handling,
|
15
|
+
the error is available in the Rack environment in the 'sns.error' key.
|
16
|
+
|
17
|
+
Endpoint is to be initialized with a hash of options. It understands three
|
18
|
+
different options:
|
19
|
+
|
20
|
+
+:topic+ (or +:topics+) specifies a filter that defines what SNS topics
|
21
|
+
are handled by this endpoint ("on-topic"). You can supply any of the
|
22
|
+
following:
|
23
|
+
- a topic ARN as a String
|
24
|
+
- a list of topic ARNs as an Array of Strings
|
25
|
+
- a regular expression matching on-topic ARNs
|
26
|
+
- a Proc which takes a topic ARN as a String and returns true or false.
|
27
|
+
You *must* specify a topic filter. Use <code>Proc.new{true}</code> if
|
28
|
+
you insist on indiscriminately accepting all notifications.
|
29
|
+
|
30
|
+
+:auto_confirm+ determines how SubscriptionConfirmation messages are handled.
|
31
|
+
- If true, the subscription is confirmed and your app is not notified.
|
32
|
+
This is the default.
|
33
|
+
- If false, the subscription is ignored and your app is not notified.
|
34
|
+
- If nil, the message is passed along to your app.
|
35
|
+
|
36
|
+
+:auto_resubscribe+ affects how on-topic UnsubscribeConfirmation messages are handled.
|
37
|
+
- If false, they are ignored and your app is also not notified.
|
38
|
+
This is the default.
|
39
|
+
- If true, they topic is automatically re-subscribed by retrieving the URL in
|
40
|
+
the `SubscribeURL` field of the SNS message, and your app is not notified.
|
41
|
+
- If nil, there is no special handling and the message is passed along to your
|
42
|
+
app.
|
43
|
+
|
44
|
+
You can install this in your config.ru:
|
45
|
+
use Heroic::SNS::Endpoint, :topics => /whatever/
|
46
|
+
|
47
|
+
For Rails, you can also install it in /config/initializers/sns_endpoint.rb:
|
48
|
+
Rails.application.config.middleware.use Heroic::SNS::Endpoint, :topic => ...
|
49
|
+
|
50
|
+
=end
|
51
|
+
|
36
52
|
class Endpoint
|
37
53
|
|
38
54
|
DEFAULT_OPTIONS = { :auto_confirm => true, :auto_resubscribe => false }
|
data/lib/heroic/sns/message.rb
CHANGED
@@ -19,7 +19,10 @@ module Heroic
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
# Encapsulates an SNS message.
|
22
|
+
# Encapsulates an SNS message. Since Endpoint takes care of authenticating
|
23
|
+
# the message, most of the time you will simply be interested in retrieving
|
24
|
+
# the +subject+ and +body+ from the message and acting on it.
|
25
|
+
#
|
23
26
|
# See: http://docs.aws.amazon.com/sns/latest/gsg/json-formats.html
|
24
27
|
class Message
|
25
28
|
|
@@ -29,6 +32,8 @@ module Heroic
|
|
29
32
|
raise Error.new("failed to parse message as JSON: #{e.message}")
|
30
33
|
end
|
31
34
|
|
35
|
+
# The message type will be one of +SubscriptionConfirmation+,
|
36
|
+
# +UnsubscribeConfirmation+, and +Notification+.
|
32
37
|
def type
|
33
38
|
@msg['Type']
|
34
39
|
end
|
@@ -37,11 +42,14 @@ module Heroic
|
|
37
42
|
@msg['TopicArn']
|
38
43
|
end
|
39
44
|
|
45
|
+
# A Universally Unique Identifier, unique for each message published. For
|
46
|
+
# a notification that Amazon SNS resends during a retry, the message ID of
|
47
|
+
# the original message is used.
|
40
48
|
def id
|
41
49
|
@msg['MessageId']
|
42
50
|
end
|
43
51
|
|
44
|
-
# The timestamp as a Time object.
|
52
|
+
# The timestamp when the message was published, as a Time object.
|
45
53
|
def timestamp
|
46
54
|
Time.xmlschema(@msg['Timestamp'])
|
47
55
|
end
|
@@ -64,6 +72,9 @@ module Heroic
|
|
64
72
|
@msg['Subject']
|
65
73
|
end
|
66
74
|
|
75
|
+
# The message payload. As far as Amazon and this class are concerned, the
|
76
|
+
# message payload is just a string of bytes. If you are expecting, for
|
77
|
+
# example, a JSON object, you will need to pass this to a JSON parser.
|
67
78
|
def body
|
68
79
|
@msg['Message']
|
69
80
|
end
|
@@ -77,13 +88,13 @@ module Heroic
|
|
77
88
|
end
|
78
89
|
|
79
90
|
# The token is used to confirm subscriptions via the SNS API. If you visit
|
80
|
-
# the
|
91
|
+
# the +subscribe_url+, you can ignore this field.
|
81
92
|
def token
|
82
93
|
@msg['Token']
|
83
94
|
end
|
84
95
|
|
85
|
-
def ==(
|
86
|
-
@msg ==
|
96
|
+
def ==(other)
|
97
|
+
other.is_a?(Message) && @msg == other.instance_variable_get(:@msg)
|
87
98
|
end
|
88
99
|
|
89
100
|
def hash
|
@@ -98,11 +109,14 @@ module Heroic
|
|
98
109
|
string << ">"
|
99
110
|
end
|
100
111
|
|
112
|
+
# Returns a JSON serialization of the message. Note that it may not be
|
113
|
+
# identical to the serialization that was retrieved from the network.
|
101
114
|
def to_json
|
102
115
|
@msg.to_json
|
103
116
|
end
|
104
117
|
|
105
|
-
# Verifies the message signature. Raises
|
118
|
+
# Verifies the message signature. Raises Error if it is not valid.
|
119
|
+
#
|
106
120
|
# See: http://docs.aws.amazon.com/sns/latest/gsg/SendMessageToHttp.verify.signature.html
|
107
121
|
def verify!
|
108
122
|
age = Time.now - timestamp
|
data/lib/heroic/sns/version.rb
CHANGED
data/test/test_lru_cache.rb
CHANGED
@@ -14,12 +14,12 @@ class LRUCacheTest < Test::Unit::TestCase
|
|
14
14
|
|
15
15
|
def test_get_put
|
16
16
|
cache = Heroic::LRUCache.new(1)
|
17
|
-
assert_nil cache.get(:
|
18
|
-
cache.put(:
|
19
|
-
assert_equal :
|
20
|
-
cache.put(:
|
21
|
-
assert_equal
|
22
|
-
assert_nil cache.get(:
|
17
|
+
assert_nil cache.get(:casey)
|
18
|
+
cache.put(:casey, :jones)
|
19
|
+
assert_equal :jones, cache.get(:casey)
|
20
|
+
cache.put(:april, :oneil)
|
21
|
+
assert_equal :oneil, cache.get(:april)
|
22
|
+
assert_nil cache.get(:casey)
|
23
23
|
end
|
24
24
|
|
25
25
|
def test_exceptions
|
@@ -32,41 +32,41 @@ class LRUCacheTest < Test::Unit::TestCase
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
assert_raises RuntimeError do
|
35
|
-
cache.get(:
|
35
|
+
cache.get(:ooze)
|
36
36
|
end
|
37
37
|
@should_throw = false
|
38
|
-
assert_equal 4, cache.get(:
|
38
|
+
assert_equal 4, cache.get(:ooze)
|
39
39
|
end
|
40
40
|
|
41
41
|
def test_dynamic
|
42
42
|
@counter = 0
|
43
|
-
cache = Heroic::LRUCache.new(3) { |k| @counter += 1;
|
43
|
+
cache = Heroic::LRUCache.new(3) { |k| @counter += 1; k.to_s.length }
|
44
44
|
assert_equal 0, @counter
|
45
45
|
cache.verify!
|
46
|
-
assert_equal
|
46
|
+
assert_equal 3, cache.get(:leo); assert_equal 1, @counter
|
47
47
|
cache.verify!
|
48
|
-
assert_equal
|
48
|
+
assert_equal 3, cache.get(:leo); assert_equal 1, @counter
|
49
49
|
cache.verify!
|
50
|
-
assert_equal
|
50
|
+
assert_equal 6, cache.get(:donnie); assert_equal 2, @counter
|
51
51
|
cache.verify!
|
52
|
-
assert_equal
|
52
|
+
assert_equal 6, cache.get(:donnie); assert_equal 2, @counter
|
53
53
|
cache.verify!
|
54
|
-
assert_equal
|
54
|
+
assert_equal 5, cache.get(:mikey); assert_equal 3, @counter
|
55
55
|
cache.verify!
|
56
|
-
assert_equal
|
56
|
+
assert_equal 5, cache.get(:mikey); assert_equal 3, @counter
|
57
57
|
cache.verify!
|
58
58
|
# raph will push leo out of cache
|
59
|
-
assert_equal
|
59
|
+
assert_equal 4, cache.get(:raph); assert_equal 4, @counter
|
60
60
|
cache.verify!
|
61
|
-
assert_equal
|
61
|
+
assert_equal 4, cache.get(:raph); assert_equal 4, @counter
|
62
62
|
cache.verify!
|
63
63
|
# mikey and donnie remain in cache
|
64
|
-
assert_equal
|
64
|
+
assert_equal 5, cache.get(:mikey); assert_equal 4, @counter
|
65
65
|
cache.verify!
|
66
|
-
assert_equal
|
66
|
+
assert_equal 6, cache.get(:donnie); assert_equal 4, @counter
|
67
67
|
cache.verify!
|
68
68
|
# leo will have to be refetched
|
69
|
-
assert_equal
|
69
|
+
assert_equal 3, cache.get(:leo); assert_equal 5, @counter
|
70
70
|
cache.verify!
|
71
71
|
end
|
72
72
|
|
@@ -76,35 +76,34 @@ class LRUCacheTest < Test::Unit::TestCase
|
|
76
76
|
cache = Heroic::LRUCache.new(100) do |k|
|
77
77
|
sleep 1 # simulate slow generation, such as network I/O
|
78
78
|
@lock.synchronize { @counter += 1 }
|
79
|
-
k.to_s
|
79
|
+
k.to_s.length
|
80
80
|
end
|
81
81
|
# load the cache with things to read
|
82
|
-
cache.put(:
|
83
|
-
cache.put(:
|
84
|
-
cache.put(:c, 'c')
|
82
|
+
cache.put(:leo, 0)
|
83
|
+
cache.put(:donnie, 0)
|
85
84
|
start_time = Time.now
|
86
|
-
#
|
87
|
-
|
88
|
-
|
85
|
+
# Start threads to fetch in background. :leo and :donnie should return
|
86
|
+
# immediately, because the values are in the cache; :mikey and :raph should
|
87
|
+
# run concurrently; the second :raph should wait on the first :raph to
|
88
|
+
# complete.
|
89
|
+
threads = [:leo, :donnie, :mikey, :raph, :raph].map do |k|
|
90
|
+
Thread.new { cache.get(k) }
|
89
91
|
end
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
# while background threads fetch, reading other keys should not be blocked
|
94
|
-
assert_equal 'a', cache.get(:a)
|
95
|
-
assert_equal 'b', cache.get(:b)
|
96
|
-
assert_equal 'c', cache.get(:c)
|
92
|
+
# Fetching values already in the cache should not block.
|
93
|
+
assert_equal 0, cache.get(:leo)
|
94
|
+
assert_equal 0, cache.get(:donnie)
|
97
95
|
assert_equal 0, @lock.synchronize { @counter }
|
98
|
-
#
|
99
|
-
assert_equal
|
100
|
-
assert_equal
|
96
|
+
# Fetching values being computed now will block until somebody computes them.
|
97
|
+
assert_equal 5, cache.get(:mikey)
|
98
|
+
assert_equal 4, cache.get(:raph)
|
101
99
|
assert_equal 2, @lock.synchronize { @counter }
|
102
|
-
#
|
103
|
-
assert_equal
|
104
|
-
assert_equal
|
100
|
+
# Fetching those same values should not trigger a recompute.
|
101
|
+
assert_equal 5, cache.get(:mikey)
|
102
|
+
assert_equal 4, cache.get(:raph)
|
105
103
|
assert_equal 2, @lock.synchronize { @counter }
|
106
|
-
#
|
107
|
-
|
104
|
+
# Let's wait for all threads to finish, then check the clock to make sure we
|
105
|
+
# only slept for a second.
|
106
|
+
threads.each { |t| t.join }
|
108
107
|
time_elapsed = (Time.now - start_time)
|
109
108
|
assert_in_delta time_elapsed, 1.0, 0.01
|
110
109
|
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: heroic-sns
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin Ragheb
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-08-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rdoc
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: rack
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -30,29 +44,21 @@ dependencies:
|
|
30
44
|
requirements:
|
31
45
|
- - ~>
|
32
46
|
- !ruby/object:Gem::Version
|
33
|
-
version: 1.7
|
47
|
+
version: '1.7'
|
34
48
|
type: :runtime
|
35
49
|
prerelease: false
|
36
50
|
version_requirements: !ruby/object:Gem::Requirement
|
37
51
|
requirements:
|
38
52
|
- - ~>
|
39
53
|
- !ruby/object:Gem::Version
|
40
|
-
version: 1.7
|
41
|
-
description:
|
42
|
-
Service (SNS)
|
43
|
-
|
54
|
+
version: '1.7'
|
55
|
+
description: |
|
56
|
+
Secure, lightweight Rack middleware for Amazon Simple Notification Service (SNS)
|
44
57
|
endpoints. SNS messages are intercepted, parsed, verified, and then passed along
|
45
|
-
|
46
|
-
to the web application via the ''sns.message'' environment key. Heroic::SNS has
|
47
|
-
no
|
48
|
-
|
58
|
+
to the web application via the 'sns.message' environment key. Heroic::SNS has no
|
49
59
|
dependencies besides Rack (specifically, the aws-sdk gem is not needed).
|
50
|
-
|
51
60
|
SNS message signatures are verified in order to reject forgeries and replay
|
52
|
-
|
53
61
|
attacks.
|
54
|
-
|
55
|
-
'
|
56
62
|
email: ben@benzado.com
|
57
63
|
executables: []
|
58
64
|
extensions: []
|
@@ -60,7 +66,7 @@ extra_rdoc_files: []
|
|
60
66
|
files:
|
61
67
|
- .gitignore
|
62
68
|
- .travis.yml
|
63
|
-
- CHANGELOG
|
69
|
+
- CHANGELOG.md
|
64
70
|
- Gemfile
|
65
71
|
- LICENSE
|
66
72
|
- README.md
|
@@ -93,12 +99,12 @@ require_paths:
|
|
93
99
|
- lib
|
94
100
|
required_ruby_version: !ruby/object:Gem::Requirement
|
95
101
|
requirements:
|
96
|
-
- -
|
102
|
+
- - '>='
|
97
103
|
- !ruby/object:Gem::Version
|
98
104
|
version: 1.8.7
|
99
105
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
106
|
requirements:
|
101
|
-
- -
|
107
|
+
- - '>='
|
102
108
|
- !ruby/object:Gem::Version
|
103
109
|
version: '0'
|
104
110
|
requirements: []
|
data/CHANGELOG
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
### 1.1.0 (June 20, 2013)
|
2
|
-
|
3
|
-
* Rework Cert logic - [@sbeckeriv]
|
4
|
-
|
5
|
-
### 1.0.1 (May 17, 2013)
|
6
|
-
|
7
|
-
* Gem housekeeping, based on [advice by dblock](http://code.dblock.org/your-first-ruby-gem)
|
8
|
-
|
9
|
-
### 1.0.0 (May 5, 2013)
|
10
|
-
|
11
|
-
* Initial public release - [@benzado]
|
12
|
-
|
13
|
-
[@benzado]: http://github.com/benzado
|