omq 0.6.5 → 0.8.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 +4 -4
- data/CHANGELOG.md +78 -1
- data/README.md +76 -81
- data/lib/omq/cli/base_runner.rb +54 -21
- data/lib/omq/cli/client_server.rb +14 -9
- data/lib/omq/cli/config.rb +7 -4
- data/lib/omq/cli/pair.rb +1 -1
- data/lib/omq/cli/pipe.rb +37 -21
- data/lib/omq/cli/req_rep.rb +8 -4
- data/lib/omq/cli/router_dealer.rb +12 -6
- data/lib/omq/cli.rb +100 -34
- data/lib/omq/version.rb +1 -1
- data/lib/omq/zmtp/engine.rb +69 -22
- data/lib/omq/zmtp/routing/peer.rb +3 -2
- data/lib/omq/zmtp/routing/rep.rb +2 -3
- data/lib/omq/zmtp/routing/req.rb +3 -7
- data/lib/omq/zmtp/routing/router.rb +3 -2
- data/lib/omq/zmtp/routing/server.rb +3 -2
- data/lib/omq/zmtp.rb +16 -13
- data/lib/omq.rb +1 -0
- metadata +15 -8
- data/lib/omq/zmtp/codec/command.rb +0 -210
- data/lib/omq/zmtp/codec/frame.rb +0 -89
- data/lib/omq/zmtp/codec/greeting.rb +0 -78
- data/lib/omq/zmtp/codec.rb +0 -18
- data/lib/omq/zmtp/connection.rb +0 -282
- data/lib/omq/zmtp/mechanism/null.rb +0 -72
- data/lib/omq/zmtp/valid_peers.rb +0 -29
data/lib/omq/zmtp/connection.rb
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OMQ
|
|
4
|
-
module ZMTP
|
|
5
|
-
# Manages one ZMTP peer connection over any transport IO.
|
|
6
|
-
#
|
|
7
|
-
# Delegates the security handshake to a Mechanism object (Null, Curve, etc.),
|
|
8
|
-
# then provides message send/receive and command send/receive on top of the
|
|
9
|
-
# framing codec.
|
|
10
|
-
#
|
|
11
|
-
class Connection
|
|
12
|
-
# @return [String] peer's socket type (from READY handshake)
|
|
13
|
-
#
|
|
14
|
-
attr_reader :peer_socket_type
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# @return [String] peer's identity (from READY handshake)
|
|
18
|
-
#
|
|
19
|
-
attr_reader :peer_identity
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# @return [Object] transport IO (#read, #write, #close)
|
|
23
|
-
#
|
|
24
|
-
attr_reader :io
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# @param io [#read, #write, #close] transport IO
|
|
28
|
-
# @param socket_type [String] our socket type name (e.g. "REQ")
|
|
29
|
-
# @param identity [String] our identity
|
|
30
|
-
# @param as_server [Boolean] whether we are the server side
|
|
31
|
-
# @param mechanism [Mechanism::Null, Mechanism::Curve] security mechanism
|
|
32
|
-
# @param heartbeat_interval [Numeric, nil] heartbeat interval in seconds
|
|
33
|
-
# @param heartbeat_ttl [Numeric, nil] TTL to send in PING
|
|
34
|
-
# @param heartbeat_timeout [Numeric, nil] timeout for PONG
|
|
35
|
-
# @param max_message_size [Integer, nil] max frame size in bytes, nil = unlimited
|
|
36
|
-
#
|
|
37
|
-
def initialize(io, socket_type:, identity: "", as_server: false,
|
|
38
|
-
mechanism: nil,
|
|
39
|
-
heartbeat_interval: nil, heartbeat_ttl: nil, heartbeat_timeout: nil,
|
|
40
|
-
max_message_size: nil)
|
|
41
|
-
@io = io
|
|
42
|
-
@socket_type = socket_type
|
|
43
|
-
@identity = identity
|
|
44
|
-
@as_server = as_server
|
|
45
|
-
@mechanism = mechanism || Mechanism::Null.new
|
|
46
|
-
@peer_socket_type = nil
|
|
47
|
-
@peer_identity = nil
|
|
48
|
-
@mutex = Mutex.new
|
|
49
|
-
@heartbeat_interval = heartbeat_interval
|
|
50
|
-
@heartbeat_ttl = heartbeat_ttl || heartbeat_interval
|
|
51
|
-
@heartbeat_timeout = heartbeat_timeout || heartbeat_interval
|
|
52
|
-
@last_received_at = nil
|
|
53
|
-
@heartbeat_task = nil
|
|
54
|
-
@max_message_size = max_message_size
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# Performs the full ZMTP handshake via the configured mechanism.
|
|
59
|
-
#
|
|
60
|
-
# @return [void]
|
|
61
|
-
# @raise [ProtocolError] on handshake failure
|
|
62
|
-
#
|
|
63
|
-
def handshake!
|
|
64
|
-
result = @mechanism.handshake!(
|
|
65
|
-
@io,
|
|
66
|
-
as_server: @as_server,
|
|
67
|
-
socket_type: @socket_type,
|
|
68
|
-
identity: @identity,
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
@peer_socket_type = result[:peer_socket_type]
|
|
72
|
-
@peer_identity = result[:peer_identity]
|
|
73
|
-
|
|
74
|
-
unless @peer_socket_type
|
|
75
|
-
raise ProtocolError, "peer READY missing Socket-Type"
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
unless ZMTP::VALID_PEERS[@socket_type.to_sym]&.include?(@peer_socket_type.to_sym)
|
|
79
|
-
raise ProtocolError,
|
|
80
|
-
"incompatible socket types: #{@socket_type} cannot connect to #{@peer_socket_type}"
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# Sends a multi-frame message (write + flush).
|
|
86
|
-
#
|
|
87
|
-
# @param parts [Array<String>] message frames
|
|
88
|
-
# @return [void]
|
|
89
|
-
#
|
|
90
|
-
def send_message(parts)
|
|
91
|
-
@mutex.synchronize do
|
|
92
|
-
write_frames(parts)
|
|
93
|
-
@io.flush
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# Writes a multi-frame message to the buffer without flushing.
|
|
99
|
-
# Call {#flush} after batching writes.
|
|
100
|
-
#
|
|
101
|
-
# @param parts [Array<String>] message frames
|
|
102
|
-
# @return [void]
|
|
103
|
-
#
|
|
104
|
-
def write_message(parts)
|
|
105
|
-
@mutex.synchronize do
|
|
106
|
-
write_frames(parts)
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
# Flushes the write buffer to the underlying IO.
|
|
112
|
-
#
|
|
113
|
-
# @return [void]
|
|
114
|
-
#
|
|
115
|
-
def flush
|
|
116
|
-
@mutex.synchronize do
|
|
117
|
-
@io.flush
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# Receives a multi-frame message.
|
|
123
|
-
# PING/PONG commands are handled automatically by #read_frame.
|
|
124
|
-
#
|
|
125
|
-
# @return [Array<String>] message frames
|
|
126
|
-
# @raise [EOFError] if connection is closed
|
|
127
|
-
#
|
|
128
|
-
def receive_message
|
|
129
|
-
frames = []
|
|
130
|
-
loop do
|
|
131
|
-
frame = read_frame
|
|
132
|
-
if frame.command?
|
|
133
|
-
yield frame if block_given?
|
|
134
|
-
next
|
|
135
|
-
end
|
|
136
|
-
frames << frame.body.freeze
|
|
137
|
-
break unless frame.more?
|
|
138
|
-
end
|
|
139
|
-
frames.freeze
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
# Starts the heartbeat sender task. Call after handshake.
|
|
144
|
-
#
|
|
145
|
-
# @return [#stop, nil] the heartbeat task, or nil if disabled
|
|
146
|
-
#
|
|
147
|
-
def start_heartbeat
|
|
148
|
-
return nil unless @heartbeat_interval
|
|
149
|
-
@last_received_at = monotonic_now
|
|
150
|
-
@heartbeat_task = Reactor.spawn_pump(annotation: "heartbeat") do
|
|
151
|
-
loop do
|
|
152
|
-
sleep @heartbeat_interval
|
|
153
|
-
# Send PING with TTL
|
|
154
|
-
send_command(Codec::Command.ping(
|
|
155
|
-
ttl: @heartbeat_ttl || 0,
|
|
156
|
-
context: "".b,
|
|
157
|
-
))
|
|
158
|
-
# Check if peer has gone silent
|
|
159
|
-
if @heartbeat_timeout && @last_received_at
|
|
160
|
-
elapsed = monotonic_now - @last_received_at
|
|
161
|
-
if elapsed > @heartbeat_timeout
|
|
162
|
-
close
|
|
163
|
-
break
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
rescue *ZMTP::CONNECTION_LOST
|
|
168
|
-
# connection closed
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
# Sends a command.
|
|
174
|
-
#
|
|
175
|
-
# @param command [Codec::Command]
|
|
176
|
-
# @return [void]
|
|
177
|
-
#
|
|
178
|
-
def send_command(command)
|
|
179
|
-
@mutex.synchronize do
|
|
180
|
-
if @mechanism.encrypted?
|
|
181
|
-
@io.write(@mechanism.encrypt(command.to_body, command: true))
|
|
182
|
-
else
|
|
183
|
-
@io.write(command.to_frame.to_wire)
|
|
184
|
-
end
|
|
185
|
-
@io.flush
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
# Reads one frame from the wire. Handles PING/PONG automatically.
|
|
191
|
-
# When using an encrypted mechanism, MESSAGE commands are decrypted
|
|
192
|
-
# back to ZMTP frames transparently.
|
|
193
|
-
#
|
|
194
|
-
# @return [Codec::Frame]
|
|
195
|
-
# @raise [EOFError] if connection is closed
|
|
196
|
-
#
|
|
197
|
-
def read_frame
|
|
198
|
-
loop do
|
|
199
|
-
frame = Codec::Frame.read_from(@io)
|
|
200
|
-
touch_heartbeat
|
|
201
|
-
|
|
202
|
-
# When CURVE is active, every wire frame is a MESSAGE envelope
|
|
203
|
-
# (data frame with "\x07MESSAGE" prefix). Decrypt to recover the
|
|
204
|
-
# inner ZMTP frame. This check only runs when encrypted? is true,
|
|
205
|
-
# so user data frames are never misdetected.
|
|
206
|
-
if @mechanism.encrypted? && frame.body.bytesize > 8 && frame.body.byteslice(0, 8) == "\x07MESSAGE".b
|
|
207
|
-
frame = @mechanism.decrypt(frame)
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
if @max_message_size && !frame.command? && frame.body.bytesize > @max_message_size
|
|
211
|
-
close
|
|
212
|
-
raise ProtocolError, "frame size #{frame.body.bytesize} exceeds max_message_size #{@max_message_size}"
|
|
213
|
-
end
|
|
214
|
-
if frame.command?
|
|
215
|
-
cmd = Codec::Command.from_body(frame.body)
|
|
216
|
-
case cmd.name
|
|
217
|
-
when "PING"
|
|
218
|
-
_, context = cmd.ping_ttl_and_context
|
|
219
|
-
send_command(Codec::Command.pong(context: context))
|
|
220
|
-
next
|
|
221
|
-
when "PONG"
|
|
222
|
-
next
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
return frame
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
# Closes the connection.
|
|
231
|
-
#
|
|
232
|
-
# @return [void]
|
|
233
|
-
#
|
|
234
|
-
def close
|
|
235
|
-
@heartbeat_task&.stop rescue nil
|
|
236
|
-
@io.close
|
|
237
|
-
rescue IOError
|
|
238
|
-
# already closed
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
private
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
# Writes message parts as ZMTP frames, encrypting if needed.
|
|
246
|
-
#
|
|
247
|
-
# @param parts [Array<String>] message frames
|
|
248
|
-
#
|
|
249
|
-
def write_frames(parts)
|
|
250
|
-
parts.each_with_index do |part, i|
|
|
251
|
-
more = i < parts.size - 1
|
|
252
|
-
if @mechanism.encrypted?
|
|
253
|
-
@io.write(@mechanism.encrypt(part.b, more: more))
|
|
254
|
-
else
|
|
255
|
-
@io.write(Codec::Frame.new(part, more: more).to_wire)
|
|
256
|
-
end
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
def touch_heartbeat
|
|
262
|
-
@last_received_at = monotonic_now if @heartbeat_interval
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def monotonic_now
|
|
267
|
-
Async::Clock.now
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
# Sends one frame to the wire.
|
|
272
|
-
#
|
|
273
|
-
# @param frame [Codec::Frame]
|
|
274
|
-
# @return [void]
|
|
275
|
-
#
|
|
276
|
-
def send_frame(frame)
|
|
277
|
-
@mutex.synchronize { @io.write(frame.to_wire) }
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
end
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OMQ
|
|
4
|
-
module ZMTP
|
|
5
|
-
module Mechanism
|
|
6
|
-
# NULL security mechanism — no encryption, no authentication.
|
|
7
|
-
#
|
|
8
|
-
# Performs the ZMTP 3.1 greeting exchange and READY command handshake.
|
|
9
|
-
#
|
|
10
|
-
class Null
|
|
11
|
-
MECHANISM_NAME = "NULL"
|
|
12
|
-
|
|
13
|
-
# Performs the full NULL handshake over +io+.
|
|
14
|
-
#
|
|
15
|
-
# 1. Exchange 64-byte greetings
|
|
16
|
-
# 2. Validate peer greeting (version, mechanism)
|
|
17
|
-
# 3. Exchange READY commands (socket type + identity)
|
|
18
|
-
#
|
|
19
|
-
# @param io [#read, #write] transport IO
|
|
20
|
-
# @param as_server [Boolean]
|
|
21
|
-
# @param socket_type [String]
|
|
22
|
-
# @param identity [String]
|
|
23
|
-
# @return [Hash] { peer_socket_type:, peer_identity: }
|
|
24
|
-
# @raise [ProtocolError]
|
|
25
|
-
#
|
|
26
|
-
def handshake!(io, as_server:, socket_type:, identity:)
|
|
27
|
-
# Send our greeting
|
|
28
|
-
io.write(Codec::Greeting.encode(mechanism: MECHANISM_NAME, as_server: as_server))
|
|
29
|
-
io.flush
|
|
30
|
-
|
|
31
|
-
# Read peer greeting
|
|
32
|
-
greeting_data = io.read_exactly(Codec::Greeting::SIZE)
|
|
33
|
-
peer_greeting = Codec::Greeting.decode(greeting_data)
|
|
34
|
-
|
|
35
|
-
unless peer_greeting[:mechanism] == MECHANISM_NAME
|
|
36
|
-
raise ProtocolError, "unsupported mechanism: #{peer_greeting[:mechanism]}"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Send our READY command
|
|
40
|
-
ready_cmd = Codec::Command.ready(socket_type: socket_type, identity: identity)
|
|
41
|
-
io.write(ready_cmd.to_frame.to_wire)
|
|
42
|
-
io.flush
|
|
43
|
-
|
|
44
|
-
# Read peer READY command
|
|
45
|
-
frame = Codec::Frame.read_from(io)
|
|
46
|
-
unless frame.command?
|
|
47
|
-
raise ProtocolError, "expected command frame, got data frame"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
peer_cmd = Codec::Command.from_body(frame.body)
|
|
51
|
-
unless peer_cmd.name == "READY"
|
|
52
|
-
raise ProtocolError, "expected READY command, got #{peer_cmd.name}"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
props = peer_cmd.properties
|
|
56
|
-
peer_socket_type = props["Socket-Type"]
|
|
57
|
-
peer_identity = props["Identity"] || ""
|
|
58
|
-
|
|
59
|
-
unless peer_socket_type
|
|
60
|
-
raise ProtocolError, "peer READY missing Socket-Type"
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
{ peer_socket_type: peer_socket_type, peer_identity: peer_identity }
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# @return [Boolean] false — NULL does not encrypt frames
|
|
67
|
-
#
|
|
68
|
-
def encrypted? = false
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
data/lib/omq/zmtp/valid_peers.rb
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OMQ
|
|
4
|
-
module ZMTP
|
|
5
|
-
# Valid socket type peer combinations per ZMTP spec.
|
|
6
|
-
#
|
|
7
|
-
VALID_PEERS = {
|
|
8
|
-
PAIR: %i[PAIR].freeze,
|
|
9
|
-
REQ: %i[REP ROUTER].freeze,
|
|
10
|
-
REP: %i[REQ DEALER].freeze,
|
|
11
|
-
DEALER: %i[REP DEALER ROUTER].freeze,
|
|
12
|
-
ROUTER: %i[REQ DEALER ROUTER].freeze,
|
|
13
|
-
PUB: %i[SUB XSUB].freeze,
|
|
14
|
-
SUB: %i[PUB XPUB].freeze,
|
|
15
|
-
XPUB: %i[SUB XSUB].freeze,
|
|
16
|
-
XSUB: %i[PUB XPUB].freeze,
|
|
17
|
-
PUSH: %i[PULL].freeze,
|
|
18
|
-
PULL: %i[PUSH].freeze,
|
|
19
|
-
CLIENT: %i[SERVER].freeze,
|
|
20
|
-
SERVER: %i[CLIENT].freeze,
|
|
21
|
-
RADIO: %i[DISH].freeze,
|
|
22
|
-
DISH: %i[RADIO].freeze,
|
|
23
|
-
SCATTER: %i[GATHER].freeze,
|
|
24
|
-
GATHER: %i[SCATTER].freeze,
|
|
25
|
-
PEER: %i[PEER].freeze,
|
|
26
|
-
CHANNEL: %i[CHANNEL].freeze,
|
|
27
|
-
}.freeze
|
|
28
|
-
end
|
|
29
|
-
end
|