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.
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