mqtt-core 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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/lib/mqtt/core/client/acknowledgement.rb +31 -0
  3. data/lib/mqtt/core/client/client_id_generator.rb +19 -0
  4. data/lib/mqtt/core/client/connection.rb +193 -0
  5. data/lib/mqtt/core/client/enumerable_subscription.rb +224 -0
  6. data/lib/mqtt/core/client/filesystem_session_store.rb +197 -0
  7. data/lib/mqtt/core/client/memory_session_store.rb +84 -0
  8. data/lib/mqtt/core/client/qos0_session_store.rb +56 -0
  9. data/lib/mqtt/core/client/qos2_session_store.rb +45 -0
  10. data/lib/mqtt/core/client/qos_tracker.rb +119 -0
  11. data/lib/mqtt/core/client/retry_strategy.rb +63 -0
  12. data/lib/mqtt/core/client/session.rb +195 -0
  13. data/lib/mqtt/core/client/session_store.rb +128 -0
  14. data/lib/mqtt/core/client/socket_factory.rb +268 -0
  15. data/lib/mqtt/core/client/subscription.rb +109 -0
  16. data/lib/mqtt/core/client/uri.rb +77 -0
  17. data/lib/mqtt/core/client.rb +700 -0
  18. data/lib/mqtt/core/packet/connect.rb +39 -0
  19. data/lib/mqtt/core/packet/publish.rb +45 -0
  20. data/lib/mqtt/core/packet/subscribe.rb +190 -0
  21. data/lib/mqtt/core/packet/unsubscribe.rb +21 -0
  22. data/lib/mqtt/core/packet.rb +168 -0
  23. data/lib/mqtt/core/type/binary.rb +35 -0
  24. data/lib/mqtt/core/type/bit_flags.rb +89 -0
  25. data/lib/mqtt/core/type/boolean_byte.rb +48 -0
  26. data/lib/mqtt/core/type/fixed_int.rb +43 -0
  27. data/lib/mqtt/core/type/list.rb +41 -0
  28. data/lib/mqtt/core/type/password.rb +36 -0
  29. data/lib/mqtt/core/type/properties.rb +124 -0
  30. data/lib/mqtt/core/type/reason_codes.rb +60 -0
  31. data/lib/mqtt/core/type/remaining.rb +30 -0
  32. data/lib/mqtt/core/type/shape.rb +177 -0
  33. data/lib/mqtt/core/type/sub_type.rb +34 -0
  34. data/lib/mqtt/core/type/utf8_string.rb +39 -0
  35. data/lib/mqtt/core/type/utf8_string_pair.rb +29 -0
  36. data/lib/mqtt/core/type/var_int.rb +56 -0
  37. data/lib/mqtt/core/version.rb +10 -0
  38. data/lib/mqtt/core.rb +5 -0
  39. data/lib/mqtt/errors.rb +39 -0
  40. data/lib/mqtt/logger.rb +92 -0
  41. data/lib/mqtt/open.rb +239 -0
  42. data/lib/mqtt/options.rb +59 -0
  43. data/lib/mqtt/version.rb +6 -0
  44. data/lib/mqtt.rb +3 -0
  45. data/lib/patches/openssl.rb +19 -0
  46. metadata +98 -0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module Core
