protocol-zmtp 0.8.1 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81270e34092271f0e0aaceed0ce882885751def59b75716872d074952e4d6b41
4
- data.tar.gz: ccbf823376d70d8654384b99c1bb30b270dc9493cd432c524a3caab175eaa364
3
+ metadata.gz: 303ee019ebede5cddca93226c136233c19b7ec6e54cad7ef0deba5f5a5f6e347
4
+ data.tar.gz: 229f46c1a06f1fe7d02a3b1a518fee8958786959fa0b43fe6587d47392c9d602
5
5
  SHA512:
6
- metadata.gz: f148b0ac01768320f35f2edf7ac67f3d51ad3e5daa4a9c9dafccb57168e8e9f616f6f27425c00b82edcd7e0af898188ae37ea3a78f404a786bdd9a94c01cb675
7
- data.tar.gz: bdab4d4b763394ea1f11aec4503a74d68f499671c93851adc96c2e34bcd7134de837b71941119a1aa029a0966c4ed4cfea8def3f5140244e50f9c2a9b3b0355d
6
+ metadata.gz: 38345cf8abc4d1ed198a90abe714d15dd5cc8877fd22ba25f672c9ff4c02a36e9c7754a6c3998fc3b0cf9dd8972e54595be4c953c4e18d0acecaf3363b5971d2
7
+ data.tar.gz: 50c74c72d1926d1bd3756cf130d57039337f75f34c47d701b7b7a936e9f3d9b57e9f203e6b841b454b9de5d99624ada8239dbfb6e2f8cf8285ae991aa94b9828
@@ -70,20 +70,14 @@ module Protocol
70
70
  end
71
71
 
72
72
 
73
- # Builds a READY command with Socket-Type, Identity, optional X-QoS,
74
- # and any extra properties supplied by upper layers (e.g. an
75
- # extension that injects an `X-Compression` property).
73
+ # Builds a READY command with Socket-Type, Identity, and any
74
+ # extra properties supplied by upper layers (e.g. an extension
75
+ # that injects +X-QoS+ or +X-Compression+).
76
76
  #
77
- # @param qos [Integer] QoS level (0 = omitted)
78
- # @param qos_hash [String] supported hash algorithms in preference order (e.g. "xXsS")
79
- # @param metadata [Hash{String => String}] additional READY properties
77
+ # @param metadata [Hash{String => String}, nil] additional READY properties
80
78
  # @return [Command]
81
- def self.ready(socket_type:, identity: "", qos: 0, qos_hash: "", metadata: nil)
79
+ def self.ready(socket_type:, identity: "", metadata: nil)
82
80
  props = { "Socket-Type" => socket_type, "Identity" => identity }
83
- if qos > 0
84
- props["X-QoS"] = qos.to_s
85
- props["X-QoS-Hash"] = qos_hash unless qos_hash.empty?
86
- end
87
81
  props.merge!(metadata) if metadata && !metadata.empty?
88
82
  new("READY", encode_properties(props))
89
83
  end
@@ -28,51 +28,6 @@ module Protocol
28
28
  FLAG_BYTES = Array.new(256) { |i| i.chr.b.freeze }.freeze
29
29
 
30
30
 
31
- # @return [String] frame body (binary)
32
- attr_reader :body
33
-
34
-
35
- # @param body [String] frame body
36
- # @param more [Boolean] more frames follow
37
- # @param command [Boolean] this is a command frame
38
- def initialize(body, more: false, command: false)
39
- @body = body.encoding == Encoding::BINARY ? body : body.b
40
- @more = more
41
- @command = command
42
- end
43
-
44
-
45
- # @return [Boolean] true if more frames follow in this message
46
- def more?
47
- @more
48
- end
49
-
50
-
51
- # @return [Boolean] true if this is a command frame
52
- def command?
53
- @command
54
- end
55
-
56
-
57
- # Encodes to wire bytes.
58
- #
59
- # @return [String] binary wire representation (flags + size + body)
60
- def to_wire
61
- size = @body.bytesize
62
- flags = 0
63
- flags |= FLAGS_MORE if @more
64
- flags |= FLAGS_COMMAND if @command
65
-
66
- if size > SHORT_MAX
67
- buf = String.new(capacity: 9 + size, encoding: Encoding::BINARY)
68
- buf << FLAG_BYTES[flags | FLAGS_LONG] << [size].pack("Q>") << @body
69
- else
70
- buf = String.new(capacity: 2 + size, encoding: Encoding::BINARY)
71
- buf << FLAG_BYTES[flags] << FLAG_BYTES[size] << @body
72
- end
73
- end
74
-
75
-
76
31
  # Encodes a multi-part message into a single wire-format string.
