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 +4 -4
- data/lib/protocol/zmtp/codec/command.rb +5 -11
- data/lib/protocol/zmtp/codec/frame.rb +99 -81
- data/lib/protocol/zmtp/connection.rb +29 -20
- data/lib/protocol/zmtp/mechanism/curve.rb +17 -35
- data/lib/protocol/zmtp/mechanism/null.rb +7 -21
- data/lib/protocol/zmtp/mechanism/plain.rb +14 -31
- data/lib/protocol/zmtp/peer_info.rb +14 -3
- data/lib/protocol/zmtp/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 303ee019ebede5cddca93226c136233c19b7ec6e54cad7ef0deba5f5a5f6e347
|
|
4
|
+
data.tar.gz: 229f46c1a06f1fe7d02a3b1a518fee8958786959fa0b43fe6587d47392c9d602
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
74
|
-
#
|
|
75
|
-
#
|
|
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
|
|
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: "",
|
|
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
|
|
86
|
-
|
|
40
|
+
s = parts.first.bytesize
|
|
41
|
+
wire = s > SHORT_MAX ? 9 + s : 2 + s
|
|
87
42
|
else
|
|
88
|
-
|
|
89
|
-
j
|
|
43
|
+
wire = 0
|
|
44
|
+
j = 0
|
|
45
|
+
|
|
90
46
|
while j < parts.size
|
|
91
|
-
s
|
|
92
|
-
|
|
93
|
-
j
|
|
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:
|
|
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]
|
|
63
|
+
buf << FLAG_BYTES[flags | FLAGS_LONG]
|
|
64
|
+
buf << [size].pack("Q>")
|
|
65
|
+
buf << body
|
|
109
66
|
else
|
|
110
|
-
buf << FLAG_BYTES[flags]
|
|
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
|
-
#
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
+
if size.zero?
|
|
127
|
+
io.read_exactly(header_size)
|
|
128
|
+
return new(EMPTY_BINARY, more: more, command: command)
|
|
129
|
+
end
|
|
143
130
|
|
|
144
|
-
|
|
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
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
# @param
|
|
153
|
-
# @param
|
|
154
|
-
# @
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 [
|
|
24
|
-
|
|
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,
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
|
226
|
+
frames << frame.body
|
|
218
227
|
break unless frame.more?
|
|
219
228
|
end
|
|
220
229
|
|
|
221
|
-
frames
|
|
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
|
|
104
|
-
@send_nonce
|
|
105
|
-
@recv_nonce
|
|
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
|
|
143
|
-
# @
|
|
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:,
|
|
140
|
+
def handshake!(io, as_server:, socket_type:, identity:, metadata: nil)
|
|
147
141
|
if @as_server
|
|
148
|
-
server_handshake!(io, socket_type:, identity:,
|
|
142
|
+
server_handshake!(io, socket_type:, identity:, metadata:)
|
|
149
143
|
else
|
|
150
|
-
client_handshake!(io, socket_type:, identity:,
|
|
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:,
|
|
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
|
|
292
|
-
|
|
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 <<
|
|
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
|
-
|
|
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:,
|
|
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
|
|
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
|
-
|
|
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
|
-
# @
|
|
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:,
|
|
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:
|
|
49
|
-
identity:
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
32
|
-
@password
|
|
33
|
-
@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
|
-
# @
|
|
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:,
|
|
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:,
|
|
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:,
|
|
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
|
-
|
|
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
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
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
|