cztop 0.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 +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
|