protocol-zmtp 0.7.1 → 0.8.1

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: e17c453960d40efb63134468319f184e1751a1812a42e72b4b10de4f10c08f7a
4
- data.tar.gz: 3aa77f8983e7b737bc9c896d4ff7b0a476805e51d1bd2668d4399c4547beb2ca
3
+ metadata.gz: 81270e34092271f0e0aaceed0ce882885751def59b75716872d074952e4d6b41
4
+ data.tar.gz: ccbf823376d70d8654384b99c1bb30b270dc9493cd432c524a3caab175eaa364
5
5
  SHA512:
6
- metadata.gz: c325ce9ad10043d55875304bf4267f5ef7ab286945126620fe43f0ec5eeedb228804ed1c6bb97a6dc135adc017124add12b56a1475bfb1ff37d74a0dc7536abc
7
- data.tar.gz: 1cdd5b8fd5abb3471c6e4147850b7e5d20c5bc47cec6fb4233c7e38845f8e5b8df2fd73463e618a128d07316c184facf678fa718674b8ba6e1cb70e9390310d5
6
+ metadata.gz: f148b0ac01768320f35f2edf7ac67f3d51ad3e5daa4a9c9dafccb57168e8e9f616f6f27425c00b82edcd7e0af898188ae37ea3a78f404a786bdd9a94c01cb675
7
+ data.tar.gz: bdab4d4b763394ea1f11aec4503a74d68f499671c93851adc96c2e34bcd7134de837b71941119a1aa029a0966c4ed4cfea8def3f5140244e50f9c2a9b3b0355d
@@ -130,9 +130,9 @@ module Protocol
130
130
  # @param ttl [Numeric] time-to-live in seconds (sent as deciseconds)
131
131
  # @param context [String] optional context bytes (up to 16 bytes)
132
132
  # @return [Command]
133
- def self.ping(ttl: 0, context: "".b)
133
+ def self.ping(ttl: 0, context: EMPTY_BINARY)
134
134
  ttl_ds = (ttl * 10).to_i
135
- new("PING", [ttl_ds].pack("n") + context.b)
135
+ new("PING", [ttl_ds].pack("n") + (context.encoding == Encoding::BINARY ? context : context.b))
136
136
  end
137
137
 
138
138
 
@@ -140,8 +140,8 @@ module Protocol
140
140
  #
141
141
  # @param context [String] context bytes echoed from the PING
142
142
  # @return [Command]
143
- def self.pong(context: "".b)
144
- new("PONG", context.b)
143
+ def self.pong(context: EMPTY_BINARY)
144
+ new("PONG", context.encoding == Encoding::BINARY ? context : context.b)
145
145
  end
146
146
 
147
147
 
@@ -150,7 +150,7 @@ module Protocol
150
150
  # @return [Array(Numeric, String)] [ttl_seconds, context_bytes]
151
151
  def ping_ttl_and_context
152
152
  ttl_ds = @data.unpack1("n")
153
- context = @data.bytesize > 2 ? @data.byteslice(2..) : "".b
153
+ context = @data.bytesize > 2 ? @data.byteslice(2..) : EMPTY_BINARY
154
154
  [ttl_ds / 10.0, context]
155
155
  end
156
156
 
@@ -31,21 +31,28 @@ module Protocol
31
31
  # @return [String] frame body (binary)
32
32
  attr_reader :body
33
33
 
34
+
34
35
  # @param body [String] frame body
35
36
  # @param more [Boolean] more frames follow
36
37
  # @param command [Boolean] this is a command frame
37
38
  def initialize(body, more: false, command: false)
38
- @body = body.b
39
+ @body = body.encoding == Encoding::BINARY ? body : body.b
39
40
  @more = more
40
41
  @command = command
41
42
  end
42
43
 
43
44
 
44
45
  # @return [Boolean] true if more frames follow in this message
45
- def more? = @more
46
+ def more?
47
+ @more
48
+ end
49
+
46
50
 
47
51
  # @return [Boolean] true if this is a command frame
48
- def command? = @command
52
+ def command?
53
+ @command
54
+ end
55
+
49
56
 
50
57
  # Encodes to wire bytes.
51
58
  #
@@ -57,9 +64,11 @@ module Protocol
57
64
  flags |= FLAGS_COMMAND if @command
58
65
 
59
66
  if size > SHORT_MAX
60
- FLAG_BYTES[flags | FLAGS_LONG] + [size].pack("Q>") + @body
67
+ buf = String.new(capacity: 9 + size, encoding: Encoding::BINARY)
68
+ buf << FLAG_BYTES[flags | FLAGS_LONG] << [size].pack("Q>") << @body
61
69
  else
