heroic-sns 1.0.1 → 1.2
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.
- checksums.yaml +6 -14
- data/.gitignore +2 -0
- data/.travis.yml +20 -4
- data/CHANGELOG.md +40 -0
- data/Gemfile +2 -0
- data/README.md +85 -63
- data/Rakefile +6 -0
- data/gemfiles/Gemfile.rack-1.x +7 -0
- data/gemfiles/Gemfile.ruby-1.8 +8 -0
- data/heroic-sns.gemspec +13 -3
- data/lib/heroic/lru_cache.rb +165 -0
- data/lib/heroic/sns/endpoint.rb +47 -30
- data/lib/heroic/sns/message.rb +34 -15
- data/lib/heroic/sns/version.rb +1 -1
- data/test/fixtures/aws-sns-endpoints.txt +27 -0
- data/test/helper.rb +16 -12
- data/test/test_endpoint.rb +13 -0
- data/test/test_lru_cache.rb +111 -0
- data/test/test_message.rb +33 -5
- metadata +50 -27
- data/CHANGELOG +0 -9
- data/description.txt +0 -6
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
metadata.gz: !binary |-
|
9
|
-
MDZkY2QwOTkwZmRkMzc4ZmMwNTcyYTc5MDRlZjQ1ZTkxN2JmZjMwZWI4MTA4
|
10
|
-
MTRmYzRjMmRjM2M3YTJiOTdlOGZlN2IyMzQyMjAyYWJiZjQ4OTc2Zjg1Yjll
|
11
|
-
MGQ5NjM5ZmM5ZmY5YjdmYjg4Y2ZmZjFiNmUzZDc2Zjk3OWQwMDI=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
NDBhNWZmMzY1N2M0YTM5MWY4Njk0ZWQyNTc0NGY0NDk2NTQ4N2U4NDIwODE1
|
14
|
-
NWMwNjhlNzYyNWUzZmE2YjZiMzBkN2Q3ZGM1NjczMDJiOGQ2MGVlNzAyZDQ5
|
15
|
-
MWY4N2RkZDJlOGQ4YWJlM2U0OTUzN2ZmNDNlZWIyODdjYmEwZDU=
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 061bb44887daecae4a05c0a3c75debc2d59c55b7e0a733bc783cc665de9d7a3f
|
4
|
+
data.tar.gz: e0f2aed506a0ebae75e408376a2c102f7825a6a9123f529286be92d6535c7416
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2007d432d0585597cb35e35cb68af3c82c9d9210b31ec227d432b481228973e25aa8c17410ac9ca6ea487cec698ea56842986c42964f88e6c3cd7c5a22ad6dba
|
7
|
+
data.tar.gz: d6c83fd48d0f59bc5194bca39773c24eae90c34384251dcd5af8ee8656d4f9794d6e08154c40d39327b91d4713ef9370dccd8873d0d274d63ce5943af898ccec
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,6 +1,22 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
|
-
-
|
4
|
-
- "2.
|
5
|
-
- "
|
6
|
-
- "
|
3
|
+
- "2.7"
|
4
|
+
- "2.6"
|
5
|
+
- "2.5"
|
6
|
+
- "2.4"
|
7
|
+
- "2.3"
|
8
|
+
- "2.2"
|
9
|
+
- "2.1"
|
10
|
+
- "2.0"
|
11
|
+
gemfile:
|
12
|
+
- Gemfile
|
13
|
+
- gemfiles/Gemfile.rack-1.x
|
14
|
+
before_install:
|
15
|
+
- gem update bundler
|
16
|
+
matrix:
|
17
|
+
exclude:
|
18
|
+
- { rvm: "2.1", gemfile: Gemfile }
|
19
|
+
- { rvm: "2.0", gemfile: Gemfile }
|
20
|
+
include:
|
21
|
+
- { rvm: "1.8.7", dist: precise, gemfile: gemfiles/Gemfile.ruby-1.8 }
|
22
|
+
- { rvm: "1.9.3", dist: trusty, gemfile: gemfiles/Gemfile.rack-1.x }
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
### 1.2
|
2
|
+
|
3
|
+
* Add Ruby 2.4, 2.5, 2.6. 2.7 to the Travis CI tests
|
4
|
+
* Relax json gem dependency - [@biinari]
|
5
|
+
* Stricter signing URL check - [@biinari]
|
6
|
+
* Show age when raising 'time is in the future' error - [@mamedov]
|
7
|
+
|
8
|
+
### 1.1.3 (July 7, 2016)
|
9
|
+
|
10
|
+
* Relax rack version requirement to allow usage with rack 2.x - [@fschwahn]
|
11
|
+
|
12
|
+
### 1.1.2 (March 23, 2016)
|
13
|
+
|
14
|
+
* Rewind the request body, in case the application wants to read it again - [@cobbr2]
|
15
|
+
|
16
|
+
### 1.1.1 (August 9, 2013)
|
17
|
+
|
18
|
+
* Ease up json gem dependency - [@speedmanly]
|
19
|
+
* Documentation cleaned up and tests improved.
|
20
|
+
|
21
|
+
### 1.1.0 (June 20, 2013)
|
22
|
+
|
23
|
+
* Rework Cert logic - [@sbeckeriv]
|
24
|
+
|
25
|
+
### 1.0.1 (May 17, 2013)
|
26
|
+
|
27
|
+
* Gem housekeeping, based on [advice by @dblock](http://code.dblock.org/your-first-ruby-gem)
|
28
|
+
|
29
|
+
### 1.0.0 (May 5, 2013)
|
30
|
+
|
31
|
+
* Initial public release - [@benzado]
|
32
|
+
|
33
|
+
[@benzado]: https://github.com/benzado
|
34
|
+
[@biinari]: https://github.com/biinari
|
35
|
+
[@cobbr2]: https://github.com/cobbr2
|
36
|
+
[@dblock]: https://github.com/dblock
|
37
|
+
[@fschwahn]: https://github.com/fschwahn
|
38
|
+
[@mamedov]: https://github.com/mamedov
|
39
|
+
[@sbeckeriv]: https://github.com/sbeckeriv
|
40
|
+
[@speedmanly]: https://github.com/speedmanly
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,23 +1,33 @@
|
|
1
1
|
# Heroic::SNS, Rack middleware for Amazon SNS endpoints
|
2
2
|
|
3
|
+
[](http://badge.fury.io/rb/heroic-sns)
|
4
|
+
[](https://travis-ci.org/benzado/heroic-sns)
|
5
|
+
|
3
6
|
Heroic::SNS provides secure, lightweight Rack middleware for AWS Simple
|
4
7
|
Notification Service (SNS) endpoints.
|
5
8
|
|
6
|
-
Any SNS messages POSTed by Amazon to your web application are
|
7
|
-
parsed, verified, and then passed along via the
|
9
|
+
Any SNS messages POSTed by Amazon to your web application are
|
10
|
+
intercepted, parsed, verified, and then passed along via the
|
11
|
+
`sns.message` environment key.
|
12
|
+
(In case you need it, the original, unparsed message is also available in the
|
13
|
+
body of the request.)
|
8
14
|
|
9
|
-
If something goes wrong, the error will be passed along via the
|
10
|
-
environment key. `Heroic::SNS::Endpoint` does not log any
|
15
|
+
If something goes wrong, the error will be passed along via the
|
16
|
+
`sns.error` environment key. `Heroic::SNS::Endpoint` does not log any
|
17
|
+
messages itself.
|
11
18
|
|
12
|
-
**Heroic::SNS aims to be secure.** All message signatures are verified
|
13
|
-
forgeries) and stale messages are rejected (to avoid replay
|
19
|
+
**Heroic::SNS aims to be secure.** All message signatures are verified
|
20
|
+
(to avoid forgeries) and stale messages are rejected (to avoid replay
|
21
|
+
attacks).
|
14
22
|
|
15
|
-
**Heroic::SNS aims to be lightweight.** Beside Ruby standard libraries
|
16
|
-
no dependencies
|
17
|
-
|
23
|
+
**Heroic::SNS aims to be lightweight.** Beside Ruby standard libraries
|
24
|
+
there are no dependencies besides [json][] and [rack][]. Specifically,
|
25
|
+
Heroic::SNS *does not* depend on [aws-sdk][]. They will be friendly to
|
26
|
+
each other, however, if you include both in a project.
|
18
27
|
|
19
|
-
[
|
28
|
+
[json]: https://rubygems.org/gems/json
|
20
29
|
[rack]: http://rack.github.io/
|
30
|
+
[aws-sdk]: https://github.com/aws/aws-sdk-ruby
|
21
31
|
|
22
32
|
## Overview
|
23
33
|
|
@@ -29,8 +39,8 @@ in a project.
|
|
29
39
|
|
30
40
|
## How to use it
|
31
41
|
|
32
|
-
Once you have installed the gem, simply add the following to your
|
33
|
-
file:
|
42
|
+
Once you have installed the gem, simply add the following to your
|
43
|
+
`config.ru` file:
|
34
44
|
|
35
45
|
use Heroic::SNS::Endpoint, :topics => /:aws-ses-bounces$/
|
36
46
|
|
@@ -38,52 +48,64 @@ On Rails, you could also install it in `/config/initializers/sns_endpoint.rb`:
|
|
38
48
|
|
39
49
|
Rails.application.config.middleware.use Heroic::SNS::Endpoint, :topic => ...
|
40
50
|
|
41
|
-
The Endpoint class takes an options hash as an argument, and understands
|
42
|
-
options:
|
51
|
+
The Endpoint class takes an options hash as an argument, and understands
|
52
|
+
these options:
|
43
53
|
|
44
|
-
|
45
|
-
|
46
|
-
|
54
|
+
### :topic
|
55
|
+
|
56
|
+
`:topic` is required, and provides a filter that defines what SNS topics
|
57
|
+
are handled by this endpoint. **A message is considered either
|
58
|
+
"on-topic" or "off-topic".** You can supply any of the following:
|
47
59
|
|
48
60
|
- a `String` containing a single topic ARN
|
61
|
+
|
49
62
|
- an `Array` of `String` representing a list of topic ARNs
|
63
|
+
|
50
64
|
- a `RegExp` which matches on-topic ARNs
|
51
|
-
|
52
|
-
|
65
|
+
|
66
|
+
- a `Proc` which accepts an ARN as an argument and returns `true` or
|
67
|
+
`false` for on-topic and off-topic ARNs, respectively.
|
53
68
|
|
54
69
|
The key `:topics` is also supported.
|
55
70
|
|
71
|
+
### :auto_confirm
|
72
|
+
|
56
73
|
`:auto_confirm` affects how on-topic subscription confirmations are handled.
|
57
74
|
|
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
|
-
|
75
|
+
- If `true`, they are confirmed by retrieving the URL in the
|
76
|
+
`SubscribeURL` field of the SNS message, and your app is NOT notified.
|
77
|
+
This is the default.
|
78
|
+
|
79
|
+
- If `false`, they are ignored; your app is NOT notified.
|
80
|
+
|
81
|
+
- If `nil`, there is no special handling and the message is passed along
|
82
|
+
to your app.
|
83
|
+
|
84
|
+
### :auto_resubscribe
|
65
85
|
|
66
86
|
`:auto_resubscribe` affects how on-topic unsubscribe confirmations are handled.
|
67
87
|
|
68
|
-
- If `
|
69
|
-
|
70
|
-
- If `false`, they are ignored and your app is also not notified.
|
71
|
-
- If `nil`, there is no special handling and the message is passed along to your
|
72
|
-
app.
|
88
|
+
- If `false`, they are ignored and your app is NOT notified. This is the
|
89
|
+
default.
|
73
90
|
|
74
|
-
|
91
|
+
- If `true`, they topic is automatically re-subscribed by retrieving the
|
92
|
+
URL in the `SubscribeURL` field of the SNS message, and your app is
|
93
|
+
NOT notified.
|
75
94
|
|
76
|
-
If
|
77
|
-
|
95
|
+
- If `nil`, there is no special handling and the message is passed along
|
96
|
+
to your app.
|
97
|
+
|
98
|
+
If you are a control-freak and want no special handling whatsoever, use
|
99
|
+
these options:
|
78
100
|
|
79
101
|
use Heroic::SNS::Endpoint, :topics => Proc.new { true }, :auto_confirm => nil, :auto_resubscribe => nil
|
80
102
|
|
81
|
-
Then the object will simply parse and verify SNS messages it finds and
|
82
|
-
along to your app, taking no action.
|
103
|
+
Then the object will simply parse and verify SNS messages it finds and
|
104
|
+
pass them along to your app, taking no action.
|
83
105
|
|
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:
|
106
|
+
Once the middleware is set up, any notifications will be made available
|
107
|
+
in your Rack environment under the `sns.message` key. If you are using
|
108
|
+
Rails, your controller would have a method like this:
|
87
109
|
|
88
110
|
skip_before_filter :verify_authenticity_token, :only => [:handle_notification]
|
89
111
|
|
@@ -98,52 +120,52 @@ controller would have a method like this:
|
|
98
120
|
head :ok
|
99
121
|
end
|
100
122
|
|
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
|
123
|
+
You must skip the authenticity token verification to allow Amazon to
|
124
|
+
POST to the controller action. Be careful not to disable it for more
|
125
|
+
actions than you need. Be sure to disable any authentication checks for
|
126
|
+
that action, too.
|
104
127
|
|
105
128
|
## Multiple endpoint URLs
|
106
129
|
|
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.
|
130
|
+
If you are receiving multiple notifications at multiple endpoint URLs,
|
131
|
+
you should only include one instance of the Endpoint in your middleware
|
132
|
+
stack, and ensure that its topic filter allows all the notifications you
|
133
|
+
are interested in to pass through.
|
111
134
|
|
112
135
|
`Endpoint` does not interact with the URL path at all; if you want your
|
113
136
|
subscriptions to go to different URLs, simply set them up that way.
|
114
137
|
|
115
138
|
## Off-topic notifications
|
116
139
|
|
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.
|
140
|
+
As a security measure, `Endpoint` requires you to set up a topic filter.
|
141
|
+
Any notifications that do not match this filter are not passed along to
|
142
|
+
your application.
|
120
143
|
|
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.
|
144
|
+
All off-topic messages are ignored with one exception: if the message is
|
145
|
+
a regular notification (meaning your app has an active subscription)
|
146
|
+
*and* the message can be verified as authentic (by checking its
|
147
|
+
signature), `Endpoint` will cancel the subscription by visiting the URL
|
148
|
+
in the `UnsubscribeURL` field of the message.
|
126
149
|
|
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
|
150
|
+
If you would rather make decision about on-topic and off-topic
|
151
|
+
notifications in your own code, simply pass `Proc.new { true }` as the
|
152
|
+
topic filter, and all messages will be treated as on topic. Be aware
|
153
|
+
that it is dangerous to leave `:auto_confirm` enabled with a permissive
|
154
|
+
topic filter, as this will allow anyone to subscribe your web app to any
|
155
|
+
SNS notification.
|
132
156
|
|
133
157
|
## Contributing
|
134
158
|
|
135
159
|
* Fork the project.
|
136
160
|
* Make your feature addition or bug fix and include tests.
|
137
|
-
* Update `CHANGELOG`.
|
161
|
+
* Update `CHANGELOG.md`.
|
138
162
|
* Send a pull request.
|
139
163
|
|
140
164
|
## Copyright and License
|
141
165
|
|
142
|
-
Copyright 2013,
|
166
|
+
Copyright 2013, 2016, 2020, Benjamin Ragheb and Contributors.
|
143
167
|
|
144
168
|
This project [is licensed under the Apache license](LICENSE).
|
145
169
|
|
146
170
|
Direct correspondence to Benjamin Ragheb via email at <ben@benzado.com>
|
147
171
|
or on Twitter [@benzado](https://twitter.com/benzado).
|
148
|
-
|
149
|
-
[](https://travis-ci.org/benzado/heroic-sns)
|
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
@@ -7,7 +7,15 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.name = 'heroic-sns'
|
8
8
|
s.version = Heroic::SNS::VERSION
|
9
9
|
s.summary = "Lightweight Rack middleware for AWS SNS endpoints"
|
10
|
-
s.description =
|
10
|
+
s.description = <<-EOD
|
11
|
+
Secure, lightweight Rack middleware for Amazon Simple Notification Service (SNS)
|
12
|
+
endpoints. SNS messages are intercepted, parsed, verified, and then passed along
|
13
|
+
to the web application via the 'sns.message' environment key. Heroic::SNS has no
|
14
|
+
dependencies besides Rack (specifically, the aws-sdk gem is not needed).
|
15
|
+
SNS message signatures are verified in order to reject forgeries and replay
|
16
|
+
attacks.
|
17
|
+
EOD
|
18
|
+
|
11
19
|
s.license = 'Apache'
|
12
20
|
|
13
21
|
s.author = "Benjamin Ragheb"
|
@@ -18,6 +26,8 @@ Gem::Specification.new do |s|
|
|
18
26
|
|
19
27
|
s.platform = Gem::Platform::RUBY
|
20
28
|
s.required_ruby_version = '>= 1.8.7'
|
21
|
-
s.
|
22
|
-
s.
|
29
|
+
s.add_development_dependency 'rdoc', '~> 4.0'
|
30
|
+
s.add_development_dependency 'test-unit', '>= 1.2.3'
|
31
|
+
s.add_runtime_dependency 'rack', '>= 1.4'
|
32
|
+
s.add_runtime_dependency 'json', '>= 1.7'
|
23
33
|
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module Heroic
|
2
|
+
|
3
|
+
# This LRU Cache is a generic key-value store that is designed to be safe for
|
4
|
+
# concurrent access. It uses a doubly-linked-list to identify which item was
|
5
|
+
# least recently retrieved, and a Hash for fast retrieval by key.
|
6
|
+
#
|
7
|
+
# To support concurrent access, it uses two levels of locks: a cache-level lock
|
8
|
+
# is used to locate or create the desired node and move it to the front of the
|
9
|
+
# LRU list. A node-level lock is used to synchronize access to the node's
|
10
|
+
# value.
|
11
|
+
#
|
12
|
+
# If a thread is busy generating a value to be stored in the cache, other
|
13
|
+
# threads will still be able to read and write to other keys with no conflict.
|
14
|
+
# However, if a second thread tries to read the value that the first thread is
|
15
|
+
# generating, it will block until the first thread has completed its work.
|
16
|
+
|
17
|
+
class LRUCache
|
18
|
+
|
19
|
+
# If you yield a block to the constructor, it will be called on every cache
|
20
|
+
# miss to generate the needed value. This is optional but recommended, as
|
21
|
+
# the block will run while holding a lock on the cache node associated with
|
22
|
+
# the key. Additional attempts to retrieve the same key will wait for your
|
23
|
+
# block to return a result, avoiding duplication of work. However, this also
|
24
|
+
# means you MUST NOT access the cache itself from the block, or you will risk
|
25
|
+
# creating deadlock. (If you need to create cacheable items from other
|
26
|
+
# cacheable items, consider using two separate caches.)
|
27
|
+
|
28
|
+
def initialize(capacity, &block)
|
29
|
+
raise ArgumentError unless capacity > 0
|
30
|
+
@capacity = capacity
|
31
|
+
@block = block || Proc.new { nil }
|
32
|
+
@lock = Mutex.new
|
33
|
+
@store = Hash.new
|
34
|
+
@leftmost = nil
|
35
|
+
@rightmost = nil
|
36
|
+
end
|
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
|
+
|
48
|
+
def get(key)
|
49
|
+
node = node_for_key(key)
|
50
|
+
node.read(&@block)
|
51
|
+
end
|
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
|
+
|
57
|
+
def put(key, value)
|
58
|
+
node = node_for_key(key)
|
59
|
+
node.write(value)
|
60
|
+
end
|
61
|
+
|
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.
|
65
|
+
|
66
|
+
def verify!
|
67
|
+
@lock.synchronize do
|
68
|
+
left_to_right = Array.new
|
69
|
+
begin
|
70
|
+
node = @leftmost
|
71
|
+
while node
|
72
|
+
left_to_right << node
|
73
|
+
node = node.right
|
74
|
+
end
|
75
|
+
end
|
76
|
+
right_to_left = Array.new
|
77
|
+
begin
|
78
|
+
node = @rightmost
|
79
|
+
while node
|
80
|
+
right_to_left << node
|
81
|
+
node = node.left
|
82
|
+
end
|
83
|
+
end
|
84
|
+
begin
|
85
|
+
raise "leftmost has a left node" if @leftmost && @leftmost.left
|
86
|
+
raise "rightmost has a right node" if @rightmost && @rightmost.right
|
87
|
+
raise "leftmost pointer mismatch" unless @leftmost == left_to_right.first
|
88
|
+
raise "rightmost pointer mismatch" unless @rightmost == right_to_left.first
|
89
|
+
raise "list size mismatch" unless right_to_left.length == left_to_right.length
|
90
|
+
raise "list order mismatch" unless left_to_right.reverse == right_to_left
|
91
|
+
raise "node missing from list" if left_to_right.length < @store.size
|
92
|
+
raise "node missing from store" if left_to_right.length > @store.size
|
93
|
+
raise "store size exceeds capacity" if @store.size > @capacity
|
94
|
+
rescue
|
95
|
+
$stderr.puts "Store: #{@store}"
|
96
|
+
$stderr.puts "L-to-R: #{left_to_right}"
|
97
|
+
$stderr.puts "R-to-L: #{right_to_left}"
|
98
|
+
raise
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
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
|
+
|
123
|
+
def node_for_key(key)
|
124
|
+
@lock.synchronize do
|
125
|
+
node = @store[key]
|
126
|
+
if node.nil?
|
127
|
+
# I am a new node, add me to the head of the list!
|
128
|
+
node = @store[key] = Node.new(key)
|
129
|
+
if @leftmost
|
130
|
+
node.right = @leftmost
|
131
|
+
@leftmost.left = node
|
132
|
+
end
|
133
|
+
@leftmost = node
|
134
|
+
@rightmost = @leftmost if @rightmost.nil?
|
135
|
+
if @store.size > @capacity
|
136
|
+
# Uh oh, time to evict the tail node!
|
137
|
+
evicted_node = @store.delete(@rightmost.key)
|
138
|
+
@rightmost = evicted_node.left
|
139
|
+
@rightmost.right = nil
|
140
|
+
end
|
141
|
+
elsif node != @leftmost
|
142
|
+
# Move me to the head of the list!
|
143
|
+
if node == @rightmost
|
144
|
+
# I was the rightmost node, now the node on my left is.
|
145
|
+
@rightmost = node.left
|
146
|
+
node.left.right = nil
|
147
|
+
else
|
148
|
+
# The node on my left should now point to the node on my right.
|
149
|
+
node.left.right = node.right
|
150
|
+
# The node on my right should point to the node on my left.
|
151
|
+
node.right.left = node.left
|
152
|
+
end
|
153
|
+
former_leftmost = @leftmost
|
154
|
+
# I am the new head node!
|
155
|
+
@leftmost = node
|
156
|
+
@leftmost.left = nil
|
157
|
+
@leftmost.right = former_leftmost
|
158
|
+
former_leftmost.left = @leftmost
|
159
|
+
end
|
160
|
+
node
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
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 }
|
@@ -94,14 +110,15 @@ module Heroic
|
|
94
110
|
def call_on_topic(env)
|
95
111
|
begin
|
96
112
|
message = Message.new(env['rack.input'].read)
|
113
|
+
env['rack.input'].rewind
|
97
114
|
check_headers!(message, env)
|
98
115
|
message.verify!
|
99
116
|
case message.type
|
100
117
|
when 'SubscriptionConfirmation'
|
101
|
-
|
118
|
+
URI.parse(message.subscribe_url).open if @auto_confirm
|
102
119
|
return OK_RESPONSE unless @auto_confirm.nil?
|
103
120
|
when 'UnsubscribeConfirmation'
|
104
|
-
|
121
|
+
URI.parse(message.subscribe_url).open if @auto_resubscribe
|
105
122
|
return OK_RESPONSE unless @auto_resubscribe.nil?
|
106
123
|
end
|
107
124
|
env['sns.message'] = message
|
@@ -122,7 +139,7 @@ module Heroic
|
|
122
139
|
begin
|
123
140
|
message = Message.new(env['rack.input'].read)
|
124
141
|
message.verify!
|
125
|
-
|
142
|
+
URI.parse(message.unsubscribe_url).open
|
126
143
|
rescue => e
|
127
144
|
raise Error.new("error handling off-topic notification: #{e.message}", message)
|
128
145
|
end
|
data/lib/heroic/sns/message.rb
CHANGED
@@ -1,23 +1,31 @@
|
|
1
1
|
require 'json'
|
2
2
|
require 'base64'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'heroic/lru_cache'
|
3
5
|
|
4
6
|
module Heroic
|
5
7
|
module SNS
|
6
8
|
|
7
9
|
MAXIMUM_ALLOWED_AGE = 3600 # reject messages older than one hour
|
10
|
+
MAXIMUM_ALLOWED_CERTIFICATES = 50
|
8
11
|
|
9
|
-
CERTIFICATE_CACHE =
|
12
|
+
CERTIFICATE_CACHE = Heroic::LRUCache.new(MAXIMUM_ALLOWED_CERTIFICATES) do |cert_url|
|
10
13
|
begin
|
11
|
-
cert_data =
|
12
|
-
|
13
|
-
rescue OpenURI::HTTPError => e
|
14
|
-
raise Error.new("unable to retrieve signing certificate: #{e.message}; URL: #{cert_url}")
|
14
|
+
cert_data = URI.parse(cert_url).open
|
15
|
+
OpenSSL::X509::Certificate.new(cert_data.read)
|
15
16
|
rescue OpenSSL::X509::CertificateError => e
|
16
|
-
raise Error.new("unable to parse signing certificate: #{e.message}; URL: #{cert_url}")
|
17
|
+
raise SNS::Error.new("unable to parse signing certificate: #{e.message}; URL: #{cert_url}")
|
18
|
+
rescue => e
|
19
|
+
raise SNS::Error.new("unable to retrieve signing certificate: #{e.message}; URL: #{cert_url}")
|
17
20
|
end
|
18
21
|
end
|
19
22
|
|
20
|
-
|
23
|
+
VALID_AWS_URL_PATTERN = %r{\Ahttps://sns\.[a-z]{2}(?:-gov)?-(?:north|south|east|west|central){1,2}-\d+\.amazonaws\.com(?:\.cn)?/}
|
24
|
+
|
25
|
+
# Encapsulates an SNS message. Since Endpoint takes care of authenticating
|
26
|
+
# the message, most of the time you will simply be interested in retrieving
|
27
|
+
# the +subject+ and +body+ from the message and acting on it.
|
28
|
+
#
|
21
29
|
# See: http://docs.aws.amazon.com/sns/latest/gsg/json-formats.html
|
22
30
|
class Message
|
23
31
|
|
@@ -27,6 +35,8 @@ module Heroic
|
|
27
35
|
raise Error.new("failed to parse message as JSON: #{e.message}")
|
28
36
|
end
|
29
37
|
|
38
|
+
# The message type will be one of +SubscriptionConfirmation+,
|
39
|
+
# +UnsubscribeConfirmation+, and +Notification+.
|
30
40
|
def type
|
31
41
|
@msg['Type']
|
32
42
|
end
|
@@ -35,11 +45,14 @@ module Heroic
|
|
35
45
|
@msg['TopicArn']
|
36
46
|
end
|
37
47
|
|
48
|
+
# A Universally Unique Identifier, unique for each message published. For
|
49
|
+
# a notification that Amazon SNS resends during a retry, the message ID of
|
50
|
+
# the original message is used.
|
38
51
|
def id
|
39
52
|
@msg['MessageId']
|
40
53
|
end
|
41
54
|
|
42
|
-
# The timestamp as a Time object.
|
55
|
+
# The timestamp when the message was published, as a Time object.
|
43
56
|
def timestamp
|
44
57
|
Time.xmlschema(@msg['Timestamp'])
|
45
58
|
end
|
@@ -62,6 +75,9 @@ module Heroic
|
|
62
75
|
@msg['Subject']
|
63
76
|
end
|
64
77
|
|
78
|
+
# The message payload. As far as Amazon and this class are concerned, the
|
79
|
+
# message payload is just a string of bytes. If you are expecting, for
|
80
|
+
# example, a JSON object, you will need to pass this to a JSON parser.
|
65
81
|
def body
|
66
82
|
@msg['Message']
|
67
83
|
end
|
@@ -75,13 +91,13 @@ module Heroic
|
|
75
91
|
end
|
76
92
|
|
77
93
|
# The token is used to confirm subscriptions via the SNS API. If you visit
|
78
|
-
# the
|
94
|
+
# the +subscribe_url+, you can ignore this field.
|
79
95
|
def token
|
80
96
|
@msg['Token']
|
81
97
|
end
|
82
98
|
|
83
|
-
def ==(
|
84
|
-
@msg ==
|
99
|
+
def ==(other)
|
100
|
+
other.is_a?(Message) && @msg == other.instance_variable_get(:@msg)
|
85
101
|
end
|
86
102
|
|
87
103
|
def hash
|
@@ -96,24 +112,27 @@ module Heroic
|
|
96
112
|
string << ">"
|
97
113
|
end
|
98
114
|
|
115
|
+
# Returns a JSON serialization of the message. Note that it may not be
|
116
|
+
# identical to the serialization that was retrieved from the network.
|
99
117
|
def to_json
|
100
118
|
@msg.to_json
|
101
119
|
end
|
102
120
|
|
103
|
-
# Verifies the message signature. Raises
|
121
|
+
# Verifies the message signature. Raises Error if it is not valid.
|
122
|
+
#
|
104
123
|
# See: http://docs.aws.amazon.com/sns/latest/gsg/SendMessageToHttp.verify.signature.html
|
105
124
|
def verify!
|
106
125
|
age = Time.now - timestamp
|
107
|
-
raise
|
126
|
+
raise Error.new("timestamp is in the future, age: #{age}", self) if age < 0
|
108
127
|
raise Error.new("timestamp is too old", self) if age > MAXIMUM_ALLOWED_AGE
|
109
128
|
if signature_version != '1'
|
110
129
|
raise Error.new("unknown signature version: #{signature_version}", self)
|
111
130
|
end
|
112
|
-
if signing_cert_url !~
|
131
|
+
if signing_cert_url !~ VALID_AWS_URL_PATTERN
|
113
132
|
raise Error.new("signing certificate is not from amazonaws.com", self)
|
114
133
|
end
|
115
134
|
text = string_to_sign # will warn of invalid Type
|
116
|
-
cert = CERTIFICATE_CACHE
|
135
|
+
cert = CERTIFICATE_CACHE.get(signing_cert_url)
|
117
136
|
digest = OpenSSL::Digest::SHA1.new
|
118
137
|
unless cert.public_key.verify(digest, signature, text)
|
119
138
|
raise Error.new("message signature is invalid", self)
|
data/lib/heroic/sns/version.rb
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
sns.af-south-1.amazonaws.com
|
2
|
+
sns.ap-east-1.amazonaws.com
|
3
|
+
sns.ap-northeast-1.amazonaws.com
|
4
|
+
sns.ap-northeast-2.amazonaws.com
|
5
|
+
sns.ap-northeast-3.amazonaws.com
|
6
|
+
sns.ap-south-1.amazonaws.com
|
7
|
+
sns.ap-southeast-1.amazonaws.com
|
8
|
+
sns.ap-southeast-2.amazonaws.com
|
9
|
+
sns.ca-central-1.amazonaws.com
|
10
|
+
sns.cn-north-1.amazonaws.com
|
11
|
+
sns.cn-northwest-1.amazonaws.com
|
12
|
+
sns.eu-central-1.amazonaws.com
|
13
|
+
sns.eu-north-1.amazonaws.com
|
14
|
+
sns.eu-south-1.amazonaws.com
|
15
|
+
sns.eu-west-1.amazonaws.com
|
16
|
+
sns.eu-west-2.amazonaws.com
|
17
|
+
sns.eu-west-3.amazonaws.com
|
18
|
+
sns.me-south-1.amazonaws.com
|
19
|
+
sns.sa-east-1.amazonaws.com
|
20
|
+
sns.us-east-1.amazonaws.com
|
21
|
+
sns.us-east-2.amazonaws.com
|
22
|
+
sns.us-west-1.amazonaws.com
|
23
|
+
sns.us-west-2.amazonaws.com
|
24
|
+
sns.us-gov-east-1.amazonaws.com
|
25
|
+
sns.us-gov-west-1.amazonaws.com
|
26
|
+
sns.cn-north-1.amazonaws.com.cn
|
27
|
+
sns.cn-northwest-1.amazonaws.com.cn
|
data/test/helper.rb
CHANGED
@@ -5,30 +5,34 @@
|
|
5
5
|
|
6
6
|
module Heroic
|
7
7
|
module SNS
|
8
|
+
# Use a fake certificate cache so tests aren't dependent on
|
9
|
+
# network access, or the fact that the certificate is also fake.
|
10
|
+
CERTIFICATE_CACHE = Class.new do
|
11
|
+
def cert_data
|
12
|
+
File.read('test/fixtures/sns.crt')
|
13
|
+
end
|
8
14
|
|
9
|
-
|
10
|
-
|
15
|
+
def get(_)
|
16
|
+
@cert ||= OpenSSL::X509::Certificate.new(cert_data)
|
17
|
+
end
|
18
|
+
end.new
|
11
19
|
|
12
|
-
|
13
|
-
|
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
|
20
|
+
TEST_CERT_URL = 'https://sns.xx-east-1.amazonaws.com/self-signed.pem'
|
21
|
+
TEST_CERT_KEY = OpenSSL::PKey::RSA.new(File.read('test/fixtures/sns.key'))
|
18
22
|
|
19
23
|
class Message
|
20
|
-
|
21
24
|
def update_timestamp!(t = Time.now)
|
22
25
|
@msg['Timestamp'] = t.utc.xmlschema(3)
|
23
26
|
end
|
24
27
|
|
28
|
+
def update_signing_cert_url!(url = nil)
|
29
|
+
@msg['SigningCertURL'] = url || TEST_CERT_URL
|
30
|
+
end
|
31
|
+
|
25
32
|
def sign!
|
26
|
-
@msg['SigningCertURL'] = TEST_CERT_URL
|
27
33
|
signature = TEST_CERT_KEY.sign(OpenSSL::Digest::SHA1.new, string_to_sign)
|
28
34
|
@msg['Signature'] = Base64::encode64(signature)
|
29
35
|
end
|
30
|
-
|
31
36
|
end
|
32
|
-
|
33
37
|
end
|
34
38
|
end
|
data/test/test_endpoint.rb
CHANGED
@@ -10,6 +10,7 @@ class EndpointTest < Test::Unit::TestCase
|
|
10
10
|
json = File.read("test/fixtures/#{name}.json")
|
11
11
|
@msg = Heroic::SNS::Message.new(json)
|
12
12
|
@msg.update_timestamp!
|
13
|
+
@msg.update_signing_cert_url!
|
13
14
|
@msg.sign!
|
14
15
|
@env = {
|
15
16
|
'HTTP_X_AMZ_SNS_MESSAGE_TYPE' => @msg.type,
|
@@ -44,4 +45,16 @@ class EndpointTest < Test::Unit::TestCase
|
|
44
45
|
assert_nothing_raised { app.call(@env) }
|
45
46
|
end
|
46
47
|
|
48
|
+
def test_rewind
|
49
|
+
result = [0, {}, []]
|
50
|
+
|
51
|
+
test = Proc.new do |env|
|
52
|
+
msg_json = JSON.parse(env['rack.input'].read)
|
53
|
+
assert_equal(msg_json['Message'],'booyakasha!')
|
54
|
+
result
|
55
|
+
end
|
56
|
+
sns('notification')
|
57
|
+
app = Heroic::SNS::Endpoint.new test, :topic => @msg.topic_arn
|
58
|
+
assert_equal result, app.call(@env)
|
59
|
+
end
|
47
60
|
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'heroic/lru_cache'
|
3
|
+
|
4
|
+
class LRUCacheTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_invalid_size
|
7
|
+
assert_raises ArgumentError do
|
8
|
+
Heroic::LRUCache.new(0) { |k| nil }
|
9
|
+
end
|
10
|
+
assert_raises ArgumentError do
|
11
|
+
Heroic::LRUCache.new(-1) { |k| nil }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_get_put
|
16
|
+
cache = Heroic::LRUCache.new(1)
|
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
|
+
end
|
24
|
+
|
25
|
+
def test_exceptions
|
26
|
+
@should_throw = true
|
27
|
+
cache = Heroic::LRUCache.new(3) do |k|
|
28
|
+
if @should_throw
|
29
|
+
raise "tried to load a value but failed"
|
30
|
+
else
|
31
|
+
4
|
32
|
+
end
|
33
|
+
end
|
34
|
+
assert_raises RuntimeError do
|
35
|
+
cache.get(:ooze)
|
36
|
+
end
|
37
|
+
@should_throw = false
|
38
|
+
assert_equal 4, cache.get(:ooze)
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_dynamic
|
42
|
+
@counter = 0
|
43
|
+
cache = Heroic::LRUCache.new(3) { |k| @counter += 1; k.to_s.length }
|
44
|
+
assert_equal 0, @counter
|
45
|
+
cache.verify!
|
46
|
+
assert_equal 3, cache.get(:leo); assert_equal 1, @counter
|
47
|
+
cache.verify!
|
48
|
+
assert_equal 3, cache.get(:leo); assert_equal 1, @counter
|
49
|
+
cache.verify!
|
50
|
+
assert_equal 6, cache.get(:donnie); assert_equal 2, @counter
|
51
|
+
cache.verify!
|
52
|
+
assert_equal 6, cache.get(:donnie); assert_equal 2, @counter
|
53
|
+
cache.verify!
|
54
|
+
assert_equal 5, cache.get(:mikey); assert_equal 3, @counter
|
55
|
+
cache.verify!
|
56
|
+
assert_equal 5, cache.get(:mikey); assert_equal 3, @counter
|
57
|
+
cache.verify!
|
58
|
+
# raph will push leo out of cache
|
59
|
+
assert_equal 4, cache.get(:raph); assert_equal 4, @counter
|
60
|
+
cache.verify!
|
61
|
+
assert_equal 4, cache.get(:raph); assert_equal 4, @counter
|
62
|
+
cache.verify!
|
63
|
+
# mikey and donnie remain in cache
|
64
|
+
assert_equal 5, cache.get(:mikey); assert_equal 4, @counter
|
65
|
+
cache.verify!
|
66
|
+
assert_equal 6, cache.get(:donnie); assert_equal 4, @counter
|
67
|
+
cache.verify!
|
68
|
+
# leo will have to be refetched
|
69
|
+
assert_equal 3, cache.get(:leo); assert_equal 5, @counter
|
70
|
+
cache.verify!
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_sync
|
74
|
+
@lock = Mutex.new
|
75
|
+
@counter = 0
|
76
|
+
cache = Heroic::LRUCache.new(100) do |k|
|
77
|
+
sleep 1 # simulate slow generation, such as network I/O
|
78
|
+
@lock.synchronize { @counter += 1 }
|
79
|
+
k.to_s.length
|
80
|
+
end
|
81
|
+
# load the cache with things to read
|
82
|
+
cache.put(:leo, 0)
|
83
|
+
cache.put(:donnie, 0)
|
84
|
+
start_time = Time.now
|
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) }
|
91
|
+
end
|
92
|
+
# Fetching values already in the cache should not block.
|
93
|
+
assert_equal 0, cache.get(:leo)
|
94
|
+
assert_equal 0, cache.get(:donnie)
|
95
|
+
assert_equal 0, @lock.synchronize { @counter }
|
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)
|
99
|
+
assert_equal 2, @lock.synchronize { @counter }
|
100
|
+
# Fetching those same values should not trigger a recompute.
|
101
|
+
assert_equal 5, cache.get(:mikey)
|
102
|
+
assert_equal 4, cache.get(:raph)
|
103
|
+
assert_equal 2, @lock.synchronize { @counter }
|
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 }
|
107
|
+
time_elapsed = (Time.now - start_time)
|
108
|
+
assert_in_delta time_elapsed, 1.0, 0.05
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
data/test/test_message.rb
CHANGED
@@ -3,11 +3,11 @@ require 'heroic/sns'
|
|
3
3
|
require 'helper'
|
4
4
|
|
5
5
|
class MessageTest < Test::Unit::TestCase
|
6
|
-
|
7
|
-
def sns(name, timestamp = Time.now)
|
6
|
+
def sns(name, options = {})
|
8
7
|
json = File.read("test/fixtures/#{name}.json")
|
9
8
|
msg = Heroic::SNS::Message.new(json)
|
10
|
-
msg.update_timestamp!(timestamp)
|
9
|
+
msg.update_timestamp!(options[:timestamp] || Time.now)
|
10
|
+
msg.update_signing_cert_url!(options[:signing_cert_url])
|
11
11
|
msg.sign!
|
12
12
|
return msg
|
13
13
|
end
|
@@ -51,13 +51,41 @@ class MessageTest < Test::Unit::TestCase
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
+
def test_untrusted_cert_url_s3
|
55
|
+
cert_url = 'https://sns.s3.amazonaws.com/self-signed.pem'
|
56
|
+
msg = sns("notification", :signing_cert_url => cert_url)
|
57
|
+
assert_equal cert_url, msg.signing_cert_url
|
58
|
+
assert_raises Heroic::SNS::Error do
|
59
|
+
msg.verify!
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_untrusted_cert_url_other
|
64
|
+
cert_url = 'https://example.com/sns.us-east-1.amazonaws.com/self-signed.pem'
|
65
|
+
msg = sns("subscription", :signing_cert_url => cert_url)
|
66
|
+
assert_equal cert_url, msg.signing_cert_url
|
67
|
+
assert_raises Heroic::SNS::Error do
|
68
|
+
msg.verify!
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Generate tests for every known AWS SNS endpoint.
|
73
|
+
# https://docs.aws.amazon.com/general/latest/gr/sns.html
|
74
|
+
File.read('test/fixtures/aws-sns-endpoints.txt').split(/\n/).each do |domain|
|
75
|
+
define_method("test_trusted_cert_url_#{domain}") do
|
76
|
+
cert_url = "https://#{domain}/certificate.pem"
|
77
|
+
msg = sns("notification", :signing_cert_url => cert_url)
|
78
|
+
assert_equal cert_url, msg.signing_cert_url
|
79
|
+
assert_nothing_raised { msg.verify! }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
54
83
|
def test_expired
|
55
84
|
t = Time.utc(1984, 5)
|
56
|
-
msg = sns("notification", t)
|
85
|
+
msg = sns("notification", :timestamp => t)
|
57
86
|
assert_equal t, msg.timestamp
|
58
87
|
assert_raises Heroic::SNS::Error do
|
59
88
|
msg.verify!
|
60
89
|
end
|
61
90
|
end
|
62
|
-
|
63
91
|
end
|
metadata
CHANGED
@@ -1,66 +1,86 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: heroic-sns
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: '1.2'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin Ragheb
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-06-15 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'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: test-unit
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.2.3
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.2.3
|
13
41
|
- !ruby/object:Gem::Dependency
|
14
42
|
name: rack
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
16
44
|
requirements:
|
17
|
-
- -
|
45
|
+
- - ">="
|
18
46
|
- !ruby/object:Gem::Version
|
19
47
|
version: '1.4'
|
20
48
|
type: :runtime
|
21
49
|
prerelease: false
|
22
50
|
version_requirements: !ruby/object:Gem::Requirement
|
23
51
|
requirements:
|
24
|
-
- -
|
52
|
+
- - ">="
|
25
53
|
- !ruby/object:Gem::Version
|
26
54
|
version: '1.4'
|
27
55
|
- !ruby/object:Gem::Dependency
|
28
56
|
name: json
|
29
57
|
requirement: !ruby/object:Gem::Requirement
|
30
58
|
requirements:
|
31
|
-
- -
|
59
|
+
- - ">="
|
32
60
|
- !ruby/object:Gem::Version
|
33
|
-
version: 1.7
|
61
|
+
version: '1.7'
|
34
62
|
type: :runtime
|
35
63
|
prerelease: false
|
36
64
|
version_requirements: !ruby/object:Gem::Requirement
|
37
65
|
requirements:
|
38
|
-
- -
|
66
|
+
- - ">="
|
39
67
|
- !ruby/object:Gem::Version
|
40
|
-
version: 1.7
|
41
|
-
description:
|
42
|
-
Service (SNS)
|
43
|
-
|
68
|
+
version: '1.7'
|
69
|
+
description: |
|
70
|
+
Secure, lightweight Rack middleware for Amazon Simple Notification Service (SNS)
|
44
71
|
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
|
-
|
72
|
+
to the web application via the 'sns.message' environment key. Heroic::SNS has no
|
49
73
|
dependencies besides Rack (specifically, the aws-sdk gem is not needed).
|
50
|
-
|
51
74
|
SNS message signatures are verified in order to reject forgeries and replay
|
52
|
-
|
53
75
|
attacks.
|
54
|
-
|
55
|
-
'
|
56
76
|
email: ben@benzado.com
|
57
77
|
executables: []
|
58
78
|
extensions: []
|
59
79
|
extra_rdoc_files: []
|
60
80
|
files:
|
61
|
-
- .gitignore
|
62
|
-
- .travis.yml
|
63
|
-
- CHANGELOG
|
81
|
+
- ".gitignore"
|
82
|
+
- ".travis.yml"
|
83
|
+
- CHANGELOG.md
|
64
84
|
- Gemfile
|
65
85
|
- LICENSE
|
66
86
|
- README.md
|
@@ -68,12 +88,15 @@ files:
|
|
68
88
|
- bin/fake-sns
|
69
89
|
- demo/config.ru
|
70
90
|
- demo/demo.erb
|
71
|
-
-
|
91
|
+
- gemfiles/Gemfile.rack-1.x
|
92
|
+
- gemfiles/Gemfile.ruby-1.8
|
72
93
|
- heroic-sns.gemspec
|
94
|
+
- lib/heroic/lru_cache.rb
|
73
95
|
- lib/heroic/sns.rb
|
74
96
|
- lib/heroic/sns/endpoint.rb
|
75
97
|
- lib/heroic/sns/message.rb
|
76
98
|
- lib/heroic/sns/version.rb
|
99
|
+
- test/fixtures/aws-sns-endpoints.txt
|
77
100
|
- test/fixtures/notification.json
|
78
101
|
- test/fixtures/sns.crt
|
79
102
|
- test/fixtures/sns.key
|
@@ -81,6 +104,7 @@ files:
|
|
81
104
|
- test/fixtures/unsubscribe.json
|
82
105
|
- test/helper.rb
|
83
106
|
- test/test_endpoint.rb
|
107
|
+
- test/test_lru_cache.rb
|
84
108
|
- test/test_message.rb
|
85
109
|
homepage: https://github.com/benzado/heroic-sns
|
86
110
|
licenses:
|
@@ -92,17 +116,16 @@ require_paths:
|
|
92
116
|
- lib
|
93
117
|
required_ruby_version: !ruby/object:Gem::Requirement
|
94
118
|
requirements:
|
95
|
-
- -
|
119
|
+
- - ">="
|
96
120
|
- !ruby/object:Gem::Version
|
97
121
|
version: 1.8.7
|
98
122
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
123
|
requirements:
|
100
|
-
- -
|
124
|
+
- - ">="
|
101
125
|
- !ruby/object:Gem::Version
|
102
126
|
version: '0'
|
103
127
|
requirements: []
|
104
|
-
|
105
|
-
rubygems_version: 2.0.3
|
128
|
+
rubygems_version: 3.0.8
|
106
129
|
signing_key:
|
107
130
|
specification_version: 4
|
108
131
|
summary: Lightweight Rack middleware for AWS SNS endpoints
|
data/CHANGELOG
DELETED
data/description.txt
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
Secure, lightweight Rack middleware for Amazon Simple Notification Service (SNS)
|
2
|
-
endpoints. SNS messages are intercepted, parsed, verified, and then passed along
|
3
|
-
to the web application via the 'sns.message' environment key. Heroic::SNS has no
|
4
|
-
dependencies besides Rack (specifically, the aws-sdk gem is not needed).
|
5
|
-
SNS message signatures are verified in order to reject forgeries and replay
|
6
|
-
attacks.
|