5
+ module Type
6
+ # list of type
7
+ class List
8
+ attr_reader :type
9
+
10
+ def initialize(type)
11
+ @type = type
12
+ end
13
+
14
+ def item_type
15
+ type
16
+ end
17
+
18
+ def default_value
19
+ []
20
+ end
21
+
22
+ def read(io)
23
+ result = []
24
+ result << type.read(io) until io.eof?
25
+ result.freeze
26
+ end
27
+
28
+ def write(value, io)
29
+ value&.each { |v| type.write(v, io) }
30
+ end
31
+
32
+ # @return [Array<#type>]
33
+ def from(arr, **)
34
+ arr ||= []
35
+ arr = [arr] unless arr.respond_to?(:map)
36
+ arr.map { |v| type.from(v, **) }.freeze
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module Core
5
+ module Type
6
+ # Password data - input can be a proc to pull a password
7
+ # converts to String with binary encoding
8
+ module Password
9
+ module_function
10
+
11
+ # return [String with binary encoding]
12
+ def read(io)
13
+ size = io.read(2)&.unpack1('S>')
14
+ io.read(size)
15
+ end
16
+
17
+ def write(value, io)
18
+ value ||= default_value
19
+ io.write([value.size, value].pack('S>A*'))
20
+ end
21
+
22
+ # @return [String(binary)]
23
+ def from(value, **)
24
+ return nil if value.nil?
25
+
26
+ value = value.call if value.respond_to?(:call)
27
+ value.to_s.b
28
+ end
29
+
30
+ def default_value
31
+ @default_value ||= ''.b.freeze
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module Core
5
+ module Type
6
+ # Properties Map (used since MQTT 5.0)
7
+ class Properties
8
+ # @!parse class PropertyType < Data; end
9
+ PropertyType = Data.define(:id, :name, :type, :packet_types) do
10
+ class << self
11
+ def create(id, name, type, packet_types = :all, types: {})
12
+ new(id, name, resolve_type(type, types), packet_types)
13
+ end
14
+
15
+ def resolve_type(input_type, types)
16
+ case input_type
17
+ when Symbol
18
+ types.fetch(input_type) { raise ProtocolError, "Unknown property type #{input_type}" }
19
+ when Array
20
+ List.new(resolve_type(input_type.first, types))
21
+ else
22
+ input_type
23
+ end
24
+ end
25
+ end
26
+
27
+ def initialize(id:, name:, type:, packet_types: [:all])
28
+ super
29
+ end
30
+
31
+ def from(value)
32
+ type.from(value)
33
+ end
34
+
35
+ def read(io, into)
36
+ if type.is_a?(Type::List)
37
+ (into[name] ||= []) << type.item_type.read(io)
38
+ else
39
+ into[name] = type.read(io)
40
+ end
41
+ end
42
+
43
+ def write(value, io, write_type: type)
44
+ if write_type.is_a?(Type::List)
45
+ value.each { |item| write(item, io, write_type: type.item_type) }
46
+ else
47
+ VarInt.write(id, io)
48
+ write_type.write(value, io)
49
+ end
50
+ end
51
+
52
+ def for_packet?(packet_type_name)
53
+ return true if packet_types == :all
54
+
55
+ packet_types.include?(packet_type_name)
56
+ end
57
+ end
58
+
59
+ def initialize(packet_type_name, property_types)
60
+ @property_types = property_types.select { |pt| pt.for_packet?(packet_type_name) }.freeze
61
+ @property_map =
62
+ Hash.new { |_h, k| raise ProtocolError, "Unknown property type #{k} for #{packet_type_name}" }
63
+ .merge!(@property_types.to_h { |pt| [pt.name, pt] })
64
+ .merge!(@property_types.to_h { |pt| [pt.id, pt] })
65
+ .freeze
66
+ end
67
+
68
+ def sub_properties
69
+ @property_types.to_h { |pt| [pt.name, pt.type] }
70
+ end
71
+
72
+ def read(io)
73
+ default_value.tap do |output|
74
+ # Where properties is the last thing in a packet and there are no properties, the length property
75
+ # is not required to be set
76
+ next if io.eof?
77
+
78
+ Packet.read_sio(io) do |sio|
79
+ until sio.eof?
80
+ id = VarInt.read(sio)
81
+ property_type = @property_map.fetch(id) do
82
+ raise ProtocolError, format('Unknown property id 0x%02x', id)
83
+ end
84
+ property_type.read(sio, output)
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ def write(values, io)
91
+ Packet.write_sio(io) do |sio|
92
+ values.each_pair do |name, value|
93
+ property_type = @property_map.fetch(name) { raise ProtocolError, "Unknown property name #{name}" }
94
+ property_type.write(value, sio)
95
+ end
96
+ end
97
+ end
98
+
99
+ # @param [Hash|nil] values
100
+ def from(values, data:)
101
+ values ||= default_value
102
+ raise ArgumentError 'Properties values must be a Hash' unless values.respond_to?(:each_pair)
103
+
104
+ # properties with entries at the top level of the data structure override the underlying hash value
105
+ @property_types.filter_map do |property_type|
106
+ property_type.name.tap { |name| values[name] = data.delete(name) if data.key?(name) }
107
+ end
108
+
109
+ # now make sure every entry in values is valid and converted to type
110
+ values.filter_map do |(name, value)|
111
+ property_type = @property_map.fetch(name) { raise ArgumentError, "Unknown property name #{name}" }
112
+ next nil unless (filled_value = property_type.from(value))
113
+
114
+ [name, filled_value]
115
+ end.to_h.freeze
116
+ end
117
+
118
+ def default_value
119
+ {}
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fixed_int'
4
+
5
+ module MQTT
6
+ module Core
7
+ module Type
8
+ # A type that resolves to a reason code
9
+ class ReasonCodes
10
+ # @!parse class ReasonCode < ::Data; end
11
+ ReasonCode = Data.define(:code, :name, :packet_types, :error) do
12
+ def for_packet?(packet_type_name)
13
+ code == 0xff || packet_types.include?(packet_type_name)
14
+ end
15
+
16
+ def success?
17
+ !failed?
18
+ end
19
+
20
+ def failed?
21
+ code >= 0x80
22
+ end
23
+
24
+ def to_s
25
+ format '%<name>s(0x%02<code>x)', name:, code:
26
+ end
27
+ end
28
+
29
+ def initialize(valid_codes)
30
+ @reason_code_map = valid_codes.to_h { |rc| [rc.code, rc] }
31
+ end
32
+
33
+ def fetch(...)
34
+ @reason_code_map.fetch(...)
35
+ end
36
+
37
+ def read(io)
38
+ # if (s)io is at eof then the reason code is assumed to be success
39
+ fetch(Int8.read(io) || 0x00) { |rc| raise ProtocolError, "Invalid Reason Code #{rc}" }
40
+ end
41
+
42
+ def write(value, io)
43
+ # use from here as sometimes the code is applied as a default
44
+ Int8.write(from(value).code, io)
45
+ end
46
+
47
+ def from(input_value, **)
48
+ input_value = input_value.code if input_value.is_a?(ReasonCode)
49
+ raise ArgumentError, "Invalid Reason Code #{input_value}" unless input_value.respond_to?(:to_i)
50
+
51
+ fetch(input_value.to_i) { |rc| raise ArgumentError, "Invalid Reason Code #{rc}" }
52
+ end
53
+
54
+ def default_value
55
+ fetch(0x00)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module Core
5
+ module Type
6
+ # remaining payload, no length prefix, reads to EOF (StringIO) and writes the whole value as binary
7
+ # string
8
+ module Remaining
9
+ module_function
10
+
11
+ def read(io)
12
+ # read to end of file (which only works because it is only ever used with a StringIO
13
+ io.read
14
+ end
15
+
16
+ def write(value, io)
17
+ io.write(value.to_s.b)
18
+ end
19
+
20
+ def from(value, **)
21
+ value.to_s
22
+ end
23
+
24
+ def default_value
25
+ ''.b
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module Core
5
+ module Type
6
+ # Base module for structured MQTT types {Packet} and {SubType}
7
+ module Shape
8
+ Field = Data.define(:name, :type, :condition) do
9
+ def write(obj, io)
10
+ type.write(obj.send(name), io) if obj.instance_exec(&condition)
11
+ end
12
+
13
+ def read(obj, io)
14
+ value = type.read(io) if obj.instance_exec(&condition)
15
+ value = type.default_value if value.nil? && type.respond_to?(:default_value)
16
+ value
17
+ end
18
+ end
19
+
20
+ # methods for defining fields
21
+ module Definition
22
+ attr_reader :fields
23
+
24
+ def flags(*flags)
25
+ Type::BitFlags.new(*flags)
26
+ end
27
+
28
+ def list(type, **fields, &)
29
+ if fields.any?
30
+ klass = Class.new(Type::SubType)
31
+ klass.instance_variable_set(
32
+ :@fields, resolve_fields(fields).tap { |resolved| define_field_methods(resolved, klass:) }
33
+ )
34
+ # Define additional methods and aliases (after field methods are defined!)
35
+ klass.class_eval(&) if block_given?
36
+ type = const_set(type.to_s.split('_').map(&:capitalize).join, klass)
37
+ elsif type.is_a?(Symbol)
38
+ type = self::VALUE_TYPES[type]
39
+ end
40
+
41
+ Type::List.new(type)
42
+ end
43
+
44
+ def resolve_fields(fields)
45
+ fields.map do |(name, type)|
46
+ type, condition = field_type_for(type)
47
+ Field.new(name:, type:, condition:)
48
+ end
49
+ end
50
+
51
+ def field_type_for(type_info)
52
+ return field_type_for({ type: type_info, if: true }) unless type_info.is_a?(Hash)
53
+
54
+ type, type_if = type_info.values_at(:type, :if)
55
+ type = self::VALUE_TYPES[type] if type.is_a?(Symbol)
56
+ condition =
57
+ case type_if
58
+ when Symbol
59
+ -> { send(type_if) }
60
+ when Proc
61
+ type_if
62
+ else
63
+ -> { true }
64
+ end
65
+ [type, condition]
66
+ end
67
+
68
+ # Avoid name clashes
69
+ def sub_property_method(_name, property_name)
70
+ property_name
71
+ end
72
+
73
+ def define_field_methods(fields, klass: self)
74
+ fields.map do |field|
75
+ name, type, _condition = field.deconstruct
76
+
77
+ if type.respond_to?(:sub_properties)
78
+ define_property_readers(name, type, klass:)
79
+ define_hash_reader(name, type, klass:)
80
+ else
81
+ klass.define_method(name) { @data[name] }
82
+ end
83
+ end
84
+ end
85
+
86
+ def define_property_readers(name, type, klass:)
87
+ type.sub_properties.each_key do |property_name|
88
+ method = klass.sub_property_method(name, property_name) if klass.respond_to?(:sub_property_method)
89
+ klass.define_method(method || property_name) { @data.dig(name, property_name) }
90
+ end
91
+ end
92
+
93
+ def define_hash_reader(name, type, klass:)
94
+ klass.define_method(name) do
95
+ type.sub_properties.keys.filter_map do |property_name|
96
+ method = klass.sub_property_method(name, property_name) if klass.respond_to?(:sub_property_method)
97
+ value = send(method || property_name)
98
+ [property_name, value] unless value.nil?
99
+ end.to_h.freeze
100
+ end
101
+ end
102
+ end
103
+
104
+ def initialize(*deserialize_args, **data)
105
+ @data = {}
106
+ apply_data(defaults)
107
+ if deserialize_args.any?
108
+ deserialize(*deserialize_args)
109
+ else
110
+ apply_data(data) if data.any?
111
+ apply_overrides(@data)
112
+ end
113
+ validate if respond_to?(:validate, true)
114
+ @data.freeze
115
+ end
116
+
117
+ def serialize(io)
118
+ serialize_fields(self.class.fields, io)
119
+ end
120
+
121
+ def to_h
122
+ @data
123
+ end
124
+
125
+ def deconstruct_keys(*keys)
126
+ to_h.slice(keys)
127
+ end
128
+
129
+ private
130
+
131
+ # rubocop:disable Metrics/AbcSize
132
+ def apply_data(data)
133
+ self.class.fields.each do |f|
134
+ # source @data (defaults)
135
+ # data[f.name] if data.key?(f.name) - overwrites or merges with defaults
136
+ # properties if data[sub_property_name]
137
+ if f.type.respond_to?(:sub_properties)
138
+ result = @data[f.name] ||= {}
139
+ result.merge!(data.delete(f.name)) if data.key?(f.name)
140
+ f.type.sub_properties.each do |property_name, property_type|
141
+ result[property_name] = property_type.from(data.delete(property_name)) if data.key?(property_name)
142
+ end
143
+ elsif data.key?(f.name)
144
+ @data[f.name] = f.type.from(data.delete(f.name))
145
+ end
146
+ end
147
+ raise ArgumentError, "Unused data for #{self.class.name} - #{data}" unless data.empty?
148
+ end
149
+ # rubocop:enable Metrics/AbcSize
150
+
151
+ # applied in the same way as user supplied data
152
+ def defaults
153
+ {}
154
+ end
155
+
156
+ def apply_overrides(data)
157
+ # do nothing by default
158
+ end
159
+
160
+ def serialize_fields(fields, io)
161
+ fields.each { |f| f.write(self, io) }
162
+ end
163
+
164
+ def deserialize_fields(fields, io)
165
+ fields.filter_map do |f|
166
+ value = f.read(self, io)
167
+ [f.name, value] if value
168
+ end.to_h
169
+ end
170
+
171
+ def deserialize(io)
172
+ @data.merge!(deserialize_fields(self.class.fields, io))
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'shape'
4
+
5
+ module MQTT
6
+ module Core
7
+ module Type
8
+ # Class representing an MQTT SubType (defined for structured lists)
9
+ class SubType
10
+ include Shape
11
+
12
+ class << self
13
+ # @!attribute [r] fields
14
+ # @return [Array] fields injected by List
15
+ attr_reader :fields
16
+
17
+ def read(io)
18
+ new(io)
19
+ end
20
+
21
+ def write(value, io)
22
+ value.serialize(io)
23
+ end
24
+
25
+ # TODO: if we had a subtype that was not within a list
26
+ # then we'd probably list the fields and pull top level content from the data
27
+ def from(value, **)
28
+ new(**value)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module Core
5
+ module Type
6
+ # UTF8 Encoded String written with length as prefix
7
+ module UTF8String
8
+ module_function
9
+
10
+ def read(io)
11
+ size = io.read(2)&.unpack1('S>')
12
+ io.read(size).force_encoding(Encoding::UTF_8).tap { |s| raise EncodingError unless valid_utf8?(s) }
13
+ end
14
+
15
+ def from(value, **)
16
+ return nil if value.nil?
17
+
18
+ value = value.to_s
19
+ return value if valid_utf8?(value)
20
+
21
+ value.encode(Encoding::UTF_8).tap { |s| raise EncodingError unless valid_utf8?(s) }
22
+ end
23
+
24
+ def write(value, io)
25
+ value ||= default_value
26
+ io.write([value.size, value].pack('S>A*'))
27
+ end
28
+
29
+ def default_value
30
+ @default_value ||= ''.encode(Encoding::UTF_8).freeze
31
+ end
32
+
33
+ def valid_utf8?(str)
34
+ str.encoding == Encoding::UTF_8 && str.valid_encoding? && !str.include?("\u0000")
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module Core
5
+ module Type
6
+ # Key value pair as UTF8 encoded strings
7
+ module UTF8StringPair
8
+ module_function
9
+
10
+ def read(io)
11
+ [UTF8String.read(io), UTF8String.read(io)].freeze
12
+ end
13
+
14
+ def from(value, **)
15
+ raise ArgumentError, 'Value must be Array<String,String>' unless value.is_a?(Array) && value.size == 2
16
+
17
+ value.map { |v| UTF8String.from(v) }.freeze
18
+ end
19
+
20
+ def write(value, io)
21
+ raise ProtocolError, 'Value must be Array<String,String>' unless value.is_a?(Array) && value.size == 2
22
+
23
+ UTF8String.write(value[0], io)
24
+ UTF8String.write(value[1], io)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module Core
5
+ module Type
6
+ # Variable Byte Integer
7
+ module VarInt
8
+ module_function
9
+
10
+ # Maximum variable integer (hence maximum packet size = 256MB)
11
+ MAX_INTVAR = 268_435_455
12
+
13
+ def read(io)
14
+ value = 0
15
+ multiplier = 1
16
+
17
+ loop do
18
+ encoded_byte = io.readbyte
19
+ return value unless encoded_byte
20
+
21
+ value += (encoded_byte & 0x7F) * multiplier
22
+
23
+ raise ProtocolError, 'Malformed Variable Byte Integer' if multiplier > 0x80**3
24
+
25
+ multiplier *= 0x80
26
+
27
+ return value if encoded_byte.nobits?(128)
28
+ end
29
+ end
30
+
31
+ def from(value)
32
+ value ||= 0
33
+ raise ArgumentError, 'Vaiue must be Integer' unless value.respond_to?(:to_int)
34
+
35
+ value = value.to_i
36
+ raise ArgumentError, "Value out of range (0 - #{MAX_INTVAR})" unless value.between?(0, MAX_INTVAR)
37
+
38
+ value
39
+ end
40
+
41
+ def write(value, io)
42
+ value ||= 0
43
+ raise ProtocolError, "Value out of range (0 - #{MAX_INTVAR})" unless value.between?(0, MAX_INTVAR)
44
+
45
+ loop do
46
+ encoded_byte = value % 0x80
47
+ value /= 0x80
48
+ encoded_byte |= 0x80 if value.positive?
49
+ io.write([encoded_byte].pack('C'))
50
+ break if value.zero?
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../version'
4
+
5
+ module MQTT
6
+ module Core
7
+ # The version number of the MQTT Core gem
8
+ VERSION = MQTT::VERSION
9
+ end
10
+ end
data/lib/mqtt/core.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'logger'
4
+ require_relative 'open'
5
+ require_relative 'core/client'
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+
5
+ module MQTT
6
+ # Super-class for other MQTT related exceptions
7
+ class Error < StandardError
8
+ # included in protocol specific errors that are retriable
9
+ module Retriable
10
+ end
11
+ end
12
+
13
+ # Raised if there is a problem with data received from a remote host
14
+ class ProtocolError < Error; end
15
+
16
+ # Raised when trying to perform a function but no connection is available
17
+ class ConnectionError < Error; end
18
+
19
+ # Raised in the disconnect handler when the client expects a session, but the broker does not have one.
20
+ class SessionNotPresent < Error; end
21
+
22
+ # Raised in the disconnect handler when the client session has expired before it can reconnect to the broker.
23
+ class SessionExpired < Error; end
24
+
25
+ # A ResponseError will be raised from packet acknowledgements
26
+ class ResponseError < Error; end
27
+
28
+ RETRIABLE_NETWORK_ERRORS = [
29
+ Errno::ECONNABORTED, # Connection aborted
30
+ Errno::ECONNRESET, # Connection reset
31
+ Errno::EHOSTUNREACH, # No route to host
32
+ Errno::ENETUNREACH, # Network unreachable
33
+ Errno::EPIPE, # Broken pipe
34
+ Errno::ETIMEDOUT, # Connection timed out
35
+ Errno::EINVAL, # Invalid argument (Windows-specific)
36
+ SocketError, # DNS and basic socket errors
37
+ IOError # Generic IO errors (including IO::TimeoutError)
38
+ ].freeze
39
+ end