62
- FLAG_BYTES[flags] + FLAG_BYTES[size] + @body
70
+ buf = String.new(capacity: 2 + size, encoding: Encoding::BINARY)
71
+ buf << FLAG_BYTES[flags] << FLAG_BYTES[size] << @body
63
72
  end
64
73
  end
65
74
 
@@ -72,14 +81,29 @@ module Protocol
72
81
  # @return [String] frozen binary wire representation
73
82
  #
74
83
  def self.encode_message(parts)
75
- buf = String.new(encoding: Encoding::BINARY)
84
+ if parts.size == 1
85
+ s = parts[0].bytesize
86
+ wire_size = s > SHORT_MAX ? 9 + s : 2 + s
87
+ else
88
+ wire_size = 0
89
+ j = 0
90
+ while j < parts.size
91
+ s = parts[j].bytesize
92
+ wire_size += s > SHORT_MAX ? 9 + s : 2 + s
93
+ j += 1
94
+ end
95
+ end
96
+
97
+ buf = String.new(capacity: wire_size, encoding: Encoding::BINARY)
76
98
  last = parts.size - 1
77
99
  i = 0
100
+
78
101
  while i < parts.size
79
102
  body = parts[i]
80
103
  body = body.b unless body.encoding == Encoding::BINARY
81
104
  size = body.bytesize
82
105
  flags = i < last ? FLAGS_MORE : 0
106
+
83
107
  if size > SHORT_MAX
84
108
  buf << FLAG_BYTES[flags | FLAGS_LONG] << [size].pack("Q>") << body
85
109
  else
@@ -87,6 +111,7 @@ module Protocol
87
111
  end
88
112
  i += 1
89
113
  end
114
+
90
115
  buf.freeze
91
116
  end
92
117
 
@@ -137,6 +162,7 @@ module Protocol
137
162
  (rest.getbyte(4) << 16) | (rest.getbyte(5) << 8) |
138
163
  rest.getbyte(6)
139
164
  end
165
+
140
166
  end
141
167
  end
142
168
  end
@@ -18,6 +18,11 @@ module Protocol
18
18
  #
19
19
  module Greeting
20
20
  SIZE = 64
21
+ # Bytes 0..10 cover the 10-byte signature plus the major-version
22
+ # byte at offset 10. ZMTP 2.0 peers only send an 11-byte signature
23
+ # phase (their full greeting is shorter than 64), so we must
24
+ # sniff the major version before committing to reading 64 bytes.
25
+ SIGNATURE_SIZE = 11
21
26
  SIGNATURE_START = 0xFF
22
27
  SIGNATURE_END = 0x7F
23
28
  VERSION_MAJOR = 3
@@ -41,6 +46,32 @@ module Protocol
41
46
  end
42
47
 
43
48
 
49
+ # Reads and decodes a ZMTP 3.x greeting from +io+, sniffing the
50
+ # major version after the first 11 bytes so a ZMTP 2.0 peer
51
+ # (which would never send the full 64 bytes) is detected and
52
+ # rejected without blocking forever on +read_exactly+.
53
+ #
54
+ # @param io [#read_exactly]
55
+ # @return [Hash] { major:, minor:, mechanism:, as_server: }
56
+ # @raise [Error] on invalid signature or unsupported version
57
+ #
58
+ def self.read_from(io)
59
+ sig = io.read_exactly(SIGNATURE_SIZE).b
60
+ major = sig.getbyte(10)
61
+
62
+ unless sig.getbyte(0) == SIGNATURE_START && sig.getbyte(9) == SIGNATURE_END
63
+ raise Error, "invalid greeting signature"
64
+ end
65
+
66
+ unless major >= 3
67
+ raise Error, "unsupported ZMTP revision 0x%02x (ZMTP/%d.x); need revision >= 3" %
68
+ [major, major == 1 ? 2 : major]
69
+ end
70
+
71
+ decode(sig + io.read_exactly(SIZE - SIGNATURE_SIZE))
72
+ end
73
+
74
+
44
75
  # Decodes a ZMTP greeting.
45
76
  #
46
77
  # @param data [String] 64-byte binary greeting
@@ -59,7 +90,7 @@ module Protocol
59
90
  minor = data.getbyte(11)
60
91
 
61
92
  unless major >= 3
62
- raise Error, "unsupported ZMTP version #{major}.#{minor} (need >= 3.0)"
93
+ raise Error, "unsupported ZMTP revision 0x%02x (need revision >= 3)" % major
63
94
  end
