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 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.