77
32
  # The result can be written to multiple connections without
78
33
  # re-encoding each time (useful for fan-out patterns like PUB).
@@ -82,33 +37,38 @@ module Protocol
82
37
  #
83
38
  def self.encode_message(parts)
84
39
  if parts.size == 1
85
- s = parts[0].bytesize
86
- wire_size = s > SHORT_MAX ? 9 + s : 2 + s
40
+ s = parts.first.bytesize
41
+ wire = s > SHORT_MAX ? 9 + s : 2 + s
87
42
  else
88
- wire_size = 0
89
- j = 0
43
+ wire = 0
44
+ j = 0
45
+
90
46
  while j < parts.size
91
- s = parts[j].bytesize
92
- wire_size += s > SHORT_MAX ? 9 + s : 2 + s
93
- j += 1
47
+ s = parts[j].bytesize
48
+ wire += s > SHORT_MAX ? 9 + s : 2 + s
49
+ j += 1
94
50
  end
95
51
  end
96
52
 
97
- buf = String.new(capacity: wire_size, encoding: Encoding::BINARY)
53
+ buf = String.new(capacity: wire, encoding: Encoding::BINARY)
98
54
  last = parts.size - 1
99
55
  i = 0
100
56
 
101
57
  while i < parts.size
102
58
  body = parts[i]
103
- body = body.b unless body.encoding == Encoding::BINARY
104
59
  size = body.bytesize
105
60
  flags = i < last ? FLAGS_MORE : 0
106
61
 
107
62
  if size > SHORT_MAX
108
- buf << FLAG_BYTES[flags | FLAGS_LONG] << [size].pack("Q>") << body
63
+ buf << FLAG_BYTES[flags | FLAGS_LONG]
64
+ buf << [size].pack("Q>")
65
+ buf << body
109
66
  else
110
- buf << FLAG_BYTES[flags] << FLAG_BYTES[size] << body
67
+ buf << FLAG_BYTES[flags]
68
+ buf << FLAG_BYTES[size]
69
+ buf << body
111
70
  end
71
+
112
72
  i += 1
113
73
  end
114
74
 
@@ -118,49 +78,107 @@ module Protocol
118
78
 
119
79
  # Reads one frame from an IO-like object.
120
80
  #
121
- # @param io [#read_exactly] must support read_exactly(n)
81
+ # Uses #peek to buffer just enough header bytes (2 for short frames,
82
+ # 9 for long), then drains header + body in a single #read_exactly.
83
+ # This is 2 calls for both short and long frames, vs the naive 3 for
84
+ # long. A speculative read_exactly(9) would be unsafe: a <7-byte
85
+ # short frame at idle would hang waiting for bytes that never arrive,
86
+ # or consume bytes from the next frame on a mixed stream.
87
+ #
88
+ # @param io [#peek, #read_exactly]
122
89
  # @return [Frame]
123
90
  # @raise [Error] on invalid frame
124
91
  # @raise [EOFError] if the connection is closed
125
92
  def self.read_from(io, max_message_size: nil)
126
- # Every valid frame has at least 2 header bytes (flags + 1 size
127
- # byte for short frames, or flags + first size byte for long).
128
- # Fetching both up-front gives short frames a 2-call read path
129
- # (header + body) instead of 3.
130
- head = io.read_exactly(2)
131
- flags = head.getbyte(0)
93
+ buf = io.peek do |b|
94
+ next false if b.bytesize < 2
95
+ (b.getbyte(0) & FLAGS_LONG) == 0 || b.bytesize >= 9
96
+ end
97
+
98
+ raise EOFError, "Stream finished before reading frame header" if buf.bytesize < 2
132
99
 