64
95
 
65
96
  mechanism = data.byteslice(MECHANISM_OFFSET, MECHANISM_LENGTH).delete("\x00")
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protocol
4
+ module ZMTP
5
+ module Codec
6
+
7
+ # ZMTP subscription encoding.
8
+ #
9
+ # Two wire formats exist and both are in active use:
10
+ #
11
+ # * **Message form (ZMTP 3.0 legacy, RFC 23).** A regular data
12
+ # frame whose body is `\x01` + prefix (subscribe) or `\x00` +
13
+ # prefix (cancel). libzmq, JeroMQ, pyzmq, CZMQ, NetMQ all send
14
+ # subscriptions in this form by default, and all accept it.
15
+ #
16
+ # * **Command form (ZMTP 3.1, RFC 37).** A COMMAND-flagged frame
17
+ # whose body is a Command named "SUBSCRIBE" or "CANCEL" with
18
+ # the prefix as the command data.
19
+ #
20
+ # Interop requires sending the message form (understood by every
21
+ # ZMTP 3.0+ peer) and accepting both forms on the receiving side.
22
+ #
23
+ module Subscription
24
+ FLAG_SUBSCRIBE = "\x01".b.freeze
25
+ FLAG_CANCEL = "\x00".b.freeze
26
+
27
+ module_function
28
+
29
+ # Builds the body of a subscription message in the legacy
30
+ # message form.
31
+ #
32
+ # @param prefix [String] topic prefix
33
+ # @param cancel [Boolean] true to build an unsubscribe
34
+ # @return [String] binary frame body
35
+ def body(prefix, cancel: false)
36
+ flag = cancel ? FLAG_CANCEL : FLAG_SUBSCRIBE
37
+ flag + (prefix.encoding == Encoding::BINARY ? prefix : prefix.b)
38
+ end
39
+
40
+
41
+ # Attempts to parse a frame as a subscription. Accepts both the
42
+ # legacy message form and the ZMTP 3.1 command form.
43
+ #
44
+ # @param frame [Frame]
45
+ # @return [Array(Symbol, String), nil] `[:subscribe, prefix]`,
46
+ # `[:cancel, prefix]`, or `nil` if the frame is not a
47
+ # subscription
48
+ def parse(frame)
49
+ if frame.command?
50
+ cmd = Command.from_body(frame.body)
51
+ case cmd.name
52
+ when "SUBSCRIBE" then [:subscribe, cmd.data]
53
+ when "CANCEL" then [:cancel, cmd.data]
54
+ end
55
+ else
56
+ body = frame.body
57
+ return nil if body.empty?
58
+
59
+ prefix = body.byteslice(1..) || "".b
60
+ case body.getbyte(0)
61
+ when 0x01 then [:subscribe, prefix]
62
+ when 0x00 then [:cancel, prefix]
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -11,3 +11,4 @@ end
11
11
  require_relative "codec/greeting"
12
12
  require_relative "codec/frame"
13
13
  require_relative "codec/command"
14
+ require_relative "codec/subscription"
@@ -15,25 +15,41 @@ module Protocol
15
15
  # @return [String] peer's socket type (from READY handshake)
16
16
  attr_reader :peer_socket_type
17
17
 
18
+
18
19
  # @return [String] peer's identity (from READY handshake)
19
20
  attr_reader :peer_identity
20
21
 
22
+
21
23
  # @return [Integer] peer's QoS level (from READY handshake, 0 if absent)
22
24
  attr_reader :peer_qos
23
25
 
26
+
24
27
  # @return [String] peer's supported hash algorithms in preference order
25
28
  attr_reader :peer_qos_hash
26
29
 
30
+
27
31
  # @return [Hash{String => String}, nil] full peer READY property hash
28
32
  # (set after a successful handshake; nil before)
29
33
  attr_reader :peer_properties
30
34
 
35
+
36
+ # @return [Integer, nil] peer ZMTP major version (from greeting)
37
+ attr_reader :peer_major
38
+
39
+
40
+ # @return [Integer, nil] peer ZMTP minor version (from greeting);
41
+ # 0 for ZMTP 3.0 peers, 1 for ZMTP 3.1+
42
+ attr_reader :peer_minor
43
+
44
+
31
45
  # @return [Object] transport IO (#read_exactly, #write, #flush, #close)
32
46
  attr_reader :io
33
47
 
48
+
34
49
  # @return [Float, nil] monotonic timestamp of last received frame
