netsnmp 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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