cztop 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +31 -0
- data/.yardopts +1 -0
- data/AUTHORS +1 -0
- data/CHANGES.md +3 -0
- data/Gemfile +10 -0
- data/Guardfile +61 -0
- data/LICENSE +5 -0
- data/Procfile +3 -0
- data/README.md +408 -0
- data/Rakefile +6 -0
- data/bin/console +7 -0
- data/bin/setup +7 -0
- data/ci-scripts/install-deps +9 -0
- data/cztop.gemspec +36 -0
- data/examples/ruby_actor/actor.rb +100 -0
- data/examples/simple_req_rep/rep.rb +12 -0
- data/examples/simple_req_rep/req.rb +35 -0
- data/examples/taxi_system/.gitignore +2 -0
- data/examples/taxi_system/Makefile +2 -0
- data/examples/taxi_system/README.gsl +115 -0
- data/examples/taxi_system/README.md +276 -0
- data/examples/taxi_system/broker.rb +98 -0
- data/examples/taxi_system/client.rb +34 -0
- data/examples/taxi_system/generate_keys.rb +24 -0
- data/examples/taxi_system/start_broker.sh +2 -0
- data/examples/taxi_system/start_clients.sh +11 -0
- data/lib/cztop/actor.rb +308 -0
- data/lib/cztop/authenticator.rb +97 -0
- data/lib/cztop/beacon.rb +96 -0
- data/lib/cztop/certificate.rb +176 -0
- data/lib/cztop/config/comments.rb +66 -0
- data/lib/cztop/config/serialization.rb +82 -0
- data/lib/cztop/config/traversing.rb +157 -0
- data/lib/cztop/config.rb +119 -0
- data/lib/cztop/frame.rb +158 -0
- data/lib/cztop/has_ffi_delegate.rb +85 -0
- data/lib/cztop/message/frames.rb +74 -0
- data/lib/cztop/message.rb +191 -0
- data/lib/cztop/monitor.rb +102 -0
- data/lib/cztop/poller.rb +334 -0
- data/lib/cztop/polymorphic_zsock_methods.rb +24 -0
- data/lib/cztop/proxy.rb +149 -0
- data/lib/cztop/send_receive_methods.rb +35 -0
- data/lib/cztop/socket/types.rb +207 -0
- data/lib/cztop/socket.rb +106 -0
- data/lib/cztop/version.rb +3 -0
- data/lib/cztop/z85.rb +157 -0
- data/lib/cztop/zsock_options.rb +334 -0
- data/lib/cztop.rb +55 -0
- data/perf/README.md +79 -0
- data/perf/inproc_lat.rb +49 -0
- data/perf/inproc_thru.rb +42 -0
- data/perf/local_lat.rb +35 -0
- data/perf/remote_lat.rb +26 -0
- metadata +297 -0
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/bin/sh -x
|
2
|
+
export BROKER_ADDRESS=tcp://127.0.0.1:4455
|
3
|
+
export BROKER_CERT=public_keys/broker
|
4
|
+
CLIENT_CERT=secret_keys/drivers/driver1_secret ./client.rb &
|
5
|
+
CLIENT_CERT=secret_keys/drivers/driver2_secret ./client.rb &
|
6
|
+
CLIENT_CERT=secret_keys/drivers/driver3_secret ./client.rb &
|
7
|
+
jobs
|
8
|
+
jobs -p
|
9
|
+
jobs -l
|
10
|
+
trap 'kill $(jobs -p)' EXIT
|
11
|
+
wait
|
data/lib/cztop/actor.rb
ADDED
@@ -0,0 +1,308 @@
|
|
1
|
+
module CZTop
|
2
|
+
# Represents a CZMQ::FFI::Zactor.
|
3
|
+
#
|
4
|
+
# = About Thread-Safety
|
5
|
+
# The instance methods of this class are thread-safe. So it's safe to call
|
6
|
+
# {#<<}, {#request} or even {#terminate} from different threads. Caution:
|
7
|
+
# Use only these methods to communicate with the low-level zactor. Don't use
|
8
|
+
# {Message#send_to} directly to send itself to an {Actor} instance, as it
|
9
|
+
# wouldn't be thread-safe.
|
10
|
+
#
|
11
|
+
# = About termination
|
12
|
+
# Actors should be terminated explicitly, either by calling {#terminate}
|
13
|
+
# from the current process or sending them the "$TERM" command (from
|
14
|
+
# outside). Not terminating them explicitly might make the process block at
|
15
|
+
# exit.
|
16
|
+
#
|
17
|
+
# @example Simple Actor with Ruby block
|
18
|
+
# result = ""
|
19
|
+
# a = CZTop::Actor.new do |msg, pipe|
|
20
|
+
# case msg[0]
|
21
|
+
# when "foo"
|
22
|
+
# pipe << "bar"
|
23
|
+
# when "append"
|
24
|
+
# result << msg[1].to_s
|
25
|
+
# when "result"
|
26
|
+
# pipe << result
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
# a.request("foo")[0] #=> "bar"
|
30
|
+
# a.request("foo")[0] #=> "bar"
|
31
|
+
# a << ["append", "baz"] << ["append", "baz"]
|
32
|
+
# a.request("result")[0] #=> "bazbaz"
|
33
|
+
#
|
34
|
+
# @see http://api.zeromq.org/czmq3-0:zactor
|
35
|
+
class Actor
|
36
|
+
include HasFFIDelegate
|
37
|
+
extend CZTop::HasFFIDelegate::ClassMethods
|
38
|
+
include ZsockOptions
|
39
|
+
include SendReceiveMethods
|
40
|
+
include PolymorphicZsockMethods
|
41
|
+
include ::CZMQ::FFI
|
42
|
+
|
43
|
+
# Raised when trying to interact with a terminated actor.
|
44
|
+
class DeadActorError < RuntimeError; end
|
45
|
+
|
46
|
+
# @return [Exception] the exception that crashed this actor, if any
|
47
|
+
attr_reader :exception
|
48
|
+
|
49
|
+
# Creates a new actor. Either pass a callback directly or a block. The
|
50
|
+
# block will be called for every received message.
|
51
|
+
#
|
52
|
+
# In case the given callback is an FFI::Pointer (to a C function), it's
|
53
|
+
# used as-is. It is expected to do the handshake (signal) itself.
|
54
|
+
#
|
55
|
+
# @param callback [FFI::Pointer, Proc, #call] pointer to a C function or
|
56
|
+
# just anything callable
|
57
|
+
# @param c_args [FFI::Pointer, nil] args, only useful if callback is an
|
58
|
+
# FFI::Pointer
|
59
|
+
# @yieldparam message [Message]
|
60
|
+
# @yieldparam pipe [Socket::PAIR]
|
61
|
+
# @see #process_messages
|
62
|
+
def initialize(callback = nil, c_args = nil, &handler)
|
63
|
+
@running = true
|
64
|
+
@mtx = Mutex.new
|
65
|
+
@callback = callback || handler
|
66
|
+
@callback = shim(@callback) unless @callback.is_a? ::FFI::Pointer
|
67
|
+
ffi_delegate = Zactor.new(@callback, c_args)
|
68
|
+
attach_ffi_delegate(ffi_delegate)
|
69
|
+
options.sndtimeo = 20#ms # see #<<
|
70
|
+
end
|
71
|
+
|
72
|
+
# Send a message to the actor.
|
73
|
+
# @param message [Object] message to send to the actor, see {Message.coerce}
|
74
|
+
# @return [self] so it's chainable
|
75
|
+
# @raise [DeadActorError] if actor is terminated
|
76
|
+
# @raise [IO::EAGAINWaitWritable, RuntimeError] anything that could be
|
77
|
+
# raised by {Message#send_to}
|
78
|
+
# @note Normally this method is asynchronous, but if the message is
|
79
|
+
# "$TERM", it blocks until the actor is terminated.
|
80
|
+
def <<(message)
|
81
|
+
message = Message.coerce(message)
|
82
|
+
|
83
|
+
if TERM == message[0]
|
84
|
+
# NOTE: can't just send this to the actor. The sender might call
|
85
|
+
# #terminate immediately, which most likely causes a hang due to race
|
86
|
+
# conditions.
|
87
|
+
terminate
|
88
|
+
else
|
89
|
+
begin
|
90
|
+
@mtx.synchronize do
|
91
|
+
raise DeadActorError if not @running
|
92
|
+
message.send_to(self)
|
93
|
+
end
|
94
|
+
rescue IO::EAGAINWaitWritable
|
95
|
+
# The sndtimeo has been reached.
|
96
|
+
#
|
97
|
+
# This should fix the race condition (mainly on JRuby) between
|
98
|
+
# @running not being set to false yet but the actor handler already
|
99
|
+
# terminating and thus not able to receive messages anymore.
|
100
|
+
#
|
101
|
+
# This shouldn't result in an infinite loop, since it'll stop as
|
102
|
+
# soon as @running is set to false by #signal_shimmed_handler_death,
|
103
|
+
# at least when using a Ruby handler.
|
104
|
+
#
|
105
|
+
# In case of a C function handler, it MUST NOT crash and only
|
106
|
+
# terminate when being sent the "$TERM" message using #terminate (so
|
107
|
+
# #await_handler_death can set
|
108
|
+
# @running to false).
|
109
|
+
retry
|
110
|
+
end
|
111
|
+
end
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
# Receive a message from the actor.
|
116
|
+
# @return [Message]
|
117
|
+
# @raise [DeadActorError] if actor is terminated
|
118
|
+
def receive
|
119
|
+
@mtx.synchronize do
|
120
|
+
raise DeadActorError if not @running
|
121
|
+
super
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Same as {#<<}, but also waits for a response from the actor and returns
|
126
|
+
# it.
|
127
|
+
# @param message [Message] the request to the actor
|
128
|
+
# @return [Message] the actor's response
|
129
|
+
# @raise [ArgumentError] if the message is "$TERM" (use {#terminate})
|
130
|
+
def request(message)
|
131
|
+
@mtx.synchronize do
|
132
|
+
raise DeadActorError if not @running
|
133
|
+
message = Message.coerce(message)
|
134
|
+
raise ArgumentError, "use #terminate" if TERM == message[0]
|
135
|
+
message.send_to(self)
|
136
|
+
Message.receive_from(self)
|
137
|
+
end
|
138
|
+
rescue IO::EAGAINWaitWritable
|
139
|
+
# same as in #<<
|
140
|
+
retry
|
141
|
+
end
|
142
|
+
|
143
|
+
# Sends a message according to a "picture".
|
144
|
+
# @see zsock_send() on http://api.zeromq.org/czmq3-0:zsock
|
145
|
+
# @note Mainly added for {Beacon}. If implemented there, it wouldn't be
|
146
|
+
# thread safe. And it's not that useful to be added to
|
147
|
+
# {SendReceiveMethods}.
|
148
|
+
# @param picture [String] message's part types
|
149
|
+
# @param args [String, Integer, ...] values, in FFI style (each one
|
150
|
+
# preceeded with it's type, like <tt>:string, "foo"</tt>)
|
151
|
+
# @return [void]
|
152
|
+
def send_picture(picture, *args)
|
153
|
+
@mtx.synchronize do
|
154
|
+
raise DeadActorError if not @running
|
155
|
+
Zsock.send(ffi_delegate, picture, *args)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Thread-safe {PolymorphicZsockMethods#wait}.
|
160
|
+
# @return [Integer]
|
161
|
+
def wait
|
162
|
+
@mtx.synchronize do
|
163
|
+
super
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Tells the actor to terminate and waits for it. Idempotent.
|
168
|
+
# @return [Boolean] whether it died just now (+false+ if it was dead
|
169
|
+
# already)
|
170
|
+
def terminate
|
171
|
+
term_msg = Message.new(TERM)
|
172
|
+
@mtx.synchronize do
|
173
|
+
return false if not @running
|
174
|
+
term_msg.send_to(self)
|
175
|
+
await_handler_death
|
176
|
+
true
|
177
|
+
end
|
178
|
+
rescue IO::EAGAINWaitWritable
|
179
|
+
# same as in #<<
|
180
|
+
retry
|
181
|
+
end
|
182
|
+
|
183
|
+
# @return [Boolean] whether this actor is dead (terminated or crashed)
|
184
|
+
def dead?
|
185
|
+
!@running
|
186
|
+
end
|
187
|
+
|
188
|
+
# @return [Boolean] whether this actor has crashed
|
189
|
+
# @see #exception
|
190
|
+
def crashed?
|
191
|
+
!!@exception # if set, it has crashed
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
# Shims the given handler. The shim is used to do the handshake, to
|
197
|
+
# {#process_messages}, and ensure we're notified when the handler has
|
198
|
+
# terminated.
|
199
|
+
#
|
200
|
+
# @param handler [Proc, #call] the handler used to process messages
|
201
|
+
# @return [FFI::Function] the callback function to be passed to the zactor
|
202
|
+
# @raise [ArgumentError] if invalid handler given
|
203
|
+
def shim(handler)
|
204
|
+
raise ArgumentError, "invalid handler" if !handler.respond_to?(:call)
|
205
|
+
|
206
|
+
@handler_thread = nil
|
207
|
+
@handler_dead_signal = Queue.new # used for signaling
|
208
|
+
|
209
|
+
Zactor.fn do |pipe_delegate, _args|
|
210
|
+
begin
|
211
|
+
@mtx.synchronize do
|
212
|
+
@handler_thread = Thread.current
|
213
|
+
@pipe = Socket::PAIR.from_ffi_delegate(pipe_delegate)
|
214
|
+
@pipe.signal # handshake, so zactor_new() returns
|
215
|
+
end
|
216
|
+
process_messages(handler)
|
217
|
+
rescue Exception
|
218
|
+
@exception = $!
|
219
|
+
ensure
|
220
|
+
signal_shimmed_handler_death
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# @return [Boolean] whether the handler is a Ruby object, like a simple
|
226
|
+
# block (as opposed to a FFI::Pointer to a C function)
|
227
|
+
def handler_shimmed?
|
228
|
+
!!@handler_thread # if it exists, it's shimmed
|
229
|
+
end
|
230
|
+
|
231
|
+
# the command which causes an actor handler to terminate
|
232
|
+
TERM = "$TERM"
|
233
|
+
|
234
|
+
# Successively receive messages that were sent to the actor and
|
235
|
+
# yield them to the given handler to process them. The a pipe (a
|
236
|
+
# {Socket::PAIR} socket) is also passed to the handler so it can send back
|
237
|
+
# the result of a command, if needed.
|
238
|
+
#
|
239
|
+
# When a message is "$TERM", or when the waiting for a message is
|
240
|
+
# interrupted, execution is aborted and the actor will terminate.
|
241
|
+
#
|
242
|
+
# @param handler [Proc, #call] the handler used to process messages
|
243
|
+
# @yieldparam message [Message] message (e.g. command) received
|
244
|
+
# @yieldparam pipe [Socket::PAIR] pipe to write back something into the
|
245
|
+
# actor
|
246
|
+
def process_messages(handler)
|
247
|
+
while true
|
248
|
+
begin
|
249
|
+
message = next_message
|
250
|
+
rescue Interrupt
|
251
|
+
break
|
252
|
+
else
|
253
|
+
break if TERM == message[0]
|
254
|
+
end
|
255
|
+
|
256
|
+
handler.call(message, @pipe)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Receives the next message even across any interrupts.
|
261
|
+
# @return [Message] the next message
|
262
|
+
def next_message
|
263
|
+
@pipe.receive
|
264
|
+
end
|
265
|
+
|
266
|
+
# Creates a new thread that will signal the definitive termination of the
|
267
|
+
# Ruby handler.
|
268
|
+
#
|
269
|
+
# This is needed to avoid the race condition between zactor_destroy()
|
270
|
+
# which will wait for a signal from the handler in case it was able to
|
271
|
+
# send the "$TERM" command, and the @callback which might still haven't
|
272
|
+
# returned, but doesn't receive any messages anymore.
|
273
|
+
#
|
274
|
+
# @return [void]
|
275
|
+
def signal_shimmed_handler_death
|
276
|
+
# NOTE: can't just use ConditionVariable, as the signaling code might be
|
277
|
+
# run BEFORE the waiting code.
|
278
|
+
|
279
|
+
Thread.new do
|
280
|
+
@handler_thread.join
|
281
|
+
|
282
|
+
# NOTE: we do this here and not in #terminate, so it also works when
|
283
|
+
# actor isn't terminated using #terminate
|
284
|
+
@running = false
|
285
|
+
|
286
|
+
@handler_dead_signal.push(nil)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Waits for the C or Ruby handler to die.
|
291
|
+
# @return [void]
|
292
|
+
def await_handler_death
|
293
|
+
if handler_shimmed?
|
294
|
+
# for Ruby block/Proc object handlers
|
295
|
+
@handler_dead_signal.pop
|
296
|
+
|
297
|
+
else
|
298
|
+
# for handlers that are passed as C functions, we rely on normal death
|
299
|
+
# signal
|
300
|
+
|
301
|
+
# can't use #wait here because of recursive deadlock
|
302
|
+
Zsock.wait(ffi_delegate)
|
303
|
+
|
304
|
+
@running = false
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module CZTop
|
2
|
+
|
3
|
+
# Authentication for ZeroMQ security mechanisms.
|
4
|
+
#
|
5
|
+
# This is implemented using an {Actor}.
|
6
|
+
#
|
7
|
+
# @see http://api.zeromq.org/czmq3-0:zauth
|
8
|
+
class Authenticator
|
9
|
+
include ::CZMQ::FFI
|
10
|
+
|
11
|
+
# function pointer to the `zauth()` function
|
12
|
+
ZAUTH_FPTR = ::CZMQ::FFI.ffi_libraries.each do |dl|
|
13
|
+
fptr = dl.find_function("zauth")
|
14
|
+
break fptr if fptr
|
15
|
+
end
|
16
|
+
raise LoadError, "couldn't find zauth()" if ZAUTH_FPTR.nil?
|
17
|
+
|
18
|
+
# This installs authentication on all {Socket}s and {Actor}s. Until you
|
19
|
+
# add policies, all incoming _NULL_ connections are allowed,
|
20
|
+
# and all _PLAIN_ and _CURVE_ connections are denied.
|
21
|
+
def initialize
|
22
|
+
@actor = Actor.new(ZAUTH_FPTR)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [Actor] the actor behind this authenticator
|
26
|
+
attr_reader :actor
|
27
|
+
|
28
|
+
# Terminates the authenticator.
|
29
|
+
# @return [void]
|
30
|
+
def terminate
|
31
|
+
@actor.terminate
|
32
|
+
end
|
33
|
+
|
34
|
+
# Enable verbose logging of commands and activity.
|
35
|
+
# @return [void]
|
36
|
+
def verbose!
|
37
|
+
@actor << "VERBOSE"
|
38
|
+
@actor.wait
|
39
|
+
end
|
40
|
+
|
41
|
+
# Add a list of IP addresses to the whitelist. For _NULL_, all clients
|
42
|
+
# from these addresses will be accepted. For _PLAIN_ and _CURVE_, they
|
43
|
+
# will be allowed to continue with authentication.
|
44
|
+
#
|
45
|
+
# @param addrs [String] IP address(es) to allow
|
46
|
+
# @return [void]
|
47
|
+
def allow(*addrs)
|
48
|
+
@actor << ["ALLOW", *addrs]
|
49
|
+
@actor.wait
|
50
|
+
end
|
51
|
+
|
52
|
+
# Add a list of IP addresses to the blacklist. For all security
|
53
|
+
# mechanisms, this rejects the connection without any further
|
54
|
+
# authentication. Use either a whitelist, or a blacklist, not not both. If
|
55
|
+
# you define both a whitelist and a blacklist, only the whitelist takes
|
56
|
+
# effect.
|
57
|
+
#
|
58
|
+
# @param addrs [String] IP address(es) to deny
|
59
|
+
# @return [void]
|
60
|
+
def deny(*addrs)
|
61
|
+
@actor << ["DENY", *addrs]
|
62
|
+
@actor.wait
|
63
|
+
end
|
64
|
+
|
65
|
+
# Configure PLAIN security mechanism using a plain-text password file. The
|
66
|
+
# password file will be reloaded automatically if modified externally.
|
67
|
+
#
|
68
|
+
# @param filename [String] path to the password file
|
69
|
+
# @return [void]
|
70
|
+
def plain(filename)
|
71
|
+
@actor << ["PLAIN", *filename]
|
72
|
+
@actor.wait
|
73
|
+
end
|
74
|
+
|
75
|
+
ANY_CERTIFICATE = "*"
|
76
|
+
|
77
|
+
# Configure CURVE authentication, using a directory that holds all public
|
78
|
+
# client certificates, i.e. their public keys. The certificates must have been
|
79
|
+
# created using {Certificate#save}/{Certificate#save_public}. You can add
|
80
|
+
# and remove certificates in that directory at any time.
|
81
|
+
#
|
82
|
+
# @param directory [String] the directory to take the keys from (the
|
83
|
+
# default value will allow any certificate)
|
84
|
+
# @return [void]
|
85
|
+
def curve(directory = ANY_CERTIFICATE)
|
86
|
+
@actor << ["CURVE", directory]
|
87
|
+
@actor.wait
|
88
|
+
end
|
89
|
+
|
90
|
+
# Configure GSSAPI authentication.
|
91
|
+
# @return [void]
|
92
|
+
def gssapi
|
93
|
+
@actor << "GSSAPI"
|
94
|
+
@actor.wait
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/cztop/beacon.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
module CZTop
|
2
|
+
# Used for LAN discovery and presence.
|
3
|
+
#
|
4
|
+
# This is implemented using an {Actor}.
|
5
|
+
#
|
6
|
+
# @see http://api.zeromq.org/czmq3-0:zbeacon
|
7
|
+
class Beacon
|
8
|
+
include ::CZMQ::FFI
|
9
|
+
|
10
|
+
# function pointer to the `zbeacon()` function
|
11
|
+
ZBEACON_FPTR = ::CZMQ::FFI.ffi_libraries.each do |dl|
|
12
|
+
fptr = dl.find_function("zbeacon")
|
13
|
+
break fptr if fptr
|
14
|
+
end
|
15
|
+
raise LoadError, "couldn't find zbeacon()" if ZBEACON_FPTR.nil?
|
16
|
+
|
17
|
+
# Initialize new Beacon.
|
18
|
+
def initialize
|
19
|
+
@actor = Actor.new(ZBEACON_FPTR)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Actor] the actor behind this Beacon
|
23
|
+
attr_reader :actor
|
24
|
+
|
25
|
+
# Terminates the beacon.
|
26
|
+
# @return [void]
|
27
|
+
def terminate
|
28
|
+
@actor.terminate
|
29
|
+
end
|
30
|
+
|
31
|
+
# Enable verbose logging of commands and activity.
|
32
|
+
# @return [void]
|
33
|
+
def verbose!
|
34
|
+
@actor << "VERBOSE"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Run the beacon on the specified UDP port.
|
38
|
+
# @param port [Integer] port number to
|
39
|
+
# @return [String] hostname, which can be used as endpoint for incoming
|
40
|
+
# connections
|
41
|
+
# @raise [SystemCallError] if the system doesn't support UDP broadcasts
|
42
|
+
def configure(port)
|
43
|
+
@actor.send_picture("si", :string, "CONFIGURE", :int, port)
|
44
|
+
hostname = Zstr.recv(@actor)
|
45
|
+
return hostname unless hostname.empty?
|
46
|
+
raise NotImplementedError, "system doesn't support UDP broadcasts"
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Integer] maximum length of data to {#publish}
|
50
|
+
MAX_BEACON_DATA = 255
|
51
|
+
|
52
|
+
# Start broadcasting a beacon.
|
53
|
+
# @param data [String] data to publish
|
54
|
+
# @param interval [Integer] interval in msec
|
55
|
+
# @raise [ArgumentError] if data is longer than {MAX_BEACON_DATA} bytes
|
56
|
+
# @return [void]
|
57
|
+
def publish(data, interval)
|
58
|
+
raise ArgumentError, "data too long" if data.bytesize > MAX_BEACON_DATA
|
59
|
+
@actor.send_picture("sbi", :string, "PUBLISH", :string, data,
|
60
|
+
:int, data.bytesize, :int, interval)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Stop broadcasting the beacon.
|
64
|
+
# @return [void]
|
65
|
+
def silence
|
66
|
+
@actor << "SILENCE"
|
67
|
+
end
|
68
|
+
|
69
|
+
# Start listening to beacons from peers.
|
70
|
+
# @param filter [String] do a prefix match on received beacons
|
71
|
+
# @return [void]
|
72
|
+
def subscribe(filter)
|
73
|
+
@actor.send_picture("sb", :string, "SUBSCRIBE",
|
74
|
+
:string, filter, :int, filter.bytesize)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Just like {#subscribe}, but subscribe to all peer beacons.
|
78
|
+
# @return [void]
|
79
|
+
def listen
|
80
|
+
@actor.send_picture("sb", :string, "SUBSCRIBE",
|
81
|
+
:string, nil, :int, 0)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Stop listening to other peers.
|
85
|
+
# @return [void]
|
86
|
+
def unsubscribe
|
87
|
+
@actor << "UNSUBSCRIBE"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Receive next beacon from a peer.
|
91
|
+
# @return [Message] 2-frame message with ([ipaddr, data])
|
92
|
+
def receive
|
93
|
+
@actor.receive
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
module CZTop
|
2
|
+
# Represents a CZMQ::FFI::Zcert.
|
3
|
+
class Certificate
|
4
|
+
include HasFFIDelegate
|
5
|
+
extend CZTop::HasFFIDelegate::ClassMethods
|
6
|
+
include ::CZMQ::FFI
|
7
|
+
|
8
|
+
# Warns if CURVE security isn't available.
|
9
|
+
# @return [void]
|
10
|
+
def self.check_curve_availability
|
11
|
+
return if Zproc.has_curve
|
12
|
+
warn "CZTop: CURVE isn't available. Consider installing libsodium."
|
13
|
+
end
|
14
|
+
|
15
|
+
# Loads a certificate from a file.
|
16
|
+
# @param filename [String, Pathname, #to_s] path to certificate file
|
17
|
+
# @return [Certificate] the loaded certificate
|
18
|
+
def self.load(filename)
|
19
|
+
ptr = Zcert.load(filename.to_s)
|
20
|
+
from_ffi_delegate(ptr)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Creates a new certificate from the given keys.
|
24
|
+
# @param public_key [String] binary public key (32 bytes)
|
25
|
+
# @param secret_key [String] binary secret key (32 bytes)
|
26
|
+
# @return [Certificate] the fresh certificate
|
27
|
+
# @raise [ArgumentError] if keys passed are invalid
|
28
|
+
# @raise [SystemCallError] if this fails
|
29
|
+
def self.new_from(public_key, secret_key)
|
30
|
+
raise ArgumentError, "no public key given" unless public_key
|
31
|
+
raise ArgumentError, "no secret key given" unless secret_key
|
32
|
+
|
33
|
+
raise ArgumentError, "invalid public key size" if public_key.bytesize != 32
|
34
|
+
raise ArgumentError, "invalid secret key size" if secret_key.bytesize != 32
|
35
|
+
|
36
|
+
ptr = Zcert.new_from(public_key, secret_key)
|
37
|
+
from_ffi_delegate(ptr)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Initialize a new in-memory certificate with random keys.
|
41
|
+
def initialize
|
42
|
+
attach_ffi_delegate(Zcert.new)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the public key either as Z85-encoded ASCII string (default) or
|
46
|
+
# binary string.
|
47
|
+
# @param format [Symbol] +:z85+ for Z85, +:binary+ for binary
|
48
|
+
# @return [String] public key
|
49
|
+
def public_key(format: :z85)
|
50
|
+
case format
|
51
|
+
when :z85
|
52
|
+
ffi_delegate.public_txt.read_string.force_encoding(Encoding::ASCII)
|
53
|
+
when :binary
|
54
|
+
ffi_delegate.public_key.read_string(32)
|
55
|
+
else
|
56
|
+
raise ArgumentError, "invalid format: %p" % format
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the secret key either as Z85-encoded ASCII string (default) or
|
61
|
+
# binary string.
|
62
|
+
# @param format [Symbol] +:z85+ for Z85, +:binary+ for binary
|
63
|
+
# @return [String] secret key
|
64
|
+
# @return [nil] if secret key is undefined (like after loading from a file
|
65
|
+
# created using {#save_public})
|
66
|
+
def secret_key(format: :z85)
|
67
|
+
case format
|
68
|
+
when :z85
|
69
|
+
key = ffi_delegate.secret_txt.read_string.force_encoding(Encoding::ASCII)
|
70
|
+
return nil if key.count("0") == 40
|
71
|
+
when :binary
|
72
|
+
key = ffi_delegate.secret_key.read_string(32)
|
73
|
+
return nil if key.count("\0") == 32
|
74
|
+
else
|
75
|
+
raise ArgumentError, "invalid format: %p" % format
|
76
|
+
end
|
77
|
+
key
|
78
|
+
end
|
79
|
+
|
80
|
+
# Get metadata.
|
81
|
+
# @param key [String] metadata key
|
82
|
+
# @return [String] value for meta key
|
83
|
+
# @return [nil] if metadata key is not set
|
84
|
+
def [](key)
|
85
|
+
ptr = ffi_delegate.meta(key)
|
86
|
+
return nil if ptr.null?
|
87
|
+
ptr.read_string
|
88
|
+
end
|
89
|
+
# Set metadata.
|
90
|
+
# @param key [String] metadata key
|
91
|
+
# @param value [String] metadata value
|
92
|
+
# @return [value]
|
93
|
+
def []=(key, value)
|
94
|
+
if value
|
95
|
+
ffi_delegate.set_meta(key, "%s", :string, value)
|
96
|
+
else
|
97
|
+
ffi_delegate.unset_meta(key)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns meta keys set.
|
102
|
+
# @return [Array<String>]
|
103
|
+
def meta_keys
|
104
|
+
zlist = ffi_delegate.meta_keys
|
105
|
+
first_key = zlist.first
|
106
|
+
return [] if first_key.null?
|
107
|
+
keys = [first_key.read_string]
|
108
|
+
while key = zlist.next
|
109
|
+
break if key.null?
|
110
|
+
keys << key.read_string
|
111
|
+
end
|
112
|
+
keys
|
113
|
+
end
|
114
|
+
|
115
|
+
# Save full certificate (public + secret) to files.
|
116
|
+
# @param filename [String, #to_s] path/filename to public file
|
117
|
+
# @return [void]
|
118
|
+
# @raise [ArgumentError] if path is invalid
|
119
|
+
# @raise [SystemCallError] if this fails
|
120
|
+
# @note This will create two files: one of the public key and one for the
|
121
|
+
# secret key. The secret filename is filename + "_secret".
|
122
|
+
def save(filename)
|
123
|
+
# see https://github.com/zeromq/czmq/issues/1244
|
124
|
+
raise ArgumentError, "filename can't be empty" if filename.to_s.empty?
|
125
|
+
rc = ffi_delegate.save(filename.to_s)
|
126
|
+
return if rc == 0
|
127
|
+
raise_zmq_err("error while saving to file %p" % filename)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Saves the public key to file in ZPL ({Config}) format.
|
131
|
+
# @param filename [String, #to_s] path/filename to public file
|
132
|
+
# @return [void]
|
133
|
+
# @raise [SystemCallError] if this fails
|
134
|
+
def save_public(filename)
|
135
|
+
rc = ffi_delegate.save_public(filename.to_s)
|
136
|
+
return if rc == 0
|
137
|
+
raise_zmq_err("error while saving to the file %p" % filename)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Saves the secret key to file in ZPL ({Config}) format.
|
141
|
+
# @param filename [String, #to_s] path/filename to secret file
|
142
|
+
# @return [void]
|
143
|
+
# @raise [SystemCallError] if this fails
|
144
|
+
def save_secret(filename)
|
145
|
+
rc = ffi_delegate.save_secret(filename.to_s)
|
146
|
+
return if rc == 0
|
147
|
+
raise_zmq_err("error while saving to the file %p" % filename)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Applies this certificate on a {Socket} or {Actor}.
|
151
|
+
# @param zocket [Socket, Actor] path/filename to secret file
|
152
|
+
# @return [void]
|
153
|
+
# @raise [SystemCallError] if secret key is undefined
|
154
|
+
def apply(zocket)
|
155
|
+
raise ArgumentError, "invalid zocket argument %p" % zocket unless zocket
|
156
|
+
return ffi_delegate.apply(zocket) unless secret_key.nil?
|
157
|
+
raise_zmq_err("secret key is undefined")
|
158
|
+
end
|
159
|
+
|
160
|
+
# Duplicates the certificate.
|
161
|
+
# @return [Certificate]
|
162
|
+
# @raise [SystemCallError] if this fails
|
163
|
+
def dup
|
164
|
+
ptr = ffi_delegate.dup
|
165
|
+
return from_ffi_delegate(ptr) unless ptr.null?
|
166
|
+
raise_zmq_err("unable to duplicate certificate")
|
167
|
+
end
|
168
|
+
|
169
|
+
# Compares this certificate to another.
|
170
|
+
# @param other [Cert] other certificate
|
171
|
+
# @return [Boolean] whether they have the same keys
|
172
|
+
def ==(other)
|
173
|
+
ffi_delegate.eq(other.ffi_delegate)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|