heroic-sns 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +8 -8
- data/.travis.yml +0 -1
- data/CHANGELOG +4 -0
- data/heroic-sns.gemspec +9 -1
- data/lib/heroic/lru_cache.rb +156 -0
- data/lib/heroic/sns/message.rb +9 -7
- data/lib/heroic/sns/version.rb +1 -1
- data/test/helper.rb +2 -2
- data/test/test_lru_cache.rb +112 -0
- metadata +4 -3
- data/description.txt +0 -6
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
ZGJmMTYxNjFmNmJlZGFkM2E4ZDMzN2I0ODY1ODJmYzc5ZWMxNTAxZA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
ODU4NTFjMTM2MTBjZTY0MWMzNDNjZGZhYTQ1YjdhZTE0OWEwODAzOQ==
|
7
7
|
!binary "U0hBNTEy":
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZTkxZjczZTVjODdjMjBlMTM0OWU2YjFmYzI4NzM1YzZmMWYzZWFlZjQ2YzMx
|
10
|
+
ODM1ODk3ZmNlMDRmYjE4NmI3MWE4ZTVmM2RmNTgwZTc2YjMwMjEzZjRhNjEy
|
11
|
+
NTVhMDZkZjlmN2IwYjkxNTdjM2M1Mzc2ODFjMzU3ZjhhZGRjZTg=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
ZTY3YzJmZWRkZTE3OGE4ODRkNjgxNjMwMmYwYzRmZGQ4ODQ1MzE1YjMzZTcw
|
14
|
+
ZmYzMTJkYzhhM2Y5MmQ2YTQ5ZGRlYmUwMWMwYTc5YWUwM2RhMGFiOGJkZmVh
|
15
|
+
OWUyODNjYWE3Njg3Y2MyMzQ1ODI2N2Q0MGE4MmU5MTU0ODIzMDU=
|
data/.travis.yml
CHANGED
data/CHANGELOG
CHANGED
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"
|
@@ -0,0 +1,156 @@
|
|
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
|
+
class Node
|
20
|
+
attr_reader :key
|
21
|
+
attr_accessor :left, :right
|
22
|
+
def initialize(key)
|
23
|
+
@key = key
|
24
|
+
@lock = Mutex.new
|
25
|
+
end
|
26
|
+
def read
|
27
|
+
@lock.synchronize { @value ||= yield(@key) }
|
28
|
+
end
|
29
|
+
def write(value)
|
30
|
+
@lock.synchronize { @value = value }
|
31
|
+
end
|
32
|
+
def to_s
|
33
|
+
sprintf '<Node:%x(%s)>', self.object_id, @key.inspect
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# If you yield a block to the constructor, it will be called on every cache
|
38
|
+
# miss to generate the needed value. This is optional but recommended, as
|
39
|
+
# the block will run while holding a lock on the cache node associated with
|
40
|
+
# the key. Additional attempts to retrieve the same key will wait for your
|
41
|
+
# block to return a result, avoiding duplication of work. However, this also
|
42
|
+
# means you MUST NOT access the cache itself from the block, or you will risk
|
43
|
+
# creating deadlock. (If you need to create cacheable items from other
|
44
|
+
# cacheable items, consider using two separate caches.)
|
45
|
+
|
46
|
+
def initialize(capacity, &block)
|
47
|
+
raise ArgumentError unless capacity > 0
|
48
|
+
@capacity = capacity
|
49
|
+
@block = block || Proc.new { nil }
|
50
|
+
@lock = Mutex.new
|
51
|
+
@store = Hash.new
|
52
|
+
@leftmost = nil
|
53
|
+
@rightmost = nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def get(key)
|
57
|
+
node = node_for_key(key)
|
58
|
+
node.read(&@block)
|
59
|
+
end
|
60
|
+
|
61
|
+
def put(key, value)
|
62
|
+
node = node_for_key(key)
|
63
|
+
node.write(value)
|
64
|
+
end
|
65
|
+
|
66
|
+
def empty!
|
67
|
+
@lock.synchronize do
|
68
|
+
@store.empty!
|
69
|
+
@leftmost = nil
|
70
|
+
@rightmost = nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Verify the list structure. Intended for testing and debugging only.
|
75
|
+
def verify!
|
76
|
+
@lock.synchronize do
|
77
|
+
left_to_right = Array.new
|
78
|
+
begin
|
79
|
+
node = @leftmost
|
80
|
+
while node
|
81
|
+
left_to_right << node
|
82
|
+
node = node.right
|
83
|
+
end
|
84
|
+
end
|
85
|
+
right_to_left = Array.new
|
86
|
+
begin
|
87
|
+
node = @rightmost
|
88
|
+
while node
|
89
|
+
right_to_left << node
|
90
|
+
node = node.left
|
91
|
+
end
|
92
|
+
end
|
93
|
+
begin
|
94
|
+
raise "leftmost has a left node" if @leftmost && @leftmost.left
|
95
|
+
raise "rightmost has a right node" if @rightmost && @rightmost.right
|
96
|
+
raise "leftmost pointer mismatch" unless @leftmost == left_to_right.first
|
97
|
+
raise "rightmost pointer mismatch" unless @rightmost == right_to_left.first
|
98
|
+
raise "list size mismatch" unless right_to_left.length == left_to_right.length
|
99
|
+
raise "list order mismatch" unless left_to_right.reverse == right_to_left
|
100
|
+
raise "node missing from list" if left_to_right.length < @store.size
|
101
|
+
raise "node missing from store" if left_to_right.length > @store.size
|
102
|
+
raise "store size exceeds capacity" if @store.size > @capacity
|
103
|
+
rescue
|
104
|
+
$stderr.puts "Store: #{@store}"
|
105
|
+
$stderr.puts "L-to-R: #{left_to_right}"
|
106
|
+
$stderr.puts "R-to-L: #{right_to_left}"
|
107
|
+
raise
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def node_for_key(key)
|
115
|
+
@lock.synchronize do
|
116
|
+
node = @store[key]
|
117
|
+
if node.nil?
|
118
|
+
# I am a new node, add me to the head of the list!
|
119
|
+
node = @store[key] = Node.new(key)
|
120
|
+
if @leftmost
|
121
|
+
node.right = @leftmost
|
122
|
+
@leftmost.left = node
|
123
|
+
end
|
124
|
+
@leftmost = node
|
125
|
+
@rightmost = @leftmost if @rightmost.nil?
|
126
|
+
if @store.size > @capacity
|
127
|
+
# Uh oh, time to evict the tail node!
|
128
|
+
evicted_node = @store.delete(@rightmost.key)
|
129
|
+
@rightmost = evicted_node.left
|
130
|
+
@rightmost.right = nil
|
131
|
+
end
|
132
|
+
elsif node != @leftmost
|
133
|
+
# Move me to the head of the list!
|
134
|
+
if node == @rightmost
|
135
|
+
# I was the rightmost node, now the node on my left is.
|
136
|
+
@rightmost = node.left
|
137
|
+
node.left.right = nil
|
138
|
+
else
|
139
|
+
# The node on my left should now point to the node on my right.
|
140
|
+
node.left.right = node.right
|
141
|
+
# The node on my right should point to the node on my left.
|
142
|
+
node.right.left = node.left
|
143
|
+
end
|
144
|
+
former_leftmost = @leftmost
|
145
|
+
# I am the new head node!
|
146
|
+
@leftmost = node
|
147
|
+
@leftmost.left = nil
|
148
|
+
@leftmost.right = former_leftmost
|
149
|
+
former_leftmost.left = @leftmost
|
150
|
+
end
|
151
|
+
node
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
data/lib/heroic/sns/message.rb
CHANGED
@@ -1,19 +1,21 @@
|
|
1
1
|
require 'json'
|
2
2
|
require 'base64'
|
3
|
+
require 'heroic/lru_cache'
|
3
4
|
|
4
5
|
module Heroic
|
5
6
|
module SNS
|
6
7
|
|
7
8
|
MAXIMUM_ALLOWED_AGE = 3600 # reject messages older than one hour
|
9
|
+
MAXIMUM_ALLOWED_CERTIFICATES = 50
|
8
10
|
|
9
|
-
CERTIFICATE_CACHE =
|
11
|
+
CERTIFICATE_CACHE = Heroic::LRUCache.new(MAXIMUM_ALLOWED_CERTIFICATES) do |cert_url|
|
10
12
|
begin
|
11
13
|
cert_data = open(cert_url)
|
12
|
-
|
13
|
-
rescue OpenURI::HTTPError => e
|
14
|
-
raise Error.new("unable to retrieve signing certificate: #{e.message}; URL: #{cert_url}")
|
14
|
+
OpenSSL::X509::Certificate.new(cert_data.read)
|
15
15
|
rescue OpenSSL::X509::CertificateError => e
|
16
|
-
raise Error.new("unable to parse signing certificate: #{e.message}; URL: #{cert_url}")
|
16
|
+
raise SNS::Error.new("unable to parse signing certificate: #{e.message}; URL: #{cert_url}")
|
17
|
+
rescue => e
|
18
|
+
raise SNS::Error.new("unable to retrieve signing certificate: #{e.message}; URL: #{cert_url}")
|
17
19
|
end
|
18
20
|
end
|
19
21
|
|
@@ -104,7 +106,7 @@ module Heroic
|
|
104
106
|
# See: http://docs.aws.amazon.com/sns/latest/gsg/SendMessageToHttp.verify.signature.html
|
105
107
|
def verify!
|
106
108
|
age = Time.now - timestamp
|
107
|
-
raise
|
109
|
+
raise Error.new("timestamp is in the future", self) if age < 0
|
108
110
|
raise Error.new("timestamp is too old", self) if age > MAXIMUM_ALLOWED_AGE
|
109
111
|
if signature_version != '1'
|
110
112
|
raise Error.new("unknown signature version: #{signature_version}", self)
|
@@ -113,7 +115,7 @@ module Heroic
|
|
113
115
|
raise Error.new("signing certificate is not from amazonaws.com", self)
|
114
116
|
end
|
115
117
|
text = string_to_sign # will warn of invalid Type
|
116
|
-
cert = CERTIFICATE_CACHE
|
118
|
+
cert = CERTIFICATE_CACHE.get(signing_cert_url)
|
117
119
|
digest = OpenSSL::Digest::SHA1.new
|
118
120
|
unless cert.public_key.verify(digest, signature, text)
|
119
121
|
raise Error.new("message signature is invalid", self)
|
data/lib/heroic/sns/version.rb
CHANGED
data/test/helper.rb
CHANGED
@@ -9,11 +9,11 @@ module Heroic
|
|
9
9
|
TEST_CERT_URL = 'https://sns.test.amazonaws.com/self-signed.pem'
|
10
10
|
TEST_CERT_KEY = OpenSSL::PKey::RSA.new(File.read('test/fixtures/sns.key'))
|
11
11
|
|
12
|
-
|
12
|
+
begin
|
13
13
|
# Insert the certificate in the cache so that these tests aren't dependent
|
14
14
|
# on network access (or the fact that the certificate is fake).
|
15
15
|
cert_data = File.read('test/fixtures/sns.crt')
|
16
|
-
OpenSSL::X509::Certificate.new(cert_data)
|
16
|
+
CERTIFICATE_CACHE.put(TEST_CERT_URL, OpenSSL::X509::Certificate.new(cert_data))
|
17
17
|
end
|
18
18
|
|
19
19
|
class Message
|
@@ -0,0 +1,112 @@
|
|
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(:foo)
|
18
|
+
cache.put(:foo, :bar)
|
19
|
+
assert_equal :bar, cache.get(:foo)
|
20
|
+
cache.put(:answer, 42)
|
21
|
+
assert_equal 42, cache.get(:answer)
|
22
|
+
assert_nil cache.get(:foo)
|
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(:foo)
|
36
|
+
end
|
37
|
+
@should_throw = false
|
38
|
+
assert_equal 4, cache.get(:foo)
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_dynamic
|
42
|
+
@counter = 0
|
43
|
+
cache = Heroic::LRUCache.new(3) { |k| @counter += 1; "hello, #{k}." }
|
44
|
+
assert_equal 0, @counter
|
45
|
+
cache.verify!
|
46
|
+
assert_equal "hello, leo.", cache.get(:leo); assert_equal 1, @counter
|
47
|
+
cache.verify!
|
48
|
+
assert_equal "hello, leo.", cache.get(:leo); assert_equal 1, @counter
|
49
|
+
cache.verify!
|
50
|
+
assert_equal "hello, donnie.", cache.get(:donnie); assert_equal 2, @counter
|
51
|
+
cache.verify!
|
52
|
+
assert_equal "hello, donnie.", cache.get(:donnie); assert_equal 2, @counter
|
53
|
+
cache.verify!
|
54
|
+
assert_equal "hello, mikey.", cache.get(:mikey); assert_equal 3, @counter
|
55
|
+
cache.verify!
|
56
|
+
assert_equal "hello, mikey.", cache.get(:mikey); assert_equal 3, @counter
|
57
|
+
cache.verify!
|
58
|
+
# raph will push leo out of cache
|
59
|
+
assert_equal "hello, raph.", cache.get(:raph); assert_equal 4, @counter
|
60
|
+
cache.verify!
|
61
|
+
assert_equal "hello, raph.", cache.get(:raph); assert_equal 4, @counter
|
62
|
+
cache.verify!
|
63
|
+
# mikey and donnie remain in cache
|
64
|
+
assert_equal "hello, mikey.", cache.get(:mikey); assert_equal 4, @counter
|
65
|
+
cache.verify!
|
66
|
+
assert_equal "hello, donnie.", cache.get(:donnie); assert_equal 4, @counter
|
67
|
+
cache.verify!
|
68
|
+
# leo will have to be refetched
|
69
|
+
assert_equal "hello, leo.", 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
|
80
|
+
end
|
81
|
+
# load the cache with things to read
|
82
|
+
cache.put(:a, 'a')
|
83
|
+
cache.put(:b, 'b')
|
84
|
+
cache.put(:c, 'c')
|
85
|
+
start_time = Time.now
|
86
|
+
# start fetch in background
|
87
|
+
td = Thread.new do
|
88
|
+
cache.get(:d)
|
89
|
+
end
|
90
|
+
te = Thread.new do
|
91
|
+
cache.get(:e)
|
92
|
+
end
|
93
|
+
# while background threads fetch, reading other keys should not be blocked
|
94
|
+
assert_equal 'a', cache.get(:a)
|
95
|
+
assert_equal 'b', cache.get(:b)
|
96
|
+
assert_equal 'c', cache.get(:c)
|
97
|
+
assert_equal 0, @lock.synchronize { @counter }
|
98
|
+
# now main thread should block until background items are fetched
|
99
|
+
assert_equal "d", cache.get(:d)
|
100
|
+
assert_equal "e", cache.get(:e)
|
101
|
+
assert_equal 2, @lock.synchronize { @counter }
|
102
|
+
# reading the same values should be fast (they are cached)
|
103
|
+
assert_equal "d", cache.get(:d)
|
104
|
+
assert_equal "e", cache.get(:e)
|
105
|
+
assert_equal 2, @lock.synchronize { @counter }
|
106
|
+
# look at time elapsed to make sure we didn't sleep more than once
|
107
|
+
[td, te].each { |t| t.join }
|
108
|
+
time_elapsed = (Time.now - start_time)
|
109
|
+
assert_in_delta time_elapsed, 1.0, 0.01
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: heroic-sns
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin Ragheb
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -68,8 +68,8 @@ files:
|
|
68
68
|
- bin/fake-sns
|
69
69
|
- demo/config.ru
|
70
70
|
- demo/demo.erb
|
71
|
-
- description.txt
|
72
71
|
- heroic-sns.gemspec
|
72
|
+
- lib/heroic/lru_cache.rb
|
73
73
|
- lib/heroic/sns.rb
|
74
74
|
- lib/heroic/sns/endpoint.rb
|
75
75
|
- lib/heroic/sns/message.rb
|
@@ -81,6 +81,7 @@ files:
|
|
81
81
|
- test/fixtures/unsubscribe.json
|
82
82
|
- test/helper.rb
|
83
83
|
- test/test_endpoint.rb
|
84
|
+
- test/test_lru_cache.rb
|
84
85
|
- test/test_message.rb
|
85
86
|
homepage: https://github.com/benzado/heroic-sns
|
86
87
|
licenses:
|
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.
|