protocol-zmtp 0.10.4 → 0.10.5
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 +14 -0
- data/lib/protocol/zmtp/codec/command.rb +39 -1
- data/lib/protocol/zmtp/codec/frame.rb +8 -0
- data/lib/protocol/zmtp/connection.rb +10 -7
- data/lib/protocol/zmtp/error.rb +5 -0
- data/lib/protocol/zmtp/mechanism/curve.rb +13 -9
- data/lib/protocol/zmtp/mechanism/null.rb +3 -3
- data/lib/protocol/zmtp/mechanism/plain.rb +5 -4
- data/lib/protocol/zmtp/peer_info.rb +1 -1
- data/lib/protocol/zmtp/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8d53545d14bf02a264d165e08adb9ab22b1f948a614856644dea0a093dc1da5
|
|
4
|
+
data.tar.gz: cf71224105abc69d8cd6df5d2ab5e45cdcbed0c88ff257a6325fdd3deeb8312c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 677c05b2057178c6c388805c53c9dd1b314c06a4c436cf448b1d0fa2f2cdc6515c7818e0f3995a398b336487bebf74f6c00f8380837f635fd9377e1c11d8b3b1
|
|
7
|
+
data.tar.gz: 912e32a6301493475a676952ad1ac1c9ddff7e1f68e0b5415b20fd344dfe32d3e887d51faa00a0dce1f8fe4fb219cd50f899517e21a5c0b47dfd33b1140b3cb9
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## [0.10.5] - 2026-10-15
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Allow updating `Connection#max_message_size` after the handshake so
|
|
10
|
+
compressed transports can bound wire envelopes separately from messages.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Reject invalid frame flags, lengths above 2^63-1, and malformed PING commands.
|
|
15
|
+
- Match handshake metadata names case-insensitively for NULL, PLAIN, and CURVE.
|
|
16
|
+
- Raise `FatalHandshakeError` on handshake ERROR responses and send ERROR when
|
|
17
|
+
PLAIN authentication rejects credentials.
|
|
18
|
+
|
|
5
19
|
## 0.10.4 - 2026-07-23
|
|
6
20
|
|
|
7
21
|
### Changed
|
|
@@ -72,7 +72,7 @@ module Protocol
|
|
|
72
72
|
|
|
73
73
|
# Builds a READY command with Socket-Type, Identity, and any
|
|
74
74
|
# extra properties supplied by upper layers (e.g. an extension
|
|
75
|
-
# that injects +X-
|
|
75
|
+
# that injects +X-Compression+).
|
|
76
76
|
#
|
|
77
77
|
# @param metadata [Hash{String => String}, nil] additional READY properties
|
|
78
78
|
# @return [Command]
|
|
@@ -83,6 +83,27 @@ module Protocol
|
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
# Builds a fatal handshake refusal.
|
|
87
|
+
def self.error(reason)
|
|
88
|
+
raise Error, "ERROR reason exceeds 255 bytes" if reason.bytesize > 255
|
|
89
|
+
|
|
90
|
+
new("ERROR", reason.bytesize.chr.b + reason.b)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ERROR is fatal even if its reason is malformed.
|
|
95
|
+
def raise_if_error!
|
|
96
|
+
return self unless @name == "ERROR"
|
|
97
|
+
|
|
98
|
+
length = @data.getbyte(0)
|
|
99
|
+
unless length && @data.bytesize == length + 1
|
|
100
|
+
raise FatalHandshakeError, "malformed ERROR command"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
raise FatalHandshakeError, "peer rejected handshake: #{@data.byteslice(1, length).inspect}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
|
|
86
107
|
# Builds a SUBSCRIBE command.
|
|
87
108
|
#
|
|
88
109
|
# @param prefix [String] subscription prefix to match
|
|
@@ -125,6 +146,9 @@ module Protocol
|
|
|
125
146
|
# @param context [String] optional context bytes (up to 16 bytes)
|
|
126
147
|
# @return [Command]
|
|
127
148
|
def self.ping(ttl: 0, context: EMPTY_BINARY)
|
|
149
|
+
raise Error, "PING TTL must be between 0 and 6553.5 seconds" unless (0..6553.5).cover?(ttl)
|
|
150
|
+
raise Error, "PING context exceeds 16 bytes" if context.bytesize > 16
|
|
151
|
+
|
|
128
152
|
ttl_ds = (ttl * 10).to_i
|
|
129
153
|
new("PING", [ttl_ds].pack("n") + (context.encoding == Encoding::BINARY ? context : context.b))
|
|
130
154
|
end
|
|
@@ -135,6 +159,8 @@ module Protocol
|
|
|
135
159
|
# @param context [String] context bytes echoed from the PING
|
|
136
160
|
# @return [Command]
|
|
137
161
|
def self.pong(context: EMPTY_BINARY)
|
|
162
|
+
raise Error, "PONG context exceeds 16 bytes" if context.bytesize > 16
|
|
163
|
+
|
|
138
164
|
new("PONG", context.encoding == Encoding::BINARY ? context : context.b)
|
|
139
165
|
end
|
|
140
166
|
|
|
@@ -143,6 +169,8 @@ module Protocol
|
|
|
143
169
|
#
|
|
144
170
|
# @return [Array(Numeric, String)] [ttl_seconds, context_bytes]
|
|
145
171
|
def ping_ttl_and_context
|
|
172
|
+
raise Error, "PING data must contain a TTL and at most 16 context bytes" unless (2..18).cover?(@data.bytesize)
|
|
173
|
+
|
|
146
174
|
ttl_ds = @data.unpack1("n")
|
|
147
175
|
context = @data.bytesize > 2 ? @data.byteslice(2..) : EMPTY_BINARY
|
|
148
176
|
[ttl_ds / 10.0, context]
|
|
@@ -157,6 +185,16 @@ module Protocol
|
|
|
157
185
|
end
|
|
158
186
|
|
|
159
187
|
|
|
188
|
+
# Property names are case-insensitive; retain their original spelling
|
|
189
|
+
# in the public metadata hash for callers that enumerate it.
|
|
190
|
+
def self.property(props, name)
|
|
191
|
+
props.each_pair do |key, value|
|
|
192
|
+
return value if key.casecmp?(name)
|
|
193
|
+
end
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
|
|
160
198
|
# Encodes a hash of properties into ZMTP property list format.
|
|
161
199
|
#
|
|
162
200
|
# @param props [Hash{String => String}] property name-value pairs
|
|
@@ -23,6 +23,7 @@ module Protocol
|
|
|
23
23
|
|
|
24
24
|
# Short frame: 1-byte size, max body 255 bytes.
|
|
25
25
|
SHORT_MAX = 255
|
|
26
|
+
LONG_MAX = (1 << 63) - 1
|
|
26
27
|
|
|
27
28
|
# Pre-computed single-byte flag strings (avoids Integer#chr + String#b per frame).
|
|
28
29
|
FLAG_BYTES = Array.new(256) { |i| i.chr.b.freeze }.freeze
|
|
@@ -102,6 +103,9 @@ module Protocol
|
|
|
102
103
|
long = (flags & FLAGS_LONG) != 0
|
|
103
104
|
command = (flags & FLAGS_COMMAND) != 0
|
|
104
105
|
|
|
106
|
+
raise Error, "reserved frame flags must be zero" unless (flags & 0xF8).zero?
|
|
107
|
+
raise Error, "command frames cannot have MORE set" if command && more
|
|
108
|
+
|
|
105
109
|
if long
|
|
106
110
|
raise EOFError, "Stream finished before reading long frame size" if buf.bytesize < 9
|
|
107
111
|
|
|
@@ -119,6 +123,8 @@ module Protocol
|
|
|
119
123
|
header_size = 2
|
|
120
124
|
end
|
|
121
125
|
|
|
126
|
+
raise Error, "frame size exceeds ZMTP's 63-bit limit" if size > LONG_MAX
|
|
127
|
+
|
|
122
128
|
if max_message_size && size > max_message_size
|
|
123
129
|
raise Error, "frame size #{size} exceeds max_message_size #{max_message_size}"
|
|
124
130
|
end
|
|
@@ -141,6 +147,8 @@ module Protocol
|
|
|
141
147
|
# @param more [Boolean] more frames follow
|
|
142
148
|
# @param command [Boolean] this is a command frame
|
|
143
149
|
def initialize(body, more: false, command: false)
|
|
150
|
+
raise Error, "command frames cannot have MORE set" if command && more
|
|
151
|
+
|
|
144
152
|
@body = body
|
|
145
153
|
@more = more
|
|
146
154
|
@command = command
|
|
@@ -47,6 +47,10 @@ module Protocol
|
|
|
47
47
|
# @return [Float, nil] monotonic timestamp of last received frame
|
|
48
48
|
attr_reader :last_received_at
|
|
49
49
|
|
|
50
|
+
# Transport wrappers may need room for bounded wire envelopes in
|
|
51
|
+
# addition to the application message limit they enforce after decoding.
|
|
52
|
+
attr_writer :max_message_size
|
|
53
|
+
|
|
50
54
|
|
|
51
55
|
# @param io [#peek, #read_exactly, #write, #flush, #close] transport IO
|
|
52
56
|
# @param socket_type [String] our socket type name (e.g. "REQ")
|
|
@@ -55,7 +59,7 @@ module Protocol
|
|
|
55
59
|
# @param mechanism [Mechanism::Null, Mechanism::Curve] security mechanism
|
|
56
60
|
# @param max_message_size [Integer, nil] max frame size in bytes, nil = unlimited
|
|
57
61
|
# @param opts [Hash{String => String}] extra READY properties to
|
|
58
|
-
# advertise (e.g. +"X-
|
|
62
|
+
# advertise (e.g. +"X-Compression" => "lz4"+). Upper-layer extensions use
|
|
59
63
|
# this to inject their own negotiated properties without the
|
|
60
64
|
# codec needing to know about them.
|
|
61
65
|
def initialize(io, socket_type:, identity: "", as_server: false,
|
|
@@ -278,12 +282,7 @@ module Protocol
|
|
|
278
282
|
# @raise [EOFError] if connection is closed
|
|
279
283
|
def read_frame
|
|
280
284
|
loop do
|
|
281
|
-
|
|
282
|
-
frame = Codec::Frame.read_from(@io, max_message_size: @max_message_size)
|
|
283
|
-
rescue Error
|
|
284
|
-
close
|
|
285
|
-
raise
|
|
286
|
-
end
|
|
285
|
+
frame = Codec::Frame.read_from(@io, max_message_size: @max_message_size)
|
|
287
286
|
|
|
288
287
|
touch_heartbeat
|
|
289
288
|
|
|
@@ -297,12 +296,16 @@ module Protocol
|
|
|
297
296
|
send_command(Codec::Command.pong(context: context))
|
|
298
297
|
next
|
|
299
298
|
when "PONG"
|
|
299
|
+
raise Error, "PONG context exceeds 16 bytes" if cmd.data.bytesize > 16
|
|
300
300
|
next
|
|
301
301
|
end
|
|
302
302
|
end
|
|
303
303
|
|
|
304
304
|
return frame
|
|
305
305
|
end
|
|
306
|
+
rescue Error
|
|
307
|
+
close
|
|
308
|
+
raise
|
|
306
309
|
end
|
|
307
310
|
|
|
308
311
|
|
data/lib/protocol/zmtp/error.rb
CHANGED
|
@@ -153,6 +153,8 @@ module Protocol
|
|
|
153
153
|
# @param command [Boolean] whether this is a command frame
|
|
154
154
|
# @return [String] binary wire bytes ready for writing
|
|
155
155
|
def encrypt(body, more: false, command: false)
|
|
156
|
+
raise Error, "command frames cannot have MORE set" if command && more
|
|
157
|
+
|
|
156
158
|
flags = 0
|
|
157
159
|
flags |= 0x01 if more
|
|
158
160
|
flags |= 0x02 if command
|
|
@@ -211,6 +213,8 @@ module Protocol
|
|
|
211
213
|
end
|
|
212
214
|
|
|
213
215
|
flags = plaintext.getbyte(0)
|
|
216
|
+
raise Error, "invalid MESSAGE flags" unless flags && (flags & 0xFC).zero?
|
|
217
|
+
|
|
214
218
|
body = plaintext.byteslice(1..) || "".b
|
|
215
219
|
Codec::Frame.new(body, more: (flags & 0x01) != 0, command: (flags & 0x02) != 0)
|
|
216
220
|
end
|
|
@@ -255,7 +259,7 @@ module Protocol
|
|
|
255
259
|
# --- Read WELCOME ---
|
|
256
260
|
welcome_frame = Codec::Frame.read_from(io)
|
|
257
261
|
raise Error, "expected command frame" unless welcome_frame.command?
|
|
258
|
-
welcome_cmd = Codec::Command.from_body(welcome_frame.body)
|
|
262
|
+
welcome_cmd = Codec::Command.from_body(welcome_frame.body).raise_if_error!
|
|
259
263
|
raise Error, "expected WELCOME, got #{welcome_cmd.name}" unless welcome_cmd.name == "WELCOME"
|
|
260
264
|
|
|
261
265
|
wdata = welcome_cmd.data
|
|
@@ -307,7 +311,7 @@ module Protocol
|
|
|
307
311
|
# --- Read READY ---
|
|
308
312
|
ready_frame = Codec::Frame.read_from(io)
|
|
309
313
|
raise Error, "expected command frame" unless ready_frame.command?
|
|
310
|
-
ready_cmd = Codec::Command.from_body(ready_frame.body)
|
|
314
|
+
ready_cmd = Codec::Command.from_body(ready_frame.body).raise_if_error!
|
|
311
315
|
raise Error, "expected READY, got #{ready_cmd.name}" unless ready_cmd.name == "READY"
|
|
312
316
|
|
|
313
317
|
rdata = ready_cmd.data
|
|
@@ -324,8 +328,8 @@ module Protocol
|
|
|
324
328
|
end
|
|
325
329
|
|
|
326
330
|
props = Codec::Command.decode_properties(r_plaintext)
|
|
327
|
-
peer_socket_type = props
|
|
328
|
-
peer_identity = props
|
|
331
|
+
peer_socket_type = Codec::Command.property(props, "Socket-Type")
|
|
332
|
+
peer_identity = Codec::Command.property(props, "Identity") || ""
|
|
329
333
|
|
|
330
334
|
@session_box = session
|
|
331
335
|
@send_nonce = 1
|
|
@@ -361,7 +365,7 @@ module Protocol
|
|
|
361
365
|
# --- Read HELLO ---
|
|
362
366
|
hello_frame = Codec::Frame.read_from(io)
|
|
363
367
|
raise Error, "expected command frame" unless hello_frame.command?
|
|
364
|
-
hello_cmd = Codec::Command.from_body(hello_frame.body)
|
|
368
|
+
hello_cmd = Codec::Command.from_body(hello_frame.body).raise_if_error!
|
|
365
369
|
raise Error, "expected HELLO, got #{hello_cmd.name}" unless hello_cmd.name == "HELLO"
|
|
366
370
|
|
|
367
371
|
hdata = hello_cmd.data
|
|
@@ -407,7 +411,7 @@ module Protocol
|
|
|
407
411
|
# --- Read INITIATE ---
|
|
408
412
|
init_frame = Codec::Frame.read_from(io)
|
|
409
413
|
raise Error, "expected command frame" unless init_frame.command?
|
|
410
|
-
init_cmd = Codec::Command.from_body(init_frame.body)
|
|
414
|
+
init_cmd = Codec::Command.from_body(init_frame.body).raise_if_error!
|
|
411
415
|
raise Error, "expected INITIATE, got #{init_cmd.name}" unless init_cmd.name == "INITIATE"
|
|
412
416
|
|
|
413
417
|
idata = init_cmd.data
|
|
@@ -468,7 +472,7 @@ module Protocol
|
|
|
468
472
|
peer = PeerInfo.new(public_key: client_permanent, identity: "")
|
|
469
473
|
unless @authenticator.call(peer)
|
|
470
474
|
send_error(io, "client key not authorized")
|
|
471
|
-
raise
|
|
475
|
+
raise FatalHandshakeError, "client key not authorized"
|
|
472
476
|
end
|
|
473
477
|
end
|
|
474
478
|
|
|
@@ -498,8 +502,8 @@ module Protocol
|
|
|
498
502
|
init_nonce_buffers!
|
|
499
503
|
|
|
500
504
|
{
|
|
501
|
-
peer_socket_type: props
|
|
502
|
-
peer_identity: props
|
|
505
|
+
peer_socket_type: Codec::Command.property(props, "Socket-Type"),
|
|
506
|
+
peer_identity: Codec::Command.property(props, "Identity") || "",
|
|
503
507
|
peer_public_key: client_permanent,
|
|
504
508
|
peer_properties: props,
|
|
505
509
|
peer_major: @peer_major,
|
|
@@ -48,14 +48,14 @@ module Protocol
|
|
|
48
48
|
raise Error, "expected command frame, got data frame"
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
peer_cmd = Codec::Command.from_body(frame.body)
|
|
51
|
+
peer_cmd = Codec::Command.from_body(frame.body).raise_if_error!
|
|
52
52
|
unless peer_cmd.name == "READY"
|
|
53
53
|
raise Error, "expected READY command, got #{peer_cmd.name}"
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
props = peer_cmd.properties
|
|
57
|
-
peer_socket_type = props
|
|
58
|
-
peer_identity = props
|
|
57
|
+
peer_socket_type = Codec::Command.property(props, "Socket-Type")
|
|
58
|
+
peer_identity = Codec::Command.property(props, "Identity") || ""
|
|
59
59
|
|
|
60
60
|
unless peer_socket_type
|
|
61
61
|
raise Error, "peer READY missing Socket-Type"
|
|
@@ -92,7 +92,8 @@ module Protocol
|
|
|
92
92
|
username, password = decode_credentials(cmd.data)
|
|
93
93
|
|
|
94
94
|
if @authenticator && !@authenticator.call(username, password)
|
|
95
|
-
|
|
95
|
+
send_command(io, Codec::Command.error("authentication failed"))
|
|
96
|
+
raise FatalHandshakeError, "authentication failed"
|
|
96
97
|
end
|
|
97
98
|
|
|
98
99
|
send_command(io, Codec::Command.new("WELCOME"))
|
|
@@ -143,13 +144,13 @@ module Protocol
|
|
|
143
144
|
|
|
144
145
|
def extract_peer_info(cmd)
|
|
145
146
|
props = cmd.properties
|
|
146
|
-
peer_socket_type = props
|
|
147
|
+
peer_socket_type = Codec::Command.property(props, "Socket-Type")
|
|
147
148
|
|
|
148
149
|
raise Error, "peer command missing Socket-Type" unless peer_socket_type
|
|
149
150
|
|
|
150
151
|
{
|
|
151
152
|
peer_socket_type: peer_socket_type,
|
|
152
|
-
peer_identity: props
|
|
153
|
+
peer_identity: Codec::Command.property(props, "Identity") || "",
|
|
153
154
|
peer_properties: props,
|
|
154
155
|
}
|
|
155
156
|
end
|
|
@@ -165,7 +166,7 @@ module Protocol
|
|
|
165
166
|
frame = Codec::Frame.read_from(io)
|
|
166
167
|
raise Error, "expected command frame, got data frame" unless frame.command?
|
|
167
168
|
|
|
168
|
-
Codec::Command.from_body(frame.body)
|
|
169
|
+
Codec::Command.from_body(frame.body).raise_if_error!
|
|
169
170
|
end
|
|
170
171
|
end
|
|
171
172
|
end
|
|
@@ -9,7 +9,7 @@ module Protocol
|
|
|
9
9
|
# - +identity+ is the peer's +ZMQ_IDENTITY+ string, or +""+ when no
|
|
10
10
|
# identity was advertised in the READY metadata.
|
|
11
11
|
#
|
|
12
|
-
# Used as a peer key by upper layers
|
|
12
|
+
# Used as a peer key by upper layers: the
|
|
13
13
|
# whole value object is frozen and equality-comparable, so it can be
|
|
14
14
|
# stored directly in a Hash without picking one anchor over the other.
|
|
15
15
|
#
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: protocol-zmtp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.10.
|
|
4
|
+
version: 0.10.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -54,7 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
54
54
|
- !ruby/object:Gem::Version
|
|
55
55
|
version: '0'
|
|
56
56
|
requirements: []
|
|
57
|
-
rubygems_version: 4.0.
|
|
57
|
+
rubygems_version: 4.0.20
|
|
58
58
|
specification_version: 4
|
|
59
59
|
summary: ZMTP 3.1 wire protocol codec and connection
|
|
60
60
|
test_files: []
|