cztop 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +31 -0
  5. data/.yardopts +1 -0
  6. data/AUTHORS +1 -0
  7. data/CHANGES.md +3 -0
  8. data/Gemfile +10 -0
  9. data/Guardfile +61 -0
  10. data/LICENSE +5 -0
  11. data/Procfile +3 -0
  12. data/README.md +408 -0
  13. data/Rakefile +6 -0
  14. data/bin/console +7 -0
  15. data/bin/setup +7 -0
  16. data/ci-scripts/install-deps +9 -0
  17. data/cztop.gemspec +36 -0
  18. data/examples/ruby_actor/actor.rb +100 -0
  19. data/examples/simple_req_rep/rep.rb +12 -0
  20. data/examples/simple_req_rep/req.rb +35 -0
  21. data/examples/taxi_system/.gitignore +2 -0
  22. data/examples/taxi_system/Makefile +2 -0
  23. data/examples/taxi_system/README.gsl +115 -0
  24. data/examples/taxi_system/README.md +276 -0
  25. data/examples/taxi_system/broker.rb +98 -0
  26. data/examples/taxi_system/client.rb +34 -0
  27. data/examples/taxi_system/generate_keys.rb +24 -0
  28. data/examples/taxi_system/start_broker.sh +2 -0
  29. data/examples/taxi_system/start_clients.sh +11 -0
  30. data/lib/cztop/actor.rb +308 -0
  31. data/lib/cztop/authenticator.rb +97 -0
  32. data/lib/cztop/beacon.rb +96 -0
  33. data/lib/cztop/certificate.rb +176 -0
  34. data/lib/cztop/config/comments.rb +66 -0
  35. data/lib/cztop/config/serialization.rb +82 -0
  36. data/lib/cztop/config/traversing.rb +157 -0
  37. data/lib/cztop/config.rb +119 -0
  38. data/lib/cztop/frame.rb +158 -0
  39. data/lib/cztop/has_ffi_delegate.rb +85 -0
  40. data/lib/cztop/message/frames.rb +74 -0
  41. data/lib/cztop/message.rb +191 -0
  42. data/lib/cztop/monitor.rb +102 -0
  43. data/lib/cztop/poller.rb +334 -0
  44. data/lib/cztop/polymorphic_zsock_methods.rb +24 -0
  45. data/lib/cztop/proxy.rb +149 -0
  46. data/lib/cztop/send_receive_methods.rb +35 -0
  47. data/lib/cztop/socket/types.rb +207 -0
  48. data/lib/cztop/socket.rb +106 -0
  49. data/lib/cztop/version.rb +3 -0
  50. data/lib/cztop/z85.rb +157 -0
  51. data/lib/cztop/zsock_options.rb +334 -0
  52. data/lib/cztop.rb +55 -0
  53. data/perf/README.md +79 -0
  54. data/perf/inproc_lat.rb +49 -0
  55. data/perf/inproc_thru.rb +42 -0
  56. data/perf/local_lat.rb +35 -0
  57. data/perf/remote_lat.rb +26 -0
  58. metadata +297 -0
@@ -0,0 +1,2 @@
1
+ #!/bin/sh -x
2
+ BROKER_ADDRESS=tcp://127.0.0.1:4455 BROKER_CERT=secret_keys/broker CLIENT_CERTS=public_keys/drivers ./broker.rb
@@ -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
@@ -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
@@ -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