100
+ flags = buf.getbyte(0)
133
101
  more = (flags & FLAGS_MORE) != 0
134
102
  long = (flags & FLAGS_LONG) != 0
135
103
  command = (flags & FLAGS_COMMAND) != 0
136
- size = long ? read_long_size(io, head.getbyte(1)) : head.getbyte(1)
104
+
105
+ if long
106
+ raise EOFError, "Stream finished before reading long frame size" if buf.bytesize < 9
107
+
108
+ size = (buf.getbyte(1) << 56) |
109
+ (buf.getbyte(2) << 48) |
110
+ (buf.getbyte(3) << 40) |
111
+ (buf.getbyte(4) << 32) |
112
+ (buf.getbyte(5) << 24) |
113
+ (buf.getbyte(6) << 16) |
114
+ (buf.getbyte(7) << 8) |
115
+ buf.getbyte(8)
116
+ header_size = 9
117
+ else
118
+ size = buf.getbyte(1)
119
+ header_size = 2
120
+ end
137
121
 
138
122
  if max_message_size && size > max_message_size
139
123
  raise Error, "frame size #{size} exceeds max_message_size #{max_message_size}"
140
124
  end
141
125
 
142
- body = size > 0 ? io.read_exactly(size) : EMPTY_BINARY
126
+ if size.zero?
127
+ io.read_exactly(header_size)
128
+ return new(EMPTY_BINARY, more: more, command: command)
129
+ end
143
130
 
144
- new(body, more: more, command: command)
131
+ wire = io.read_exactly(header_size + size)
132
+ new(wire.byteslice(header_size, size), more: more, command: command)
145
133
  end
146
134
 
147
135
 
148
- # Reads the remaining 7 bytes of a long frame's 8-byte big-endian
149
- # size field and combines them with +msb+ (already consumed as the
150
- # second byte of the 2-byte speculative header read).
151
- #
152
- # @param io [#read_exactly]
153
- # @param msb [Integer] first (most-significant) byte of the size
154
- # @return [Integer] full 64-bit frame size
136
+ # @return [String] frame body (binary)
137
+ attr_reader :body
138
+
139
+
140
+ # @param body [String] frame body
141
+ # @param more [Boolean] more frames follow
142
+ # @param command [Boolean] this is a command frame
143
+ def initialize(body, more: false, command: false)
144
+ @body = body
145
+ @more = more
146
+ @command = command
147
+ end
148
+
149
+
150
+ # @return [Boolean] true if more frames follow in this message
151
+ def more?
152
+ @more
153
+ end
154
+
155
+
156
+ # @return [Boolean] true if this is a command frame
157
+ def command?
158
+ @command
159
+ end
160
+
161
+
162
+ # Encodes to wire bytes.
155
163
  #
156
- def self.read_long_size(io, msb)
157
- rest = io.read_exactly(7)
158
-
159
- (msb << 56) |
160
- (rest.getbyte(0) << 48) | (rest.getbyte(1) << 40) |
161
- (rest.getbyte(2) << 32) | (rest.getbyte(3) << 24) |
162
- (rest.getbyte(4) << 16) | (rest.getbyte(5) << 8) |
163
- rest.getbyte(6)
164
+ # @return [String] binary wire representation (flags + size + body)
165
+ def to_wire
166
+ size = @body.bytesize
167
+ flags = 0
168
+ flags |= FLAGS_MORE if @more
169
+ flags |= FLAGS_COMMAND if @command
170
+
171
+ if size > SHORT_MAX
172
+ buf = String.new(capacity: 9 + size, encoding: Encoding::BINARY)
173
+ buf << FLAG_BYTES[flags | FLAGS_LONG]
174
+ buf << [size].pack("Q>")
175
+ buf << @body
176
+ else
177
+ buf = String.new(capacity: 2 + size, encoding: Encoding::BINARY)
178
+ buf << FLAG_BYTES[flags]
179
+ buf << FLAG_BYTES[size]
180
+ buf << @body
181
+ end
164
182
  end
165
183
 
166
184
  end
@@ -20,16 +20,14 @@ module Protocol
20
20
  attr_reader :peer_identity
21
21
 
22
22
 