35
50
  attr_reader :last_received_at
36
51
 
52
+
37
53
  # @param io [#read_exactly, #write, #flush, #close] transport IO
38
54
  # @param socket_type [String] our socket type name (e.g. "REQ")
39
55
  # @param identity [String] our identity
@@ -52,6 +68,8 @@ module Protocol
52
68
  @peer_qos = nil
53
69
  @peer_qos_hash = nil
54
70
  @peer_properties = nil
71
+ @peer_major = nil
72
+ @peer_minor = nil
55
73
  @qos = qos
56
74
  @qos_hash = qos_hash
57
75
  @mutex = Mutex.new
@@ -70,20 +88,20 @@ module Protocol
70
88
  # @return [void]
71
89
  # @raise [Error] on handshake failure
72
90
  def handshake!
73
- result = @mechanism.handshake!(
74
- @io,
91
+ result = @mechanism.handshake! @io,
75
92
  as_server: @as_server,
76
93
  socket_type: @socket_type,
77
94
  identity: @identity,
78
95
  qos: @qos,
79
- qos_hash: @qos_hash,
80
- )
96
+ qos_hash: @qos_hash
81
97
 
82
98
  @peer_socket_type = result[:peer_socket_type]
83
99
  @peer_identity = result[:peer_identity]
84
100
  @peer_qos = result[:peer_qos] || 0
85
101
  @peer_qos_hash = result[:peer_qos_hash] || ""
86
102
  @peer_properties = result[:peer_properties]
103
+ @peer_major = result[:peer_major]
104
+ @peer_minor = result[:peer_minor]
87
105
 
88
106
  unless @peer_socket_type
89
107
  raise Error, "peer READY missing Socket-Type"
@@ -187,15 +205,19 @@ module Protocol
187
205
  # @raise [EOFError] if connection is closed
188
206
  def receive_message
189
207
  frames = []
208
+
190
209
  loop do
191
210
  frame = read_frame
211
+
192
212
  if frame.command?
193
213
  yield frame if block_given?
194
214
  next
195
215
  end
216
+
196
217
  frames << frame.body.freeze
197
218
  break unless frame.more?
198
219
  end
220
+
199
221
  frames.freeze
200
222
  end
201
223
 
@@ -233,6 +255,7 @@ module Protocol
233
255
  close
234
256
  raise
235
257
  end
258
+
236
259
  touch_heartbeat
237
260
 
238
261
  frame = @mechanism.decrypt(frame) if @mechanism.encrypted?
@@ -248,6 +271,7 @@ module Protocol
248
271
  next
249
272
  end
250
273
  end
274
+
251
275
  return frame
252
276
  end
253
277
  end
@@ -315,9 +339,11 @@ module Protocol
315
339
  last = parts.size - 1
316
340
 
317
341
  i = 0
342
+
318
343
  while i < parts.size
319
344
  part = parts[i]
320
345
  more = i < last
346
+
321
347
  if encrypted
322
348
  body = part.encoding == Encoding::BINARY ? part : part.b
323
349
  @io.write(@mechanism.encrypt(body, more: more))
@@ -325,18 +351,23 @@ module Protocol
325
351
  body = part.encoding == Encoding::BINARY ? part : part.b
326
352
  size = body.bytesize
327
353
  flags = more ? Codec::Frame::FLAGS_MORE : 0
354
+
328
355
  buf.clear
356
+
329
357
  if size > Codec::Frame::SHORT_MAX
330
358
  [flags | Codec::Frame::FLAGS_LONG, size].pack("CQ>", buffer: buf)
331
359
  else
332
360
  [flags, size].pack("CC", buffer: buf)
333
361
  end
362
+
334
363
  @io.write(buf)
335
364
  @io.write(body)
336
365
  end
366
+
337
367
  i += 1
338
368
  end
339
369
  end
370
+
340
371
  end
341
372
  end
342
373
  end
@@ -233,10 +233,12 @@ module Protocol
233
233
 
234
234
  io.write(Codec::Greeting.encode(mechanism: MECHANISM_NAME, as_server: false))
235
235
  io.flush
236
- peer_greeting = Codec::Greeting.decode(io.read_exactly(Codec::Greeting::SIZE))
236
+ peer_greeting = Codec::Greeting.read_from(io)
237
237
  unless peer_greeting[:mechanism] == MECHANISM_NAME
238
238
  raise Error, "expected CURVE mechanism, got #{peer_greeting[:mechanism]}"
239
239
  end
