heroic-sns 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|