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,24 @@
|
|
1
|
+
module CZTop
|
2
|
+
|
3
|
+
# These are methods that can be used on a {Socket} as well as an {Actor}.
|
4
|
+
# @see http://api.zeromq.org/czmq3-0:zsock
|
5
|
+
module PolymorphicZsockMethods
|
6
|
+
# Sends a signal.
|
7
|
+
# @param status [Integer] signal (0-255)
|
8
|
+
def signal(status = 0)
|
9
|
+
::CZMQ::FFI::Zsock.signal(ffi_delegate, status)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Waits for a signal.
|
13
|
+
# @return [Integer] the received signal
|
14
|
+
def wait
|
15
|
+
::CZMQ::FFI::Zsock.wait(ffi_delegate)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Set socket to use unbounded pipes (HWM=0); use this in cases when you are
|
19
|
+
# totally certain the message volume can fit in memory.
|
20
|
+
def set_unbounded
|
21
|
+
::CZMQ::FFI::Zsock.set_unbounded(ffi_delegate)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/cztop/proxy.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
module CZTop
|
2
|
+
# Steerable proxy which switches messages between a frontend and a backend
|
3
|
+
# socket.
|
4
|
+
#
|
5
|
+
# This is implemented using an {Actor}.
|
6
|
+
#
|
7
|
+
# @see http://api.zeromq.org/czmq3-0:zproxy
|
8
|
+
class Proxy
|
9
|
+
include ::CZMQ::FFI
|
10
|
+
|
11
|
+
# function pointer to the `zmonitor()` function
|
12
|
+
ZPROXY_FPTR = ::CZMQ::FFI.ffi_libraries.each do |dl|
|
13
|
+
fptr = dl.find_function("zproxy")
|
14
|
+
break fptr if fptr
|
15
|
+
end
|
16
|
+
raise LoadError, "couldn't find zproxy()" if ZPROXY_FPTR.nil?
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@actor = Actor.new(ZPROXY_FPTR)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Actor] the actor behind this proxy
|
23
|
+
attr_reader :actor
|
24
|
+
|
25
|
+
# Terminates the proxy.
|
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
|
+
@actor.wait
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns a configurator object which you can use to configure the
|
39
|
+
# frontend socket.
|
40
|
+
# @return [Configurator] (memoized) frontend configurator
|
41
|
+
def frontend
|
42
|
+
@frontend ||= Configurator.new(self, :frontend)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns a configurator object which you can use to configure the backend
|
46
|
+
# socket.
|
47
|
+
# @return [Configurator] (memoized) backend configurator
|
48
|
+
def backend
|
49
|
+
@backend ||= Configurator.new(self, :backend)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Captures all proxied messages and delivers them to a PULL socket bound
|
53
|
+
# to the specified endpoint.
|
54
|
+
# @note The PULL socket has to be bound before calling this method.
|
55
|
+
# @param endpoint [String] the endpoint to which the PULL socket is bound to
|
56
|
+
# @return [void]
|
57
|
+
def capture(endpoint)
|
58
|
+
@actor << ["CAPTURE", endpoint]
|
59
|
+
@actor.wait
|
60
|
+
end
|
61
|
+
|
62
|
+
# Pauses proxying of any messages.
|
63
|
+
# @note This causes any messages to be queued up and potentialy hit the
|
64
|
+
# high-water mark on the frontend or backend socket, causing messages to
|
65
|
+
# be dropped or writing applications to block.
|
66
|
+
# @return [void]
|
67
|
+
def pause
|
68
|
+
@actor << "PAUSE"
|
69
|
+
@actor.wait
|
70
|
+
end
|
71
|
+
|
72
|
+
# Resume proxying of messages.
|
73
|
+
# @note This is only needed after a call to {#pause}, not to start the
|
74
|
+
# proxy. Proxying starts as soon as the frontend and backend sockets are
|
75
|
+
# properly attached.
|
76
|
+
# @return [void]
|
77
|
+
def resume
|
78
|
+
@actor << "RESUME"
|
79
|
+
@actor.wait
|
80
|
+
end
|
81
|
+
|
82
|
+
# Used to configure the socket on one side of a {Proxy}.
|
83
|
+
class Configurator
|
84
|
+
# @return [Array<Symbol>] supported socket types
|
85
|
+
SOCKET_TYPES = %i[
|
86
|
+
PAIR PUB SUB REQ REP
|
87
|
+
DEALER ROUTER PULL PUSH
|
88
|
+
XPUB XSUB
|
89
|
+
]
|
90
|
+
|
91
|
+
# @param proxy [Proxy] the proxy instance
|
92
|
+
# @param side [Symbol] :frontend or :backend
|
93
|
+
def initialize(proxy, side)
|
94
|
+
@proxy = proxy
|
95
|
+
@side = case side
|
96
|
+
when :frontend then "FRONTEND"
|
97
|
+
when :backend then "BACKEND"
|
98
|
+
else raise ArgumentError, "invalid side: #{side.inspect}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [Proxy] the proxy this {Configurator} works on
|
103
|
+
attr_reader :proxy
|
104
|
+
|
105
|
+
# @return [String] the side, either "FRONTEND" or "BACKEND"
|
106
|
+
attr_reader :side
|
107
|
+
|
108
|
+
# Creates and binds a serverish socket.
|
109
|
+
# @param socket_type [Symbol] one of {SOCKET_TYPES}
|
110
|
+
# @param endpoint [String] endpoint to bind to
|
111
|
+
# @raise [ArgumentError] if the given socket type is invalid
|
112
|
+
# @return [void]
|
113
|
+
def bind(socket_type, endpoint)
|
114
|
+
unless SOCKET_TYPES.include?(socket_type)
|
115
|
+
raise ArgumentError, "invalid socket type: #{socket_type}"
|
116
|
+
end
|
117
|
+
@proxy.actor << [ @side, socket_type.to_s, endpoint ]
|
118
|
+
@proxy.actor.wait
|
119
|
+
end
|
120
|
+
|
121
|
+
# Set ZAP domain for authentication.
|
122
|
+
# @param domain [String] the ZAP domain
|
123
|
+
def domain=(domain)
|
124
|
+
@proxy.actor << [ "DOMAIN", @side, domain ]
|
125
|
+
@proxy.actor.wait
|
126
|
+
end
|
127
|
+
|
128
|
+
# Configure PLAIN authentication on this socket.
|
129
|
+
# @note You'll have to use a {CZTop::Authenticator}.
|
130
|
+
def PLAIN_server!
|
131
|
+
@proxy.actor << [ "PLAIN", @side ]
|
132
|
+
@proxy.actor.wait
|
133
|
+
end
|
134
|
+
|
135
|
+
# Configure CURVE authentication on this socket.
|
136
|
+
# @note You'll have to use a {CZTop::Authenticator}.
|
137
|
+
# @param cert [Certificate] this server's certificate,
|
138
|
+
# so remote clients are able to authenticate this server
|
139
|
+
def CURVE_server!(cert)
|
140
|
+
public_key = cert.public_key
|
141
|
+
secret_key = cert.secret_key or
|
142
|
+
raise ArgumentError, "no secret key in certificate"
|
143
|
+
|
144
|
+
@proxy.actor << [ "CURVE", @side, public_key, secret_key ]
|
145
|
+
@proxy.actor.wait
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module CZTop
|
2
|
+
|
3
|
+
# These are methods that can be used on a {Socket} as well as an {Actor},
|
4
|
+
# but actually just pass through to methods of {Message} (which take
|
5
|
+
# a polymorphic reference, in Ruby as well as in C).
|
6
|
+
# @see http://api.zeromq.org/czmq3-0:zmsg
|
7
|
+
module SendReceiveMethods
|
8
|
+
# Sends a message.
|
9
|
+
#
|
10
|
+
# @param message [Message, String, Array<parts>] the message to send
|
11
|
+
# @raise [IO::EAGAINWaitWritable] if send timeout has been reached (see
|
12
|
+
# {ZsockOptions::OptionsAccessor#sndtimeo=})
|
13
|
+
# @raise [Interrupt, ArgumentError, SystemCallError] anything raised by
|
14
|
+
# {Message#send_to}
|
15
|
+
# @return [self]
|
16
|
+
# @see Message.coerce
|
17
|
+
# @see Message#send_to
|
18
|
+
def <<(message)
|
19
|
+
Message.coerce(message).send_to(self)
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
# Receives a message.
|
24
|
+
#
|
25
|
+
# @return [Message]
|
26
|
+
# @raise [IO::EAGAINWaitReadable] if receive timeout has been reached (see
|
27
|
+
# {ZsockOptions::OptionsAccessor#rcvtimeo=})
|
28
|
+
# @raise [Interrupt, ArgumentError, SystemCallError] anything raised by
|
29
|
+
# {Message.receive_from}
|
30
|
+
# @see Message.receive_from
|
31
|
+
def receive
|
32
|
+
Message.receive_from(self)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
module CZTop
|
2
|
+
class Socket
|
3
|
+
# Socket types. Each constant in this namespace holds the type code used
|
4
|
+
# for the zsock_new() function.
|
5
|
+
module Types
|
6
|
+
PAIR = 0
|
7
|
+
PUB = 1
|
8
|
+
SUB = 2
|
9
|
+
REQ = 3
|
10
|
+
REP = 4
|
11
|
+
DEALER = 5
|
12
|
+
ROUTER = 6
|
13
|
+
PULL = 7
|
14
|
+
PUSH = 8
|
15
|
+
XPUB = 9
|
16
|
+
XSUB = 10
|
17
|
+
STREAM = 11
|
18
|
+
SERVER = 12
|
19
|
+
CLIENT = 13
|
20
|
+
end
|
21
|
+
|
22
|
+
# All the available type codes, mapped to their Symbol equivalent.
|
23
|
+
# @return [Hash<Integer, Symbol>]
|
24
|
+
TypeNames = Hash[
|
25
|
+
Types.constants.map { |name| i = Types.const_get(name); [ i, name ] }
|
26
|
+
].freeze
|
27
|
+
|
28
|
+
# @param type [Symbol, Integer] type from {Types} or like +:PUB+
|
29
|
+
# @return [REQ, REP, PUSH, PULL, ... ] the new socket
|
30
|
+
# @see Types
|
31
|
+
# @example Creating a socket by providing its type as a parameter
|
32
|
+
# my_sock = CZTop::Socket.new_by_type(:DEALER, "tcp://example.com:4000")
|
33
|
+
def self.new_by_type(type)
|
34
|
+
case type
|
35
|
+
when Integer
|
36
|
+
type_code = type
|
37
|
+
type_name = TypeNames[type_code] or
|
38
|
+
raise ArgumentError, "invalid type %p" % type
|
39
|
+
type_class = Socket.const_get(type_name)
|
40
|
+
when Symbol
|
41
|
+
type_code = Types.const_get(type)
|
42
|
+
type_class = Socket.const_get(type)
|
43
|
+
else
|
44
|
+
raise ArgumentError, "invalid socket type: %p" % type
|
45
|
+
end
|
46
|
+
ffi_delegate = Zsock.new(type_code)
|
47
|
+
sock = type_class.allocate
|
48
|
+
sock.attach_ffi_delegate(ffi_delegate)
|
49
|
+
sock
|
50
|
+
end
|
51
|
+
|
52
|
+
# Client socket for the ZeroMQ Client-Server Pattern.
|
53
|
+
# @see http://rfc.zeromq.org/spec:41
|
54
|
+
class CLIENT < Socket
|
55
|
+
# @param endpoints [String] endpoints to connect to
|
56
|
+
def initialize(endpoints = nil)
|
57
|
+
attach_ffi_delegate(Zsock.new_client(endpoints))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Server socket for the ZeroMQ Client-Server Pattern.
|
62
|
+
# @see http://rfc.zeromq.org/spec:41
|
63
|
+
class SERVER < Socket
|
64
|
+
# @param endpoints [String] endpoints to bind to
|
65
|
+
def initialize(endpoints = nil)
|
66
|
+
attach_ffi_delegate(Zsock.new_server(endpoints))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Request socket for the ZeroMQ Request-Reply Pattern.
|
71
|
+
# @see http://rfc.zeromq.org/spec:28
|
72
|
+
class REQ < Socket
|
73
|
+
# @param endpoints [String] endpoints to connect to
|
74
|
+
def initialize(endpoints = nil)
|
75
|
+
attach_ffi_delegate(Zsock.new_req(endpoints))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Reply socket for the ZeroMQ Request-Reply Pattern.
|
80
|
+
# @see http://rfc.zeromq.org/spec:28
|
81
|
+
class REP < Socket
|
82
|
+
# @param endpoints [String] endpoints to bind to
|
83
|
+
def initialize(endpoints = nil)
|
84
|
+
attach_ffi_delegate(Zsock.new_rep(endpoints))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Dealer socket for the ZeroMQ Request-Reply Pattern.
|
89
|
+
# @see http://rfc.zeromq.org/spec:28
|
90
|
+
class DEALER < Socket
|
91
|
+
# @param endpoints [String] endpoints to connect to
|
92
|
+
def initialize(endpoints = nil)
|
93
|
+
attach_ffi_delegate(Zsock.new_dealer(endpoints))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Router socket for the ZeroMQ Request-Reply Pattern.
|
98
|
+
# @see http://rfc.zeromq.org/spec:28
|
99
|
+
class ROUTER < Socket
|
100
|
+
# @param endpoints [String] endpoints to bind to
|
101
|
+
def initialize(endpoints = nil)
|
102
|
+
attach_ffi_delegate(Zsock.new_router(endpoints))
|
103
|
+
end
|
104
|
+
|
105
|
+
# Send a message to a specific receiver. This is a shorthand for when
|
106
|
+
# you send a message to a specific receiver with no hops in between.
|
107
|
+
# @param receiver [String] receiving peer's socket identity
|
108
|
+
# @param message [Message] the message to send
|
109
|
+
# @note Do NOT use the message afterwards. It'll have been modified and
|
110
|
+
# destroyed.
|
111
|
+
def send_to(receiver, message)
|
112
|
+
message = Message.coerce(message)
|
113
|
+
message.prepend "" # separator frame
|
114
|
+
message.prepend receiver # receiver envelope
|
115
|
+
self << message
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Publish socket for the ZeroMQ Publish-Subscribe Pattern.
|
120
|
+
# @see http://rfc.zeromq.org/spec:29
|
121
|
+
class PUB < Socket
|
122
|
+
# @param endpoints [String] endpoints to bind to
|
123
|
+
def initialize(endpoints = nil)
|
124
|
+
attach_ffi_delegate(Zsock.new_pub(endpoints))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Subscribe socket for the ZeroMQ Publish-Subscribe Pattern.
|
129
|
+
# @see http://rfc.zeromq.org/spec:29
|
130
|
+
class SUB < Socket
|
131
|
+
# @param endpoints [String] endpoints to connect to
|
132
|
+
# @param subscription [String] what to subscribe to
|
133
|
+
def initialize(endpoints = nil, subscription = nil)
|
134
|
+
attach_ffi_delegate(Zsock.new_sub(endpoints, subscription))
|
135
|
+
end
|
136
|
+
|
137
|
+
# Subscribes to the given prefix string.
|
138
|
+
# @param prefix [String] prefix string to subscribe to
|
139
|
+
# @return [void]
|
140
|
+
def subscribe(prefix)
|
141
|
+
ffi_delegate.set_subscribe(prefix)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Unsubscribes from the given prefix.
|
145
|
+
# @param prefix [String] prefix string to unsubscribe from
|
146
|
+
# @return [void]
|
147
|
+
def unsubscribe(prefix)
|
148
|
+
ffi_delegate.set_unsubscribe(prefix)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Extended publish socket for the ZeroMQ Publish-Subscribe Pattern.
|
153
|
+
# @see http://rfc.zeromq.org/spec:29
|
154
|
+
class XPUB < Socket
|
155
|
+
# @param endpoints [String] endpoints to bind to
|
156
|
+
def initialize(endpoints = nil)
|
157
|
+
attach_ffi_delegate(Zsock.new_xpub(endpoints))
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Extended subscribe socket for the ZeroMQ Publish-Subscribe Pattern.
|
162
|
+
# @see http://rfc.zeromq.org/spec:29
|
163
|
+
class XSUB < Socket
|
164
|
+
# @param endpoints [String] endpoints to connect to
|
165
|
+
def initialize(endpoints = nil)
|
166
|
+
attach_ffi_delegate(Zsock.new_xsub(endpoints))
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Push socket for the ZeroMQ Pipeline Pattern.
|
171
|
+
# @see http://rfc.zeromq.org/spec:30
|
172
|
+
class PUSH < Socket
|
173
|
+
# @param endpoints [String] endpoints to connect to
|
174
|
+
def initialize(endpoints = nil)
|
175
|
+
attach_ffi_delegate(Zsock.new_push(endpoints))
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Pull socket for the ZeroMQ Pipeline Pattern.
|
180
|
+
# @see http://rfc.zeromq.org/spec:30
|
181
|
+
class PULL < Socket
|
182
|
+
# @param endpoints [String] endpoints to bind to
|
183
|
+
def initialize(endpoints = nil)
|
184
|
+
attach_ffi_delegate(Zsock.new_pull(endpoints))
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Pair socket for inter-thread communication.
|
189
|
+
# @see http://rfc.zeromq.org/spec:31
|
190
|
+
class PAIR < Socket
|
191
|
+
# @param endpoints [String] endpoints to connect to
|
192
|
+
def initialize(endpoints = nil)
|
193
|
+
attach_ffi_delegate(Zsock.new_pair(endpoints))
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Stream socket for the native pattern over. This is useful when
|
198
|
+
# communicating with a non-ZMQ peer, done over TCP.
|
199
|
+
# @see http://api.zeromq.org/4-2:zmq-socket#toc16
|
200
|
+
class STREAM < Socket
|
201
|
+
# @param endpoints [String] endpoints to connect to
|
202
|
+
def initialize(endpoints = nil)
|
203
|
+
attach_ffi_delegate(Zsock.new_stream(endpoints))
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
data/lib/cztop/socket.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
module CZTop
|
2
|
+
# Represents a CZMQ::FFI::Zsock.
|
3
|
+
class Socket
|
4
|
+
include HasFFIDelegate
|
5
|
+
extend CZTop::HasFFIDelegate::ClassMethods
|
6
|
+
include ZsockOptions
|
7
|
+
include SendReceiveMethods
|
8
|
+
include PolymorphicZsockMethods
|
9
|
+
include CZMQ::FFI
|
10
|
+
|
11
|
+
# @!group CURVE Security
|
12
|
+
|
13
|
+
# Enables CURVE security and makes this socket a CURVE server.
|
14
|
+
# @param cert [Certificate] this server's certificate,
|
15
|
+
# so remote clients are able to authenticate this server
|
16
|
+
# @note You'll have to use a {CZTop::Authenticator}.
|
17
|
+
# @return [void]
|
18
|
+
def CURVE_server!(cert)
|
19
|
+
options.CURVE_server = true
|
20
|
+
cert.apply(self) # NOTE: desired: raises if no secret key in cert
|
21
|
+
end
|
22
|
+
|
23
|
+
# Enables CURVE security and makes this socket a CURVE client.
|
24
|
+
# @param client_cert [Certificate] client's certificate, to secure
|
25
|
+
# communication (and be authenticated by the server)
|
26
|
+
# @param server_cert [Certificate] the remote server's certificate, so
|
27
|
+
# this socket is able to authenticate the server
|
28
|
+
# @return [void]
|
29
|
+
# @raise [SecurityError] if the server's secret key is set in server_cert,
|
30
|
+
# which means it's not secret anymore
|
31
|
+
# @raise [SystemCallError] if there's no secret key in client_cert
|
32
|
+
def CURVE_client!(client_cert, server_cert)
|
33
|
+
if server_cert.secret_key
|
34
|
+
raise SecurityError, "server's secret key not secret"
|
35
|
+
end
|
36
|
+
|
37
|
+
client_cert.apply(self) # NOTE: desired: raises if no secret key in cert
|
38
|
+
options.CURVE_serverkey = server_cert.public_key
|
39
|
+
end
|
40
|
+
|
41
|
+
# @!endgroup
|
42
|
+
|
43
|
+
# @return [String] last bound endpoint, if any
|
44
|
+
def last_endpoint
|
45
|
+
ffi_delegate.endpoint
|
46
|
+
end
|
47
|
+
|
48
|
+
# Connects to an endpoint.
|
49
|
+
# @param endpoint [String]
|
50
|
+
# @return [void]
|
51
|
+
# @raise [ArgumentError] if the endpoint is incorrect
|
52
|
+
def connect(endpoint)
|
53
|
+
rc = ffi_delegate.connect("%s", :string, endpoint)
|
54
|
+
raise ArgumentError, "incorrect endpoint: %p" % endpoint if rc == -1
|
55
|
+
end
|
56
|
+
|
57
|
+
# Disconnects from an endpoint.
|
58
|
+
# @param endpoint [String]
|
59
|
+
# @raise [ArgumentError] if the endpoint is incorrect
|
60
|
+
def disconnect(endpoint)
|
61
|
+
rc = ffi_delegate.disconnect("%s", :string, endpoint)
|
62
|
+
raise ArgumentError, "incorrect endpoint: %p" % endpoint if rc == -1
|
63
|
+
end
|
64
|
+
|
65
|
+
# Closes and destroys the native socket.
|
66
|
+
# @note Don't try to use it anymore afterwards.
|
67
|
+
def close
|
68
|
+
ffi_delegate.destroy
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Integer] last automatically selected, bound TCP port, if any
|
72
|
+
# @return [nil] if not bound to a TCP port yet
|
73
|
+
attr_reader :last_tcp_port
|
74
|
+
|
75
|
+
# Binds to an endpoint.
|
76
|
+
# @note When binding to an automatically selected TCP port, this will set
|
77
|
+
# {#last_tcp_port}.
|
78
|
+
# @param endpoint [String]
|
79
|
+
# @return [void]
|
80
|
+
# @raise [SystemCallError] in case of failure
|
81
|
+
def bind(endpoint)
|
82
|
+
rc = ffi_delegate.bind("%s", :string, endpoint)
|
83
|
+
raise_zmq_err("unable to bind to %p" % endpoint) if rc == -1
|
84
|
+
@last_tcp_port = rc if rc > 0
|
85
|
+
end
|
86
|
+
|
87
|
+
# Unbinds from an endpoint.
|
88
|
+
# @param endpoint [String]
|
89
|
+
# @return [void]
|
90
|
+
# @raise [ArgumentError] if the endpoint is incorrect
|
91
|
+
def unbind(endpoint)
|
92
|
+
rc = ffi_delegate.unbind("%s", :string, endpoint)
|
93
|
+
raise ArgumentError, "incorrect endpoint: %p" % endpoint if rc == -1
|
94
|
+
end
|
95
|
+
|
96
|
+
# Inspects this {Socket}.
|
97
|
+
# @return [String] shows class, native address, and {#last_endpoint}
|
98
|
+
def inspect
|
99
|
+
"#<%s:0x%x last_endpoint=%p>" % [
|
100
|
+
self.class,
|
101
|
+
to_ptr.address,
|
102
|
+
last_endpoint
|
103
|
+
]
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/cztop/z85.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
module CZTop
|
2
|
+
# Represents a CZMQ::FFI::Zarmour in Z85 mode.
|
3
|
+
#
|
4
|
+
# Use this class to encode to and from the Z85 encoding algorithm.
|
5
|
+
# @see http://rfc.zeromq.org/spec:32
|
6
|
+
class Z85
|
7
|
+
include HasFFIDelegate
|
8
|
+
extend CZTop::HasFFIDelegate::ClassMethods
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
attach_ffi_delegate(CZMQ::FFI::Zarmour.new)
|
12
|
+
ffi_delegate.set_mode(:mode_z85)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Encodes to Z85.
|
16
|
+
# @param input [String] possibly binary input data
|
17
|
+
# @return [String] Z85 encoded data as ASCII string
|
18
|
+
# @raise [ArgumentError] if input length isn't divisible by 4 with no
|
19
|
+
# remainder
|
20
|
+
# @raise [SystemCallError] if this fails
|
21
|
+
def encode(input)
|
22
|
+
raise ArgumentError, "wrong input length" if input.bytesize % 4 > 0
|
23
|
+
input = input.dup.force_encoding(Encoding::BINARY)
|
24
|
+
ptr = ffi_delegate.encode(input, input.bytesize)
|
25
|
+
raise_zmq_err if ptr.null?
|
26
|
+
z85 = ptr.read_string
|
27
|
+
z85.encode!(Encoding::ASCII)
|
28
|
+
return z85
|
29
|
+
end
|
30
|
+
|
31
|
+
# Decodes from Z85.
|
32
|
+
# @param input [String] Z85 encoded data
|
33
|
+
# @return [String] original data as binary string
|
34
|
+
# @raise [ArgumentError] if input length isn't divisible by 5 with no
|
35
|
+
# remainder
|
36
|
+
# @raise [SystemCallError] if this fails
|
37
|
+
def decode(input)
|
38
|
+
raise ArgumentError, "wrong input length" if input.bytesize % 5 > 0
|
39
|
+
FFI::MemoryPointer.new(:size_t) do |size_ptr|
|
40
|
+
buffer_ptr = ffi_delegate.decode(input, size_ptr)
|
41
|
+
raise_zmq_err if buffer_ptr.null?
|
42
|
+
decoded_string = buffer_ptr.read_string(_size(size_ptr) - 1)
|
43
|
+
return decoded_string
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Gets correct size, depending on the platform.
|
50
|
+
# @return [Integer]
|
51
|
+
# @see https://github.com/ffi/ffi/issues/398
|
52
|
+
# @see https://github.com/ffi/ffi/issues/333
|
53
|
+
def _size(size_ptr)
|
54
|
+
if RUBY_ENGINE == "jruby"
|
55
|
+
# NOTE: JRuby FFI doesn't have #read_uint64, nor does it have
|
56
|
+
# Pointer::SIZE
|
57
|
+
return size_ptr.read_ulong_long
|
58
|
+
end
|
59
|
+
|
60
|
+
if ::FFI::Pointer::SIZE == 8 # 64 bit
|
61
|
+
size_ptr.read_uint64
|
62
|
+
else
|
63
|
+
size_ptr.read_uint32
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Z85 with simple padding. This allows you to {#encode} input of any
|
68
|
+
# length.
|
69
|
+
#
|
70
|
+
# = Encoding Procedure
|
71
|
+
#
|
72
|
+
# If the data to be encoded is empty (0 bytes), it is encoded to the empty
|
73
|
+
# string, just like in Z85.
|
74
|
+
#
|
75
|
+
# Otherwise, a length information is prepended and, if needed, padding (1,
|
76
|
+
# 2, or 3 NULL bytes) is appended to bring the resulting blob to
|
77
|
+
# a multiple of 4 bytes.
|
78
|
+
#
|
79
|
+
# The length information is encoded similarly to lengths of messages
|
80
|
+
# (frames) in ZMTP. Up to 127 bytes, the data's length is encoded with
|
81
|
+
# a single byte (specifically, with the 7 least significant bits in it).
|
82
|
+
#
|
83
|
+
# +--------+-------------------------------+------------+
|
84
|
+
# | length | data | padding |
|
85
|
+
# | 1 byte | up to 127 bytes | 0-3 bytes |
|
86
|
+
# +--------+-------------------------------+------------+
|
87
|
+
#
|
88
|
+
# If the data is 128 bytes or more, the most significant bit will be set
|
89
|
+
# to indicate that fact, and a 64 bit unsigned integer in network byte
|
90
|
+
# order is appended after this first byte to encode the length of the
|
91
|
+
# data. This means that up to 16EiB (exbibytes) can be encoded, which
|
92
|
+
# will be enough for the foreseeable future.
|
93
|
+
#
|
94
|
+
# +--------+-----------+----------------------------------+------------+
|
95
|
+
# | big? | length | data | padding |
|
96
|
+
# | 1 byte | 8 bytes | 128 bytes or much more | 0-3 bytes |
|
97
|
+
# +--------+-----------+----------------------------------+------------+
|
98
|
+
#
|
99
|
+
# The resulting blob is encoded using {CZTop::Z85#encode}.
|
100
|
+
# {CZTop::Z85#decode} does the inverse.
|
101
|
+
#
|
102
|
+
# @note Warning: This won't be compatible with other implementations of
|
103
|
+
# Z85. Only use this if you really need padding, like when you can't
|
104
|
+
# guarantee the input for {#encode} is always a multiple of 4 bytes.
|
105
|
+
#
|
106
|
+
class Padded < Z85
|
107
|
+
# Encododes to Z85, with padding if needed.
|
108
|
+
#
|
109
|
+
# If input isn't empty, 8 additional bytes for the encoded length will
|
110
|
+
# be prepended. If needed, 1 to 3 bytes of padding will be appended.
|
111
|
+
#
|
112
|
+
# If input is empty, returns the empty string.
|
113
|
+
#
|
114
|
+
# @param input [String] possibly binary input data
|
115
|
+
# @return [String] Z85 encoded data as ASCII string, including encoded
|
116
|
+
# length and padding
|
117
|
+
# @raise [SystemCallError] if this fails
|
118
|
+
def encode(input)
|
119
|
+
return super if input.empty?
|
120
|
+
length = input.bytesize
|
121
|
+
if length < 1<<7 # up to 127 bytes
|
122
|
+
encoded_length = [length].pack("C")
|
123
|
+
|
124
|
+
else # larger input
|
125
|
+
low = length & 0xFFFFFFFF
|
126
|
+
high = (length >> 32) & 0xFFFFFFFF
|
127
|
+
encoded_length = [ 1<<7, high, low ].pack("CNN")
|
128
|
+
end
|
129
|
+
padding = "\0" * ((4 - ((length+1) % 4)) % 4)
|
130
|
+
super("#{encoded_length}#{input}#{padding}")
|
131
|
+
end
|
132
|
+
|
133
|
+
# Decodes from Z85 with padding.
|
134
|
+
#
|
135
|
+
# @param input [String] Z85 encoded data (including encoded length and
|
136
|
+
# padding, or empty string)
|
137
|
+
# @return [String] original data as binary string
|
138
|
+
# @raise [ArgumentError] if input is invalid or truncated
|
139
|
+
# @raise [SystemCallError] if this fails
|
140
|
+
def decode(input)
|
141
|
+
return super if input.empty?
|
142
|
+
decoded = super
|
143
|
+
length = decoded.byteslice(0, 1).unpack("C")[0]
|
144
|
+
if (1<<7 & length).zero? # up to 127 bytes
|
145
|
+
decoded = decoded.byteslice(1, length) # extract payload
|
146
|
+
|
147
|
+
else # larger input
|
148
|
+
length = decoded.byteslice(1, 8).unpack("NN")
|
149
|
+
.inject(0) { |sum, i| (sum << 32) + i }
|
150
|
+
decoded = decoded.byteslice(9, length) # extract payload
|
151
|
+
end
|
152
|
+
raise ArgumentError, "input truncated" if decoded.bytesize < length
|
153
|
+
return decoded
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|