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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- N2Q1NmY1MjNhNGViOTI4NDk1N2FjZmQzYWZjNTE2ZTM4MzM3OTlkNA==
4
+ ZGJmMTYxNjFmNmJlZGFkM2E4ZDMzN2I0ODY1ODJmYzc5ZWMxNTAxZA==
5
5
  data.tar.gz: !binary |-
6
- ZmFkMjY5YzdiMDQ3ODMwZjQyNDNkNzY1ZGM3MzU3OWVmNWYzNDJkYQ==
6
+ ODU4NTFjMTM2MTBjZTY0MWMzNDNjZGZhYTQ1YjdhZTE0OWEwODAzOQ==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- MDZkY2QwOTkwZmRkMzc4ZmMwNTcyYTc5MDRlZjQ1ZTkxN2JmZjMwZWI4MTA4
10
- MTRmYzRjMmRjM2M3YTJiOTdlOGZlN2IyMzQyMjAyYWJiZjQ4OTc2Zjg1Yjll
11
- MGQ5NjM5ZmM5ZmY5YjdmYjg4Y2ZmZjFiNmUzZDc2Zjk3OWQwMDI=
9
+ ZTkxZjczZTVjODdjMjBlMTM0OWU2YjFmYzI4NzM1YzZmMWYzZWFlZjQ2YzMx
10
+ ODM1ODk3ZmNlMDRmYjE4NmI3MWE4ZTVmM2RmNTgwZTc2YjMwMjEzZjRhNjEy
11
+ NTVhMDZkZjlmN2IwYjkxNTdjM2M1Mzc2ODFjMzU3ZjhhZGRjZTg=
12
12
  data.tar.gz: !binary |-
13
- NDBhNWZmMzY1N2M0YTM5MWY4Njk0ZWQyNTc0NGY0NDk2NTQ4N2U4NDIwODE1
14
- NWMwNjhlNzYyNWUzZmE2YjZiMzBkN2Q3ZGM1NjczMDJiOGQ2MGVlNzAyZDQ5
15
- MWY4N2RkZDJlOGQ4YWJlM2U0OTUzN2ZmNDNlZWIyODdjYmEwZDU=
13
+ ZTY3YzJmZWRkZTE3OGE4ODRkNjgxNjMwMmYwYzRmZGQ4ODQ1MzE1YjMzZTcw
14
+ ZmYzMTJkYzhhM2Y5MmQ2YTQ5ZGRlYmUwMWMwYTc5YWUwM2RhMGFiOGJkZmVh
15
+ OWUyODNjYWE3Njg3Y2MyMzQ1ODI2N2Q0MGE4MmU5MTU0ODIzMDU=
@@ -1,6 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
- - ruby-head
4
3
  - "2.0.0"
5
4
  - "1.9.3"
6
5
  - "1.8.7"
data/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ ### 1.1.0 (June 20, 2013)
2
+
3
+ * Rework Cert logic - [@sbeckeriv]
4
+
1
5
  ### 1.0.1 (May 17, 2013)
2
6
 
3
7
  * Gem housekeeping, based on [advice by dblock](http://code.dblock.org/your-first-ruby-gem)
@@ -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"
@@ -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
@@ -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 = Hash.new do |cache, cert_url|
11
+ CERTIFICATE_CACHE = Heroic::LRUCache.new(MAXIMUM_ALLOWED_CERTIFICATES) do |cert_url|
10
12
  begin
11
13
  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
+ 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 Errow.new("timestamp is in the future", self) if age < 0
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[signing_cert_url]
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)
@@ -1,5 +1,5 @@
1
1
  module Heroic
2
2
  module SNS
3
- VERSION = '1.0.1'
3
+ VERSION = '1.1.0'
4
4
  end
5
5
  end
@@ -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
- CERTIFICATE_CACHE[TEST_CERT_URL] = begin
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.1
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-05-17 00:00:00.000000000 Z
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:
@@ -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.