netsnmp 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b080e928ed8014ae935e3bafd2356c21304b60f1c29c2c5327f7694e2825567a
4
- data.tar.gz: de052efcd0958285cf8ae5609fadca6519d94e48af2076f74d1a76886e6f5309
3
+ metadata.gz: 60a9c68e56e5ad0600d056eedd315d8639cfd3965aa5e1eb62f5f3f935b4ad6a
4
+ data.tar.gz: ea560ac1131bff9ca6af0fec5e426bd37edbfbc7520ceb629f1de279a905ced0
5
5
  SHA512:
6
- metadata.gz: 9ead6354c915b389219ef88a0227fe509347897b4980278cd4daf08d410014effeb1863c2d4aa36ddcc019c9dd018f9ec383771812e50de75ed3f6d29d35fd38
7
- data.tar.gz: 323eb76b9ed6538704897d000b03d100c04b3443dd2b7c6b2313da67db065a0adca5f31431eef0b571eb9a29a4d9762fd6151fc90840c149cdf023634ab802d3
6
+ metadata.gz: 8410a3a38d7d7eab07692009ae4d870dbae8e7abc50dbc724d9ad3f8a4e029b441ffba62e9477ae587f8845a561cb0ba2e2c5613540d615bdce0cf62cb448098
7
+ data.tar.gz: 85d0d898983f99dedb4a71cf6bd33f665cfda496bb8a60eb76be36a96de6f28a9984505c564701bfab6bf74e8367544ec2f5adbcf8faf152d9a71679b73b5df1
data/README.md CHANGED
@@ -32,7 +32,7 @@ This gem provides:
32
32
 
33
33
  * Implementation in ruby of the SNMP Protocol for v3, v2c and v1 (most notable the rfc3414 and 3826).
34
34
  * Client/Manager API with simple interface for get, genext, set and walk.
35
- * No dependencies.
35
+ * Pure Ruby.
36
36
  * Support for concurrency and evented I/O.
37
37
 
38
38
  ## Why?
@@ -54,11 +54,12 @@ All of these issues are resolved here.
54
54
  ## Features
55
55
 
56
56
  * Client Interface, which supports SNMP v3, v2c, and v1
57
- * Supports get, getnext, set and walk calls.
58
- * MIB support.
57
+ * Supports get, getnext, set and walk calls
58
+ * MIB support
59
59
  * Proxy IO object support (for eventmachine/celluloid-io)
60
60
  * Ruby >= 2.1 support (modern)
61
61
  * Pure Ruby (no FFI)
62
+ * Easy PDU debugging
62
63
 
63
64
  ## Examples
64
65
 
