secure_carrot 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/LICENSE +20 -0
- data/README.markdown +34 -0
- data/Rakefile +42 -0
- data/VERSION.yml +5 -0
- data/lib/amqp/buffer.rb +401 -0
- data/lib/amqp/exchange.rb +51 -0
- data/lib/amqp/frame.rb +121 -0
- data/lib/amqp/header.rb +27 -0
- data/lib/amqp/protocol.rb +209 -0
- data/lib/amqp/queue.rb +144 -0
- data/lib/amqp/server.rb +187 -0
- data/lib/amqp/spec.rb +820 -0
- data/lib/carrot.rb +92 -0
- data/lib/examples/simple_pop.rb +13 -0
- data/protocol/amqp-0.8.json +617 -0
- data/protocol/amqp-0.8.xml +3908 -0
- data/protocol/codegen.rb +173 -0
- data/protocol/doc.txt +281 -0
- data/test/carrot_test.rb +25 -0
- data/test/test_helper.rb +18 -0
- metadata +102 -0
@@ -0,0 +1,209 @@
|
|
1
|
+
module Carrot::AMQP
|
2
|
+
module Protocol
|
3
|
+
#:stopdoc:
|
4
|
+
class Class::Method
|
5
|
+
def initialize *args
|
6
|
+
opts = args.pop if args.last.is_a? Hash
|
7
|
+
opts ||= {}
|
8
|
+
|
9
|
+
@debug = 1 # XXX hack, p(obj) == '' if no instance vars are set
|
10
|
+
|
11
|
+
if args.size == 1 and args.first.is_a? Buffer
|
12
|
+
buf = args.shift
|
13
|
+
else
|
14
|
+
buf = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
self.class.arguments.each do |type, name|
|
18
|
+
val = buf ? buf.read(type) :
|
19
|
+
args.shift || opts[name] || opts[name.to_s]
|
20
|
+
instance_variable_set("@#{name}", val)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def arguments
|
25
|
+
self.class.arguments.inject({}) do |hash, (type, name)|
|
26
|
+
hash.update name => instance_variable_get("@#{name}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_binary
|
31
|
+
buf = Buffer.new
|
32
|
+
buf.write :short, self.class.parent.id
|
33
|
+
buf.write :short, self.class.id
|
34
|
+
|
35
|
+
bits = []
|
36
|
+
|
37
|
+
self.class.arguments.each do |type, name|
|
38
|
+
val = instance_variable_get("@#{name}")
|
39
|
+
if type == :bit
|
40
|
+
bits << (val || false)
|
41
|
+
else
|
42
|
+
unless bits.empty?
|
43
|
+
buf.write :bit, bits
|
44
|
+
bits = []
|
45
|
+
end
|
46
|
+
buf.write type, val
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
buf.write :bit, bits unless bits.empty?
|
51
|
+
buf.rewind
|
52
|
+
|
53
|
+
buf
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
to_binary.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_frame channel = 0
|
61
|
+
Frame::Method.new(self, channel)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
#:startdoc:
|
66
|
+
#
|
67
|
+
# Contains a properties hash that holds some potentially interesting
|
68
|
+
# information.
|
69
|
+
# * :delivery_mode
|
70
|
+
# 1 equals transient.
|
71
|
+
# 2 equals persistent. Unconsumed persistent messages will survive
|
72
|
+
# a server restart when they are stored in a durable queue.
|
73
|
+
# * :redelivered
|
74
|
+
# True or False
|
75
|
+
# * :routing_key
|
76
|
+
# The routing string used for matching this message to this queue.
|
77
|
+
# * :priority
|
78
|
+
# An integer in the range of 0 to 9 inclusive.
|
79
|
+
# * :content_type
|
80
|
+
# Always "application/octet-stream" (byte stream)
|
81
|
+
# * :exchange
|
82
|
+
# The source exchange which published this message.
|
83
|
+
# * :message_count
|
84
|
+
# The number of unconsumed messages contained in the queue.
|
85
|
+
# * :delivery_tag
|
86
|
+
# A monotonically increasing integer. This number should not be trusted
|
87
|
+
# as a sequence number. There is no guarantee it won't get reset.
|
88
|
+
class Header
|
89
|
+
def initialize *args
|
90
|
+
opts = args.pop if args.last.is_a? Hash
|
91
|
+
opts ||= {}
|
92
|
+
|
93
|
+
first = args.shift
|
94
|
+
|
95
|
+
if first.is_a? ::Class and first.ancestors.include? Protocol::Class
|
96
|
+
@klass = first
|
97
|
+
@size = args.shift || 0
|
98
|
+
@weight = args.shift || 0
|
99
|
+
@properties = opts
|
100
|
+
|
101
|
+
elsif first.is_a? Buffer or first.is_a? String
|
102
|
+
buf = first
|
103
|
+
buf = Buffer.new(buf) unless buf.is_a? Buffer
|
104
|
+
|
105
|
+
@klass = Protocol.classes[buf.read(:short)]
|
106
|
+
@weight = buf.read(:short)
|
107
|
+
@size = buf.read(:longlong)
|
108
|
+
|
109
|
+
props = buf.read(:properties, *klass.properties.map{|type,_| type })
|
110
|
+
@properties = Hash[*klass.properties.map{|_,name| name }.zip(props).reject{|k,v| v.nil? }.flatten]
|
111
|
+
|
112
|
+
else
|
113
|
+
raise ArgumentError, 'Invalid argument'
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
attr_accessor :klass, :size, :weight, :properties
|
118
|
+
|
119
|
+
def to_binary
|
120
|
+
buf = Buffer.new
|
121
|
+
buf.write :short, klass.id
|
122
|
+
buf.write :short, weight # XXX rabbitmq only supports weight == 0
|
123
|
+
buf.write :longlong, size
|
124
|
+
buf.write :properties, (klass.properties.map do |type, name|
|
125
|
+
[ type, properties[name] || properties[name.to_s] ]
|
126
|
+
end)
|
127
|
+
buf.rewind
|
128
|
+
buf
|
129
|
+
end
|
130
|
+
|
131
|
+
def to_s
|
132
|
+
to_binary.to_s
|
133
|
+
end
|
134
|
+
|
135
|
+
def to_frame channel = 0
|
136
|
+
Frame::Header.new(self, channel)
|
137
|
+
end
|
138
|
+
|
139
|
+
def == header
|
140
|
+
[ :klass, :size, :weight, :properties ].inject(true) do |eql, field|
|
141
|
+
eql and __send__(field) == header.__send__(field)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def method_missing meth, *args, &blk
|
146
|
+
@properties.has_key?(meth) || @klass.properties.find{|_,name| name == meth } ? @properties[meth] :
|
147
|
+
super
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.parse buf
|
152
|
+
buf = Buffer.new(buf) unless buf.is_a? Buffer
|
153
|
+
class_id, method_id = buf.read(:short, :short)
|
154
|
+
classes[class_id].methods[method_id].new(buf)
|
155
|
+
end
|
156
|
+
#:stopdoc:
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
if $0 =~ /bacon/ or $0 == __FILE__
|
161
|
+
require 'bacon'
|
162
|
+
include AMQP
|
163
|
+
|
164
|
+
describe Protocol do
|
165
|
+
should 'instantiate methods with arguments' do
|
166
|
+
meth = Protocol::Connection::StartOk.new nil, 'PLAIN', nil, 'en_US'
|
167
|
+
meth.locale.should == 'en_US'
|
168
|
+
end
|
169
|
+
|
170
|
+
should 'instantiate methods with named parameters' do
|
171
|
+
meth = Protocol::Connection::StartOk.new :locale => 'en_US',
|
172
|
+
:mechanism => 'PLAIN'
|
173
|
+
meth.locale.should == 'en_US'
|
174
|
+
end
|
175
|
+
|
176
|
+
should 'convert methods to binary' do
|
177
|
+
meth = Protocol::Connection::Secure.new :challenge => 'secret'
|
178
|
+
meth.to_binary.should.be.kind_of? Buffer
|
179
|
+
|
180
|
+
meth.to_s.should == [ 10, 20, 6, 'secret' ].pack('nnNa*')
|
181
|
+
end
|
182
|
+
|
183
|
+
should 'convert binary to method' do
|
184
|
+
orig = Protocol::Connection::Secure.new :challenge => 'secret'
|
185
|
+
copy = Protocol.parse orig.to_binary
|
186
|
+
orig.should == copy
|
187
|
+
end
|
188
|
+
|
189
|
+
should 'convert headers to binary' do
|
190
|
+
head = Protocol::Header.new Protocol::Basic,
|
191
|
+
size = 5,
|
192
|
+
weight = 0,
|
193
|
+
:content_type => 'text/json',
|
194
|
+
:delivery_mode => 1,
|
195
|
+
:priority => 1
|
196
|
+
head.to_s.should == [ 60, weight, 0, size, 0b1001_1000_0000_0000, 9, 'text/json', 1, 1 ].pack('nnNNnCa*CC')
|
197
|
+
end
|
198
|
+
|
199
|
+
should 'convert binary to header' do
|
200
|
+
orig = Protocol::Header.new Protocol::Basic,
|
201
|
+
size = 5,
|
202
|
+
weight = 0,
|
203
|
+
:content_type => 'text/json',
|
204
|
+
:delivery_mode => 1,
|
205
|
+
:priority => 1
|
206
|
+
Protocol::Header.new(orig.to_binary).should == orig
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
data/lib/amqp/queue.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
module Carrot::AMQP
|
2
|
+
class Queue
|
3
|
+
attr_reader :name, :carrot
|
4
|
+
attr_accessor :delivery_tag
|
5
|
+
|
6
|
+
def initialize(carrot, name, opts = {})
|
7
|
+
@opts = opts
|
8
|
+
@name = name
|
9
|
+
@carrot = carrot
|
10
|
+
server.send_frame(
|
11
|
+
Protocol::Queue::Declare.new({ :queue => name, :nowait => true }.merge(opts))
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def pop(opts = {})
|
16
|
+
self.delivery_tag = nil
|
17
|
+
server.send_frame(
|
18
|
+
Protocol::Basic::Get.new({ :queue => name, :consumer_tag => name, :no_ack => !opts.delete(:ack), :nowait => true }.merge(opts))
|
19
|
+
)
|
20
|
+
method = server.next_method
|
21
|
+
return unless method.kind_of?(Protocol::Basic::GetOk)
|
22
|
+
|
23
|
+
self.delivery_tag = method.delivery_tag
|
24
|
+
|
25
|
+
header = server.next_payload
|
26
|
+
|
27
|
+
msg = ''
|
28
|
+
while msg.length < header.size
|
29
|
+
msg << server.next_payload
|
30
|
+
end
|
31
|
+
|
32
|
+
msg
|
33
|
+
end
|
34
|
+
|
35
|
+
def ack
|
36
|
+
server.send_frame(
|
37
|
+
Protocol::Basic::Ack.new(:delivery_tag => delivery_tag)
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
def publish(data, opts = {})
|
42
|
+
exchange.publish(data, opts)
|
43
|
+
end
|
44
|
+
|
45
|
+
def message_count
|
46
|
+
status.first
|
47
|
+
end
|
48
|
+
|
49
|
+
def consumer_count
|
50
|
+
status.last
|
51
|
+
end
|
52
|
+
|
53
|
+
def status(opts = {}, &blk)
|
54
|
+
server.send_frame(
|
55
|
+
Protocol::Queue::Declare.new({ :queue => name, :passive => true }.merge(opts))
|
56
|
+
)
|
57
|
+
method = server.next_method
|
58
|
+
return [nil, nil] if method.kind_of?(Protocol::Connection::Close)
|
59
|
+
|
60
|
+
[method.message_count, method.consumer_count]
|
61
|
+
end
|
62
|
+
|
63
|
+
def bind(exchange, opts = {})
|
64
|
+
exchange = exchange.respond_to?(:name) ? exchange.name : exchange
|
65
|
+
bindings[exchange] = opts
|
66
|
+
server.send_frame(
|
67
|
+
Protocol::Queue::Bind.new({ :queue => name, :exchange => exchange, :routing_key => opts.delete(:key), :nowait => true }.merge(opts))
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def unbind(exchange, opts = {})
|
72
|
+
exchange = exchange.respond_to?(:name) ? exchange.name : exchange
|
73
|
+
bindings.delete(exchange)
|
74
|
+
|
75
|
+
server.send_frame(
|
76
|
+
Protocol::Queue::Unbind.new({
|
77
|
+
:queue => name, :exchange => exchange, :routing_key => opts.delete(:key), :nowait => true }.merge(opts)
|
78
|
+
)
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
def delete(opts = {})
|
83
|
+
server.send_frame(
|
84
|
+
Protocol::Queue::Delete.new({ :queue => name, :nowait => true }.merge(opts))
|
85
|
+
)
|
86
|
+
carrot.queues.delete(name)
|
87
|
+
end
|
88
|
+
|
89
|
+
def purge(opts = {})
|
90
|
+
server.send_frame(
|
91
|
+
Protocol::Queue::Purge.new({ :queue => name, :nowait => true }.merge(opts))
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
def server
|
96
|
+
carrot.server
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# Is a wrapper around publish to send persistent messages.
|
101
|
+
|
102
|
+
def send_message(data,opts={})
|
103
|
+
opts.merge!(:persistent => true)
|
104
|
+
exchange.publish(data,opts)
|
105
|
+
end
|
106
|
+
|
107
|
+
def encrypt_message(message, password)
|
108
|
+
encrypted_message = message.encrypt(:symmetric, :password => password)
|
109
|
+
encrypted_message
|
110
|
+
end
|
111
|
+
|
112
|
+
def decrypt_message(message, password)
|
113
|
+
decrypted_message = message.decrypt(:symmetric, :password => password)
|
114
|
+
decrypted_message
|
115
|
+
end
|
116
|
+
|
117
|
+
##
|
118
|
+
# Is a wrapper around publish to send persistent and encrypted messages using symmetric key.
|
119
|
+
|
120
|
+
def encrypt_and_send_message(message, password, opts={})
|
121
|
+
opts.merge!(:persistent => true)
|
122
|
+
encrypted_message = encrypt_message(message, password)
|
123
|
+
exchange.publish(encrypted_message,opts)
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# This method will receive and decrypt messages using symmetric key.
|
128
|
+
|
129
|
+
def receive_and_decrypt_message(password, opts={})
|
130
|
+
msg = pop(opts)
|
131
|
+
decrypted_message = decrypt_message(msg, password)
|
132
|
+
decrypted_message
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
def exchange
|
137
|
+
@exchange ||= Exchange.new(carrot, :direct, '', :key => name)
|
138
|
+
end
|
139
|
+
|
140
|
+
def bindings
|
141
|
+
@bindings ||= {}
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/amqp/server.rb
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'thread'
|
3
|
+
require 'timeout'
|
4
|
+
|
5
|
+
module Carrot::AMQP
|
6
|
+
class Server
|
7
|
+
CONNECT_TIMEOUT = 1.0
|
8
|
+
RETRY_DELAY = 10.0
|
9
|
+
DEFAULT_PORT = 5672
|
10
|
+
|
11
|
+
attr_reader :host, :port, :status
|
12
|
+
attr_accessor :channel, :ticket
|
13
|
+
|
14
|
+
class ServerDown < StandardError; end
|
15
|
+
class ProtocolError < StandardError; end
|
16
|
+
|
17
|
+
def initialize(opts = {})
|
18
|
+
@host = opts[:host] || 'localhost'
|
19
|
+
@port = opts[:port] || DEFAULT_PORT
|
20
|
+
@user = opts[:user] || 'guest'
|
21
|
+
@pass = opts[:pass] || 'guest'
|
22
|
+
@vhost = opts[:vhost] || '/'
|
23
|
+
@insist = opts[:insist]
|
24
|
+
@status = 'NOT CONNECTED'
|
25
|
+
|
26
|
+
@multithread = opts[:multithread]
|
27
|
+
start_session
|
28
|
+
end
|
29
|
+
|
30
|
+
def send_frame(*args)
|
31
|
+
args.each do |data|
|
32
|
+
data.ticket = ticket if ticket and data.respond_to?(:ticket=)
|
33
|
+
data = data.to_frame(channel) unless data.is_a?(Frame)
|
34
|
+
data.channel = channel
|
35
|
+
|
36
|
+
log :send, data
|
37
|
+
write(data.to_s)
|
38
|
+
end
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def next_frame
|
43
|
+
frame = Frame.parse(buffer)
|
44
|
+
log :received, frame
|
45
|
+
frame
|
46
|
+
end
|
47
|
+
|
48
|
+
def next_method
|
49
|
+
next_payload
|
50
|
+
end
|
51
|
+
|
52
|
+
def next_payload
|
53
|
+
frame = next_frame
|
54
|
+
frame and frame.payload
|
55
|
+
end
|
56
|
+
|
57
|
+
def close
|
58
|
+
send_frame(
|
59
|
+
Protocol::Channel::Close.new(:reply_code => 200, :reply_text => 'bye', :method_id => 0, :class_id => 0)
|
60
|
+
)
|
61
|
+
puts "Error closing channel #{channel}" unless next_method.is_a?(Protocol::Channel::CloseOk)
|
62
|
+
|
63
|
+
self.channel = 0
|
64
|
+
send_frame(
|
65
|
+
Protocol::Connection::Close.new(:reply_code => 200, :reply_text => 'Goodbye', :class_id => 0, :method_id => 0)
|
66
|
+
)
|
67
|
+
puts "Error closing connection" unless next_method.is_a?(Protocol::Connection::CloseOk)
|
68
|
+
|
69
|
+
rescue ServerDown => e
|
70
|
+
ensure
|
71
|
+
close_socket
|
72
|
+
end
|
73
|
+
|
74
|
+
def read(*args)
|
75
|
+
send_command(:read, *args)
|
76
|
+
end
|
77
|
+
|
78
|
+
def write(*args)
|
79
|
+
send_command(:write, *args)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def buffer
|
85
|
+
@buffer ||= Buffer.new(self)
|
86
|
+
end
|
87
|
+
|
88
|
+
def send_command(cmd, *args)
|
89
|
+
begin
|
90
|
+
socket.__send__(cmd, *args)
|
91
|
+
rescue Errno::EPIPE, IOError, Errno::ECONNRESET, Errno::EINVAL => e
|
92
|
+
raise ServerDown, e.message
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def socket
|
97
|
+
return @socket if @socket and not @socket.closed?
|
98
|
+
|
99
|
+
begin
|
100
|
+
# Attempt to connect.
|
101
|
+
mutex.lock if multithread?
|
102
|
+
@socket = timeout(CONNECT_TIMEOUT) do
|
103
|
+
TCPSocket.new(host, port)
|
104
|
+
end
|
105
|
+
|
106
|
+
if Socket.constants.include? 'TCP_NODELAY'
|
107
|
+
@socket.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
108
|
+
end
|
109
|
+
@status = 'CONNECTED'
|
110
|
+
rescue SocketError, SystemCallError, IOError, Timeout::Error => e
|
111
|
+
msg = e.message << " - #{@host}:#{@port}"
|
112
|
+
raise ServerDown, e.message
|
113
|
+
ensure
|
114
|
+
mutex.unlock if multithread?
|
115
|
+
end
|
116
|
+
|
117
|
+
@socket
|
118
|
+
end
|
119
|
+
|
120
|
+
def start_session
|
121
|
+
@channel = 0
|
122
|
+
write(HEADER)
|
123
|
+
write([1, 1, VERSION_MAJOR, VERSION_MINOR].pack('C4'))
|
124
|
+
raise ProtocolError, 'bad start connection' unless next_method.is_a?(Protocol::Connection::Start)
|
125
|
+
|
126
|
+
send_frame(
|
127
|
+
Protocol::Connection::StartOk.new(
|
128
|
+
{:platform => 'Ruby', :product => 'Carrot', :information => 'http://github.com/famosagle/carrot', :version => VERSION},
|
129
|
+
'AMQPLAIN',
|
130
|
+
{:LOGIN => @user, :PASSWORD => @pass},
|
131
|
+
'en_US'
|
132
|
+
)
|
133
|
+
)
|
134
|
+
|
135
|
+
method = next_method
|
136
|
+
raise ProtocolError, "Bad AMQP Credentials. user: #{@user}, pass: #{@pass}" if method.nil?
|
137
|
+
|
138
|
+
if method.is_a?(Protocol::Connection::Tune)
|
139
|
+
send_frame(
|
140
|
+
Protocol::Connection::TuneOk.new( :channel_max => 0, :frame_max => 131072, :heartbeat => 0)
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
send_frame(
|
145
|
+
Protocol::Connection::Open.new(:virtual_host => @vhost, :capabilities => '', :insist => @insist)
|
146
|
+
)
|
147
|
+
raise ProtocolError, 'bad open connection' unless next_method.is_a?(Protocol::Connection::OpenOk)
|
148
|
+
|
149
|
+
@channel = 1
|
150
|
+
send_frame(Protocol::Channel::Open.new)
|
151
|
+
raise ProtocolError, "cannot open channel #{channel}" unless next_method.is_a?(Protocol::Channel::OpenOk)
|
152
|
+
|
153
|
+
send_frame(
|
154
|
+
Protocol::Access::Request.new(:realm => '/data', :read => true, :write => true, :active => true, :passive => true)
|
155
|
+
)
|
156
|
+
method = next_method
|
157
|
+
raise ProtocolError, 'access denied' unless method.is_a?(Protocol::Access::RequestOk)
|
158
|
+
self.ticket = method.ticket
|
159
|
+
end
|
160
|
+
|
161
|
+
def multithread?
|
162
|
+
@multithread
|
163
|
+
end
|
164
|
+
|
165
|
+
def close_socket(reason=nil)
|
166
|
+
# Close the socket. The server is not considered dead.
|
167
|
+
mutex.lock if multithread?
|
168
|
+
@socket.close if @socket and not @socket.closed?
|
169
|
+
@socket = nil
|
170
|
+
@status = "NOT CONNECTED"
|
171
|
+
ensure
|
172
|
+
mutex.unlock if multithread?
|
173
|
+
end
|
174
|
+
|
175
|
+
def mutex
|
176
|
+
@mutex ||= Mutex.new
|
177
|
+
end
|
178
|
+
|
179
|
+
def log(*args)
|
180
|
+
return unless Carrot.logging?
|
181
|
+
require 'pp'
|
182
|
+
pp args
|
183
|
+
puts
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
end
|