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