schoefmax-warren 0.8.8
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +13 -0
- data/LICENSE +11 -0
- data/Manifest +32 -0
- data/Rakefile +23 -0
- data/examples/authed/receiver.rb +25 -0
- data/examples/authed/secret.rb +1 -0
- data/examples/authed/sender.rb +40 -0
- data/examples/simple/amqp_mass_sender.rb +21 -0
- data/examples/simple/amqp_receiver.rb +18 -0
- data/examples/simple/amqp_sender.rb +36 -0
- data/examples/simple/bunny_receiver.rb +18 -0
- data/examples/simple/bunny_sender.rb +36 -0
- data/lib/warren.rb +23 -0
- data/lib/warren/adapters/amqp_adapter.rb +87 -0
- data/lib/warren/adapters/bunny_adapter.rb +93 -0
- data/lib/warren/adapters/dummy_adapter.rb +12 -0
- data/lib/warren/adapters/test_adapter.rb +27 -0
- data/lib/warren/connection.rb +140 -0
- data/lib/warren/filters/shared_secret.rb +70 -0
- data/lib/warren/filters/yaml.rb +20 -0
- data/lib/warren/message_filter.rb +76 -0
- data/lib/warren/queue.rb +71 -0
- data/readme.rdoc +46 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/warren/adapters/test_adapter_spec.rb +29 -0
- data/spec/warren/connection_spec.rb +122 -0
- data/spec/warren/queue_spec.rb +73 -0
- data/spec/warren/warren_spec.rb +9 -0
- data/tasks/rdoc.rake +7 -0
- data/tasks/rspec.rake +24 -0
- data/warren.gemspec +33 -0
- metadata +111 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
|
3
|
+
class DummyAdapter < Warren::Queue
|
4
|
+
def self.publish queue_name, payload, &blk
|
5
|
+
puts "publishing #{payload.inspect} to #{queue_name}"
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.subscribe queue_name, &block
|
9
|
+
puts "subscribing to #{queue_name}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
|
3
|
+
class TestAdapter < Warren::Queue
|
4
|
+
ConnectionFailed = Class.new(Exception)
|
5
|
+
#
|
6
|
+
def self.publish queue_name, payload, &blk
|
7
|
+
raise TestAdapter::ConnectionFailed if fail?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
#
|
12
|
+
# def self.subscribe queue_name, &block
|
13
|
+
# puts "subscribing to #{queue_name}"
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def self.fail?
|
20
|
+
File.exists?(file_name) && (File.read(file_name, 4) == "FAIL")
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.file_name
|
24
|
+
"#{WARREN_ROOT}/tmp/warren.txt"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require "yaml"
|
2
|
+
module Warren
|
3
|
+
class Connection
|
4
|
+
|
5
|
+
attr_reader :options
|
6
|
+
|
7
|
+
#
|
8
|
+
# Creates a new connection by reading the options from
|
9
|
+
# WARREN_ROOT/config/warren.yml or specify the file to read as an
|
10
|
+
# argument or by passing a hash of connection details in.
|
11
|
+
#
|
12
|
+
# Warren::Connection.new # reads WARREN_ROOT/config/warren.yml
|
13
|
+
# Warren::Connection.new("file.yml") # reads file.yml
|
14
|
+
# Warren::Connection.new({"foo" => "bar"}) # uses the hash
|
15
|
+
#
|
16
|
+
# Reads WARREN_ENV out of the yaml'd hash (just like ActiveRecord)
|
17
|
+
# "development" by default (and RAILS_ENV if running under rails)
|
18
|
+
#
|
19
|
+
# Raises InvalidConnectionDetails if no params are found for the current
|
20
|
+
# environment.
|
21
|
+
#
|
22
|
+
# todo: fail nicely if the env isn't defined
|
23
|
+
#
|
24
|
+
def initialize params = nil
|
25
|
+
get_connection_details(params)
|
26
|
+
# Make the details publically accessible
|
27
|
+
@options = @opts
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Raised if connection details are missing or invalid
|
32
|
+
# Check the error message for more details
|
33
|
+
#
|
34
|
+
InvalidConnectionDetails = Class.new(Exception)
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
#
|
39
|
+
# Short version: This is the brain of this class and sets everything up.
|
40
|
+
#
|
41
|
+
# Long version: Works out where the connection details need to come from
|
42
|
+
# (params or yml) and parses them from there. Then figures out if the details
|
43
|
+
# are there for the current env and raises a nice error if there aren't any
|
44
|
+
# detail for the current env. Then we symblize all the keys and get the adapter
|
45
|
+
# to check its happy with the connection details we have.
|
46
|
+
#
|
47
|
+
def get_connection_details params
|
48
|
+
case params
|
49
|
+
when NilClass
|
50
|
+
# Read from config/warren.yml
|
51
|
+
read_config_file
|
52
|
+
|
53
|
+
when String
|
54
|
+
# See if it exists as a file
|
55
|
+
parse_config_file(params)
|
56
|
+
|
57
|
+
when Hash
|
58
|
+
# Use it as-is
|
59
|
+
@opts = params
|
60
|
+
|
61
|
+
else
|
62
|
+
# See if it responds to :read
|
63
|
+
if respond_to?(:read)
|
64
|
+
# Parse it as yaml
|
65
|
+
parse_config(params)
|
66
|
+
else
|
67
|
+
# Have no idea what to do with it
|
68
|
+
raise InvalidConnectionDetails, "Don't know what to do with the params passed. Please pass a hash, filename or nothing."
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Make sure the hash keys are symbols
|
73
|
+
@opts = symbolize_keys(@opts)
|
74
|
+
# Parse out the current environment
|
75
|
+
parse_environment
|
76
|
+
# Call the adapter to figure out if the details are alright
|
77
|
+
check_connection_details
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Reads in either config/warren.yml or the passed argument as a YAML file
|
82
|
+
#
|
83
|
+
def read_config_file filename=nil
|
84
|
+
filename ||= "#{WARREN_ROOT}/config/warren.yml"
|
85
|
+
parse_config_file(filename)
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
# Parses the config file into a hash from yaml.
|
90
|
+
#
|
91
|
+
def parse_config_file filename
|
92
|
+
if File.exists?(filename)
|
93
|
+
@opts = YAML.load_file(filename)
|
94
|
+
else
|
95
|
+
raise InvalidConnectionDetails, "File not found: '#{filename}'"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Parses the object using YAML#load
|
101
|
+
#
|
102
|
+
def parse_config obj
|
103
|
+
@opts = YAML.load(obj)
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
# Modifies the hash into just the details for the
|
108
|
+
# current environment, or raises InvalidConnectionDetails
|
109
|
+
#
|
110
|
+
def parse_environment
|
111
|
+
# keys are symbolified already
|
112
|
+
unless @opts.has_key?(WARREN_ENV.to_sym)
|
113
|
+
raise InvalidConnectionDetails, "No details for current environment '#{WARREN_ENV}'"
|
114
|
+
end
|
115
|
+
@opts = @opts[WARREN_ENV.to_sym]
|
116
|
+
end
|
117
|
+
|
118
|
+
#
|
119
|
+
# Changes all keys into symbols
|
120
|
+
#
|
121
|
+
def symbolize_keys(hash)
|
122
|
+
hash.each do |key, value|
|
123
|
+
hash.delete(key)
|
124
|
+
# Make it recursive
|
125
|
+
hash[key.to_sym] = (value.is_a?(Hash) ? symbolize_keys(value) : value)
|
126
|
+
end
|
127
|
+
hash
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# Calls the adapter to check the connection details
|
132
|
+
# Returns true or raises InvalidConnectionDetails
|
133
|
+
#
|
134
|
+
def check_connection_details
|
135
|
+
return true unless Warren::Queue.adapter.respond_to?(:check_connection_details)
|
136
|
+
Warren::Queue.adapter.send(:check_connection_details, @opts)
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
begin
|
2
|
+
require "hmac-sha2"
|
3
|
+
rescue LoadError => e
|
4
|
+
puts "Error loading the `ruby-hmac` gem."
|
5
|
+
exit!
|
6
|
+
end
|
7
|
+
|
8
|
+
module Warren
|
9
|
+
class MessageFilter
|
10
|
+
# Hashes the message using a secret salt, stores the hash
|
11
|
+
# in the message and then checks its the same when pulled
|
12
|
+
# off the other end.
|
13
|
+
#
|
14
|
+
# Basic trust implementation to make sure the message
|
15
|
+
# hasn't been tampered with in transit and came from
|
16
|
+
# an "authorised" app.
|
17
|
+
#
|
18
|
+
# Make sure both the publisher and subscriber use the same
|
19
|
+
# key else you'll get KeyValidationError error raised.
|
20
|
+
#
|
21
|
+
class SharedSecret
|
22
|
+
# Raised when no key (salt) is provided
|
23
|
+
class NoKeyError < Exception; end
|
24
|
+
# Raised when there is a key mismatch error
|
25
|
+
class KeyValidationError < Exception; end
|
26
|
+
|
27
|
+
# Sets the key to use
|
28
|
+
def self.key= key
|
29
|
+
@@key = key
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the current key
|
33
|
+
# Raises NoKeyError if no key has been assigned yet
|
34
|
+
def self.key
|
35
|
+
raise NoKeyError if @@key.nil?
|
36
|
+
@@key
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the hashed message
|
40
|
+
#
|
41
|
+
# Expects that msg#to_s returns a string
|
42
|
+
# to hash against.
|
43
|
+
#
|
44
|
+
def self.secret msg
|
45
|
+
HMAC::SHA256.hexdigest(self.key, msg.to_s)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Called when the message is being packed for
|
49
|
+
# transit. Returns a hash.
|
50
|
+
def self.pack msg
|
51
|
+
# Make sure its a hash
|
52
|
+
msg = {:secret_msg => msg} unless msg.is_a? Hash
|
53
|
+
# And add our secret into the hash
|
54
|
+
msg[:secret] = self.secret(msg.to_s)
|
55
|
+
msg
|
56
|
+
end
|
57
|
+
|
58
|
+
# Called when unpacking the message from transit.
|
59
|
+
# Returns the original object.
|
60
|
+
def self.unpack msg
|
61
|
+
# Check the secret exists in the msg and matches the secret_string
|
62
|
+
raise KeyValidationError unless msg.delete(:secret) == self.secret(msg)
|
63
|
+
# see if its a hash we created, it'll only contain the key "secret_msg" if it is
|
64
|
+
msg = msg[:secret_msg] if msg.keys == [:secret_msg]
|
65
|
+
msg
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module Warren
|
4
|
+
class MessageFilter
|
5
|
+
# Packs the message into a YAML string
|
6
|
+
# for transferring safely across the wire
|
7
|
+
class Yaml < MessageFilter
|
8
|
+
|
9
|
+
# Returns a YAML string
|
10
|
+
def self.pack msg
|
11
|
+
YAML.dump(msg)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns original message
|
15
|
+
def self.unpack msg
|
16
|
+
YAML.load(msg)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Warren
|
2
|
+
# Handles filtering messages going onto/coming off the queue
|
3
|
+
class MessageFilter
|
4
|
+
# Array of filters to be run on the message before its
|
5
|
+
# pushed to rabbit.
|
6
|
+
#
|
7
|
+
# NB: These get called in reverse order from the array -
|
8
|
+
# the last filter to be added gets called first.
|
9
|
+
@@filters = []
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Adds a filter to the list
|
13
|
+
#
|
14
|
+
# A valid filter is just a class that defines
|
15
|
+
# <tt>self.pack</tt> and <tt>self.unpack</tt>
|
16
|
+
# methods, which both accept a single argument,
|
17
|
+
# act upon it, and return the output.
|
18
|
+
#
|
19
|
+
# Example filter class (See also message_filters/*.rb)
|
20
|
+
#
|
21
|
+
# class Foo
|
22
|
+
# def self.pack msg
|
23
|
+
# msg.reverse # Assumes msg responds to reverse
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# def self.unpack msg
|
27
|
+
# msg.reverse # Does the opposite of Foo#pack
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
def << filter
|
32
|
+
@@filters << filter
|
33
|
+
end
|
34
|
+
alias :add_filter :<<
|
35
|
+
end
|
36
|
+
|
37
|
+
# Called when a subclass is created, adds the subclass to the queue
|
38
|
+
def self.inherited klass
|
39
|
+
add_filter klass
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns current array of filters
|
43
|
+
def self.filters
|
44
|
+
@@filters
|
45
|
+
end
|
46
|
+
|
47
|
+
# Resets the filters to default
|
48
|
+
def self.reset_filters
|
49
|
+
@@filters = [Warren::MessageFilter::Yaml]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Runs the raw message through all the filters
|
53
|
+
# and returns the filtered version
|
54
|
+
def self.pack msg
|
55
|
+
@@filters.reverse.each do |f|
|
56
|
+
# puts "Packing with #{f}"
|
57
|
+
msg = f.send(:pack, msg)
|
58
|
+
end
|
59
|
+
msg
|
60
|
+
end
|
61
|
+
|
62
|
+
# Runs the filtered message through all the
|
63
|
+
# filters and returns the raw version
|
64
|
+
def self.unpack msg
|
65
|
+
@@filters.each do |f|
|
66
|
+
# puts "Unpacking with #{f}"
|
67
|
+
msg = f.unpack(msg)
|
68
|
+
end
|
69
|
+
msg
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Make sure the YAML filter is added first
|
76
|
+
require File.expand_path(File.dirname(__FILE__) + "/filters/yaml")
|
data/lib/warren/queue.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
module Warren
|
2
|
+
class Queue
|
3
|
+
@@connection = nil
|
4
|
+
@@adapter = nil
|
5
|
+
|
6
|
+
#
|
7
|
+
# Raised if no connection has been defined yet.
|
8
|
+
#
|
9
|
+
NoConnectionDetails = Class.new(Exception)
|
10
|
+
|
11
|
+
#
|
12
|
+
# Raised if a block is expected by the method but none is given.
|
13
|
+
#
|
14
|
+
NoBlockGiven = Class.new(Exception)
|
15
|
+
|
16
|
+
#
|
17
|
+
# Raised if an adapter isn't set
|
18
|
+
#
|
19
|
+
NoAdapterSet = Class.new(Exception)
|
20
|
+
|
21
|
+
#
|
22
|
+
# Sets the current connection
|
23
|
+
#
|
24
|
+
def self.connection= conn
|
25
|
+
@@connection = (conn.is_a?(Warren::Connection) ? conn : Warren::Connection.new(conn) )
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Returns the current connection details
|
30
|
+
#
|
31
|
+
def self.connection
|
32
|
+
@@connection ||= Warren::Connection.new
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Sets the adapter when this class is subclassed.
|
37
|
+
#
|
38
|
+
def self.inherited klass
|
39
|
+
@@adapter = klass
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Sets the adapter manually
|
44
|
+
#
|
45
|
+
def self.adapter= klass
|
46
|
+
@@adapter = klass
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Returns the current adapter or raises NoAdapterSet exception
|
51
|
+
#
|
52
|
+
def self.adapter
|
53
|
+
@@adapter || raise(NoAdapterSet)
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Publishes the message to the queue
|
58
|
+
#
|
59
|
+
def self.publish *args, &blk
|
60
|
+
self.adapter.publish(*args, &blk)
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Sends the subscribe message to the adapter class
|
65
|
+
#
|
66
|
+
def self.subscribe *args, &blk
|
67
|
+
self.adapter.subscribe(*args, &blk)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
data/readme.rdoc
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
= Warren
|
2
|
+
|
3
|
+
Library for sending and receiving messages, complete with en/decrypting messages on either side of the transport.
|
4
|
+
|
5
|
+
It was written to handle sending messages between two nodes using RabbitMQ, which is why the two default adapters are for synchronous and asynchronous rabbitmq client libraries. You can delegate the sending & receiving to any custom class you want, simply by subclassing Warren::Queue. (Isn't ruby magic marvelous!)
|
6
|
+
|
7
|
+
The filtering works in much the same way as the adapter class. There is a default YAML filter that is always called last before sending the message and first when receiving the message, simply to make sure the message is a string when sent + received. You can then add custom classes onto the stack in any order you want, simply by subclassing Warren::MessageFilter. Add them in the same order on the receiving side and warren takes care of calling them in reverse order.
|
8
|
+
|
9
|
+
Start by looking at examples/ to see how to use it, and then lib/warren/adapters/ to see how to implement your own adapter class and lib/warren/filters to see how to implement your own filters.
|
10
|
+
|
11
|
+
== Installation
|
12
|
+
|
13
|
+
gem install brightbox-warren
|
14
|
+
|
15
|
+
== Usage
|
16
|
+
|
17
|
+
require "rubygems"
|
18
|
+
require "warren"
|
19
|
+
# Use the bunny adapter to connect to RabbitMQ (Bunny is an AMQP client that works with Rails/Passenger apps)
|
20
|
+
require "warren/adapters/bunny_adapter"
|
21
|
+
# If you're running in development and don't want to actually push messages onto the queue then instead of loading the bunny adapter use the dummy adapter
|
22
|
+
require "warren/adapters/dummy_adapter"
|
23
|
+
|
24
|
+
# See examples/ for more
|
25
|
+
|
26
|
+
== Rails
|
27
|
+
|
28
|
+
Add this to your environment.rb
|
29
|
+
|
30
|
+
config.gem "brightbox-warren", :lib => "warren", :version => ">= 0.8"
|
31
|
+
|
32
|
+
Add the config into config/warren.yml with the details for each environment. Works just the same as database.yml:
|
33
|
+
|
34
|
+
development:
|
35
|
+
user: rabbit
|
36
|
+
pass: carrots53
|
37
|
+
host: rabbit.warren
|
38
|
+
logging: false
|
39
|
+
|
40
|
+
And then in an initializer file (or bottom of environment.rb) require the adapter you want to use (for rabbitmq I suggest bunny - amqp uses eventmachine and was giving me issues under passenger.) And then any filters you want to use.
|
41
|
+
|
42
|
+
require "warren/adapters/bunny_adapter"
|
43
|
+
|
44
|
+
== License
|
45
|
+
|
46
|
+
Licensed under the MIT license. See LICENSE for more details.
|