23
- # @return [Integer] peer's QoS level (from READY handshake, 0 if absent)
24
- attr_reader :peer_qos
25
-
26
-
27
- # @return [String] peer's supported hash algorithms in preference order
28
- attr_reader :peer_qos_hash
23
+ # @return [Object, nil] peer's CURVE long-term public key post-handshake
24
+ # (+crypto::PublicKey+ when the mechanism is CURVE; +nil+ for NULL).
25
+ attr_reader :peer_public_key
29
26
 
30
27
 
31
28
  # @return [Hash{String => String}, nil] full peer READY property hash
32
- # (set after a successful handshake; nil before)
29
+ # (set after a successful handshake; nil before).
30
+ # Upper layers extract their own X-* properties from here.
33
31
  attr_reader :peer_properties
34
32
 
35
33
 
@@ -42,7 +40,7 @@ module Protocol
42
40
  attr_reader :peer_minor
43
41
 
44
42
 
45
- # @return [Object] transport IO (#read_exactly, #write, #flush, #close)
43
+ # @return [Object] transport IO (#peek, #read_exactly, #write, #flush, #close)
46
44
  attr_reader :io
47
45
 
48
46
 
@@ -50,14 +48,18 @@ module Protocol
50
48
  attr_reader :last_received_at
51
49
 
52
50
 
53
- # @param io [#read_exactly, #write, #flush, #close] transport IO
51
+ # @param io [#peek, #read_exactly, #write, #flush, #close] transport IO
54
52
  # @param socket_type [String] our socket type name (e.g. "REQ")
55
53
  # @param identity [String] our identity
56
54
  # @param as_server [Boolean] whether we are the server side
57
55
  # @param mechanism [Mechanism::Null, Mechanism::Curve] security mechanism
58
56
  # @param max_message_size [Integer, nil] max frame size in bytes, nil = unlimited
57
+ # @param opts [Hash{String => String}] extra READY properties to
58
+ # advertise (e.g. +"X-QoS" => "1"+). Upper-layer extensions use
59
+ # this to inject their own negotiated properties without the
60
+ # codec needing to know about them.
59
61
  def initialize(io, socket_type:, identity: "", as_server: false,
60
- mechanism: nil, max_message_size: nil, qos: 0, qos_hash: "")
62
+ mechanism: nil, max_message_size: nil, **opts)
61
63
  @io = io
62
64
  @socket_type = socket_type
63
65
  @identity = identity
@@ -65,13 +67,11 @@ module Protocol
65
67
  @mechanism = mechanism || Mechanism::Null.new
66
68
  @peer_socket_type = nil
67
69
  @peer_identity = nil
68
- @peer_qos = nil
69
- @peer_qos_hash = nil
70
+ @peer_public_key = nil
70
71
  @peer_properties = nil
71
72
  @peer_major = nil
72
73
  @peer_minor = nil
73
- @qos = qos
74
- @qos_hash = qos_hash
74
+ @metadata = opts.empty? ? nil : opts.transform_keys(&:to_s)
75
75
  @mutex = Mutex.new
76
76
  @max_message_size = max_message_size
77
77
  @last_received_at = nil
@@ -92,13 +92,11 @@ module Protocol
92
92
  as_server: @as_server,
93
93
  socket_type: @socket_type,
94
94
  identity: @identity,
95
- qos: @qos,
96
- qos_hash: @qos_hash
95
+ metadata: @metadata
97
96
 
98
97
  @peer_socket_type = result[:peer_socket_type]
99
98
  @peer_identity = result[:peer_identity]
100
- @peer_qos = result[:peer_qos] || 0
101
- @peer_qos_hash = result[:peer_qos_hash] || ""
99
+ @peer_public_key = result[:peer_public_key]
102
100
  @peer_properties = result[:peer_properties]
103
101
  @peer_major = result[:peer_major]
104
102
  @peer_minor = result[:peer_minor]
@@ -114,6 +112,17 @@ module Protocol
114
112
  end
115
113
 
116
114
 
