protocol-zmtp 0.10.3 → 0.10.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ad013d03184eae1f5298d9ebbd5bc9a1fd2657ac7c45002a298547b36ad7f63d
4
- data.tar.gz: 137204024322094d2e76ef7103444e27143c18176b8a19b06c7e978c0b1fccd1
3
+ metadata.gz: ea36d2dec54002414471143e5dcffcbc6fcedb10a53a52f9c22684e5657c4a0d
4
+ data.tar.gz: 17e2377d7534476344dd47164d8b6fde121526b4d2715af4dfb5c4682bd98bf8
5
5
  SHA512:
6
- metadata.gz: 8ddbde5122489f94110878c0854167627a39207aa3c428aafdbec536dbcbcd39e0c9522ab23a97dc464766c0a42f7b8d6cf4689322486355880b53aa1def6385
7
- data.tar.gz: 6f069bf0af5fdff20496f376ed792bd2e3059cb64c1374674eaf7fbbbcaf2226b2f8cf69f9be9b49d994b7ab0a2e0fca26373248572b65db36653bba40846395
6
+ metadata.gz: 8ecaa0325141c8e1e41f8389aa1b431328b2c9b0c83c912f301119bd49503a001889aec9db285485fe786022033927588e3843dd8af60dfb1597619ca57aff40
7
+ data.tar.gz: 67af1e45d9834d9ae7ed47352758eca7b92817691bf80518addfb66de28979d4a0bbaf2ec53ff802c8fbefd788a88bfeb6d794b9669cda1fdc57be20af00f669
data/CHANGELOG.md ADDED
@@ -0,0 +1,297 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## 0.10.4 - 2026-07-23
6
+
7
+ ### Changed
8
+
9
+ - Release source is now the `zeromq/omq.rb` monorepo.
10
+
11
+ ## 0.10.3 — 2026-05-30
12
+
13
+ ### Fixed
14
+
15
+ - **CURVE: COMMAND flag in encrypted inner byte must be 0x02, not 0x04.**
16
+ CurveZMQ's inner plaintext flags byte uses a compact layout where
17
+ bit 1 (0x02) means COMMAND, but encrypt/decrypt used the wire-level
18
+ COMMAND bit (0x04). This broke interop with libzmq: a CURVE-encrypted
19
+ SUBSCRIBE from a libzmq SUB was treated as data and silently dropped,
20
+ so the PUB never registered the subscription.
21
+
22
+ ## 0.10.2 — 2026-05-28
23
+
24
+ - Bump version (was missed in 0.10.1).
25
+
26
+ ## 0.10.1 — 2026-05-28
27
+
28
+ ### Added
29
+
30
+ - **`Connection#write_wire_batch`** writes multiple pre-encoded wire
31
+ byte strings under a single mutex acquisition, amortizing lock
32
+ overhead for fan-out send pumps.
33
+
34
+ ### Changed
35
+
36
+ - **Single-write short frames.** Frames with bodies <= 255 bytes now
37
+ combine the 2-byte header and body into one `@io.write` call,
38
+ halving per-frame io-stream mutex overhead (+5-7% small-message
39
+ TCP throughput).
40
+
41
+ ### Fixed
42
+
43
+ - **NULL mechanism greeting: `as-server` field is now always 0.**
44
+ RFC 23 requires `as-server = 0` for the NULL mechanism, but the
45
+ server side was forwarding the caller's flag (`0x01`). This broke
46
+ interoperability with strict ZMTP 3.x peers that validate the field.
47
+
48
+ ## 0.10.0 — 2026-04-20
49
+
50
+ ### Added
51
+
52
+ - **`PeerInfo` extended with `identity`.** `PeerInfo = Data.define(:public_key, :identity)`
53
+ now bundles both post-handshake peer anchors. CURVE authenticators
54
+ still receive a `PeerInfo` during auth (with `identity: ""`, since
55
+ identity arrives post-auth).
56
+ - **`Connection#peer_info`.** After a successful handshake, returns a
57
+ frozen `PeerInfo` combining the peer's CURVE public key (if any) and
58
+ `ZMQ_IDENTITY`. Usable directly as a Hash key. Nil before handshake.
59
+ Upper layers (e.g. omq-qos levels 2/3) use the whole value as a
60
+ stable per-peer identifier across reconnects.
61
+ - **Mechanism return hashes carry `peer_public_key`.** Both
62
+ `Mechanism::Null#handshake!` and `Mechanism::Curve` handshake paths now
63
+ return `peer_public_key:` alongside `peer_identity:` etc. (nil for
64
+ NULL; the peer's long-term `crypto::PublicKey` for CURVE).
65
+
66
+ ### Changed
67
+
68
+ - **Drop hot-path binary-encoding coercion and frame-body freezes.**
69
+ `Frame#initialize` and `Frame.encode_message` no longer copy non-binary
70
+ bodies via `String#b` — callers (OMQ's `Writable#send`, ZMTP command
71
+ builders) already hand in binary bytes, and for an outgoing message the
72
+ receiver (`String.new(capacity:) << body`) treats any encoding as raw
73
+ bytes once the buffer itself is binary. `Connection#receive_message`
74
+ also stops freezing each frame body and the returned parts array; the
75
+ caller decides what to freeze. Net effect: fewer allocations per
76
+ message and no per-part `.freeze` on the receive path.
77
+
78
+ ## 0.9.0 — 2026-04-18
79
+
80
+ ### Changed
81
+
82
+ - **`Frame.read_from` uses `io.peek` to collapse the long-frame read
83
+ path.** The previous implementation issued 2 `read_exactly` calls for
84
+ short frames (header + body) and 3 for long frames (2-byte header,
85
+ remaining 7 size bytes, body). It now peeks just enough header bytes
86
+ (2 for short, 9 for long), decodes the size from the peek buffer, and
87
+ drains header + body in a single `read_exactly(header_size + size)`.
88
+ Long frames drop to the same 2-call read path as short frames; the
89
+ `read_long_size` helper is gone. A speculative `read_exactly(9)` would
90
+ not be safe here — a <7-byte short frame at idle would hang waiting
91
+ for bytes that never arrive, or steal bytes from the next frame on a
92
+ mixed stream.
93
+
94
+ - **Transport IO must now also respond to `#peek`** in addition to
95
+ `#read_exactly`, `#write`, `#flush`, and `#close`. `io-stream`'s
96
+ `Async::IO::Stream` already provides this; custom transport wrappers
97
+ need a `#peek(&block)` that yields the current read buffer and fills
98
+ it until the block returns truthy.
99
+
100
+ ## 0.8.1 — 2026-04-16
101
+
102
+ ### Changed
103
+
104
+ - **`Frame#initialize` skips redundant `.b` copy.** Body is now kept
105
+ as-is when it is already `Encoding::BINARY`, avoiding a per-frame
106
+ String allocation on the read path (`read_exactly` and `byteslice`
107
+ already return binary strings).
108
+
109
+ - **`Frame#to_wire` uses `String.new(capacity:)` with `<<` appends**
110
+ instead of `+` concatenation, reducing from 3 intermediate String
111
+ allocations per frame to 1 pre-sized buffer.
112
+
113
+ - **`Frame.encode_message` pre-computes wire size** for the output
114
+ buffer `capacity:` hint, avoiding re-allocations during fan-out.
115
+ Single-part messages (the common case) skip the iteration and
116
+ inline the size calculation.
117
+
118
+ - **`Command.ping` / `.pong` default to `EMPTY_BINARY`** instead of
119
+ `"".b`, which allocated a mutable copy on every call despite
120
+ `frozen_string_literal: true`. `#ping_ttl_and_context` uses
121
+ `EMPTY_BINARY` for the empty-context fallback.
122
+
123
+ - **`Subscription.body` drops redundant outer `.b`** — both operands
124
+ are already binary, so the concatenation result is binary without
125
+ a second encoding conversion.
126
+
127
+ ## 0.8.0 — 2026-04-15
128
+
129
+ ### Added
130
+
131
+ - **`Codec::Subscription`** — helper module that unifies ZMTP 3.0
132
+ message-form (`\x01`/`\x00` + prefix data frame) and ZMTP 3.1
133
+ command-form (`SUBSCRIBE`/`CANCEL` command frame) subscription
134
+ encodings. `.body(prefix, cancel:)` builds the message-form body;
135
+ `.parse(frame)` returns `[:subscribe|:cancel, prefix]` for either
136
+ form, letting upper layers accept both without branching.
137
+ - **`Connection#peer_major` / `#peer_minor`** — ZMTP wire revision of
138
+ the peer, captured from the greeting. Lets upper layers pick the
139
+ subscription wire form (and other version-gated features) per peer.
140
+ Populated by `Mechanism::Null`, `Mechanism::Plain`, and
141
+ `Mechanism::Curve` via their handshake result hash.
142
+ - **`Codec::Greeting.read_from(io)`** — reads the 11-byte signature
143
+ phase first, validates the revision byte, then reads the rest of
144
+ the 64-byte greeting. A ZMTP/2.0 peer (revision `0x01`) is now
145
+ rejected loudly with `unsupported ZMTP revision 0x01 (ZMTP/2.x);
146
+ need revision >= 3`, instead of hanging forever in `read_exactly`
147
+ waiting for bytes that never arrive.
148
+
149
+ ### Changed
150
+
151
+ - **Greeting error messages** refer to the ZMTP *revision byte*
152
+ (`0x03`) rather than a "version #{major}.#{minor}" — the byte at
153
+ offset 10 is the wire revision, which only accidentally matches the
154
+ spec major in ZMTP/3.x.
155
+
156
+ ## 0.7.1 — 2026-04-14
157
+
158
+ ### Changed
159
+
160
+ - **Short-frame read fast path.** `Frame.read_from` now fetches the
161
+ 2-byte header (flags + first size byte) in a single `read_exactly`
162
+ call instead of two separate 1-byte reads. Short frames (≤255 bytes,
163
+ the vast majority of ZMTP traffic) now hit a 2-call read path
164
+ (header + body) instead of 3. Long frames read the remaining 7 size
165
+ bytes via the extracted `read_long_size` helper.
166
+
167
+ ## 0.7.0 — 2026-04-13
168
+
169
+ ### Added
170
+
171
+ - **Extension metadata hook on the handshake mechanisms.** `Mechanism::Null`,
172
+ `Mechanism::Plain`, and `Mechanism::Curve` expose a `metadata` accessor
173
+ (`Hash{String => String}`) that upper layers can populate before
174
+ `#handshake!`. Any entries are merged into the outgoing READY properties
175
+ (and INITIATE, for CURVE/PLAIN client side). `Codec::Command.ready` gained
176
+ a matching `metadata:` kwarg. Used by `omq-rfc-zstd` to advertise the
177
+ `X-Compression` property without forking the handshake code path.
178
+
179
+ - **`Connection#peer_properties`.** The full peer READY property hash is
180
+ now retained after a successful handshake (previously only Socket-Type,
181
+ Identity, and the X-QoS pair were extracted). Extensions can inspect
182
+ the peer's advertised properties to negotiate optional features.
183
+ Returned by all three mechanisms as `peer_properties:` in the
184
+ handshake result hash.
185
+
186
+ ## 0.6.0 — 2026-04-12
187
+
188
+ ### Changed
189
+
190
+ - **Consolidated empty-binary constant.** `Codec::Frame::EMPTY_BODY`
191
+ and `Codec::Command::EMPTY_DATA` are gone, replaced by a single
192
+ `Codec::EMPTY_BINARY` shared across the codec module. Anything
193
+ referencing the old constants needs to switch to
194
+ `Protocol::ZMTP::Codec::EMPTY_BINARY`.
195
+
196
+ ## 0.5.1 — 2026-04-10
197
+
198
+ ### Changed
199
+
200
+ - **Reduced allocations on hot paths.** Pre-computed `FLAG_BYTES` lookup
201
+ table eliminates `Integer#chr` + `String#b` per frame. `encode_message`
202
+ inlines wire encoding instead of creating throwaway Frame objects per
203
+ part. `write_frames` and `Command#to_body` skip redundant `.b` when
204
+ strings are already binary. `EMPTY_BODY` and `EMPTY_DATA` constants
205
+ replace per-call `"".b` allocations.
206
+
207
+ ## 0.5.0 — 2026-04-10
208
+
209
+ ### Fixed
210
+
211
+ - **Wire writes are now cancellation-safe.** `Connection#send_message`,
212
+ `#write_message`, `#write_messages`, `#write_wire`, and `#send_command`
213
+ each wrap their `@mutex.synchronize` block in
214
+ `Async::Task#defer_cancel`. A ZMTP frame is two `@io.write` calls
215
+ (header then body); under a socket-level barrier cascade (`barrier.stop`)
216
+ an `Async::Cancel` could previously land between those two writes,
217
+ leaving the peer with a header pointing at a body that never arrives
218
+ and an unrecoverable framer desync. `defer_cancel` holds the
219
+ cancellation until the write finishes, so cascading teardown only
220
+ unwinds at frame boundaries. Mutex protection is unchanged — it
221
+ guards against thread races; `defer_cancel` guards against fiber
222
+ cancellation. Non-Async callers (e.g. tests) fall through to the
223
+ unwrapped path.
224
+
225
+ ## 0.4.0 — 2026-04-09
226
+
227
+ ### Added
228
+
229
+ - **`Connection#write_messages`** — batched multipart send for work-stealing
230
+ send pumps. Dequeue a batch at once and write it under a single mutex
231
+ acquisition instead of one lock per message. Amortizes lock overhead on
232
+ high-throughput paths.
233
+
234
+ ### Changed
235
+
236
+ - **Zero-alloc frame headers on the unencrypted hot send path.**
237
+ `#write_frames` used to allocate, per part, a `Codec::Frame` object, a
238
+ `.b` body copy, a 1-or-9-byte header String, and a concatenated wire
239
+ String (a copy of the entire body just to glue the header on). The body
240
+ copy was the dominant allocation — every 64 KB send produced a 64 KB
241
+ throwaway String, feeding the GC at ~1.2 GB/s under sustained load.
242
+ Headers are now packed into a reusable `@header_buf` via
243
+ `Array#pack(buffer:)`, header and body are written separately
244
+ (io-stream buffers them into one syscall per batch), the `Frame`
245
+ object is gone, and the body is no longer copied. `@mechanism.encrypted?`
246
+ is hoisted out of the per-frame loop. The encrypted path is unchanged —
247
+ CURVE/BLAKE3ZMQ still authenticate over the full encoded frame and need
248
+ a single wire String per part.
249
+
250
+ Bench impact downstream in omq (push_pull + router_dealer, 1 and 3 peers,
251
+ all transports): 48 improvements, 0 regressions across 72 measurements.
252
+ Peaks: `router_dealer tcp 3p 64B +40.7%`, `push_pull ipc 1p 64 KB +28.8%`
253
+ (crosses 1.3 GB/s).
254
+
255
+ ## 0.3.0 — 2026-04-07
256
+
257
+ - Replace `[Async {}].each(&:wait)` with `Barrier` in tests.
258
+ - YARD documentation on all public methods and classes.
259
+ - Code style: two blank lines between methods and constants.
260
+ - Fix `#read_frame` decryption for non-CURVE encrypted mechanisms
261
+ (e.g. BLAKE3ZMQ). Previously only CURVE's `\x07MESSAGE`-wrapped command
262
+ frames were decrypted; inline-encrypted command frames (SUBSCRIBE, PING,
263
+ etc.) were silently dropped, breaking PUB/SUB over BLAKE3ZMQ.
264
+
265
+ - **Breaking:** `Mechanism::Curve` API is now kwargs-only:
266
+ `Curve.server(public_key:, secret_key:, crypto:)` and
267
+ `Curve.client(server_key:, crypto:)`. Client keys are optional — when
268
+ omitted, an ephemeral permanent keypair is auto-generated. INITIATE
269
+ always contains `C + vouch + metadata` per RFC 26.
270
+ - **Breaking:** Authenticator now receives a `Protocol::ZMTP::PeerInfo`
271
+ (with a `crypto::PublicKey`) via `#call`. The `#include?` duck-typing
272
+ is removed. Sends an ERROR command to the client on rejection.
273
+ - Add `Protocol::ZMTP::PeerInfo` shared across mechanisms.
274
+ - Add `#maintenance` to `Mechanism::Curve` for automatic cookie key rotation.
275
+ Returns `{ interval: 60, task: <Proc> }` on server-side mechanisms so the
276
+ host application can rotate the cookie key every 60 seconds, limiting the
277
+ forward secrecy exposure window.
278
+
279
+ ## 0.2.0
280
+
281
+ - Add `Mechanism::Plain` — PLAIN authentication (RFC 24). Carries username and
282
+ password in a `HELLO` command during the handshake; no frame encryption.
283
+ Accepts an optional `authenticator:` callable on the server side for
284
+ credential validation.
285
+
286
+ ## 0.1.2
287
+
288
+ - Check frame size against `max_message_size` before reading the body from the
289
+ wire. Previously, the entire frame was allocated into memory before the size
290
+ check, allowing a malicious peer to cause arbitrary memory allocation with a
291
+ single oversized frame header.
292
+ - Size limit applies to all frames including commands — an attacker cannot bypass
293
+ the check by setting the command flag.
294
+
295
+ ## 0.1.1
296
+
297
+ - Initial public release.
data/README.md CHANGED
@@ -1,27 +1,27 @@
1
1
  # Protocol::ZMTP
2
2
 
3
3
  [![Gem Version](https://img.shields.io/gem/v/protocol-zmtp)](https://rubygems.org/gems/protocol-zmtp)
4
- [![CI](https://github.com/paddor/protocol-zmtp/actions/workflows/ci.yml/badge.svg)](https://github.com/paddor/protocol-zmtp/actions/workflows/ci.yml)
4
+ [![CI](https://github.com/zeromq/omq.rb/actions/workflows/ci.yml/badge.svg)](https://github.com/zeromq/omq.rb/actions/workflows/ci.yml)
5
5
 
6
- ZMTP 3.1 wire protocol codec, connection, NULL and CURVE mechanisms.
6
+ ZMTP 3.1 wire protocol: codec, connection, NULL and CURVE mechanisms.
7
7
  No runtime dependencies.
8
8
 
9
9
  ## What's in the box
10
10
 
11
- - **Codec::Frame** ZMTP frame encode/decode (flags, size, body)
12
- - **Codec::Greeting** 64-byte greeting exchange
13
- - **Codec::Command** READY, PING/PONG, SUBSCRIBE, etc.
14
- - **Connection** per-connection frame I/O, handshake, PING/PONG
15
- - **Mechanism::Null** NULL security (no encryption)
16
- - **Mechanism::Curve** CurveZMQ (RFC 26) with pluggable crypto backend
17
- - **Z85** ZeroMQ RFC 32 encoding
11
+ - **Codec::Frame**: ZMTP frame encode/decode (flags, size, body)
12
+ - **Codec::Greeting**: 64-byte greeting exchange
13
+ - **Codec::Command**: READY, PING/PONG, SUBSCRIBE, etc.
14
+ - **Connection**: per-connection frame I/O, handshake, PING/PONG
15
+ - **Mechanism::Null**: NULL security (no encryption)
16
+ - **Mechanism::Curve**: CurveZMQ (RFC 26) with pluggable crypto backend
17
+ - **Z85**: ZeroMQ RFC 32 encoding
18
18
 
19
19
  ## Usage
20
20
 
21
21
  ```ruby
22
22
  require "protocol/zmtp"
23
23
 
24
- # NULL mechanism no encryption
24
+ # NULL mechanism, no encryption
25
25
  conn = Protocol::ZMTP::Connection.new(
26
26
  io,
27
27
  socket_type: "REQ",
@@ -31,7 +31,7 @@ conn.handshake!
31
31
  conn.send_message(["hello"])
32
32
  msg = conn.receive_message
33
33
 
34
- # CURVE mechanism pass any NaCl-compatible backend
34
+ # CURVE mechanism, pass any NaCl-compatible backend
35
35
  require "protocol/zmtp/mechanism/curve"
36
36
  require "nuckle" # or: require "rbnacl"
37
37
 
@@ -42,7 +42,7 @@ server_mech = Protocol::ZMTP::Mechanism::Curve.server(
42
42
 
43
43
  ## CURVE crypto backend
44
44
 
45
- `Mechanism::Curve` accepts a `crypto:` parameter any module that
45
+ `Mechanism::Curve` accepts a `crypto:` parameter. Any module that
46
46
  provides the NaCl API:
47
47
 
48
48
  ```ruby
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Protocol
4
4
  module ZMTP
5
- VERSION = "0.10.3"
5
+ VERSION = "0.10.4"
6
6
  end
7
7
  end
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.3
4
+ version: 0.10.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -18,6 +18,7 @@ executables: []
18
18
  extensions: []
19
19
  extra_rdoc_files: []
20
20
  files:
21
+ - CHANGELOG.md
21
22
  - LICENSE
22
23
  - README.md
23
24
  - lib/protocol/zmtp.rb
@@ -35,7 +36,7 @@ files:
35
36
  - lib/protocol/zmtp/valid_peers.rb
36
37
  - lib/protocol/zmtp/version.rb
37
38
  - lib/protocol/zmtp/z85.rb
38
- homepage: https://github.com/paddor/protocol-zmtp
39
+ homepage: https://github.com/zeromq/omq.rb/tree/main/gems/protocol-zmtp
39
40
  licenses:
40
41
  - ISC
41
42
  metadata: {}
@@ -53,7 +54,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
54
  - !ruby/object:Gem::Version
54
55
  version: '0'
55
56
  requirements: []
56
- rubygems_version: 4.0.10
57
+ rubygems_version: 4.0.16
57
58
  specification_version: 4
58
59
  summary: ZMTP 3.1 wire protocol codec and connection
59
60
  test_files: []