240
+ @peer_major = peer_greeting[:major]
241
+ @peer_minor = peer_greeting[:minor]
240
242
 
241
243
 
242
244
  # --- HELLO ---
@@ -348,6 +350,8 @@ module Protocol
348
350
  peer_qos: peer_qos,
349
351
  peer_qos_hash: peer_qos_hash,
350
352
  peer_properties: props,
353
+ peer_major: @peer_major,
354
+ peer_minor: @peer_minor,
351
355
  }
352
356
  end
353
357
 
@@ -359,10 +363,12 @@ module Protocol
359
363
  def server_handshake!(io, socket_type:, identity:, qos: 0, qos_hash: "")
360
364
  io.write(Codec::Greeting.encode(mechanism: MECHANISM_NAME, as_server: true))
361
365
  io.flush
362
- peer_greeting = Codec::Greeting.decode(io.read_exactly(Codec::Greeting::SIZE))
366
+ peer_greeting = Codec::Greeting.read_from(io)
363
367
  unless peer_greeting[:mechanism] == MECHANISM_NAME
364
368
  raise Error, "expected CURVE mechanism, got #{peer_greeting[:mechanism]}"
365
369
  end
370
+ @peer_major = peer_greeting[:major]
371
+ @peer_minor = peer_greeting[:minor]
366
372
 
367
373
 
368
374
  # --- Read HELLO ---
@@ -514,6 +520,8 @@ module Protocol
514
520
  peer_qos: (props["X-QoS"] || "0").to_i,
515
521
  peer_qos_hash: props["X-QoS-Hash"] || "",
516
522
  peer_properties: props,
523
+ peer_major: @peer_major,
524
+ peer_minor: @peer_minor,
517
525
  }
518
526
  end
519
527
 
@@ -32,14 +32,13 @@ module Protocol
32
32
  # @param as_server [Boolean]
33
33
  # @param socket_type [String]
34
34
  # @param identity [String]
35
- # @return [Hash] { peer_socket_type:, peer_identity:, peer_qos:, peer_qos_hash:, peer_properties: }
35
+ # @return [Hash] { peer_socket_type:, peer_identity:, peer_qos:, peer_qos_hash:, peer_properties:, peer_major:, peer_minor: }
36
36
  # @raise [Error]
37
37
  def handshake!(io, as_server:, socket_type:, identity:, qos: 0, qos_hash: "")
38
38
  io.write(Codec::Greeting.encode(mechanism: MECHANISM_NAME, as_server: as_server))
39
39
  io.flush
40
40
 
41
- greeting_data = io.read_exactly(Codec::Greeting::SIZE)
42
- peer_greeting = Codec::Greeting.decode(greeting_data)
41
+ peer_greeting = Codec::Greeting.read_from(io)
43
42
 
44
43
  unless peer_greeting[:mechanism] == MECHANISM_NAME
45
44
  raise Error, "unsupported mechanism: #{peer_greeting[:mechanism]}"
@@ -81,6 +80,8 @@ module Protocol
81
80
  peer_qos: peer_qos,
82
81
  peer_qos_hash: peer_qos_hash,
83
82
  peer_properties: props,
83
+ peer_major: peer_greeting[:major],
84
+ peer_minor: peer_greeting[:minor],
84
85
  }
85
86
  end
86
87
 
@@ -47,8 +47,7 @@ module Protocol
47
47
  io.write(Codec::Greeting.encode(mechanism: MECHANISM_NAME, as_server: as_server))
48
48
  io.flush
49
49
 
50
- greeting_data = io.read_exactly(Codec::Greeting::SIZE)
51
- peer_greeting = Codec::Greeting.decode(greeting_data)
50
+ peer_greeting = Codec::Greeting.read_from(io)
52
51
 
53
52
  unless peer_greeting[:mechanism] == MECHANISM_NAME
54
53
  raise Error, "unsupported mechanism: #{peer_greeting[:mechanism]}"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Protocol
4
4
  module ZMTP
5
- VERSION = "0.7.1"
5
+ VERSION = "0.8.1"
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.7.1
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger
@@ -25,6 +25,7 @@ files:
25
25
  - lib/protocol/zmtp/codec/command.rb
26
26
  - lib/protocol/zmtp/codec/frame.rb
27
27
  - lib/protocol/zmtp/codec/greeting.rb
28
+ - lib/protocol/zmtp/codec/subscription.rb
28
29
  - lib/protocol/zmtp/connection.rb
29
30
  - lib/protocol/zmtp/error.rb
30
31
  - lib/protocol/zmtp/mechanism/curve.rb