115
+ # Returns a {PeerInfo} value bundling the peer's CURVE public key
116
+ # and identity for use as a stable per-peer key (frozen, hash-usable).
117
+ # Nil before the handshake has completed.
118
+ #
119
+ # @return [PeerInfo, nil]
120
+ def peer_info
121
+ return nil unless @peer_socket_type
122
+ PeerInfo.new(public_key: @peer_public_key, identity: @peer_identity)
123
+ end
124
+
125
+
117
126
  # Sends a multi-frame message (write + flush).
118
127
  #
119
128
  # @param parts [Array<String>] message frames
@@ -214,11 +223,11 @@ module Protocol
214
223
  next
215
224
  end
216
225
 
217
- frames << frame.body.freeze
226
+ frames << frame.body
218
227
  break unless frame.more?
219
228
  end
220
229
 
221
- frames.freeze
230
+ frames
222
231
  end
223
232
 
224
233
 
@@ -23,10 +23,6 @@ module Protocol
23
23
  class Curve
24
24
  MECHANISM_NAME = "CURVE"
25
25
 
26
- # Extra READY/INITIATE properties merged in by extensions
27
- # (e.g. ZMTP-Zstd's X-Compression). nil = none.
28
- attr_accessor :metadata
29
-
30
26
 
31
27
  # Nonce prefixes.
32
28
  NONCE_PREFIX_HELLO = "CurveZMQHELLO---"
@@ -100,10 +96,9 @@ module Protocol
100
96
  end
101
97
  end
102
98
 
103
- @session_box = nil
104
- @send_nonce = 0
105
- @recv_nonce = -1
106
- @metadata = nil
99
+ @session_box = nil
100
+ @send_nonce = 0
101
+ @recv_nonce = -1
107
102
  end
108
103
 
109
104
 
@@ -139,15 +134,14 @@ module Protocol
139
134
  # @param as_server [Boolean] ignored -- uses the value from #initialize
140
135
  # @param socket_type [String] our socket type name
141
136
  # @param identity [String] our identity
142
- # @param qos [Integer] QoS level
143
- # @param qos_hash [String] supported hash algorithms
144
- # @return [Hash] { peer_socket_type:, peer_identity:, peer_qos:, peer_qos_hash: }
137
+ # @param metadata [Hash{String => String}, nil] extra READY properties
138
+ # @return [Hash] { peer_socket_type:, peer_identity:, peer_properties: }
145
139
  # @raise [Error] on handshake failure
146
- def handshake!(io, as_server:, socket_type:, identity:, qos: 0, qos_hash: "")
140
+ def handshake!(io, as_server:, socket_type:, identity:, metadata: nil)
147
141
  if @as_server
148
- server_handshake!(io, socket_type:, identity:, qos:, qos_hash:)
142
+ server_handshake!(io, socket_type:, identity:, metadata:)
149
143
  else
150
- client_handshake!(io, socket_type:, identity:, qos:, qos_hash:)
144
+ client_handshake!(io, socket_type:, identity:, metadata:)
151
145
  end
152
146
  end
153
147
 
@@ -227,7 +221,7 @@ module Protocol
227
221
  # Client-side handshake
228
222
  # ----------------------------------------------------------------
229
223
 
230
- def client_handshake!(io, socket_type:, identity:, qos: 0, qos_hash: "")
224
+ def client_handshake!(io, socket_type:, identity:, metadata: nil)
231
225
  cn_secret = @crypto::PrivateKey.generate
232
226
  cn_public = cn_secret.public_key
233
227
 
@@ -288,18 +282,14 @@ module Protocol
288
282
  vouch = @crypto::Box.new(sn_public, @permanent_secret).encrypt(vouch_nonce, vouch_plaintext)
289
283
 
290
284
  props = { "Socket-Type" => socket_type, "Identity" => identity }
291
- if qos > 0
292
- props["X-QoS"] = qos.to_s
293
- props["X-QoS-Hash"] = qos_hash unless qos_hash.empty?
294
- end
295
- props.merge!(@metadata) if @metadata
296
- metadata = Codec::Command.encode_properties(props)
285
+ props.merge!(metadata) if metadata && !metadata.empty?
286
+ metadata_bytes = Codec::Command.encode_properties(props)
297
287
 
298
288
  initiate_box_plaintext = "".b
299
289
  initiate_box_plaintext << @permanent_public.to_s
