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 CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- N2Q1NmY1MjNhNGViOTI4NDk1N2FjZmQzYWZjNTE2ZTM4MzM3OTlkNA==
5
- data.tar.gz: !binary |-
6
- ZmFkMjY5YzdiMDQ3ODMwZjQyNDNkNzY1ZGM3MzU3OWVmNWYzNDJkYQ==
7
- !binary "U0hBNTEy":
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
@@ -1,4 +1,6 @@
1
1
  /capture
2
+ /html
2
3
  /pkg
3
4
  /tmp
4
5
  /Gemfile.lock
6
+ /gemfiles/*.lock
@@ -1,6 +1,22 @@
1
1
  language: ruby
2
2
  rvm:
3
- - ruby-head
4
- - "2.0.0"
5
- - "1.9.3"
6
- - "1.8.7"
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 }
@@ -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
@@ -3,3 +3,5 @@ source "http://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem "rake"
6
+ gem "rack", "~> 2.0"
7
+ gem "test-unit", "~> 3.3"
data/README.md CHANGED
@@ -1,23 +1,33 @@
1
1
  # Heroic::SNS, Rack middleware for Amazon SNS endpoints
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/heroic-sns.svg)](http://badge.fury.io/rb/heroic-sns)
4
+ [![Build Status](https://travis-ci.org/benzado/heroic-sns.png?branch=master)](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 intercepted,
7
- parsed, verified, and then passed along via the `sns.message` environment key.
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 `sns.error`
10
- environment key. `Heroic::SNS::Endpoint` does not log any messages itself.
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 (to avoid
13
- forgeries) and stale messages are rejected (to avoid replay attacks).
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 there are
16
- no dependencies beside [rack][]. Specifically, Heroic::SNS *does not* depend on [aws-sdk][]. They will be friendly to each other, however, if you include both
17
- in a project.
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
- [aws-sdk]: https://github.com/aws/aws-sdk-ruby
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 `config.ru`
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 these
42
- options:
51
+ The Endpoint class takes an options hash as an argument, and understands
52
+ these options:
43
53
 
44
- `:topic` is required, and provides a filter that defines what SNS topics are
45
- handled by this endpoint. **A message is considered either "on-topic" or
46
- "off-topic".** You can supply any of the following:
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
- - a `Proc` which accepts an ARN as an argument and returns `true` or `false` for
52
- on-topic and off-topic ARNs, respectively.
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 `SubscribeURL`
59
- field of the SNS message, and your app is not notified.
60
- - If `false`, they are ignored; your app is also not notified.
61
- - If `nil`, there is no special handling and the message is passed along to your
62
- app.
63
-
64
- The default is `true`.
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 `true`, they topic is automatically re-subscribed by retrieving the URL in
69
- the `SubscribeURL` field of the SNS message, and your app is not notified.
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
- The default is `false`.
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 you are a control-freak and want no special handling whatsoever, use these
77
- options:
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 pass them
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 in your
85
- Rack environment under the `sns.message` key. If you are using Rails, your
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 POST to the
102
- controller action. Be careful not to disable it for more actions than you need.
103
- Be sure to disable any authentication checks for that action, too.
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, you
108
- should only include one instance of the Endpoint in your middleware stack, and
109
- ensure that its topic filter allows all the notifications you are interested in
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. Any
118
- notifications that do not match this filter are not passed along to your
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 a
122
- regular notification (meaning your app has an active subscription) *and* the
123
- message can be verified as authentic (by checking its signature), `Endpoint`
124
- will cancel the subscription by visiting the URL in the `UnsubscribeURL` field
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 notifications in
128
- your own code, simply pass `Proc.new { true }` as the topic filter, and all
129
- messages will be treated as on topic. Be aware that it is dangerous to leave
130
- `:auto_confirm` enabled with a permissive topic filter, as this will allow
131
- anyone to subscribe your web app to any SNS notification.
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, Heroic Software Inc and Contributors.
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
- [![Build Status](https://travis-ci.org/benzado/heroic-sns.png?branch=master)](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
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec :path => ".."
4
+
5
+ gem "rake", "~> 10.3"
6
+ gem "rack", "~> 1.0"
7
+ gem "json", "~> 1.7"
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec :path => ".."
4
+
5
+ gem "rake", "~> 10.3"
6
+ gem "rack", "~> 1.0"
7
+ gem "json", "~> 1.7"
8
+ gem "test-unit", "1.2.3"
@@ -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 = File.read('description.txt')
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.add_runtime_dependency 'rack', '~> 1.4'
22
- s.add_runtime_dependency 'json', '~> 1.7.7'
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
@@ -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
- open(message.subscribe_url) if @auto_confirm
118
+ URI.parse(message.subscribe_url).open if @auto_confirm
102
119
  return OK_RESPONSE unless @auto_confirm.nil?
103
120
  when 'UnsubscribeConfirmation'
104
- open(message.subscribe_url) if @auto_resubscribe
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
- open(message.unsubscribe_url)
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
@@ -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 = Hash.new do |cache, cert_url|
12
+ CERTIFICATE_CACHE = Heroic::LRUCache.new(MAXIMUM_ALLOWED_CERTIFICATES) do |cert_url|
10
13
  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}")
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
- # Encapsulates an SNS message.
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 :subscribe_url, you can ignore this field.
94
+ # the +subscribe_url+, you can ignore this field.
79
95
  def token
80
96
  @msg['Token']
81
97
  end
82
98
 
83
- def ==(other_message)
84
- @msg == other_message.instance_variable_get(:@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 an exception if it is not valid.
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 Errow.new("timestamp is in the future", self) if age < 0
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 !~ %r[^https://.*amazonaws\.com/]
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[signing_cert_url]
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)
@@ -1,5 +1,5 @@
1
1
  module Heroic
2
2
  module SNS
3
- VERSION = '1.0.1'
3
+ VERSION = '1.2'
4
4
  end
5
5
  end
@@ -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
@@ -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
- 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'))
15
+ def get(_)
16
+ @cert ||= OpenSSL::X509::Certificate.new(cert_data)
17
+ end
18
+ end.new
11
19
 
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
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
@@ -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
@@ -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.0.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: 2013-05-17 00:00:00.000000000 Z
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.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.7
41
- description: ! 'Secure, lightweight Rack middleware for Amazon Simple Notification
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
- - description.txt
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
- rubyforge_project:
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
@@ -1,9 +0,0 @@
1
- ### 1.0.1 (May 17, 2013)
2
-
3
- * Gem housekeeping, based on [advice by dblock](http://code.dblock.org/your-first-ruby-gem)
4
-
5
- ### 1.0.0 (May 5, 2013)
6
-
7
- * Initial public release - [@benzado]
8
-
9
- [@benzado]: http://github.com/benzado
@@ -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.