mqtt-v5 0.0.1.ci.release
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 +7 -0
- data/lib/mqtt/v5/async/client.rb +27 -0
- data/lib/mqtt/v5/client/authenticator.rb +83 -0
- data/lib/mqtt/v5/client/connection.rb +192 -0
- data/lib/mqtt/v5/client/session.rb +45 -0
- data/lib/mqtt/v5/client.rb +61 -0
- data/lib/mqtt/v5/errors.rb +19 -0
- data/lib/mqtt/v5/packet/auth.rb +57 -0
- data/lib/mqtt/v5/packet/connack.rb +110 -0
- data/lib/mqtt/v5/packet/connect.rb +130 -0
- data/lib/mqtt/v5/packet/disconnect.rb +78 -0
- data/lib/mqtt/v5/packet/ping_req.rb +20 -0
- data/lib/mqtt/v5/packet/ping_resp.rb +20 -0
- data/lib/mqtt/v5/packet/pub_ack.rb +60 -0
- data/lib/mqtt/v5/packet/pub_comp.rb +53 -0
- data/lib/mqtt/v5/packet/pub_rec.rb +60 -0
- data/lib/mqtt/v5/packet/pub_rel.rb +53 -0
- data/lib/mqtt/v5/packet/publish.rb +122 -0
- data/lib/mqtt/v5/packet/reason_code.rb +112 -0
- data/lib/mqtt/v5/packet/sub_ack.rb +66 -0
- data/lib/mqtt/v5/packet/subscribe.rb +87 -0
- data/lib/mqtt/v5/packet/unsub_ack.rb +59 -0
- data/lib/mqtt/v5/packet/unsubscribe.rb +112 -0
- data/lib/mqtt/v5/packet.rb +147 -0
- data/lib/mqtt/v5/packets.rb +25 -0
- data/lib/mqtt/v5/topic_alias/cache.rb +87 -0
- data/lib/mqtt/v5/topic_alias/frequency_weighted_policy.rb +50 -0
- data/lib/mqtt/v5/topic_alias/length_weighted_policy.rb +20 -0
- data/lib/mqtt/v5/topic_alias/lru_policy.rb +36 -0
- data/lib/mqtt/v5/topic_alias/manager.rb +143 -0
- data/lib/mqtt/v5/topic_alias/policy.rb +34 -0
- data/lib/mqtt/v5/topic_alias/weighted_policy.rb +48 -0
- data/lib/mqtt/v5/topic_alias.rb +27 -0
- data/lib/mqtt/v5/version.rb +11 -0
- data/lib/mqtt/v5.rb +4 -0
- metadata +88 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../errors'
|
|
4
|
+
|
|
5
|
+
module MQTT
|
|
6
|
+
module V5
|
|
7
|
+
module Packet
|
|
8
|
+
ReasonCode = Data.define(:code, :name, :error)
|
|
9
|
+
|
|
10
|
+
# Encapsulates reason codes
|
|
11
|
+
class ReasonCode < Data
|
|
12
|
+
# @!attribute [r] code
|
|
13
|
+
# @return [Integer] the reason code value
|
|
14
|
+
# @!attribute [r] name
|
|
15
|
+
# @return [String] the reason code name
|
|
16
|
+
# @!attribute [r] error
|
|
17
|
+
# @return [Class,nil] the error class for failed reason codes
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
|
21
|
+
def register(code, name, packet_types, retriable = false)
|
|
22
|
+
@reason_codes ||= Hash.new { |h, k| h[k] = {} }
|
|
23
|
+
rc = create(code, name, retriable)
|
|
24
|
+
packet_types.each { |packet_type| @reason_codes[packet_type][code] = rc }
|
|
25
|
+
end
|
|
26
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
|
27
|
+
|
|
28
|
+
def find(packet_name, reason_code)
|
|
29
|
+
@reason_codes.dig(packet_name, reason_code) || unknown_reason_code(packet_name, reason_code)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def failed?(reason_code)
|
|
33
|
+
reason_code >= 0x80
|
|
34
|
+
# define readers
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def create(code, name, retriable)
|
|
40
|
+
error = define_error_class(code, name, retriable) if code >= 0x80
|
|
41
|
+
new(code, name, error)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def unknown_reason_code(packet_name, reason_code)
|
|
45
|
+
new(reason_code, "Unknown ReasonCode for #{packet_name}", UnknownReasonCode)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def define_error_class(code, name, retriable)
|
|
49
|
+
klass = Class.new(ResponseError)
|
|
50
|
+
klass.include(Error::Retriable) if retriable
|
|
51
|
+
klass.instance_variable_set(:@code, code)
|
|
52
|
+
MQTT::V5.const_set(name.split(/\s+/).map(&:capitalize).join, klass)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def failed?
|
|
57
|
+
self.class.failed?(code)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def success!(reason_string)
|
|
61
|
+
return self unless failed?
|
|
62
|
+
|
|
63
|
+
raise error, reason_string || name
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_s
|
|
67
|
+
format '%<name>s(0x%02<code>x)', name:, code:
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Success/Failed for packets with a single reason_code field
|
|
72
|
+
module ReasonCodeAck
|
|
73
|
+
def defaults
|
|
74
|
+
super.merge!({ reason_code: 0x00 })
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def success?
|
|
78
|
+
!failed?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @return [self]
|
|
82
|
+
# @raise [ResponseError]
|
|
83
|
+
def success!
|
|
84
|
+
reason_code_data.success!(reason_string) && self
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def failed?
|
|
88
|
+
ReasonCode.failed?(reason_code)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def to_s
|
|
92
|
+
"#{super}: #{reason_code_data}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def reason_code_data
|
|
96
|
+
ReasonCode.find(packet_name, reason_code)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Module for packets with a :reason_codes field
|
|
101
|
+
module ReasonCodeListAck
|
|
102
|
+
def reason_codes_data
|
|
103
|
+
reason_codes.map { |rc| ReasonCode.find(packet_name, rc) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def to_s
|
|
107
|
+
"#{super}: #{reason_codes_data.map { |rc| format('0x%02<code>x', code: rc.code) }.tally}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../packet'
|
|
4
|
+
|
|
5
|
+
module MQTT
|
|
6
|
+
module V5
|
|
7
|
+
module Packet
|
|
8
|
+
# MQTT 5.0 SUBACK packet
|
|
9
|
+
#
|
|
10
|
+
# Subscription acknowledgement sent by broker in response to a SUBSCRIBE packet.
|
|
11
|
+
#
|
|
12
|
+
# @see Core::Client#subscribe
|
|
13
|
+
# @see https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901171 MQTT 5.0 Spec §3.9
|
|
14
|
+
class SubAck
|
|
15
|
+
include Packet
|
|
16
|
+
|
|
17
|
+
fixed(9)
|
|
18
|
+
|
|
19
|
+
# @!attribute [r] packet_identifier
|
|
20
|
+
# @return [Integer] packet identifier matching the SUBSCRIBE packet (receive only)
|
|
21
|
+
|
|
22
|
+
# @!group Properties
|
|
23
|
+
|
|
24
|
+
# @!attribute [r] reason_string
|
|
25
|
+
# @return [String<UTF8>] human-readable reason for the response
|
|
26
|
+
# @!attribute [r] user_properties
|
|
27
|
+
# @return [Array<String, String>] user-defined properties as key-value pairs
|
|
28
|
+
|
|
29
|
+
# @!endgroup
|
|
30
|
+
|
|
31
|
+
# @!parse include ReasonCodeListAck
|
|
32
|
+
# @!attribute [r] reason_codes
|
|
33
|
+
# acknowledgement status for each subscription
|
|
34
|
+
#
|
|
35
|
+
# ✅ Success:
|
|
36
|
+
#
|
|
37
|
+
# - `0x00` Granted QoS 0
|
|
38
|
+
# - `0x01` Granted QoS 1
|
|
39
|
+
# - `0x02` Granted QoS 2
|
|
40
|
+
#
|
|
41
|
+
# ❌ Error:
|
|
42
|
+
#
|
|
43
|
+
# - `0x80` Unspecified error
|
|
44
|
+
# - `0x83` Implementation specific error
|
|
45
|
+
# - `0x87` Not authorized
|
|
46
|
+
# - `0x8F` Topic Filter invalid
|
|
47
|
+
# - `0x91` Packet Identifier in use
|
|
48
|
+
# - `0x97` Quota exceeded
|
|
49
|
+
# - `0xA1` Subscription Identifiers not supported
|
|
50
|
+
# - `0xA2` Wildcard Subscriptions not supported
|
|
51
|
+
#
|
|
52
|
+
# @return [Array<ReasonCode>]
|
|
53
|
+
|
|
54
|
+
variable(
|
|
55
|
+
packet_identifier: :int16,
|
|
56
|
+
properties:
|
|
57
|
+
)
|
|
58
|
+
payload(
|
|
59
|
+
reason_codes: # automatically includes ReasonCodeListAck
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
alias return_codes reason_codes
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../packet'
|
|
4
|
+
require 'mqtt/core/packet/subscribe'
|
|
5
|
+
|
|
6
|
+
module MQTT
|
|
7
|
+
module V5
|
|
8
|
+
module Packet
|
|
9
|
+
# MQTT 5.0 SUBSCRIBE packet
|
|
10
|
+
#
|
|
11
|
+
# Sent by client to subscribe to one or more topic filters.
|
|
12
|
+
#
|
|
13
|
+
# @see Core::Client#subscribe
|
|
14
|
+
# @see https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901161 MQTT 5.0 Spec §3.8
|
|
15
|
+
class Subscribe
|
|
16
|
+
include Packet
|
|
17
|
+
|
|
18
|
+
fixed(8, [:reserved, 4, 0b0010])
|
|
19
|
+
|
|
20
|
+
# @!attribute [r] packet_identifier
|
|
21
|
+
# @return [Integer] packet identifier for matching with SUBACK (managed automatically by session)
|
|
22
|
+
|
|
23
|
+
# @!group Properties
|
|
24
|
+
|
|
25
|
+
# @!attribute [r] subscription_identifier
|
|
26
|
+
# @return [Integer] identifier for this subscription
|
|
27
|
+
# @!attribute [r] user_properties
|
|
28
|
+
# @return [Array<String, String>] user-defined properties as key-value pairs
|
|
29
|
+
|
|
30
|
+
# @!endgroup
|
|
31
|
+
|
|
32
|
+
# @!parse
|
|
33
|
+
# class TopicFilter
|
|
34
|
+
# # @!attribute [r] topic_filter
|
|
35
|
+
# # @return [String<UTF8>] topic filter pattern
|
|
36
|
+
# # @!attribute [r] max_qos
|
|
37
|
+
# # @return [Integer] maximum QoS level accepted by the client: 0, 1, or 2
|
|
38
|
+
# # @!attribute [r] no_local
|
|
39
|
+
# # @return [Boolean] do not forward messages back to same client id
|
|
40
|
+
# # @!attribute [r] retain_as_published
|
|
41
|
+
# # @return [Boolean] retain flag from PUBLISH should be preserved
|
|
42
|
+
# # @!attribute [r] retain
|
|
43
|
+
# # @return [Integer]
|
|
44
|
+
# # retained message handling
|
|
45
|
+
# #
|
|
46
|
+
# # - `0` Send retained messages at the time of subscribe
|
|
47
|
+
# # - `1` Send retained messages only if subscription does not currently exist
|
|
48
|
+
# # - `2` Do not send retained messages at the time of subscribe
|
|
49
|
+
# end
|
|
50
|
+
|
|
51
|
+
# @!attribute [r] topic_filters
|
|
52
|
+
# @return [Array<TopicFilter>] list of topic filters to subscribe to
|
|
53
|
+
|
|
54
|
+
variable(
|
|
55
|
+
packet_identifier: :int16,
|
|
56
|
+
properties:
|
|
57
|
+
)
|
|
58
|
+
payload(
|
|
59
|
+
topic_filters: list(
|
|
60
|
+
:topic_filter,
|
|
61
|
+
topic_filter: :utf8string,
|
|
62
|
+
subscription_options: flags(
|
|
63
|
+
[:reserved, 2],
|
|
64
|
+
[:retain, 2],
|
|
65
|
+
:retain_as_published,
|
|
66
|
+
:no_local,
|
|
67
|
+
[:max_qos, 2]
|
|
68
|
+
)
|
|
69
|
+
) do
|
|
70
|
+
alias_method :requested_qos, :max_qos
|
|
71
|
+
end
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
MAX_QOS_FIELD = :max_qos
|
|
75
|
+
include MQTT::Core::Packet::Subscribe
|
|
76
|
+
|
|
77
|
+
def match?(publish_packet)
|
|
78
|
+
if subscription_identifier&.positive?
|
|
79
|
+
publish_packet.match_subscription_identifier?(subscription_identifier)
|
|
80
|
+
else
|
|
81
|
+
super
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../packet'
|
|
4
|
+
module MQTT
|
|
5
|
+
module V5
|
|
6
|
+
module Packet
|
|
7
|
+
# MQTT 5.0 UNSUBACK packet
|
|
8
|
+
#
|
|
9
|
+
# Unsubscribe acknowledgement sent by broker in response to an UNSUBSCRIBE packet.
|
|
10
|
+
#
|
|
11
|
+
# @see Core::Client::Subscription#unsubscribe
|
|
12
|
+
# @see https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901187 MQTT 5.0 Spec §3.11
|
|
13
|
+
class UnsubAck
|
|
14
|
+
include Packet
|
|
15
|
+
|
|
16
|
+
fixed(11)
|
|
17
|
+
|
|
18
|
+
# @!attribute [r] packet_identifier
|
|
19
|
+
# @return [Integer] packet identifier matching the UNSUBSCRIBE packet (receive only)
|
|
20
|
+
|
|
21
|
+
# @!group Properties
|
|
22
|
+
|
|
23
|
+
# @!attribute [r] reason_string
|
|
24
|
+
# @return [String<UTF8>] human-readable reason for the response
|
|
25
|
+
# @!attribute [r] user_properties
|
|
26
|
+
# @return [Array<String, String>] user-defined properties as key-value pairs
|
|
27
|
+
|
|
28
|
+
# @!endgroup
|
|
29
|
+
|
|
30
|
+
# @!parse include ReasonCodeListAck
|
|
31
|
+
# @!attribute [r] reason_codes
|
|
32
|
+
# acknowledgement status for each topic filter
|
|
33
|
+
#
|
|
34
|
+
# ✅ Success:
|
|
35
|
+
#
|
|
36
|
+
# - `0x00` Success
|
|
37
|
+
# - `0x11` No subscription existed
|
|
38
|
+
#
|
|
39
|
+
# ❌ Error:
|
|
40
|
+
#
|
|
41
|
+
# - `0x80` Unspecified error
|
|
42
|
+
# - `0x83` Implementation specific error
|
|
43
|
+
# - `0x87` Not authorized
|
|
44
|
+
# - `0x8F` Topic Filter invalid
|
|
45
|
+
# - `0x91` Packet Identifier in use
|
|
46
|
+
#
|
|
47
|
+
# @return [Array<ReasonCode>]
|
|
48
|
+
|
|
49
|
+
variable(
|
|
50
|
+
packet_identifier: :int16,
|
|
51
|
+
properties:
|
|
52
|
+
)
|
|
53
|
+
payload(
|
|
54
|
+
reason_codes: # automatically includes ReasonCodeListAck
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../packet'
|
|
4
|
+
require 'mqtt/core/packet/unsubscribe'
|
|
5
|
+
|
|
6
|
+
module MQTT
|
|
7
|
+
module V5
|
|
8
|
+
module Packet
|
|
9
|
+
# MQTT 5.0 UNSUBSCRIBE packet
|
|
10
|
+
#
|
|
11
|
+
# Sent by client to unsubscribe from one or more topic filters.
|
|
12
|
+
#
|
|
13
|
+
# @see Core::Client::Subscription#unsubscribe
|
|
14
|
+
# @see https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901179 MQTT 5.0 Spec §3.10
|
|
15
|
+
class Unsubscribe
|
|
16
|
+
include Packet
|
|
17
|
+
include Core::Packet::Unsubscribe
|
|
18
|
+
|
|
19
|
+
fixed(10, [:reserved, 4, 0b0010])
|
|
20
|
+
|
|
21
|
+
# @!attribute [r] packet_identifier
|
|
22
|
+
# @return [Integer] packet identifier for matching with UNSUBACK (managed automatically by session)
|
|
23
|
+
|
|
24
|
+
# @!group Properties
|
|
25
|
+
|
|
26
|
+
# @!attribute [r] user_properties
|
|
27
|
+
# @return [Array<String, String>] user-defined properties as key-value pairs
|
|
28
|
+
|
|
29
|
+
# @!endgroup
|
|
30
|
+
|
|
31
|
+
# @!attribute [r] topic_filters
|
|
32
|
+
# @return [Array<String<UTF8>>] list of topic filters to unsubscribe from
|
|
33
|
+
|
|
34
|
+
variable(
|
|
35
|
+
packet_identifier: :int16,
|
|
36
|
+
properties:
|
|
37
|
+
)
|
|
38
|
+
payload(
|
|
39
|
+
topic_filters: list(:utf8string)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def apply_data(data)
|
|
43
|
+
@ignore_failed = data.delete(:ignore_failed) { false }
|
|
44
|
+
@ignore_no_subscription = data.delete(:ignore_no_subscription) { true }
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Hash<String,Symbol>]
|
|
49
|
+
# Map of topic_filter to ack status
|
|
50
|
+
# * :success - subscription existed and was unsubscribed from the server
|
|
51
|
+
# * :no_subscription - subscription did not exist on the server
|
|
52
|
+
# * :failed - subscription failed to unsubscribe
|
|
53
|
+
def filter_status(unsuback)
|
|
54
|
+
topic_filters.zip(unsuback.reason_codes).to_h { |tf, rc| [tf, classify(rc)] }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Array<String>]
|
|
58
|
+
def unsubscribed_topic_filters(unsuback = nil)
|
|
59
|
+
return topic_filters unless unsuback
|
|
60
|
+
|
|
61
|
+
topic_filters.zip(unsuback.reason_codes).filter_map { |tf, rc| tf unless failed?(rc) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Version dependent partition topic_filters into success and failures
|
|
65
|
+
#
|
|
66
|
+
# Attributes @ignore_no_subscription and @ignore_failed can control whether these statuses are considered
|
|
67
|
+
# successful or not.
|
|
68
|
+
# @return [Array<Hash<String,Symbol>>] pair of Maps as per #filter_status
|
|
69
|
+
def partition_success(unsuback)
|
|
70
|
+
filter_status(unsuback).partition { |(_tf, ack_status)| ack_success?(ack_status) }.map(&:to_h)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def success!(unsuback)
|
|
74
|
+
_, failed = partition_success(unsuback)
|
|
75
|
+
|
|
76
|
+
raise UnsubscribeError, failed unless failed.empty?
|
|
77
|
+
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def failed?(reason_code)
|
|
84
|
+
ReasonCode.failed?(reason_code)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def classify(reason_code)
|
|
88
|
+
if failed?(reason_code)
|
|
89
|
+
:failed
|
|
90
|
+
elsif reason_code == 0x17
|
|
91
|
+
:no_subscription
|
|
92
|
+
else
|
|
93
|
+
:success
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def ack_success?(ack_status)
|
|
98
|
+
case ack_status
|
|
99
|
+
when :success
|
|
100
|
+
true
|
|
101
|
+
when :no_subscription
|
|
102
|
+
@ignore_no_subscription
|
|
103
|
+
when :failed
|
|
104
|
+
@ignore_failed
|
|
105
|
+
else
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mqtt/core/packet'
|
|
4
|
+
require_relative 'version'
|
|
5
|
+
require_relative 'errors'
|
|
6
|
+
require_relative 'packet/reason_code'
|
|
7
|
+
|
|
8
|
+
module MQTT
|
|
9
|
+
module V5
|
|
10
|
+
# MQTT 5.0 base packet definition
|
|
11
|
+
#
|
|
12
|
+
# Inherited constants (referenced via self:: in {MQTT::Core::Packet}
|
|
13
|
+
module Packet
|
|
14
|
+
VALUE_TYPES = {
|
|
15
|
+
utf8string: MQTT::Core::Type::UTF8String,
|
|
16
|
+
binary: MQTT::Core::Type::Binary,
|
|
17
|
+
remaining: MQTT::Core::Type::Remaining,
|
|
18
|
+
utf8pair: MQTT::Core::Type::UTF8StringPair,
|
|
19
|
+
int8: MQTT::Core::Type::Int8,
|
|
20
|
+
int16: MQTT::Core::Type::Int16,
|
|
21
|
+
int32: MQTT::Core::Type::Int32,
|
|
22
|
+
intvar: MQTT::Core::Type::VarInt,
|
|
23
|
+
varint: MQTT::Core::Type::VarInt,
|
|
24
|
+
boolean: MQTT::Core::Type::BooleanByte
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# Available property types for MQTT version 5.0
|
|
28
|
+
# @note :user_properties
|
|
29
|
+
# The spec uses the singular 'user_property' - but it is the only property that can occur more than once
|
|
30
|
+
# and it the key in the pair can itself appear more than once, so we represent it as Array<String,String>
|
|
31
|
+
# and give it the plural name
|
|
32
|
+
|
|
33
|
+
# Attributes stored as a list of properties
|
|
34
|
+
PROPERTY_TYPES = [
|
|
35
|
+
[0x01, :payload_format_indicator, :int8, %i[publish will]],
|
|
36
|
+
[0x02, :message_expiry_interval, :int32, %i[publish will]],
|
|
37
|
+
[0x03, :content_type, :utf8string, %i[publish will]],
|
|
38
|
+
[0x08, :response_topic, :utf8string, %i[publish will]],
|
|
39
|
+
[0x09, :correlation_data, :binary, %i[publish will]],
|
|
40
|
+
# @!attribute [r] subscription_identifier
|
|
41
|
+
# @return [Integer] (publish)
|
|
42
|
+
# @return [Array<Integer>] (subscribe)
|
|
43
|
+
[0x0B, :subscription_identifier, :varint, %i[subscribe]],
|
|
44
|
+
[0x08, :subscription_identifiers, [:varint], %i[publish]], # this is only sent by servers
|
|
45
|
+
[0x11, :session_expiry_interval, :int32, %i[connect connack disconnect]],
|
|
46
|
+
[0x12, :assigned_client_identifier, :utf8string, [:connack]],
|
|
47
|
+
[0x13, :server_keep_alive, :int16, [:connack]],
|
|
48
|
+
[0x15, :authentication_method, :utf8string, %i[connect connack auth]],
|
|
49
|
+
[0x16, :authentication_data, :binary, %i[connect connack auth]],
|
|
50
|
+
[0x17, :request_problem_information, :boolean, [:connect]],
|
|
51
|
+
[0x18, :will_delay_interval, :int32, [:will]],
|
|
52
|
+
[0x19, :request_response_information, :boolean, [:connect]],
|
|
53
|
+
[0x1A, :response_information, :utf8string, [:connack]],
|
|
54
|
+
[0x1C, :server_reference, :utf8string, %i[connack disconnect]],
|
|
55
|
+
[0x1F, :reason_string, :utf8string, %i[connack puback pubrec pubrel pubcomp suback unsuback disconnect auth]],
|
|
56
|
+
[0x21, :receive_maximum, :int16, %i[connect connack]],
|
|
57
|
+
[0x22, :topic_alias_maximum, :int16, %i[connect connack]],
|
|
58
|
+
[0x23, :topic_alias, :int16, [:publish]],
|
|
59
|
+
[0x24, :maximum_qos, :int8, [:connack]],
|
|
60
|
+
[0x25, :retain_available, :boolean, [:connack]],
|
|
61
|
+
# @!attribute [r] user_properties
|
|
62
|
+
# @return [Array<String,String>] list of key,value pairs, same key may appear more than once
|
|
63
|
+
[0x26, :user_properties, [:utf8pair], :all],
|
|
64
|
+
[0x27, :maximum_packet_size, :int32, %i[connect connack]],
|
|
65
|
+
[0x28, :wildcard_subscription_available, :boolean, [:connack]],
|
|
66
|
+
[0x29, :subscription_identifier_available, :boolean, [:connack]],
|
|
67
|
+
[0x2A, :shared_subscription_available, :boolean, [:connack]]
|
|
68
|
+
].map { |data| MQTT::Core::Type::Properties::PropertyType.create(*data, types: VALUE_TYPES) }.freeze
|
|
69
|
+
|
|
70
|
+
# If the sender is compliant with this specification it will not send Malformed Packets or cause Protocol Errors
|
|
71
|
+
|
|
72
|
+
# Reason codes
|
|
73
|
+
REASON_CODES = [
|
|
74
|
+
[0x00, 'Success', %i[connack puback pubrec pubrel pubcomp unsuback auth]],
|
|
75
|
+
[0x00, 'Normal disconnection', %i[disconnect]],
|
|
76
|
+
[0x00, 'Granted QoS 0', %i[suback]],
|
|
77
|
+
[0x01, 'Granted QoS 1', %i[suback]],
|
|
78
|
+
[0x02, 'Granted QoS 2', %i[suback]],
|
|
79
|
+
[0x04, 'Disconnect with Will Message', %i[disconnect]],
|
|
80
|
+
[0x10, 'No matching subscribers', %i[puback pubrec]],
|
|
81
|
+
[0x11, 'No subscription existed', %i[unsuback]],
|
|
82
|
+
[0x18, 'Continue authentication', %i[auth]],
|
|
83
|
+
[0x19, 'Re-authenticate', %i[auth]],
|
|
84
|
+
[0x80, 'Unspecified error', %i[connack puback pubrec suback unsuback disconnect], true],
|
|
85
|
+
[0x81, 'Malformed Packet', %i[connack disconnect]],
|
|
86
|
+
[0x82, 'Protocol Error', %i[connack disconnect]],
|
|
87
|
+
[0x83, 'Implementation specific error', %i[connack puback pubrec suback unsuback disconnect], true],
|
|
88
|
+
[0x84, 'Unsupported Protocol Version', %i[connack]],
|
|
89
|
+
[0x85, 'Client Identifier not valid', %i[connack]],
|
|
90
|
+
[0x86, 'Bad User Name or Password', %i[connack]],
|
|
91
|
+
[0x87, 'Not authorized', %i[connack puback pubrec suback unsuback disconnect]],
|
|
92
|
+
[0x88, 'Server unavailable', %i[connack], true],
|
|
93
|
+
[0x89, 'Server busy', %i[connack disconnect], true],
|
|
94
|
+
[0x8A, 'Banned', %i[connack]],
|
|
95
|
+
[0x8B, 'Server shutting down', %i[disconnect], true],
|
|
96
|
+
[0x8C, 'Bad authentication method', %i[connack disconnect]],
|
|
97
|
+
[0x8D, 'Keep alive timeout', %i[disconnect], true],
|
|
98
|
+
[0x8E, 'Session taken over', %i[disconnect]],
|
|
99
|
+
[0x8F, 'Topic Filter invalid', %i[suback unsuback disconnect]],
|
|
100
|
+
[0x90, 'Topic Name invalid', %i[connack puback pubrec disconnect]],
|
|
101
|
+
[0x91, 'Packet Identifier in use', %i[puback pubrec suback unsuback]],
|
|
102
|
+
[0x92, 'Packet Identifier not found', %i[pubrel pubcomp]],
|
|
103
|
+
[0x93, 'Receive Maximum exceeded', %i[disconnect], true],
|
|
104
|
+
[0x94, 'Topic Alias invalid', %i[disconnect]],
|
|
105
|
+
[0x95, 'Packet too large', %i[connack disconnect]],
|
|
106
|
+
[0x96, 'Message rate too high', %i[disconnect], true],
|
|
107
|
+
[0x97, 'Quota exceeded', %i[connack puback pubrec suback disconnect]],
|
|
108
|
+
[0x98, 'Administrative action', %i[disconnect]],
|
|
109
|
+
[0x99, 'Payload format invalid', %i[connack puback pubrec disconnect]],
|
|
110
|
+
[0x9A, 'Retain not supported', %i[connack disconnect]],
|
|
111
|
+
[0x9B, 'QoS not supported', %i[connack disconnect]],
|
|
112
|
+
[0x9C, 'Use another server', %i[connack disconnect]],
|
|
113
|
+
[0x9E, 'Shared Subscriptions not supported', %i[suback disconnect]],
|
|
114
|
+
[0x9F, 'Connection rate exceeded', %i[connack disconnect], true],
|
|
115
|
+
[0xA0, 'Maximum connect time', %i[disconnect]],
|
|
116
|
+
[0xA1, 'Subscription Identifiers not supported', %i[suback disconnect]],
|
|
117
|
+
[0xA2, 'Wildcard Subscriptions not supported', %i[suback disconnect]]
|
|
118
|
+
].freeze.tap { |codes| codes.each { |code| ReasonCode.register(*code) } }
|
|
119
|
+
|
|
120
|
+
# rubocop:disable Style/MutableConstant
|
|
121
|
+
PACKET_TYPES = {}
|
|
122
|
+
# rubocop:enable Style/MutableConstant
|
|
123
|
+
|
|
124
|
+
# Additional class methods for the packet
|
|
125
|
+
module Definition
|
|
126
|
+
def reason_code
|
|
127
|
+
include ReasonCodeAck
|
|
128
|
+
|
|
129
|
+
{ type: :int8, default: 0x00 }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def reason_codes
|
|
133
|
+
include ReasonCodeListAck
|
|
134
|
+
|
|
135
|
+
list(:int8)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
extend MQTT::Core::Packet::ModuleMethods
|
|
140
|
+
|
|
141
|
+
def self.included(mod)
|
|
142
|
+
mod.include(MQTT::Core::Packet)
|
|
143
|
+
mod.extend(Definition)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# MQTT 5.0
|
|
4
|
+
module MQTT
|
|
5
|
+
# MQTT 5.0 protocol implementation
|
|
6
|
+
module V5
|
|
7
|
+
require_relative 'packet/connect'
|
|
8
|
+
require_relative 'packet/connack'
|
|
9
|
+
require_relative 'packet/disconnect'
|
|
10
|
+
require_relative 'packet/ping_req'
|
|
11
|
+
require_relative 'packet/ping_resp'
|
|
12
|
+
require_relative 'packet/publish'
|
|
13
|
+
require_relative 'packet/pub_ack'
|
|
14
|
+
require_relative 'packet/pub_rec'
|
|
15
|
+
require_relative 'packet/pub_rel'
|
|
16
|
+
require_relative 'packet/pub_comp'
|
|
17
|
+
require_relative 'packet/subscribe'
|
|
18
|
+
require_relative 'packet/sub_ack'
|
|
19
|
+
require_relative 'packet/unsubscribe'
|
|
20
|
+
require_relative 'packet/unsub_ack'
|
|
21
|
+
require_relative 'packet/auth'
|
|
22
|
+
|
|
23
|
+
MQTT::V5::Packet::PACKET_TYPES.freeze
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MQTT
|
|
4
|
+
module V5
|
|
5
|
+
module TopicAlias
|
|
6
|
+
# Bidirectional topic<->alias storage
|
|
7
|
+
# @!visibility private
|
|
8
|
+
class Cache
|
|
9
|
+
# @!visibility private
|
|
10
|
+
attr_reader :max_assigned
|
|
11
|
+
|
|
12
|
+
# @return [Integer] maximum number of topic aliases this cache can hold
|
|
13
|
+
attr_reader :max
|
|
14
|
+
|
|
15
|
+
# @return [Integer] the total size of topic_names held in this cache
|
|
16
|
+
attr_reader :cached_bytes
|
|
17
|
+
|
|
18
|
+
def self.create(max)
|
|
19
|
+
max&.positive? ? new(max: max) : nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(max:)
|
|
23
|
+
@max = max
|
|
24
|
+
@aliases = {} # String keys: topic => alias, Integer keys: alias => topic
|
|
25
|
+
@max_assigned = 0
|
|
26
|
+
@cached_bytes = 0
|
|
27
|
+
@available = []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Resolve topic to alias or alias to topic
|
|
31
|
+
# @param topic_or_alias [String, Integer]
|
|
32
|
+
# @return [Integer, String, nil]
|
|
33
|
+
def resolve(topic_or_alias)
|
|
34
|
+
@aliases[topic_or_alias]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @!visibility private
|
|
38
|
+
def assign
|
|
39
|
+
return false if full?
|
|
40
|
+
|
|
41
|
+
@available.pop || (@max_assigned += 1)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Boolean] true if there are {#max} entries in the map
|
|
45
|
+
def full?
|
|
46
|
+
@available.empty? && max_assigned >= max
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Integer] number of entries in the map
|
|
50
|
+
def size
|
|
51
|
+
@max_assigned - @available.size
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @!visibility private
|
|
55
|
+
# Add bidirectional mapping
|
|
56
|
+
# @param alias_id [Integer]
|
|
57
|
+
# @param topic [String]
|
|
58
|
+
def add(alias_id, topic)
|
|
59
|
+
raise ProtocolError, "AliasId (#{alias_id}) must be between 1 and #{max}" unless alias_id.between?(1, max)
|
|
60
|
+
|
|
61
|
+
@max_assigned = alias_id if alias_id > @max_assigned
|
|
62
|
+
@cached_bytes += topic.bytesize - (@aliases[alias_id]&.bytesize || 0)
|
|
63
|
+
@aliases[alias_id] = topic
|
|
64
|
+
@aliases[topic] = alias_id
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @!visibility private
|
|
68
|
+
# Explicitly Remove a topic (a previously aliased topic later explicitly set to use alias false)
|
|
69
|
+
# @param topic [String]
|
|
70
|
+
# rubocop:disable Naming/PredicateMethod
|
|
71
|
+
def remove(topic)
|
|
72
|
+
alias_id = @aliases.delete(topic)
|
|
73
|
+
return false unless alias_id
|
|
74
|
+
|
|
75
|
+
@available.push(alias_id)
|
|
76
|
+
@cached_bytes -= @aliases.delete(alias_id)&.bytesize || 0
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
# rubocop:enable Naming/PredicateMethod
|
|
80
|
+
|
|
81
|
+
def topics
|
|
82
|
+
@aliases.keys.select { |k| k.is_a?(String) }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|