300
290
  initiate_box_plaintext << vouch_nonce.byteslice(8, 16)
301
291
  initiate_box_plaintext << vouch
302
- initiate_box_plaintext << metadata
292
+ initiate_box_plaintext << metadata_bytes
303
293
 
304
294
  init_short_nonce = [1].pack("Q>")
305
295
  init_nonce = NONCE_PREFIX_INITIATE + init_short_nonce
@@ -336,8 +326,6 @@ module Protocol
336
326
  props = Codec::Command.decode_properties(r_plaintext)
337
327
  peer_socket_type = props["Socket-Type"]
338
328
  peer_identity = props["Identity"] || ""
339
- peer_qos = (props["X-QoS"] || "0").to_i
340
- peer_qos_hash = props["X-QoS-Hash"] || ""
341
329
 
342
330
  @session_box = session
343
331
  @send_nonce = 1
@@ -347,8 +335,7 @@ module Protocol
347
335
  {
348
336
  peer_socket_type: peer_socket_type,
349
337
  peer_identity: peer_identity,
350
- peer_qos: peer_qos,
351
- peer_qos_hash: peer_qos_hash,
338
+ peer_public_key: @server_public,
352
339
  peer_properties: props,
353
340
  peer_major: @peer_major,
354
341
  peer_minor: @peer_minor,
@@ -360,7 +347,7 @@ module Protocol
360
347
  # Server-side handshake
361
348
  # ----------------------------------------------------------------
362
349
 
363
- def server_handshake!(io, socket_type:, identity:, qos: 0, qos_hash: "")
350
+ def server_handshake!(io, socket_type:, identity:, metadata: nil)
364
351
  io.write(Codec::Greeting.encode(mechanism: MECHANISM_NAME, as_server: true))
365
352
  io.flush
366
353
  peer_greeting = Codec::Greeting.read_from(io)
@@ -478,7 +465,7 @@ module Protocol
478
465
  end
479
466
 
480
467
  if @authenticator
481
- peer = PeerInfo.new(public_key: client_permanent)
468
+ peer = PeerInfo.new(public_key: client_permanent, identity: "")
482
469
  unless @authenticator.call(peer)
483
470
  send_error(io, "client key not authorized")
484
471
  raise Error, "client key not authorized"
@@ -488,11 +475,7 @@ module Protocol
488
475
 
489
476
  # --- READY ---
490
477
  ready_props = { "Socket-Type" => socket_type, "Identity" => identity }
491
- if qos > 0
492
- ready_props["X-QoS"] = qos.to_s
493
- ready_props["X-QoS-Hash"] = qos_hash unless qos_hash.empty?
494
- end
495
- ready_props.merge!(@metadata) if @metadata
478
+ ready_props.merge!(metadata) if metadata && !metadata.empty?
496
479
  ready_metadata = Codec::Command.encode_properties(ready_props)
497
480
 
498
481
  r_short_nonce = [1].pack("Q>")
@@ -517,8 +500,7 @@ module Protocol
517
500
  {
518
501
  peer_socket_type: props["Socket-Type"],
519
502
  peer_identity: props["Identity"] || "",
520
- peer_qos: (props["X-QoS"] || "0").to_i,
521
- peer_qos_hash: props["X-QoS-Hash"] || "",
503
+ peer_public_key: client_permanent,
522
504
  peer_properties: props,
523
505
  peer_major: @peer_major,
524
506
  peer_minor: @peer_minor,
@@ -11,16 +11,6 @@ module Protocol
11
11
  class Null
12
12
  MECHANISM_NAME = "NULL"
13
13
 
14
- # Extra READY properties an upper layer (e.g. an OMQ extension)
15
- # wants this side to advertise. Mutated before #handshake!.
16
- # @return [Hash{String => String}]
17
- attr_accessor :metadata
18
-
19
-
20
- def initialize
21
- @metadata = nil
22
- end
23
-
24
14
 
25
15
  # Performs the full NULL handshake over +io+.
26
16
  #
@@ -32,9 +22,10 @@ module Protocol
32
22
  # @param as_server [Boolean]
33
23
  # @param socket_type [String]
34
24
  # @param identity [String]
35
- # @return [Hash] { peer_socket_type:, peer_identity:, peer_qos:, peer_qos_hash:, peer_properties:, peer_major:, peer_minor: }
25
+ # @param metadata [Hash{String => String}, nil] extra READY properties
26
+ # @return [Hash] { peer_socket_type:, peer_identity:, peer_public_key:, peer_properties:, peer_major:, peer_minor: }
36
27
  # @raise [Error]
37
- def handshake!(io, as_server:, socket_type:, identity:, qos: 0, qos_hash: "")
28
+ def handshake!(io, as_server:, socket_type:, identity:, metadata: nil)
38
29
  io.write(Codec::Greeting.encode(mechanism: MECHANISM_NAME, as_server: as_server))
39
30
  io.flush
40
31
 
@@ -45,11 +36,9 @@ module Protocol
45
36
  end
46
37
 
47
38
  ready_cmd = Codec::Command.ready(
48
- socket_type: socket_type,
49
- identity: identity,
50
- qos: qos,
51
- qos_hash: qos_hash,
52
- metadata: @metadata,
39
+ socket_type: socket_type,
40
+ identity: identity,
41
+ metadata: metadata,
53
42
  )
54
43
  io.write(ready_cmd.to_frame.to_wire)
55
44
  io.flush
@@ -67,8 +56,6 @@ module Protocol
67
56
  props = peer_cmd.properties
68
57
  peer_socket_type = props["Socket-Type"]
69
58
  peer_identity = props["Identity"] || ""
70
- peer_qos = (props["X-QoS"] || "0").to_i
71
- peer_qos_hash = props["X-QoS-Hash"] || ""
72
59
 
73
60
  unless peer_socket_type
74
61
  raise Error, "peer READY missing Socket-Type"
@@ -77,8 +64,7 @@ module Protocol
77
64
  {
78
65
  peer_socket_type: peer_socket_type,
79
66
  peer_identity: peer_identity,
80
- peer_qos: peer_qos,
81
- peer_qos_hash: peer_qos_hash,
67
+ peer_public_key: nil,
82
68
  peer_properties: props,
83
69
  peer_major: peer_greeting[:major],
84
70
  peer_minor: peer_greeting[:minor],
@@ -16,22 +16,15 @@ module Protocol
16
16
  MECHANISM_NAME = "PLAIN"
17
17
 
18
18
 
19
- # Extra READY/INITIATE properties an upper layer wants this side to
20
- # advertise. Mutated before #handshake!.
21
- # @return [Hash{String => String}]
22
- attr_accessor :metadata
23
-
24
-
25
19
  # @param username [String] client username (max 255 bytes)
26
20
  # @param password [String] client password (max 255 bytes)
27
21
  # @param authenticator [#call, nil] server-side credential verifier;
28
22
  # called as +authenticator.call(username, password)+ and must return
29
23
  # truthy to accept the connection. When +nil+, all credentials pass.
30
24
  def initialize(username: "", password: "", authenticator: nil)
31
- @username = username
32
- @password = password
33
- @authenticator = authenticator
34
- @metadata = nil
25
+ @username = username
26
+ @password = password
27
+ @authenticator = authenticator
35
28
  end
36
29
 
37
30
 
@@ -41,9 +34,10 @@ module Protocol
41
34
  # @param as_server [Boolean]
42
35
  # @param socket_type [String]
43
36
  # @param identity [String]
44
- # @return [Hash] { peer_socket_type:, peer_identity: }
37
+ # @param metadata [Hash{String => String}, nil] extra READY properties
38
+ # @return [Hash] { peer_socket_type:, peer_identity:, peer_properties: }
45
39
  # @raise [Error]
46
- def handshake!(io, as_server:, socket_type:, identity:, qos: 0, qos_hash: "")
40
+ def handshake!(io, as_server:, socket_type:, identity:, metadata: nil)
47
41
  io.write(Codec::Greeting.encode(mechanism: MECHANISM_NAME, as_server: as_server))
48
42
  io.flush
49
43
 
@@ -54,11 +48,9 @@ module Protocol
54
48
  end
55
49
 
56
50
  if as_server
57
- server_handshake! io, socket_type: socket_type, identity: identity,
58
- qos: qos, qos_hash: qos_hash
51
+ server_handshake! io, socket_type: socket_type, identity: identity, metadata: metadata
59
52
  else
60
- client_handshake! io, socket_type: socket_type, identity: identity,
61
- qos: qos, qos_hash: qos_hash
53
+ client_handshake! io, socket_type: socket_type, identity: identity, metadata: metadata
62
54
  end
63
55
  end
64
56
 
@@ -72,7 +64,7 @@ module Protocol
72
64
  private
73
65
 
74
66
 
75
- def client_handshake!(io, socket_type:, identity:, qos: 0, qos_hash: "")
67
+ def client_handshake!(io, socket_type:, identity:, metadata: nil)
76
68
  send_command(io, hello_command)
77
69
 
78
70
  cmd = read_command(io)
@@ -80,15 +72,9 @@ module Protocol
80
72
 
81
73
  props = {
82
74
  "Socket-Type" => socket_type,
83
- "Identity" => identity
75
+ "Identity" => identity,
84
76
  }
85
-
86
- if qos > 0
87
- props["X-QoS"] = qos.to_s
88
- props["X-QoS-Hash"] = qos_hash unless qos_hash.empty?
89
- end
90
-
91
- props.merge!(@metadata) if @metadata && !@metadata.empty?
77
+ props.merge!(metadata) if metadata && !metadata.empty?
92
78
  initiate = Codec::Command.new("INITIATE", Codec::Command.encode_properties(props))
93
79
  send_command(io, initiate)
94
80
 
@@ -99,7 +85,7 @@ module Protocol
99
85
  end
100
86
 
101
87
 
102
- def server_handshake!(io, socket_type:, identity:, qos: 0, qos_hash: "")
88
+ def server_handshake!(io, socket_type:, identity:, metadata: nil)
103
89
  cmd = read_command(io)
104
90
  raise Error, "expected HELLO, got #{cmd.name}" unless cmd.name == "HELLO"
105
91
 
@@ -116,8 +102,7 @@ module Protocol
116
102
 
117
103
  peer_info = extract_peer_info(cmd)
118
104
 
119
- ready = Codec::Command.ready socket_type: socket_type, identity: identity,
120
- qos: qos, qos_hash: qos_hash, metadata: @metadata
105
+ ready = Codec::Command.ready socket_type: socket_type, identity: identity, metadata: metadata
121
106
  send_command io, ready
122
107
 
123
108
  peer_info
@@ -165,9 +150,7 @@ module Protocol
165
150
  {
166
151
  peer_socket_type: peer_socket_type,
167
152
  peer_identity: props["Identity"] || "",
168
- peer_qos: (props["X-QoS"] || "0").to_i,
169
- peer_qos_hash: props["X-QoS-Hash"] || "",
170
- peer_properties: props
153
+ peer_properties: props,
171
154
  }
172
155
  end
173
156
 
@@ -2,8 +2,19 @@
2
2
 
3
3
  module Protocol
4
4
  module ZMTP
5
- # Context passed to an authenticator during authentication.
6
- # +public_key+ is a +crypto::PublicKey+ instance.
7
- PeerInfo = Data.define(:public_key)
5
+ # Post-handshake identity of the peer on a {Connection}.
6
+ #
7
+ # - +public_key+ is the peer's CURVE long-term +crypto::PublicKey+,
8
+ # or +nil+ when the mechanism does not authenticate a key (e.g. NULL).
9
+ # - +identity+ is the peer's +ZMQ_IDENTITY+ string, or +""+ when no
10
+ # identity was advertised in the READY metadata.
11
+ #
12
+ # Used as a peer key by upper layers (e.g. omq-qos levels 2/3): the
13
+ # whole value object is frozen and equality-comparable, so it can be
14
+ # stored directly in a Hash without picking one anchor over the other.
15
+ #
16
+ # Also passed to CURVE authenticators during the handshake (at that
17
+ # point +identity+ is +""+, since it arrives post-auth).
18
+ PeerInfo = Data.define(:public_key, :identity)
8
19
  end
9
20
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Protocol
4
4
  module ZMTP
5
- VERSION = "0.8.1"
5
+ VERSION = "0.10.0"
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.8.1
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger