omq 0.7.0 → 0.8.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/CHANGELOG.md +38 -1
- data/lib/omq/version.rb +1 -1
- data/lib/omq/zmtp/engine.rb +35 -9
- data/lib/omq/zmtp.rb +16 -13
- metadata +15 -8
- data/lib/omq/zmtp/codec/command.rb +0 -210
- data/lib/omq/zmtp/codec/frame.rb +0 -89
- data/lib/omq/zmtp/codec/greeting.rb +0 -78
- data/lib/omq/zmtp/codec.rb +0 -18
- data/lib/omq/zmtp/connection.rb +0 -282
- data/lib/omq/zmtp/mechanism/null.rb +0 -72
- data/lib/omq/zmtp/valid_peers.rb +0 -29
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 852e5a45a7b9f61b32004aa2079e1289291c3ac2ecc5efeb78911d88d3b63b2e
|
|
4
|
+
data.tar.gz: b475a15807b8e98d4a852ec78b07ecaa4a0b15e68d3aec2fd9b2702533cb5d5f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0afb8f440937ae11bebe4ce3ecc1df599103f27300ed721b14437d708bd1cba1cd5b0aeb6873bd7281d054c42f6353abc29b1c8c542f4708152c2ad6b047b5f0
|
|
7
|
+
data.tar.gz: b3477d54c520f7ed09dc4647b84841c9937d863036c4301afc3110bd959fd794bbd91db56d0204f317cd66ef62bf63ad8b1772521dc48f96912ae01e9bea8f81
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 — 2026-03-31
|
|
4
|
+
|
|
5
|
+
### Breaking
|
|
6
|
+
|
|
7
|
+
- **CURVE mechanism moved to protocol-zmtp** — `OMQ::ZMTP::Mechanism::Curve`
|
|
8
|
+
is now `Protocol::ZMTP::Mechanism::Curve` with a required `crypto:` parameter.
|
|
9
|
+
Pass `crypto: RbNaCl` (libsodium) or `crypto: Nuckle` (pure Ruby). The
|
|
10
|
+
omq-curve and omq-kurve gems are superseded.
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
# Before (omq-curve)
|
|
14
|
+
require "omq/curve"
|
|
15
|
+
rep.mechanism = OMQ::Curve.server(pub, sec)
|
|
16
|
+
|
|
17
|
+
# After (protocol-zmtp + any NaCl backend)
|
|
18
|
+
require "protocol/zmtp/mechanism/curve"
|
|
19
|
+
require "nuckle" # or: require "rbnacl"
|
|
20
|
+
rep.mechanism = Protocol::ZMTP::Mechanism::Curve.server(pub, sec, crypto: Nuckle)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **Protocol layer extracted into protocol-zmtp gem** — Codec (Frame,
|
|
26
|
+
Greeting, Command), Connection, Mechanism::Null, Mechanism::Curve,
|
|
27
|
+
ValidPeers, and Z85 now live in the
|
|
28
|
+
[protocol-zmtp](https://github.com/paddor/protocol-zmtp) gem. OMQ
|
|
29
|
+
re-exports them under `OMQ::ZMTP::` for backwards compatibility.
|
|
30
|
+
protocol-zmtp has zero runtime dependencies.
|
|
31
|
+
- **Unified CURVE mechanism** — one implementation with a pluggable
|
|
32
|
+
`crypto:` backend replaces the two near-identical copies in omq-curve
|
|
33
|
+
(RbNaCl) and omq-kurve (Nuckle). 1,088 → 467 lines (57% reduction).
|
|
34
|
+
- **Heartbeat ownership** — `Connection#start_heartbeat` removed.
|
|
35
|
+
Connection tracks timestamps only; the engine drives the PING/PONG loop.
|
|
36
|
+
- **CI no longer needs libsodium** — CURVE tests use
|
|
37
|
+
[nuckle](https://github.com/paddor/nuckle) (pure Ruby) by default.
|
|
38
|
+
Cross-backend interop tests run when rbnacl is available.
|
|
39
|
+
|
|
3
40
|
## 0.7.0 — 2026-03-30
|
|
4
41
|
|
|
5
42
|
### Breaking
|
|
@@ -502,4 +539,4 @@ Initial release. Pure Ruby implementation of ZMTP 3.1 (ZeroMQ) using Async.
|
|
|
502
539
|
- Linger on close (drain send queue before closing)
|
|
503
540
|
- `max_message_size` enforcement
|
|
504
541
|
- Works inside Async reactors or standalone (shared IO thread)
|
|
505
|
-
- Optional CURVE encryption via the [
|
|
542
|
+
- Optional CURVE encryption via the [protocol-zmtp](https://github.com/paddor/protocol-zmtp) gem
|
data/lib/omq/version.rb
CHANGED
data/lib/omq/zmtp/engine.rb
CHANGED
|
@@ -432,17 +432,14 @@ module OMQ
|
|
|
432
432
|
def setup_connection(io, as_server:, endpoint: nil, done: nil)
|
|
433
433
|
conn = Connection.new(
|
|
434
434
|
io,
|
|
435
|
-
socket_type:
|
|
436
|
-
identity:
|
|
437
|
-
as_server:
|
|
438
|
-
mechanism:
|
|
439
|
-
|
|
440
|
-
heartbeat_ttl: @options.heartbeat_ttl,
|
|
441
|
-
heartbeat_timeout: @options.heartbeat_timeout,
|
|
442
|
-
max_message_size: @options.max_message_size,
|
|
435
|
+
socket_type: @socket_type.to_s,
|
|
436
|
+
identity: @options.identity,
|
|
437
|
+
as_server: as_server,
|
|
438
|
+
mechanism: @options.mechanism&.dup,
|
|
439
|
+
max_message_size: @options.max_message_size,
|
|
443
440
|
)
|
|
444
441
|
conn.handshake!
|
|
445
|
-
conn
|
|
442
|
+
start_heartbeat(conn)
|
|
446
443
|
@connections << conn
|
|
447
444
|
@connection_endpoints[conn] = endpoint if endpoint
|
|
448
445
|
@connection_promises[conn] = done if done
|
|
@@ -454,6 +451,35 @@ module OMQ
|
|
|
454
451
|
end
|
|
455
452
|
|
|
456
453
|
|
|
454
|
+
# Spawns a heartbeat task for the connection.
|
|
455
|
+
# The connection only tracks timestamps — the engine drives the loop.
|
|
456
|
+
#
|
|
457
|
+
# @param conn [Connection]
|
|
458
|
+
# @return [void]
|
|
459
|
+
#
|
|
460
|
+
def start_heartbeat(conn)
|
|
461
|
+
interval = @options.heartbeat_interval
|
|
462
|
+
return unless interval
|
|
463
|
+
|
|
464
|
+
ttl = @options.heartbeat_ttl || interval
|
|
465
|
+
timeout = @options.heartbeat_timeout || interval
|
|
466
|
+
conn.touch_heartbeat
|
|
467
|
+
|
|
468
|
+
@tasks << Reactor.spawn_pump(annotation: "heartbeat") do
|
|
469
|
+
loop do
|
|
470
|
+
sleep interval
|
|
471
|
+
conn.send_command(Codec::Command.ping(ttl: ttl, context: "".b))
|
|
472
|
+
if conn.heartbeat_expired?(timeout)
|
|
473
|
+
conn.close
|
|
474
|
+
break
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
rescue *CONNECTION_LOST
|
|
478
|
+
# connection closed
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
|
|
457
483
|
# Spawns a background task that reconnects to the given endpoint
|
|
458
484
|
# with exponential back-off based on the reconnect_interval option.
|
|
459
485
|
#
|
data/lib/omq/zmtp.rb
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "protocol/zmtp"
|
|
4
|
+
require "io/stream"
|
|
5
|
+
|
|
3
6
|
module OMQ
|
|
4
7
|
# ZMTP 3.1 protocol internals.
|
|
5
8
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
9
|
+
# The wire protocol (codec, connection, mechanisms) lives in the
|
|
10
|
+
# protocol-zmtp gem. This module re-exports those classes under the
|
|
11
|
+
# OMQ::ZMTP namespace and adds the transport/routing/engine layers.
|
|
8
12
|
#
|
|
9
13
|
module ZMTP
|
|
10
|
-
|
|
14
|
+
# Re-export protocol-zmtp classes
|
|
15
|
+
Codec = Protocol::ZMTP::Codec
|
|
16
|
+
Connection = Protocol::ZMTP::Connection
|
|
17
|
+
ProtocolError = Protocol::ZMTP::Error
|
|
18
|
+
VALID_PEERS = Protocol::ZMTP::VALID_PEERS
|
|
19
|
+
|
|
20
|
+
module Mechanism
|
|
21
|
+
Null = Protocol::ZMTP::Mechanism::Null
|
|
22
|
+
Curve = Protocol::ZMTP::Mechanism::Curve if defined?(Protocol::ZMTP::Mechanism::Curve)
|
|
23
|
+
end
|
|
11
24
|
|
|
12
25
|
# Errors raised when a peer disconnects or resets the connection.
|
|
13
26
|
CONNECTION_LOST = [
|
|
@@ -32,24 +45,14 @@ module OMQ
|
|
|
32
45
|
end
|
|
33
46
|
end
|
|
34
47
|
|
|
35
|
-
# Constants
|
|
36
|
-
require_relative "zmtp/valid_peers"
|
|
37
|
-
|
|
38
|
-
# Codec
|
|
39
|
-
require_relative "zmtp/codec"
|
|
40
|
-
|
|
41
48
|
# Transport
|
|
42
49
|
require_relative "zmtp/transport/inproc"
|
|
43
50
|
require_relative "zmtp/transport/tcp"
|
|
44
51
|
require_relative "zmtp/transport/ipc"
|
|
45
52
|
|
|
46
|
-
# Mechanisms
|
|
47
|
-
require_relative "zmtp/mechanism/null"
|
|
48
|
-
|
|
49
53
|
# Core
|
|
50
54
|
require_relative "zmtp/reactor"
|
|
51
55
|
require_relative "zmtp/options"
|
|
52
|
-
require_relative "zmtp/connection"
|
|
53
56
|
require_relative "zmtp/routing"
|
|
54
57
|
require_relative "zmtp/routing/round_robin"
|
|
55
58
|
require_relative "zmtp/routing/fan_out"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: omq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: protocol-zmtp
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: async
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -83,13 +97,7 @@ files:
|
|
|
83
97
|
- lib/omq/socket.rb
|
|
84
98
|
- lib/omq/version.rb
|
|
85
99
|
- lib/omq/zmtp.rb
|
|
86
|
-
- lib/omq/zmtp/codec.rb
|
|
87
|
-
- lib/omq/zmtp/codec/command.rb
|
|
88
|
-
- lib/omq/zmtp/codec/frame.rb
|
|
89
|
-
- lib/omq/zmtp/codec/greeting.rb
|
|
90
|
-
- lib/omq/zmtp/connection.rb
|
|
91
100
|
- lib/omq/zmtp/engine.rb
|
|
92
|
-
- lib/omq/zmtp/mechanism/null.rb
|
|
93
101
|
- lib/omq/zmtp/options.rb
|
|
94
102
|
- lib/omq/zmtp/reactor.rb
|
|
95
103
|
- lib/omq/zmtp/readable.rb
|
|
@@ -119,7 +127,6 @@ files:
|
|
|
119
127
|
- lib/omq/zmtp/transport/inproc.rb
|
|
120
128
|
- lib/omq/zmtp/transport/ipc.rb
|
|
121
129
|
- lib/omq/zmtp/transport/tcp.rb
|
|
122
|
-
- lib/omq/zmtp/valid_peers.rb
|
|
123
130
|
- lib/omq/zmtp/writable.rb
|
|
124
131
|
homepage: https://github.com/zeromq/omq
|
|
125
132
|
licenses:
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OMQ
|
|
4
|
-
module ZMTP
|
|
5
|
-
module Codec
|
|
6
|
-
# ZMTP command encode/decode.
|
|
7
|
-
#
|
|
8
|
-
# Command frame body format:
|
|
9
|
-
# 1 byte: command name length
|
|
10
|
-
# N bytes: command name
|
|
11
|
-
# remaining: command data
|
|
12
|
-
#
|
|
13
|
-
# READY command data = property list:
|
|
14
|
-
# 1 byte: property name length
|
|
15
|
-
# N bytes: property name
|
|
16
|
-
# 4 bytes: property value length (big-endian)
|
|
17
|
-
# N bytes: property value
|
|
18
|
-
#
|
|
19
|
-
class Command
|
|
20
|
-
# @return [String] command name (e.g. "READY", "SUBSCRIBE")
|
|
21
|
-
#
|
|
22
|
-
attr_reader :name
|
|
23
|
-
|
|
24
|
-
# @return [String] command data (binary)
|
|
25
|
-
#
|
|
26
|
-
attr_reader :data
|
|
27
|
-
|
|
28
|
-
# @param name [String] command name
|
|
29
|
-
# @param data [String] command data
|
|
30
|
-
#
|
|
31
|
-
def initialize(name, data = "".b)
|
|
32
|
-
@name = name
|
|
33
|
-
@data = data.b
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Encodes as a command frame body.
|
|
37
|
-
#
|
|
38
|
-
# @return [String] binary body (name-length + name + data)
|
|
39
|
-
#
|
|
40
|
-
def to_body
|
|
41
|
-
name_bytes = @name.b
|
|
42
|
-
name_bytes.bytesize.chr.b + name_bytes + @data
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Encodes as a complete command Frame.
|
|
46
|
-
#
|
|
47
|
-
# @return [Frame]
|
|
48
|
-
#
|
|
49
|
-
def to_frame
|
|
50
|
-
Frame.new(to_body, command: true)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Decodes a command from a frame body.
|
|
54
|
-
#
|
|
55
|
-
# @param body [String] binary frame body
|
|
56
|
-
# @return [Command]
|
|
57
|
-
# @raise [ProtocolError] on malformed command
|
|
58
|
-
#
|
|
59
|
-
def self.from_body(body)
|
|
60
|
-
body = body.b
|
|
61
|
-
raise ProtocolError, "command body too short" if body.bytesize < 1
|
|
62
|
-
|
|
63
|
-
name_len = body.getbyte(0)
|
|
64
|
-
|
|
65
|
-
raise ProtocolError, "command name truncated" if body.bytesize < 1 + name_len
|
|
66
|
-
|
|
67
|
-
name = body.byteslice(1, name_len)
|
|
68
|
-
data = body.byteslice(1 + name_len..)
|
|
69
|
-
new(name, data)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Builds a READY command with Socket-Type and Identity properties.
|
|
73
|
-
#
|
|
74
|
-
# @param socket_type [String] e.g. "REQ", "REP", "PAIR"
|
|
75
|
-
# @param identity [String] peer identity (can be empty)
|
|
76
|
-
# @return [Command]
|
|
77
|
-
#
|
|
78
|
-
def self.ready(socket_type:, identity: "")
|
|
79
|
-
props = encode_properties(
|
|
80
|
-
"Socket-Type" => socket_type,
|
|
81
|
-
"Identity" => identity,
|
|
82
|
-
)
|
|
83
|
-
new("READY", props)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Builds a SUBSCRIBE command.
|
|
87
|
-
#
|
|
88
|
-
# @param prefix [String] subscription prefix
|
|
89
|
-
# @return [Command]
|
|
90
|
-
#
|
|
91
|
-
def self.subscribe(prefix)
|
|
92
|
-
new("SUBSCRIBE", prefix.b)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Builds a CANCEL command (unsubscribe).
|
|
96
|
-
#
|
|
97
|
-
# @param prefix [String] subscription prefix to cancel
|
|
98
|
-
# @return [Command]
|
|
99
|
-
#
|
|
100
|
-
def self.cancel(prefix)
|
|
101
|
-
new("CANCEL", prefix.b)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# Builds a JOIN command (RADIO/DISH group subscription).
|
|
105
|
-
#
|
|
106
|
-
# @param group [String] group name
|
|
107
|
-
# @return [Command]
|
|
108
|
-
#
|
|
109
|
-
def self.join(group)
|
|
110
|
-
new("JOIN", group.b)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# Builds a LEAVE command (RADIO/DISH group unsubscription).
|
|
114
|
-
#
|
|
115
|
-
# @param group [String] group name
|
|
116
|
-
# @return [Command]
|
|
117
|
-
#
|
|
118
|
-
def self.leave(group)
|
|
119
|
-
new("LEAVE", group.b)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Builds a PING command.
|
|
123
|
-
#
|
|
124
|
-
# @param ttl [Numeric] time-to-live in seconds (sent as deciseconds)
|
|
125
|
-
# @param context [String] optional context bytes (up to 16 bytes)
|
|
126
|
-
# @return [Command]
|
|
127
|
-
#
|
|
128
|
-
def self.ping(ttl: 0, context: "".b)
|
|
129
|
-
ttl_ds = (ttl * 10).to_i
|
|
130
|
-
new("PING", [ttl_ds].pack("n") + context.b)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Builds a PONG command.
|
|
134
|
-
#
|
|
135
|
-
# @param context [String] context bytes from the PING
|
|
136
|
-
# @return [Command]
|
|
137
|
-
#
|
|
138
|
-
def self.pong(context: "".b)
|
|
139
|
-
new("PONG", context.b)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Extracts TTL (in seconds) and context from a PING command's data.
|
|
143
|
-
#
|
|
144
|
-
# @return [Array(Numeric, String)] [ttl_seconds, context_bytes]
|
|
145
|
-
#
|
|
146
|
-
def ping_ttl_and_context
|
|
147
|
-
ttl_ds = @data.unpack1("n")
|
|
148
|
-
context = @data.bytesize > 2 ? @data.byteslice(2..) : "".b
|
|
149
|
-
[ttl_ds / 10.0, context]
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Parses READY command data as a property list.
|
|
153
|
-
#
|
|
154
|
-
# @return [Hash<String, String>] property name => value
|
|
155
|
-
# @raise [ProtocolError] on malformed properties
|
|
156
|
-
#
|
|
157
|
-
def properties
|
|
158
|
-
self.class.decode_properties(@data)
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Encodes a hash of properties into ZMTP property list format.
|
|
162
|
-
#
|
|
163
|
-
# @param props [Hash<String, String>]
|
|
164
|
-
# @return [String] binary property list
|
|
165
|
-
#
|
|
166
|
-
def self.encode_properties(props)
|
|
167
|
-
parts = props.map do |name, value|
|
|
168
|
-
name_bytes = name.b
|
|
169
|
-
value_bytes = value.b
|
|
170
|
-
name_bytes.bytesize.chr.b + name_bytes + [value_bytes.bytesize].pack("N") + value_bytes
|
|
171
|
-
end
|
|
172
|
-
parts.join
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Decodes a ZMTP property list from binary data.
|
|
176
|
-
#
|
|
177
|
-
# @param data [String] binary property list
|
|
178
|
-
# @return [Hash<String, String>] property name => value
|
|
179
|
-
# @raise [ProtocolError] on malformed properties
|
|
180
|
-
#
|
|
181
|
-
def self.decode_properties(data)
|
|
182
|
-
result = {}
|
|
183
|
-
offset = 0
|
|
184
|
-
|
|
185
|
-
while offset < data.bytesize
|
|
186
|
-
raise ProtocolError, "property name truncated" if offset + 1 > data.bytesize
|
|
187
|
-
name_len = data.getbyte(offset)
|
|
188
|
-
offset += 1
|
|
189
|
-
|
|
190
|
-
raise ProtocolError, "property name truncated" if offset + name_len > data.bytesize
|
|
191
|
-
name = data.byteslice(offset, name_len)
|
|
192
|
-
offset += name_len
|
|
193
|
-
|
|
194
|
-
raise ProtocolError, "property value length truncated" if offset + 4 > data.bytesize
|
|
195
|
-
value_len = data.byteslice(offset, 4).unpack1("N")
|
|
196
|
-
offset += 4
|
|
197
|
-
|
|
198
|
-
raise ProtocolError, "property value truncated" if offset + value_len > data.bytesize
|
|
199
|
-
value = data.byteslice(offset, value_len)
|
|
200
|
-
offset += value_len
|
|
201
|
-
|
|
202
|
-
result[name] = value
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
result
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
end
|
data/lib/omq/zmtp/codec/frame.rb
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OMQ
|
|
4
|
-
module ZMTP
|
|
5
|
-
module Codec
|
|
6
|
-
# ZMTP frame encode/decode.
|
|
7
|
-
#
|
|
8
|
-
# Wire format:
|
|
9
|
-
# Byte 0: flags (bit 0=MORE, bit 1=LONG, bit 2=COMMAND)
|
|
10
|
-
# Next 1-8: size (1-byte if short, 8-byte big-endian if LONG)
|
|
11
|
-
# Next N: body
|
|
12
|
-
#
|
|
13
|
-
class Frame
|
|
14
|
-
FLAGS_MORE = 0x01
|
|
15
|
-
FLAGS_LONG = 0x02
|
|
16
|
-
FLAGS_COMMAND = 0x04
|
|
17
|
-
|
|
18
|
-
# Short frame: 1-byte size, max body 255 bytes.
|
|
19
|
-
#
|
|
20
|
-
SHORT_MAX = 255
|
|
21
|
-
|
|
22
|
-
# @return [String] frame body (binary)
|
|
23
|
-
#
|
|
24
|
-
attr_reader :body
|
|
25
|
-
|
|
26
|
-
# @param body [String] frame body
|
|
27
|
-
# @param more [Boolean] more frames follow
|
|
28
|
-
# @param command [Boolean] this is a command frame
|
|
29
|
-
#
|
|
30
|
-
def initialize(body, more: false, command: false)
|
|
31
|
-
@body = body.b
|
|
32
|
-
@more = more
|
|
33
|
-
@command = command
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# @return [Boolean] true if more frames follow in this message
|
|
37
|
-
#
|
|
38
|
-
def more? = @more
|
|
39
|
-
|
|
40
|
-
# @return [Boolean] true if this is a command frame
|
|
41
|
-
#
|
|
42
|
-
def command? = @command
|
|
43
|
-
|
|
44
|
-
# Encodes to wire bytes.
|
|
45
|
-
#
|
|
46
|
-
# @return [String] binary wire representation (flags + size + body)
|
|
47
|
-
#
|
|
48
|
-
def to_wire
|
|
49
|
-
size = @body.bytesize
|
|
50
|
-
flags = 0
|
|
51
|
-
flags |= FLAGS_MORE if @more
|
|
52
|
-
flags |= FLAGS_COMMAND if @command
|
|
53
|
-
|
|
54
|
-
if size > SHORT_MAX
|
|
55
|
-
(flags | FLAGS_LONG).chr.b + [size].pack("Q>") + @body
|
|
56
|
-
else
|
|
57
|
-
flags.chr.b + size.chr.b + @body
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Reads one frame from an IO-like object.
|
|
62
|
-
#
|
|
63
|
-
# @param io [#read_exactly] must support read_exactly(n)
|
|
64
|
-
# @return [Frame]
|
|
65
|
-
# @raise [ProtocolError] on invalid frame
|
|
66
|
-
# @raise [EOFError] if the connection is closed
|
|
67
|
-
#
|
|
68
|
-
def self.read_from(io)
|
|
69
|
-
flags = io.read_exactly(1).getbyte(0)
|
|
70
|
-
|
|
71
|
-
more = (flags & FLAGS_MORE) != 0
|
|
72
|
-
long = (flags & FLAGS_LONG) != 0
|
|
73
|
-
command = (flags & FLAGS_COMMAND) != 0
|
|
74
|
-
|
|
75
|
-
size = if long
|
|
76
|
-
io.read_exactly(8).unpack1("Q>")
|
|
77
|
-
else
|
|
78
|
-
io.read_exactly(1).getbyte(0)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
body = size > 0 ? io.read_exactly(size) : "".b
|
|
82
|
-
|
|
83
|
-
new(body, more: more, command: command)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OMQ
|
|
4
|
-
module ZMTP
|
|
5
|
-
module Codec
|
|
6
|
-
# ZMTP 3.1 greeting encode/decode.
|
|
7
|
-
#
|
|
8
|
-
# The greeting is always exactly 64 bytes:
|
|
9
|
-
# Offset Bytes Field
|
|
10
|
-
# 0 1 0xFF (signature start)
|
|
11
|
-
# 1-8 8 0x00 padding
|
|
12
|
-
# 9 1 0x7F (signature end)
|
|
13
|
-
# 10 1 major version
|
|
14
|
-
# 11 1 minor version
|
|
15
|
-
# 12-31 20 mechanism (null-padded ASCII)
|
|
16
|
-
# 32 1 as-server flag (0x00 or 0x01)
|
|
17
|
-
# 33-63 31 filler (0x00)
|
|
18
|
-
#
|
|
19
|
-
module Greeting
|
|
20
|
-
SIZE = 64
|
|
21
|
-
SIGNATURE_START = 0xFF
|
|
22
|
-
SIGNATURE_END = 0x7F
|
|
23
|
-
VERSION_MAJOR = 3
|
|
24
|
-
VERSION_MINOR = 1
|
|
25
|
-
MECHANISM_OFFSET = 12
|
|
26
|
-
MECHANISM_LENGTH = 20
|
|
27
|
-
AS_SERVER_OFFSET = 32
|
|
28
|
-
|
|
29
|
-
# Encodes a ZMTP 3.1 greeting.
|
|
30
|
-
#
|
|
31
|
-
# @param mechanism [String] security mechanism name (e.g. "NULL")
|
|
32
|
-
# @param as_server [Boolean] whether this peer is the server
|
|
33
|
-
# @return [String] 64-byte binary greeting
|
|
34
|
-
#
|
|
35
|
-
def self.encode(mechanism: "NULL", as_server: false)
|
|
36
|
-
buf = "\xFF".b + ("\x00" * 8) + "\x7F".b
|
|
37
|
-
buf << [VERSION_MAJOR, VERSION_MINOR].pack("CC")
|
|
38
|
-
buf << mechanism.b.ljust(MECHANISM_LENGTH, "\x00")
|
|
39
|
-
buf << (as_server ? "\x01" : "\x00")
|
|
40
|
-
buf << ("\x00" * 31)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Decodes a ZMTP greeting.
|
|
44
|
-
#
|
|
45
|
-
# @param data [String] 64-byte binary greeting
|
|
46
|
-
# @return [Hash] { major:, minor:, mechanism:, as_server: }
|
|
47
|
-
# @raise [ProtocolError] on invalid greeting
|
|
48
|
-
#
|
|
49
|
-
def self.decode(data)
|
|
50
|
-
raise ProtocolError, "greeting too short (#{data.bytesize} bytes)" if data.bytesize < SIZE
|
|
51
|
-
|
|
52
|
-
data = data.b
|
|
53
|
-
|
|
54
|
-
unless data.getbyte(0) == SIGNATURE_START && data.getbyte(9) == SIGNATURE_END
|
|
55
|
-
raise ProtocolError, "invalid greeting signature"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
major = data.getbyte(10)
|
|
59
|
-
minor = data.getbyte(11)
|
|
60
|
-
|
|
61
|
-
unless major >= 3
|
|
62
|
-
raise ProtocolError, "unsupported ZMTP version #{major}.#{minor} (need >= 3.0)"
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
mechanism = data.byteslice(MECHANISM_OFFSET, MECHANISM_LENGTH).delete("\x00")
|
|
66
|
-
as_server = data.getbyte(AS_SERVER_OFFSET) == 1
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
major: major,
|
|
70
|
-
minor: minor,
|
|
71
|
-
mechanism: mechanism,
|
|
72
|
-
as_server: as_server,
|
|
73
|
-
}
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
data/lib/omq/zmtp/codec.rb
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OMQ
|
|
4
|
-
module ZMTP
|
|
5
|
-
# ZMTP 3.1 wire protocol codec.
|
|
6
|
-
#
|
|
7
|
-
module Codec
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
# Raised on ZMTP protocol violations.
|
|
11
|
-
#
|
|
12
|
-
class ProtocolError < RuntimeError; end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
require_relative "codec/greeting"
|
|
17
|
-
require_relative "codec/frame"
|
|
18
|
-
require_relative "codec/command"
|
data/lib/omq/zmtp/connection.rb
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OMQ
|
|
4
|
-
module ZMTP
|
|
5
|
-
# Manages one ZMTP peer connection over any transport IO.
|
|
6
|
-
#
|
|
7
|
-
# Delegates the security handshake to a Mechanism object (Null, Curve, etc.),
|
|
8
|
-
# then provides message send/receive and command send/receive on top of the
|
|
9
|
-
# framing codec.
|
|
10
|
-
#
|
|
11
|
-
class Connection
|
|
12
|
-
# @return [String] peer's socket type (from READY handshake)
|
|
13
|
-
#
|
|
14
|
-
attr_reader :peer_socket_type
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# @return [String] peer's identity (from READY handshake)
|
|
18
|
-
#
|
|
19
|
-
attr_reader :peer_identity
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# @return [Object] transport IO (#read, #write, #close)
|
|
23
|
-
#
|
|
24
|
-
attr_reader :io
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# @param io [#read, #write, #close] transport IO
|
|
28
|
-
# @param socket_type [String] our socket type name (e.g. "REQ")
|
|
29
|
-
# @param identity [String] our identity
|
|
30
|
-
# @param as_server [Boolean] whether we are the server side
|
|
31
|
-
# @param mechanism [Mechanism::Null, Mechanism::Curve] security mechanism
|
|
32
|
-
# @param heartbeat_interval [Numeric, nil] heartbeat interval in seconds
|
|
33
|
-
# @param heartbeat_ttl [Numeric, nil] TTL to send in PING
|
|
34
|
-
# @param heartbeat_timeout [Numeric, nil] timeout for PONG
|
|
35
|
-
# @param max_message_size [Integer, nil] max frame size in bytes, nil = unlimited
|
|
36
|
-
#
|
|
37
|
-
def initialize(io, socket_type:, identity: "", as_server: false,
|
|
38
|
-
mechanism: nil,
|
|
39
|
-
heartbeat_interval: nil, heartbeat_ttl: nil, heartbeat_timeout: nil,
|
|
40
|
-
max_message_size: nil)
|
|
41
|
-
@io = io
|
|
42
|
-
@socket_type = socket_type
|
|
43
|
-
@identity = identity
|
|
44
|
-
@as_server = as_server
|
|
45
|
-
@mechanism = mechanism || Mechanism::Null.new
|
|
46
|
-
@peer_socket_type = nil
|
|
47
|
-
@peer_identity = nil
|
|
48
|
-
@mutex = Mutex.new
|
|
49
|
-
@heartbeat_interval = heartbeat_interval
|
|
50
|
-
@heartbeat_ttl = heartbeat_ttl || heartbeat_interval
|
|
51
|
-
@heartbeat_timeout = heartbeat_timeout || heartbeat_interval
|
|
52
|
-
@last_received_at = nil
|
|
53
|
-
@heartbeat_task = nil
|
|
54
|
-
@max_message_size = max_message_size
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# Performs the full ZMTP handshake via the configured mechanism.
|
|
59
|
-
#
|
|
60
|
-
# @return [void]
|
|
61
|
-
# @raise [ProtocolError] on handshake failure
|
|
62
|
-
#
|
|
63
|
-
def handshake!
|
|
64
|
-
result = @mechanism.handshake!(
|
|
65
|
-
@io,
|
|
66
|
-
as_server: @as_server,
|
|
67
|
-
socket_type: @socket_type,
|
|
68
|
-
identity: @identity,
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
@peer_socket_type = result[:peer_socket_type]
|
|
72
|
-
@peer_identity = result[:peer_identity]
|
|
73
|
-
|
|
74
|
-
unless @peer_socket_type
|
|
75
|
-
raise ProtocolError, "peer READY missing Socket-Type"
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
unless ZMTP::VALID_PEERS[@socket_type.to_sym]&.include?(@peer_socket_type.to_sym)
|
|
79
|
-
raise ProtocolError,
|
|
80
|
-
"incompatible socket types: #{@socket_type} cannot connect to #{@peer_socket_type}"
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
# Sends a multi-frame message (write + flush).
|
|
86
|
-
#
|
|
87
|
-
# @param parts [Array<String>] message frames
|
|
88
|
-
# @return [void]
|
|
89
|
-
#
|
|
90
|
-
def send_message(parts)
|
|
91
|
-
@mutex.synchronize do
|
|
92
|
-
write_frames(parts)
|
|
93
|
-
@io.flush
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# Writes a multi-frame message to the buffer without flushing.
|
|
99
|
-
# Call {#flush} after batching writes.
|
|
100
|
-
#
|
|
101
|
-
# @param parts [Array<String>] message frames
|
|
102
|
-
# @return [void]
|
|
103
|
-
#
|
|
104
|
-
def write_message(parts)
|
|
105
|
-
@mutex.synchronize do
|
|
106
|
-
write_frames(parts)
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
# Flushes the write buffer to the underlying IO.
|
|
112
|
-
#
|
|
113
|
-
# @return [void]
|
|
114
|
-
#
|
|
115
|
-
def flush
|
|
116
|
-
@mutex.synchronize do
|
|
117
|
-
@io.flush
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
# Receives a multi-frame message.
|
|
123
|
-
# PING/PONG commands are handled automatically by #read_frame.
|
|
124
|
-
#
|
|
125
|
-
# @return [Array<String>] message frames
|
|
126
|
-
# @raise [EOFError] if connection is closed
|
|
127
|
-
#
|
|
128
|
-
def receive_message
|
|
129
|
-
frames = []
|
|
130
|
-
loop do
|
|
131
|
-
frame = read_frame
|
|
132
|
-
if frame.command?
|
|
133
|
-
yield frame if block_given?
|
|
134
|
-
next
|
|
135
|
-
end
|
|
136
|
-
frames << frame.body.freeze
|
|
137
|
-
break unless frame.more?
|
|
138
|
-
end
|
|
139
|
-
frames.freeze
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
# Starts the heartbeat sender task. Call after handshake.
|
|
144
|
-
#
|
|
145
|
-
# @return [#stop, nil] the heartbeat task, or nil if disabled
|
|
146
|
-
#
|
|
147
|
-
def start_heartbeat
|
|
148
|
-
return nil unless @heartbeat_interval
|
|
149
|
-
@last_received_at = monotonic_now
|
|
150
|
-
@heartbeat_task = Reactor.spawn_pump(annotation: "heartbeat") do
|
|
151
|
-
loop do
|
|
152
|
-
sleep @heartbeat_interval
|
|
153
|
-
# Send PING with TTL
|
|
154
|
-
send_command(Codec::Command.ping(
|
|
155
|
-
ttl: @heartbeat_ttl || 0,
|
|
156
|
-
context: "".b,
|
|
157
|
-
))
|
|
158
|
-
# Check if peer has gone silent
|
|
159
|
-
if @heartbeat_timeout && @last_received_at
|
|
160
|
-
elapsed = monotonic_now - @last_received_at
|
|
161
|
-
if elapsed > @heartbeat_timeout
|
|
162
|
-
close
|
|
163
|
-
break
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
rescue *ZMTP::CONNECTION_LOST
|
|
168
|
-
# connection closed
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
# Sends a command.
|
|
174
|
-
#
|
|
175
|
-
# @param command [Codec::Command]
|
|
176
|
-
# @return [void]
|
|
177
|
-
#
|
|
178
|
-
def send_command(command)
|
|
179
|
-
@mutex.synchronize do
|
|
180
|
-
if @mechanism.encrypted?
|
|
181
|
-
@io.write(@mechanism.encrypt(command.to_body, command: true))
|
|
182
|
-
else
|
|
183
|
-
@io.write(command.to_frame.to_wire)
|
|
184
|
-
end
|
|
185
|
-
@io.flush
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
# Reads one frame from the wire. Handles PING/PONG automatically.
|
|
191
|
-
# When using an encrypted mechanism, MESSAGE commands are decrypted
|
|
192
|
-
# back to ZMTP frames transparently.
|
|
193
|
-
#
|
|
194
|
-
# @return [Codec::Frame]
|
|
195
|
-
# @raise [EOFError] if connection is closed
|
|
196
|
-
#
|
|
197
|
-
def read_frame
|
|
198
|
-
loop do
|
|
199
|
-
frame = Codec::Frame.read_from(@io)
|
|
200
|
-
touch_heartbeat
|
|
201
|
-
|
|
202
|
-
# When CURVE is active, every wire frame is a MESSAGE envelope
|
|
203
|
-
# (data frame with "\x07MESSAGE" prefix). Decrypt to recover the
|
|
204
|
-
# inner ZMTP frame. This check only runs when encrypted? is true,
|
|
205
|
-
# so user data frames are never misdetected.
|
|
206
|
-
if @mechanism.encrypted? && frame.body.bytesize > 8 && frame.body.byteslice(0, 8) == "\x07MESSAGE".b
|
|
207
|
-
frame = @mechanism.decrypt(frame)
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
if @max_message_size && !frame.command? && frame.body.bytesize > @max_message_size
|
|
211
|
-
close
|
|
212
|
-
raise ProtocolError, "frame size #{frame.body.bytesize} exceeds max_message_size #{@max_message_size}"
|
|
213
|
-
end
|
|
214
|
-
if frame.command?
|
|
215
|
-
cmd = Codec::Command.from_body(frame.body)
|
|
216
|
-
case cmd.name
|
|
217
|
-
when "PING"
|
|
218
|
-
_, context = cmd.ping_ttl_and_context
|
|
219
|
-
send_command(Codec::Command.pong(context: context))
|
|
220
|
-
next
|
|
221
|
-
when "PONG"
|
|
222
|
-
next
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
return frame
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
# Closes the connection.
|
|
231
|
-
#
|
|
232
|
-
# @return [void]
|
|
233
|
-
#
|
|
234
|
-
def close
|
|
235
|
-
@heartbeat_task&.stop rescue nil
|
|
236
|
-
@io.close
|
|
237
|
-
rescue IOError
|
|
238
|
-
# already closed
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
private
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
# Writes message parts as ZMTP frames, encrypting if needed.
|
|
246
|
-
#
|
|
247
|
-
# @param parts [Array<String>] message frames
|
|
248
|
-
#
|
|
249
|
-
def write_frames(parts)
|
|
250
|
-
parts.each_with_index do |part, i|
|
|
251
|
-
more = i < parts.size - 1
|
|
252
|
-
if @mechanism.encrypted?
|
|
253
|
-
@io.write(@mechanism.encrypt(part.b, more: more))
|
|
254
|
-
else
|
|
255
|
-
@io.write(Codec::Frame.new(part, more: more).to_wire)
|
|
256
|
-
end
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
def touch_heartbeat
|
|
262
|
-
@last_received_at = monotonic_now if @heartbeat_interval
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def monotonic_now
|
|
267
|
-
Async::Clock.now
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
# Sends one frame to the wire.
|
|
272
|
-
#
|
|
273
|
-
# @param frame [Codec::Frame]
|
|
274
|
-
# @return [void]
|
|
275
|
-
#
|
|
276
|
-
def send_frame(frame)
|
|
277
|
-
@mutex.synchronize { @io.write(frame.to_wire) }
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
end
|
|
281
|
-
end
|
|
282
|
-
end
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OMQ
|
|
4
|
-
module ZMTP
|
|
5
|
-
module Mechanism
|
|
6
|
-
# NULL security mechanism — no encryption, no authentication.
|
|
7
|
-
#
|
|
8
|
-
# Performs the ZMTP 3.1 greeting exchange and READY command handshake.
|
|
9
|
-
#
|
|
10
|
-
class Null
|
|
11
|
-
MECHANISM_NAME = "NULL"
|
|
12
|
-
|
|
13
|
-
# Performs the full NULL handshake over +io+.
|
|
14
|
-
#
|
|
15
|
-
# 1. Exchange 64-byte greetings
|
|
16
|
-
# 2. Validate peer greeting (version, mechanism)
|
|
17
|
-
# 3. Exchange READY commands (socket type + identity)
|
|
18
|
-
#
|
|
19
|
-
# @param io [#read, #write] transport IO
|
|
20
|
-
# @param as_server [Boolean]
|
|
21
|
-
# @param socket_type [String]
|
|
22
|
-
# @param identity [String]
|
|
23
|
-
# @return [Hash] { peer_socket_type:, peer_identity: }
|
|
24
|
-
# @raise [ProtocolError]
|
|
25
|
-
#
|
|
26
|
-
def handshake!(io, as_server:, socket_type:, identity:)
|
|
27
|
-
# Send our greeting
|
|
28
|
-
io.write(Codec::Greeting.encode(mechanism: MECHANISM_NAME, as_server: as_server))
|
|
29
|
-
io.flush
|
|
30
|
-
|
|
31
|
-
# Read peer greeting
|
|
32
|
-
greeting_data = io.read_exactly(Codec::Greeting::SIZE)
|
|
33
|
-
peer_greeting = Codec::Greeting.decode(greeting_data)
|
|
34
|
-
|
|
35
|
-
unless peer_greeting[:mechanism] == MECHANISM_NAME
|
|
36
|
-
raise ProtocolError, "unsupported mechanism: #{peer_greeting[:mechanism]}"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Send our READY command
|
|
40
|
-
ready_cmd = Codec::Command.ready(socket_type: socket_type, identity: identity)
|
|
41
|
-
io.write(ready_cmd.to_frame.to_wire)
|
|
42
|
-
io.flush
|
|
43
|
-
|
|
44
|
-
# Read peer READY command
|
|
45
|
-
frame = Codec::Frame.read_from(io)
|
|
46
|
-
unless frame.command?
|
|
47
|
-
raise ProtocolError, "expected command frame, got data frame"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
peer_cmd = Codec::Command.from_body(frame.body)
|
|
51
|
-
unless peer_cmd.name == "READY"
|
|
52
|
-
raise ProtocolError, "expected READY command, got #{peer_cmd.name}"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
props = peer_cmd.properties
|
|
56
|
-
peer_socket_type = props["Socket-Type"]
|
|
57
|
-
peer_identity = props["Identity"] || ""
|
|
58
|
-
|
|
59
|
-
unless peer_socket_type
|
|
60
|
-
raise ProtocolError, "peer READY missing Socket-Type"
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
{ peer_socket_type: peer_socket_type, peer_identity: peer_identity }
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# @return [Boolean] false — NULL does not encrypt frames
|
|
67
|
-
#
|
|
68
|
-
def encrypted? = false
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
data/lib/omq/zmtp/valid_peers.rb
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module OMQ
|
|
4
|
-
module ZMTP
|
|
5
|
-
# Valid socket type peer combinations per ZMTP spec.
|
|
6
|
-
#
|
|
7
|
-
VALID_PEERS = {
|
|
8
|
-
PAIR: %i[PAIR].freeze,
|
|
9
|
-
REQ: %i[REP ROUTER].freeze,
|
|
10
|
-
REP: %i[REQ DEALER].freeze,
|
|
11
|
-
DEALER: %i[REP DEALER ROUTER].freeze,
|
|
12
|
-
ROUTER: %i[REQ DEALER ROUTER].freeze,
|
|
13
|
-
PUB: %i[SUB XSUB].freeze,
|
|
14
|
-
SUB: %i[PUB XPUB].freeze,
|
|
15
|
-
XPUB: %i[SUB XSUB].freeze,
|
|
16
|
-
XSUB: %i[PUB XPUB].freeze,
|
|
17
|
-
PUSH: %i[PULL].freeze,
|
|
18
|
-
PULL: %i[PUSH].freeze,
|
|
19
|
-
CLIENT: %i[SERVER].freeze,
|
|
20
|
-
SERVER: %i[CLIENT].freeze,
|
|
21
|
-
RADIO: %i[DISH].freeze,
|
|
22
|
-
DISH: %i[RADIO].freeze,
|
|
23
|
-
SCATTER: %i[GATHER].freeze,
|
|
24
|
-
GATHER: %i[SCATTER].freeze,
|
|
25
|
-
PEER: %i[PEER].freeze,
|
|
26
|
-
CHANNEL: %i[CHANNEL].freeze,
|
|
27
|
-
}.freeze
|
|
28
|
-
end
|
|
29
|
-
end
|