@@ -236,10 +237,24 @@ NETSNMP::Client.new(share_options.merge(proxy: router_proxy, security_parameters
236
237
  end
237
238
  ```
238
239
 
240
+ ## Compatibility
241
+
242
+ This library supports and is tested against ruby versions 2.1 or more recent, including ruby 3. It also supports and tests against Truffleruby.
243
+
239
244
  ## OpenSSL
240
245
 
241
246
  All encoding/decoding/encryption/decryption/digests are done using `openssl`, which is (still) a part of the standard library. If at some point `openssl` is removed and not specifically distributed, you'll have to install it yourself. Hopefully this will never happen.
242
247
 
248
+ ## Debugging
249
+
250
+ You can either set the `NETSNMP_DEBUG` to the desided debug level (currently, 1 and 2). The logs will be written to stderr.
251
+
252
+ You can also set it for a specific client:
253
+
254
+ ```ruby
255
+ manager2 = NETSNMP::Client.new(debug: $stderr, debug_level: 2, ....)
256
+ ```
257
+
243
258
 
244
259
  ## Tests
245
260
 
data/lib/netsnmp.rb CHANGED
@@ -33,51 +33,9 @@ rescue LoadError
33
33
  end
34
34
  end
35
35
 
36
- module NETSNMP
37
- module IsNumericExtensions
38
- refine String do
39
- def integer?
40
- each_byte do |byte|
41
- return false unless byte >= 48 && byte <= 57
42
- end
43
- true
44
- end
45
- end
46
- end
47
-
48
- module StringExtensions
49
- # If you wonder why this is there: the oauth feature uses a refinement to enhance the
50
- # Regexp class locally with #match? , but this is never tested, because ActiveSupport
51
- # monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
52
- # :nocov:
53
- refine(String) do
54
- def match?(*args)
55
- !match(*args).nil?
56
- end
57
- end
58
- end
59
-
60
- def self.debug=(io)
61
- @debug_output = io
62
- end
63
-
64
- def self.debug(&blk)
65
- @debug_output << blk.call + "\n" if @debug_output
66
- end
67
-
68
- unless defined?(Hexdump) # support the hexdump gem
69
- module Hexdump
70
- def self.dump(data, width: 8)
71
- pairs = data.unpack("H*").first.scan(/.{4}/)
72
- pairs.each_slice(width).map do |row|
73
- row.join(" ")
74
- end.join("\n")
75
- end
76
- end
77
- end
78
- end
79
-
80
36
  require "netsnmp/errors"
37
+ require "netsnmp/extensions"
38
+ require "netsnmp/loggable"
81
39
 
82
40
  require "netsnmp/timeticks"
83
41
 
@@ -153,7 +153,7 @@ module NETSNMP
153
153
  retries = @retries
154
154
  begin
155
155
  yield
156
- rescue Timeout::Error => e
156
+ rescue Timeout::Error, IdNotInTimeWindowError => e
157
157
  raise e if retries.zero?
158
158
  retries -= 1
159
159
  retry
@@ -22,13 +22,12 @@ module NETSNMP
22
22
  end
23
23
 
24
24
  encrypted_data = cipher.update(decrypted_data) + cipher.final
25
- NETSNMP.debug { "encrypted:\n#{Hexdump.dump(encrypted_data)}" }
26
25
 
27
26
  [encrypted_data, salt]
28
27
  end
29
28
 
30
29
  def decrypt(encrypted_data, salt:, engine_boots:, engine_time:)
31
- raise Error, "invalid priv salt received" unless (salt.length % 8).zero?
30
+ raise Error, "invalid priv salt received" unless !salt.empty? && (salt.length % 8).zero?
32
31
 
33
32
  cipher = OpenSSL::Cipher::AES128.new(:CFB)
34
33
  cipher.padding = 0
@@ -39,7 +38,6 @@ module NETSNMP
39
38
  cipher.key = aes_key
40
39
  cipher.iv = iv
41
40
  decrypted_data = cipher.update(encrypted_data) + cipher.final
42
- NETSNMP.debug { "decrypted:\n#{Hexdump.dump(decrypted_data)}" }
43
41
 
44
42
  hlen, bodylen = OpenSSL::ASN1.traverse(decrypted_data) { |_, _, x, y, *| break x, y }
45
43
  decrypted_data.byteslice(0, hlen + bodylen)
@@ -24,7 +24,6 @@ module NETSNMP
24
24
  end
25
25
 
26
26
  encrypted_data = cipher.update(decrypted_data) + cipher.final
27
- NETSNMP.debug { "encrypted:\n#{Hexdump.dump(encrypted_data)}" }
28
27
  [encrypted_data, salt]
29
28
  end
30
29
 
@@ -41,7 +40,6 @@ module NETSNMP
41
40
  cipher.key = des_key
42
41
  cipher.iv = iv
43
42
  decrypted_data = cipher.update(encrypted_data) + cipher.final
44
- NETSNMP.debug { "decrypted:\n#{Hexdump.dump(decrypted_data)}" }
45
43
 
46
44
  hlen, bodylen = OpenSSL::ASN1.traverse(decrypted_data) { |_, _, x, y, *| break x, y }
47
45
  decrypted_data.byteslice(0, hlen + bodylen)
@@ -4,4 +4,5 @@ module NETSNMP
4
4
  Error = Class.new(StandardError)
5
5
  ConnectionFailed = Class.new(Error)
6
6
  AuthenticationFailed = Class.new(Error)
7
+ IdNotInTimeWindowError = Class.new(Error)
7
8
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NETSNMP
4
+ module IsNumericExtensions
5
+ refine String do
6
+ def integer?
7
+ each_byte do |byte|
8
+ return false unless byte >= 48 && byte <= 57
9
+ end
10
+ true
11
+ end
12
+ end
13
+ end
14
+
15
+ module StringExtensions
16
+ refine(String) do
17
+ unless String.method_defined?(:match?)
18
+ def match?(*args)
19
+ !match(*args).nil?
20
+ end
21
+ end
22
+
23
+ unless String.method_defined?(:unpack1)
24
+ def unpack1(format)
25
+ unpack(format).first
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ module ASNExtensions
32
+ ASN_COLORS = {
33
+ OpenSSL::ASN1::Sequence => 34, # blue
34
+ OpenSSL::ASN1::OctetString => 32, # green
35
+ OpenSSL::ASN1::Integer => 33, # yellow
36
+ OpenSSL::ASN1::ObjectId => 35, # magenta
37
+ OpenSSL::ASN1::ASN1Data => 36 # cyan
38
+ }.freeze
39
+
40
+ # basic types
41
+ ASN_COLORS.each_key do |klass|
42
+ refine(klass) do
43
+ def to_hex
44
+ "#{colorize_hex} (#{value.to_s.inspect})"
45
+ end
46
+ end
47
+ end
48
+
49
+ # composite types
50
+ refine(OpenSSL::ASN1::Sequence) do
51
+ def to_hex
52
+ values = value.map(&:to_der).join
53
+ hex_values = value.map(&:to_hex).map { |s| s.gsub(/(\t+)/) { "\t#{Regexp.last_match(1)}" } }.map { |s| "\n\t#{s}" }.join
54
+ der = to_der
55
+ der = der.sub(values, "")
56
+
57
+ "#{colorize_hex(der)}#{hex_values}"
58
+ end
59
+ end
60
+
61
+ refine(OpenSSL::ASN1::ASN1Data) do
62
+ attr_reader :label
63
+
64
+ def with_label(label)
65
+ @label = label
66
+ self
67
+ end
68
+
69
+ def to_hex
70
+ case value
71
+ when Array
72
+ values = value.map(&:to_der).join
73
+ hex_values = value.map(&:to_hex)
74
+ .map { |s| s.gsub(/(\t+)/) { "\t#{Regexp.last_match(1)}" } }
75
+ .map { |s| "\n\t#{s}" }.join
76
+ der = to_der
77
+ der = der.sub(values, "")
78
+ else
79
+ der = to_der
80
+ hex_values = nil
81
+ end
82
+
83
+ "#{colorize_hex(der)}#{hex_values}"
84
+ end
85
+
86
+ private
87
+
88
+ def colorize_hex(der = to_der)
89
+ hex = Hexdump.dump(der, separator: " ")
90
+ lbl = @label || self.class.name.split("::").last
91
+ "#{lbl}: \e[#{ASN_COLORS[self.class]}m#{hex}\e[0m"
92
+ end
93
+ end
94
+ end
95
+
96
+ module Hexdump
97
+ using StringExtensions
98
+
99
+ def self.dump(data, width: 8, in_groups_of: 4, separator: "\n")
100
+ pairs = data.unpack1("H*").scan(/.{#{in_groups_of}}/)
101
+ pairs.each_slice(width).map do |row|
102
+ row.join(" ")
103
+ end.join(separator)
104
+ end
105
+ end
106
+
107
+ # Like a string, but it prints an hex-string version of itself
108
+ class HexString < String
109
+ def inspect
110
+ Hexdump.dump(self, in_groups_of: 2, separator: " ")
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NETSNMP
4
+ module Loggable
5
+ DEBUG = ENV.key?("NETSNMP_DEBUG") ? $stderr : nil
6
+ DEBUG_LEVEL = (ENV["NETSNMP_DEBUG"] || 1).to_i
7
+
8
+ def initialize(debug: DEBUG, debug_level: DEBUG_LEVEL, **opts)
9
+ super(**opts)
10
+ @debug = debug
11
+ @debug_level = debug_level
12
+ end
13
+
14
+ private
15
+
16
+ COLORS = {
17
+ black: 30,
18
+ red: 31,
19
+ green: 32,
20
+ yellow: 33,
21
+ blue: 34,
22
+ magenta: 35,
23
+ cyan: 36,
24
+ white: 37
25
+ }.freeze
26
+
27
+ def log(level: @debug_level)
28
+ return unless @debug
29
+ return unless @debug_level >= level
30
+
31
+ debug_stream = @debug
32
+
33
+ debug_stream << (+"\n" << yield << "\n")
34
+ end
35
+ end
36
+ end
@@ -2,41 +2,71 @@
2
2
 
3
3
  module NETSNMP
4
4
  # Factory for the SNMP v3 Message format
5
- module Message
6
- module_function
5
+ class Message
6
+ using ASNExtensions
7
7
 
8
- AUTHNONE = OpenSSL::ASN1::OctetString.new("\x00" * 12)
8
+ prepend Loggable
9
+
10
+ AUTHNONE = OpenSSL::ASN1::OctetString.new("\x00" * 12).with_label(:auth_mask)
9
11
  PRIVNONE = OpenSSL::ASN1::OctetString.new("")
10
- MSG_MAX_SIZE = OpenSSL::ASN1::Integer.new(65507)
11
- MSG_SECURITY_MODEL = OpenSSL::ASN1::Integer.new(3) # usmSecurityModel
12
- MSG_VERSION = OpenSSL::ASN1::Integer.new(3)
12
+ MSG_MAX_SIZE = OpenSSL::ASN1::Integer.new(65507).with_label(:max_message_size)
13
+ MSG_SECURITY_MODEL = OpenSSL::ASN1::Integer.new(3).with_label(:security_model) # usmSecurityModel
14
+ MSG_VERSION = OpenSSL::ASN1::Integer.new(3).with_label(:message_version)
13
15
  MSG_REPORTABLE = 4
14
16
 
17
+ def initialize(**); end
18
+
15
19
  # @param [String] payload of an snmp v3 message which can be decoded
16
20
  # @param [NETSMP::SecurityParameters, #decode] security_parameters knowns how to decode the stream
17
21
  #
18
22
  # @return [NETSNMP::ScopedPDU] the decoded PDU
19
23
  #
20
24
  def decode(stream, security_parameters:)
21
- asn_tree = OpenSSL::ASN1.decode(stream)
22
- _version, _headers, sec_params, pdu_payload = asn_tree.value
25
+ log { "received encoded V3 message" }
26
+ log { Hexdump.dump(stream) }
27
+ asn_tree = OpenSSL::ASN1.decode(stream).with_label(:v3_message)
28
+
29
+ version, headers, sec_params, pdu_payload = asn_tree.value
30
+ version.with_label(:message_version)
31
+ headers.with_label(:headers)
32
+ sec_params.with_label(:security_params)
33
+ pdu_payload.with_label(:pdu)
34
+
35
+ _, _, message_flags, = headers.value
23
36
 
24
- sec_params_asn = OpenSSL::ASN1.decode(sec_params.value).value
37
+ # get last byte
38
+ # discard the left-outermost bits and keep the remaining two
39
+ security_level = message_flags.with_label(:message_flags).value.unpack("C*").last & 3
25
40
 
26
- engine_id, engine_boots, engine_time, _username, auth_param, priv_param = sec_params_asn.map(&:value)
41
+ sec_params_asn = OpenSSL::ASN1.decode(sec_params.value).with_label(:security_params)
42
+
43
+ engine_id, engine_boots, engine_time, username, auth_param, priv_param = sec_params_asn.value
44
+ engine_id.with_label(:engine_id)
45
+ engine_boots.with_label(:engine_boots)
46
+ engine_time.with_label(:engine_time)
47
+ username.with_label(:username)
48
+ auth_param.with_label(:auth_param)
49
+ priv_param.with_label(:priv_param)
50
+
51
+ log(level: 2) { asn_tree.to_hex }
52
+ log(level: 2) { sec_params_asn.to_hex }
27
53
 
28
54
  # validate_authentication
29
- security_parameters.verify(stream.sub(auth_param, AUTHNONE.value), auth_param)
55
+ auth_param = auth_param.value
56
+ security_parameters.verify(stream.sub(auth_param, AUTHNONE.value), auth_param, security_level: security_level)
30
57
 
31
- engine_boots = engine_boots.to_i
32
- engine_time = engine_time.to_i
58
+ engine_boots = engine_boots.value.to_i
59
+ engine_time = engine_time.value.to_i
33
60
 
34
- encoded_pdu = security_parameters.decode(pdu_payload, salt: priv_param,
61
+ encoded_pdu = security_parameters.decode(pdu_payload, salt: priv_param.value,
35
62
  engine_boots: engine_boots,
36
- engine_time: engine_time)
63
+ engine_time: engine_time,
64
+ security_level: security_level)
37
65
 
66
+ log { "received response PDU" }
38
67
  pdu = ScopedPDU.decode(encoded_pdu)
39
- [pdu, engine_id, engine_boots, engine_time]
68
+ log(level: 2) { pdu.to_hex }
69
+ [pdu, engine_id.value, engine_boots, engine_time]
40
70
  end
41
71
 
42
72
  # @param [NETSNMP::ScopedPDU] the PDU to encode in the message
@@ -45,36 +75,48 @@ module NETSNMP
45
75
  # @return [String] the byte representation of an SNMP v3 Message
46
76
  #
47
77
  def encode(pdu, security_parameters:, engine_boots: 0, engine_time: 0)
78
+ log(level: 2) { pdu.to_hex }
79
+ log { "encoding PDU in V3 message..." }
48
80
  scoped_pdu, salt_param = security_parameters.encode(pdu, salt: PRIVNONE,
49
81
  engine_boots: engine_boots,
50
82
  engine_time: engine_time)
51
83
 
52
84
  sec_params = OpenSSL::ASN1::Sequence.new([
53
- OpenSSL::ASN1::OctetString.new(security_parameters.engine_id),
54
- OpenSSL::ASN1::Integer.new(engine_boots),
55
- OpenSSL::ASN1::Integer.new(engine_time),
56
- OpenSSL::ASN1::OctetString.new(security_parameters.username),
85
+ OpenSSL::ASN1::OctetString.new(security_parameters.engine_id).with_label(:engine_id),
86
+ OpenSSL::ASN1::Integer.new(engine_boots).with_label(:engine_boots),
87
+ OpenSSL::ASN1::Integer.new(engine_time).with_label(:engine_time),
88
+ OpenSSL::ASN1::OctetString.new(security_parameters.username).with_label(:username),
57
89
  AUTHNONE,
58
90
  salt_param
59
- ])
91
+ ]).with_label(:security_params)
92
+ log(level: 2) { sec_params.to_hex }
93
+
60
94
  message_flags = MSG_REPORTABLE | security_parameters.security_level
61
- message_id = OpenSSL::ASN1::Integer.new(SecureRandom.random_number(2147483647))
95
+ message_id = OpenSSL::ASN1::Integer.new(SecureRandom.random_number(2147483647)).with_label(:message_id)
62
96
  headers = OpenSSL::ASN1::Sequence.new([
63
- message_id, MSG_MAX_SIZE,
64
- OpenSSL::ASN1::OctetString.new([String(message_flags)].pack("h*")),
97
+ message_id,
98
+ MSG_MAX_SIZE,
99
+ OpenSSL::ASN1::OctetString.new([String(message_flags)].pack("h*")).with_label(:message_flags),
65
100
  MSG_SECURITY_MODEL
66
- ])
101
+ ]).with_label(:headers)
67
102
 
68
103
  encoded = OpenSSL::ASN1::Sequence([
69
104
  MSG_VERSION,
70
105
  headers,
71
- OpenSSL::ASN1::OctetString.new(sec_params.to_der),
106
+ OpenSSL::ASN1::OctetString.new(sec_params.to_der).with_label(:security_params),
72
107
  scoped_pdu
73
- ]).to_der
108
+ ]).with_label(:v3_message)
109
+ log(level: 2) { encoded.to_hex }
110
+
111
+ encoded = encoded.to_der
112
+ log { Hexdump.dump(encoded) }
74
113
  signature = security_parameters.sign(encoded)
75
114
  if signature
76
- auth_salt = OpenSSL::ASN1::OctetString.new(signature)
115
+ log { "signing V3 message..." }
116
+ auth_salt = OpenSSL::ASN1::OctetString.new(signature).with_label(:auth)
117
+ log(level: 2) { auth_salt.to_hex }
77
118
  encoded.sub!(AUTHNONE.to_der, auth_salt.to_der)
119
+ log { Hexdump.dump(encoded) }
78
120
  end
79
121
  encoded
80
122
  end
data/lib/netsnmp/mib.rb CHANGED
@@ -48,12 +48,10 @@ module NETSNMP
48
48
  [prefix, *suffix].join(".")
49
49
  end
50
50
 
51
- # This is a helper function, do not rely on this functionality in future
52
- # versions
53
51
  def identifier(oid)
54
- @object_identifiers.select do |_, full_oid|
55
- full_oid.start_with?(oid)
56
- end
52
+ @object_identifiers.select do |_, ids_oid|
53
+ oid.start_with?(ids_oid)
54
+ end.sort_by(&:size).first
57
55
  end
58
56
 
59
57
  #
@@ -86,7 +84,7 @@ module NETSNMP
86
84
  true
87
85
  end
88
86
 
89
- TYPES = ["OBJECT-TYPE", "OBJECT IDENTIFIER", "MODULE-IDENTITY"].freeze
87
+ TYPES = ["OBJECT IDENTIFIER", "OBJECT-TYPE", "MODULE-IDENTITY"].freeze
90
88
 
91
89
  STATIC_MIB_TO_OID = {
92
90
  "iso" => "1"
@@ -98,41 +96,54 @@ module NETSNMP
98
96
  def do_load(mod)
99
97
  data = @parser_mutex.synchronize { PARSER.parse(File.read(mod)) }
100
98
 
101
- imports = load_imports(data)
99
+ imports = load_imports(data[:imports])
102
100
 
103
- data[:declarations].each_with_object(@object_identifiers) do |dec, types|
104
- next unless TYPES.include?(dec[:type])
101
+ declarations = Hash[
102
+ data[:declarations].reject { |dec| !dec.key?(:name) || !TYPES.include?(dec[:type]) }
103
+ .map { |dec| [String(dec[:name]), String(dec[:value]).split(/ +/)] }
104
+ ]
105
105
 
106
- oid = String(dec[:value]).split(/ +/).flat_map do |cp|
107
- if cp.integer?
108
- cp
109
- else
110
- STATIC_MIB_TO_OID[cp] || @object_identifiers[cp] || begin
111
- imported_mod, = imports.find do |_, identifiers|
112
- identifiers.include?(cp)
113
- end
106
+ declarations.each do |nme, value|
107
+ store_oid_in_identifiers(nme, value, imports: imports, declarations: declarations)
108
+ end
109
+ end
114
110
 
115
- raise Error, "didn't find a module to import \"#{cp}\" from" unless imported_mod
111
+ def store_oid_in_identifiers(nme, value, imports:, declarations:)
112
+ oid = value.flat_map do |cp|
113
+ if cp.integer?
114
+ cp
115
+ elsif @object_identifiers.key?(cp)
116
+ @object_identifiers[cp]
117
+ elsif declarations.key?(cp)
118
+ store_oid_in_identifiers(cp, declarations[cp], imports: imports, declarations: declarations)
119
+ @object_identifiers[cp]
120
+ else
121
+ STATIC_MIB_TO_OID[cp] || begin
122
+ imported_mod, = imports.find do |_, identifiers|
123
+ identifiers.include?(cp)
124
+ end
116
125
 
117
- load(imported_mod)
126
+ raise Error, "didn't find a module to import \"#{cp}\" from" unless imported_mod
118
127
 
119
- @object_identifiers[cp]
120
- end
128
+ load(imported_mod)
129
+
130
+ @object_identifiers[cp]
121
131
  end
122
- end.join(".")
132
+ end
133
+ end.join(".")
123
134
 
124
- types[String(dec[:name])] = oid
125
- end
135
+ @object_identifiers[nme] = oid
126
136
  end
127
137
 
128
138
  #
129
139
  # Reformats the import lists into an hash indexed by module name, to a list of
130
140
  # imported names
131
141
  #
132
- def load_imports(data)
133
- return unless data[:imports]
142
+ def load_imports(imports)
143
+ return unless imports
134
144
 
135
- data[:imports].each_with_object({}) do |import, imp|
145
+ imports = [imports] unless imports.respond_to?(:to_ary)
146
+ imports.each_with_object({}) do |import, imp|
136
147
  imp[String(import[:name])] = case import[:ids]
137
148
  when Hash
138
149
  [String(import[:ids][:name])]
data/lib/netsnmp/pdu.rb CHANGED
@@ -5,7 +5,11 @@ module NETSNMP
5
5
  # Abstracts the PDU base structure into a ruby object. It gives access to its varbinds.
6
6
  #
7
7
  class PDU
8
+ using ASNExtensions
9
+
8
10
  MAXREQUESTID = 2147483647
11
+
12
+ using ASNExtensions
9
13
  class << self
10
14
  def decode(der)
11
15
  asn_tree = case der
@@ -53,6 +57,7 @@ module NETSNMP
53
57
  when :inform then 6
54
58
  when :trap then 7
55
59
  when :response then 2
60
+ when :report then 8
56
61
  else raise Error, "#{type} is not supported as type"
57
62
  end
58
63
  new(type: typ, **args)
@@ -85,6 +90,10 @@ module NETSNMP
85
90
  to_asn.to_der
86
91
  end
87
92
 
93
+ def to_hex
94
+ to_asn.to_hex
95
+ end
96
+
88
97
  # Adds a request varbind to the pdu
89
98
  #
90
99
  # @param [OID] oid a valid oid
@@ -96,25 +105,27 @@ module NETSNMP
96
105
  alias << add_varbind
97
106
 
98
107
  def to_asn
99
- request_id_asn = OpenSSL::ASN1::Integer.new(@request_id)
100
- error_asn = OpenSSL::ASN1::Integer.new(@error_status)
101
- error_index_asn = OpenSSL::ASN1::Integer.new(@error_index)
108
+ request_id_asn = OpenSSL::ASN1::Integer.new(@request_id).with_label(:request_id)
109
+ error_asn = OpenSSL::ASN1::Integer.new(@error_status).with_label(:error)
110
+ error_index_asn = OpenSSL::ASN1::Integer.new(@error_index).with_label(:error_index)
102
111
 
103
- varbind_asns = OpenSSL::ASN1::Sequence.new(@varbinds.map(&:to_asn))
112
+ varbind_asns = OpenSSL::ASN1::Sequence.new(@varbinds.map(&:to_asn)).with_label(:varbinds)
104
113
 
105
114
  request_asn = OpenSSL::ASN1::ASN1Data.new([request_id_asn,
106
115
  error_asn, error_index_asn,
107
116
  varbind_asns], @type,
108
- :CONTEXT_SPECIFIC)
117
+ :CONTEXT_SPECIFIC).with_label(:request)
109
118
 
110
- OpenSSL::ASN1::Sequence.new([*encode_headers_asn, request_asn])
119
+ OpenSSL::ASN1::Sequence.new([*encode_headers_asn, request_asn]).with_label(:pdu)
111
120
  end
112
121
 
113
122
  private
114
123
 
115
124
  def encode_headers_asn
116
- [OpenSSL::ASN1::Integer.new(@version),
117
- OpenSSL::ASN1::OctetString.new(@community)]
125
+ [
126
+ OpenSSL::ASN1::Integer.new(@version).with_label(:snmp_version),
127
+ OpenSSL::ASN1::OctetString.new(@community).with_label(:community)
128
+ ]
118
129
  end
119
130
 
120
131
  # http://www.tcpipguide.com/free/t_SNMPVersion2SNMPv2MessageFormats-5.htm#Table_219
@@ -2,6 +2,8 @@
2
2
 
3
3
  module NETSNMP
4
4
  class ScopedPDU < PDU
5
+ using ASNExtensions
6
+
5
7
  attr_reader :engine_id
6
8
 
7
9
  def initialize(type:, headers:, **options)
@@ -12,8 +14,10 @@ module NETSNMP
12
14
  private
13
15
 
14
16
  def encode_headers_asn
15
- [OpenSSL::ASN1::OctetString.new(@engine_id || ""),
16
- OpenSSL::ASN1::OctetString.new(@context || "")]
17
+ [
18
+ OpenSSL::ASN1::OctetString.new(@engine_id || "").with_label(:engine_id),
19
+ OpenSSL::ASN1::OctetString.new(@context || "").with_label(:context)
20
+ ]
17
21
  end
18
22
  end
19
23
  end
@@ -8,6 +8,9 @@ module NETSNMP
8
8
  # It also provides validation of the security options passed with a client is initialized in v3 mode.
9
9
  class SecurityParameters
10
10
  using StringExtensions
11
+ using ASNExtensions
12
+
13
+ prepend Loggable
11
14
 
12
15
  IPAD = "\x36" * 64
13
16
  OPAD = "\x5c" * 64
@@ -72,7 +75,10 @@ module NETSNMP
72
75
  if encryption
73
76
  encrypted_pdu, salt = encryption.encrypt(pdu.to_der, engine_boots: engine_boots,
74
77
  engine_time: engine_time)
75
- [OpenSSL::ASN1::OctetString.new(encrypted_pdu), OpenSSL::ASN1::OctetString.new(salt)]
78
+ [
79
+ OpenSSL::ASN1::OctetString.new(encrypted_pdu).with_label(:encrypted_pdu),
80
+ OpenSSL::ASN1::OctetString.new(salt).with_label(:salt)
81
+ ]
76
82
  else
77
83
  [pdu.to_asn, salt]
78
84
  end
@@ -82,15 +88,16 @@ module NETSNMP
82
88
  # @param [String] salt the salt from the incoming der
83
89
  # @param [Integer] engine_time the reported engine time
84
90
  # @param [Integer] engine_boots the reported engine boots
85
- def decode(der, salt:, engine_time:, engine_boots:)
91
+ def decode(der, salt:, engine_time:, engine_boots:, security_level: @security_level)
86
92
  asn = OpenSSL::ASN1.decode(der)
87
- if encryption
88
- encrypted_pdu = asn.value
89
- pdu_der = encryption.decrypt(encrypted_pdu, salt: salt, engine_time: engine_time, engine_boots: engine_boots)
90
- OpenSSL::ASN1.decode(pdu_der)
91
- else
92
- asn
93
- end
93
+ return asn if security_level < 3
94
+
95
+ return asn unless encryption
96
+
97
+ encrypted_pdu = asn.value
98
+ pdu_der = encryption.decrypt(encrypted_pdu, salt: salt, engine_time: engine_time, engine_boots: engine_boots)
99
+ log(level: 2) { "message has been decrypted" }
100
+ OpenSSL::ASN1.decode(pdu_der)
94
101
  end
95
102
 
96
103
  # @param [String] message the already encoded snmp v3 message
@@ -120,10 +127,11 @@ module NETSNMP
120
127
  # @param [String] salt the incoming payload''s salt
121
128
  #
122
129
  # @raise [NETSNMP::Error] if the message's integration has been violated
123
- def verify(stream, salt)
124
- return if @security_level < 1
130
+ def verify(stream, salt, security_level: @security_level)
131
+ return if security_level < 1
125
132
  verisalt = sign(stream)
126
133
  raise Error, "invalid message authentication salt" unless verisalt == salt
134
+ log(level: 2) { "message has been verified" }
127
135
  end
128
136
 
129
137
  def must_revalidate?
@@ -4,6 +4,8 @@ module NETSNMP
4
4
  # Let's just remind that there is no session in snmp, this is just an abstraction.
5
5
  #
6
6
  class Session
7
+ prepend Loggable
8
+
7
9
  TIMEOUT = 2
8
10
 
9
11
  # @param [Hash] opts the options set
@@ -36,9 +38,16 @@ module NETSNMP
36
38
  # @return [NETSNMP::PDU] the response pdu
37
39
  #
38
40
  def send(pdu)
41
+ log { "sending request..." }
42
+ log(level: 2) { pdu.to_hex }
39
43
  encoded_request = pdu.to_der
44
+ log { Hexdump.dump(encoded_request) }
40
45
  encoded_response = @transport.send(encoded_request)
41
- PDU.decode(encoded_response)
46
+ log { "received response" }
47
+ log { Hexdump.dump(encoded_response) }
48
+ response_pdu = PDU.decode(encoded_response)
49
+ log(level: 2) { response_pdu.to_hex }
50
+ response_pdu
42
51
  end
43
52
 
44
53
  private
@@ -8,6 +8,7 @@ module NETSNMP
8
8
  @context = context
9
9
  @security_parameters = opts.delete(:security_parameters)
10
10
  super
11
+ @message_serializer = Message.new(**opts)
11
12
  end
12
13
 
13
14
  # @see {NETSNMP::Session#build_pdu}
@@ -20,10 +21,18 @@ module NETSNMP
20
21
 
21
22
  # @see {NETSNMP::Session#send}
22
23
  def send(pdu)
24
+ log { "sending request..." }
23
25
  encoded_request = encode(pdu)
24
26
  encoded_response = @transport.send(encoded_request)
25
- pdu, = decode(encoded_response)
26
- pdu
27
+ response_pdu, *args = decode(encoded_response)
28
+ if response_pdu.type == 8
29
+ varbind = response_pdu.varbinds.first
30
+ if varbind.oid == "1.3.6.1.6.3.15.1.1.2.0" # IdNotInTimeWindow
31
+ _, @engine_boots, @engine_time = args
32
+ raise IdNotInTimeWindowError, "request timestamp is already out of time window"
33
+ end
34
+ end
35
+ response_pdu
27
36
  end
28
37
 
29
38
  private
@@ -60,7 +69,8 @@ module NETSNMP
60
69
  report_sec_params = SecurityParameters.new(security_level: 0,
61
70
  username: @security_parameters.username)
62
71
  pdu = ScopedPDU.build(:get, headers: [])
63
- encoded_report_pdu = Message.encode(pdu, security_parameters: report_sec_params)
72
+ log { "sending probe..." }
73
+ encoded_report_pdu = @message_serializer.encode(pdu, security_parameters: report_sec_params)
64
74
 
65
75
  encoded_response_pdu = @transport.send(encoded_report_pdu)
66
76
 
@@ -69,13 +79,13 @@ module NETSNMP
69
79
  end
70
80
 
71
81
  def encode(pdu)
72
- Message.encode(pdu, security_parameters: @security_parameters,
73
- engine_boots: @engine_boots,
74
- engine_time: @engine_time)
82
+ @message_serializer.encode(pdu, security_parameters: @security_parameters,
83
+ engine_boots: @engine_boots,
84
+ engine_time: @engine_time)
75
85
  end
76
86
 
77
87
  def decode(stream, security_parameters: @security_parameters)
78
- Message.decode(stream, security_parameters: security_parameters)
88
+ @message_serializer.decode(stream, security_parameters: security_parameters)
79
89
  end
80
90
  end
81
91
  end
@@ -4,6 +4,8 @@ module NETSNMP
4
4
  # Abstracts the PDU variable structure into a ruby object
5
5
  #
6
6
  class Varbind
7
+ using StringExtensions
8
+
7
9
  attr_reader :oid, :value
8
10
 
9
11
  def initialize(oid, value: nil, type: nil)
@@ -48,18 +50,15 @@ module NETSNMP
48
50
  def convert_val(asn_value)
49
51
  case asn_value
50
52
  when OpenSSL::ASN1::OctetString
51
- # yes, we are forcing all output to UTF-8
53
+ val = asn_value.value
54
+
52
55
  # it's kind of common in snmp, some stuff can't be converted,
53
56
  # like Hexa Strings. Parse them into a readable format a la netsnmp
54
- val = asn_value.value
55
- begin
56
- val.encode("UTF-8")
57
- rescue Encoding::UndefinedConversionError,
58
- Encoding::ConverterNotFoundError,
59
- Encoding::InvalidByteSequenceError
60
- # hexdump me!
61
- val.unpack("H*")[0].upcase.scan(/../).join(" ")
62
- end
57
+ # https://github.com/net-snmp/net-snmp/blob/ed90aaaaea0d9cc6c5c5533f1863bae598d3b820/snmplib/mib.c#L650
58
+ is_hex_string = val.each_char.any? { |c| !c.match?(/[[:print:]]/) && !c.match?(/[[:space:]]/) }
59
+
60
+ val = HexString.new(val) if is_hex_string
61
+ val
63
62
  when OpenSSL::ASN1::Primitive
64
63
  val = asn_value.value
65
64
  val = val.to_i if val.is_a?(OpenSSL::BN)
@@ -81,11 +80,11 @@ module NETSNMP
81
80
  when :ipaddress then 0
82
81
  when :counter32
83
82
  asn_val = [value].pack("N*")
84
- asn_val = asn_val[1..-1] while asn_val[0] == "\x00".b && asn_val[1].unpack("B").first != "1"
83
+ asn_val = asn_val[1..-1] while asn_val[0] == "\x00".b && asn_val[1].unpack1("B") != "1"
85
84
  1
86
85
  when :gauge
87
86
  asn_val = [value].pack("N*")
88
- asn_val = asn_val[1..-1] while asn_val[0] == "\x00".b && asn_val[1].unpack("B").first != "1"
87
+ asn_val = asn_val[1..-1] while asn_val[0] == "\x00".b && asn_val[1].unpack1("B") != "1"
89
88
  2
90
89
  when :timetick
91
90
  return Timetick.new(value).to_asn
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NETSNMP
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/sig/loggable.rbs ADDED
@@ -0,0 +1,16 @@
1
+ module NETSNMP
2
+ module Loggable
3
+ interface _Debugger
4
+ def <<: (string) -> void
5
+ end
6
+
7
+ DEBUG_LEVEL: Integer
8
+ DEBUG: _Debugger
9
+
10
+ private
11
+
12
+ def initialize: (?debug: _DEBUGGER, ?debug_level: Integer, **untyped) -> void
13
+
14
+ def log: (?level: Integer) { () -> String } -> void
15
+ end
16
+ end
data/sig/message.rbs CHANGED
@@ -1,7 +1,9 @@
1
1
  module NETSNMP
2
- module Message
3
- def self?.decode: (String stream, security_parameters: SecurityParameters) -> [ScopedPDU, String, Integer, Integer]
2
+ class Message
3
+ prepend Loggable
4
4
 
5
- def self?.encode: (ScopedPDU pdu, security_parameters: SecurityParameters, ?engine_boots: Integer, ?engine_time: Integer) -> String
5
+ def decode: (String stream, security_parameters: SecurityParameters) -> [ScopedPDU, String, Integer, Integer]
6
+
7
+ def encode: (ScopedPDU pdu, security_parameters: SecurityParameters, ?engine_boots: Integer, ?engine_time: Integer) -> String
6
8
  end
7
9
  end
data/sig/mib.rbs CHANGED
@@ -1,5 +1,8 @@
1
1
  module NETSNMP
2
2
  module MIB
3
+ type import = {ids: Array[{name: string}], name: string} | {ids: {name: string}, name: string}
4
+
5
+
3
6
  MIBDIRS: Array[String]
4
7
  PARSER: Parser
5
8
 
@@ -12,7 +15,7 @@ module NETSNMP
12
15
 
13
16
  def self?.load: (String mod) -> void
14
17
 
15
- def self?.load_imports: (?Hash[Symbol, untyped] data) -> Hash[String, Array[String]]?
18
+ def self?.load_imports: ((Array[import] | import)? data) -> Hash[String, Array[String]]?
16
19
  def self?.load_defaults: () -> void
17
20
  end
18
21
  end
data/sig/netsnmp.rbs CHANGED
@@ -16,8 +16,4 @@ module NETSNMP
16
16
  end
17
17
 
18
18
  type snmp_version = 0 | 1 | 3 | :v1 | :v2c | :v3 | nil
19
-
20
- def self.debug=: (_Logger) -> void
21
-
22
- def self.debug: { () -> string } -> void
23
19
  end
data/sig/pdu.rbs CHANGED
@@ -12,7 +12,7 @@ module NETSNMP
12
12
  # }
13
13
 
14
14
  attr_reader varbinds: Array[Varbind]
15
- attr_reader type: pdu_type
15
+ attr_reader type: Integer
16
16
  attr_reader version: snmp_version
17
17
  attr_reader community: String
18
18
  attr_reader request_id: Integer
@@ -1,5 +1,7 @@
1
1
  module NETSNMP
2
2
  class SecurityParameters
3
+ prepend Loggable
4
+
3
5
  type security_level = :noauth | :auth_no_priv | :auth_priv | 0 | 1 | 3 | nil
4
6
 
5
7
  type auth_protocol = :md5 | :sha
@@ -21,11 +23,11 @@ module NETSNMP
21
23
 
22
24
  def encode: (_ToAsn, salt: OpenSSL::ASN1::ASN1Data, engine_time: Integer, engine_boots: Integer) -> [OpenSSL::ASN1::ASN1Data, OpenSSL::ASN1::ASN1Data]
23
25
 
24
- def decode: (OpenSSL::ASN1::ASN1Data | String der, salt: OpenSSL::ASN1::ASN1Data | String, engine_time: Integer, engine_boots: Integer) -> OpenSSL::ASN1::ASN1Data
26
+ def decode: (OpenSSL::ASN1::ASN1Data | String der, salt: OpenSSL::ASN1::ASN1Data | String, engine_time: Integer, engine_boots: Integer, ?security_level: Integer?) -> OpenSSL::ASN1::ASN1Data
25
27
 
26
28
  def sign: (String message) -> String?
27
29
 
28
- def verify: (String stream, String salt) -> void
30
+ def verify: (String stream, String salt, ?security_level: Integer?) -> void
29
31
 
30
32
  def must_revalidate?: () -> bool
31
33
 
data/sig/session.rbs CHANGED
@@ -1,5 +1,7 @@
1
1
  module NETSNMP
2
2
  class Session
3
+ prepend Loggable
4
+
3
5
  @transport: _Transport
4
6
  @version: 0 | 1 | 3
5
7
  @community: String?
data/spec/client_spec.rb CHANGED
@@ -67,6 +67,24 @@ RSpec.describe NETSNMP::Client do
67
67
  WALK
68
68
  end
69
69
  let(:set_oid_result) { 43 }
70
+
71
+ context "when the returned value is a hex-string" do
72
+ let(:protocol_options) do
73
+ {
74
+ version: "2c",
75
+ community: "foreignformats/winxp1"
76
+ }
77
+ end
78
+ let(:hex_get_oid) { "1.3.6.1.2.1.25.3.7.1.3.10.1" }
79
+ let(:hex_get_result) { "\x01\x00\x00\x00" }
80
+ let(:hex_get_output) { "01 00 00 00" }
81
+ let(:value) { subject.get(oid: hex_get_oid) }
82
+
83
+ it "returns the string, which outputs the hex-representation" do
84
+ expect(value).to eq(hex_get_result)
85
+ expect(value.inspect).to include(hex_get_output)
86
+ end
87
+ end
70
88
  end
71
89
  end
72
90
 
data/spec/spec_helper.rb CHANGED
@@ -10,7 +10,6 @@ end
10
10
 
11
11
  if defined?(SimpleCov)
12
12
  SimpleCov.start do
13
- minimum_coverage 85
14
13
  add_filter ".bundle"
15
14
  add_filter "/spec/"
16
15
  end
data/spec/varbind_spec.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe NETSNMP::Varbind do
4
+ using NETSNMP::StringExtensions
5
+
4
6
  describe "#to_der" do
5
7
  it { expect(described_class.new(".1.3.6.1.2.1.1.1.0").to_der).to eq("0\f\006\b+\006\001\002\001\001\001\000\005\000".b) }
6
8
 
@@ -25,7 +27,7 @@ RSpec.describe NETSNMP::Varbind do
25
27
  gauge = 127
26
28
  varbind = described_class.new(".1.3.6.1.2.1.1.3.0", type: :gauge, value: gauge)
27
29
  value_str = varbind.to_der[12..-1]
28
- header = value_str[0].unpack("B8").first
30
+ header = value_str[0].unpack1("B8")
29
31
 
30
32
  # Class: Primitive Application
31
33
  expect(header[0..1]).to eq("01")
@@ -43,7 +45,7 @@ RSpec.describe NETSNMP::Varbind do
43
45
  gauge = 128
44
46
  varbind = described_class.new(".1.3.6.1.2.1.1.3.0", type: :gauge, value: gauge)
45
47
  value_str = varbind.to_der[12..-1]
46
- header = value_str[0].unpack("B8").first
48
+ header = value_str[0].unpack1("B8")
47
49
 
48
50
  # Class: Primitive Application
49
51
  expect(header[0..1]).to eq("01")
@@ -61,7 +63,7 @@ RSpec.describe NETSNMP::Varbind do
61
63
  gauge = 805
62
64
  varbind = described_class.new(".1.3.6.1.2.1.1.3.0", type: :gauge, value: gauge)
63
65
  value_str = varbind.to_der[12..-1]
64
- header = value_str[0].unpack("B8").first
66
+ header = value_str[0].unpack1("B8")
65
67
 
66
68
  # Class: Primitive Application
67
69
  expect(header[0..1]).to eq("01")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: netsnmp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-16 00:00:00.000000000 Z
11
+ date: 2021-02-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parslet
@@ -41,6 +41,8 @@ files:
41
41
  - lib/netsnmp/encryption/des.rb
42
42
  - lib/netsnmp/encryption/none.rb
43
43
  - lib/netsnmp/errors.rb
44
+ - lib/netsnmp/extensions.rb
45
+ - lib/netsnmp/loggable.rb
44
46
  - lib/netsnmp/message.rb
45
47
  - lib/netsnmp/mib.rb
46
48
  - lib/netsnmp/mib/parser.rb
@@ -54,6 +56,7 @@ files:
54
56
  - lib/netsnmp/varbind.rb
55
57
  - lib/netsnmp/version.rb
56
58
  - sig/client.rbs
59
+ - sig/loggable.rbs
57
60
  - sig/message.rbs
58
61
  - sig/mib.rbs
59
62
  - sig/mib/parser.rbs
@@ -101,21 +104,21 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
104
  version: '0'
102
105
  requirements:
103
106
  - net-snmp
104
- rubygems_version: 3.1.4
107
+ rubygems_version: 3.2.3
105
108
  signing_key:
106
109
  specification_version: 4
107
110
  summary: SNMP Client library
108
111
  test_files:
109
- - spec/timeticks_spec.rb
110
- - spec/spec_helper.rb
111
- - spec/session_spec.rb
112
112
  - spec/client_spec.rb
113
+ - spec/handlers/celluloid_spec.rb
113
114
  - spec/mib_spec.rb
114
115
  - spec/oid_spec.rb
115
- - spec/varbind_spec.rb
116
- - spec/support/request_examples.rb
117
- - spec/support/celluloid.rb
118
116
  - spec/pdu_spec.rb
119
117
  - spec/security_parameters_spec.rb
118
+ - spec/session_spec.rb
119
+ - spec/spec_helper.rb
120
+ - spec/support/celluloid.rb
121
+ - spec/support/request_examples.rb
122
+ - spec/timeticks_spec.rb
120
123
  - spec/v3_session_spec.rb
121
- - spec/handlers/celluloid_spec.rb
124
+ - spec/varbind_spec.rb