netsnmp 0.1.8 → 0.4.1

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -27
  3. data/lib/netsnmp.rb +3 -21
  4. data/lib/netsnmp/client.rb +4 -5
  5. data/lib/netsnmp/encryption/aes.rb +1 -3
  6. data/lib/netsnmp/encryption/des.rb +0 -2
  7. data/lib/netsnmp/errors.rb +1 -0
  8. data/lib/netsnmp/extensions.rb +113 -0
  9. data/lib/netsnmp/loggable.rb +36 -0
  10. data/lib/netsnmp/message.rb +70 -28
  11. data/lib/netsnmp/mib.rb +172 -0
  12. data/lib/netsnmp/mib/parser.rb +750 -0
  13. data/lib/netsnmp/oid.rb +7 -12
  14. data/lib/netsnmp/pdu.rb +23 -12
  15. data/lib/netsnmp/scoped_pdu.rb +8 -2
  16. data/lib/netsnmp/security_parameters.rb +22 -14
  17. data/lib/netsnmp/session.rb +14 -16
  18. data/lib/netsnmp/v3_session.rb +21 -9
  19. data/lib/netsnmp/varbind.rb +27 -22
  20. data/lib/netsnmp/version.rb +1 -1
  21. data/sig/client.rbs +24 -0
  22. data/sig/loggable.rbs +16 -0
  23. data/sig/message.rbs +9 -0
  24. data/sig/mib.rbs +21 -0
  25. data/sig/mib/parser.rbs +7 -0
  26. data/sig/netsnmp.rbs +19 -0
  27. data/sig/oid.rbs +18 -0
  28. data/sig/openssl.rbs +20 -0
  29. data/sig/pdu.rbs +48 -0
  30. data/sig/scoped_pdu.rbs +15 -0
  31. data/sig/security_parameters.rbs +58 -0
  32. data/sig/session.rbs +38 -0
  33. data/sig/timeticks.rbs +7 -0
  34. data/sig/v3_session.rbs +21 -0
  35. data/sig/varbind.rbs +30 -0
  36. data/spec/client_spec.rb +26 -8
  37. data/spec/handlers/celluloid_spec.rb +4 -3
  38. data/spec/mib_spec.rb +13 -0
  39. data/spec/session_spec.rb +2 -2
  40. data/spec/spec_helper.rb +9 -5
  41. data/spec/support/request_examples.rb +2 -2
  42. data/spec/v3_session_spec.rb +4 -4
  43. data/spec/varbind_spec.rb +5 -3
  44. metadata +31 -71
  45. data/.coveralls.yml +0 -1
  46. data/.gitignore +0 -14
  47. data/.rspec +0 -2
  48. data/.rubocop.yml +0 -11
  49. data/.rubocop_todo.yml +0 -69
  50. data/.travis.yml +0 -28
  51. data/Gemfile +0 -22
  52. data/Rakefile +0 -30
  53. data/netsnmp.gemspec +0 -31
  54. data/spec/support/Dockerfile +0 -14
  55. data/spec/support/specs.sh +0 -51
  56. data/spec/support/stop_docker.sh +0 -5
data/lib/netsnmp/oid.rb CHANGED
@@ -4,21 +4,16 @@ module NETSNMP
4
4
  # Abstracts the OID structure
5
5
  #
6
6
  module OID
7
+ using StringExtensions unless String.method_defined?(:match?)
8
+
7
9
  OIDREGEX = /^[\d\.]*$/
8
10
 
9
11
  module_function
10
12
 
11
- def build(o)
12
- case o
13
- when OID then o
14
- when Array
15
- o.join(".")
16
- when OIDREGEX
17
- o = o[1..-1] if o.start_with?(".")
18
- o
19
- # TODO: MIB to OID
20
- else raise Error, "can't convert #{o} to OID"
21
- end
13
+ def build(id)
14
+ oid = MIB.oid(id)
15
+ oid = oid[1..-1] if oid.start_with?(".")
16
+ oid
22
17
  end
23
18
 
24
19
  def to_asn(oid)
@@ -29,7 +24,7 @@ module NETSNMP
29
24
  # @return [true, false] whether the given OID belongs to the sub-tree
30
25
  #
31
26
  def parent?(parent_oid, child_oid)
32
- child_oid.match(/\A#{parent_oid}\./)
27
+ child_oid.match?(/\A#{parent_oid}\./)
33
28
  end
34
29
  end
35
30
  end
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,9 +57,10 @@ 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
- new(args.merge(type: typ))
63
+ new(type: typ, **args)
59
64
  end
60
65
  end
61
66
 
@@ -64,7 +69,7 @@ module NETSNMP
64
69
  attr_reader :version, :community, :request_id
65
70
 
66
71
  def initialize(type:, headers:,
67
- request_id: nil,
72
+ request_id: SecureRandom.random_number(MAXREQUESTID),
68
73
  error_status: 0,
69
74
  error_index: 0,
70
75
  varbinds: [])
@@ -75,9 +80,9 @@ module NETSNMP
75
80
  @type = type
76
81
  @varbinds = []
77
82
  varbinds.each do |varbind|
78
- add_varbind(varbind)
83
+ add_varbind(**varbind)
79
84
  end
80
- @request_id = request_id || SecureRandom.random_number(MAXREQUESTID)
85
+ @request_id = request_id
81
86
  check_error_status(@error_status)
82
87
  end
83
88
 
@@ -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)
@@ -9,9 +11,13 @@ module NETSNMP
9
11
  super(type: type, headers: [3, nil], **options)
10
12
  end
11
13
 
14
+ private
15
+
12
16
  def encode_headers_asn
13
- [OpenSSL::ASN1::OctetString.new(@engine_id || ""),
14
- 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
+ ]
15
21
  end
16
22
  end
17
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
@@ -24,7 +27,7 @@ module NETSNMP
24
27
 
25
28
  # @param [String] username the snmp v3 username
26
29
  # @param [String] engine_id the device engine id (initialized to '' for report)
27
- # @param [Symbol, integer] security_level allowed snmp v3 security level (:auth_priv, :auh_no_priv, etc)
30
+ # @param [Symbol, integer] security_level allowed snmp v3 security level (:auth_priv, :auth_no_priv, etc)
28
31
  # @param [Symbol, nil] auth_protocol a supported authentication protocol (currently supported: :md5, :sha)
29
32
  # @param [Symbol, nil] priv_protocol a supported privacy protocol (currently supported: :des, :aes)
30
33
  # @param [String, nil] auth_password the authentication password
@@ -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
@@ -99,7 +106,7 @@ module NETSNMP
99
106
  # @note this method is used in the process of authenticating a message
100
107
  def sign(message)
101
108
  # don't sign unless you have to
102
- return nil unless @auth_protocol
109
+ return unless @auth_protocol
103
110
 
104
111
  key = auth_key.dup
105
112
 
@@ -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?
@@ -211,7 +219,7 @@ module NETSNMP
211
219
  end
212
220
 
213
221
  def authorizable?
214
- @auth_protocol && @auth_protocol != :none
222
+ @auth_protocol != :none
215
223
  end
216
224
  end
217
225
  end
@@ -4,13 +4,15 @@ 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
10
12
  def initialize(version: 1, community: "public", **options)
11
13
  @version = version
12
14
  @community = community
13
- validate(options)
15
+ validate(**options)
14
16
  end
15
17
 
16
18
  # Closes the session
@@ -36,23 +38,27 @@ module NETSNMP
36
38
  # @return [NETSNMP::PDU] the response pdu
37
39
  #
38
40
  def send(pdu)
39
- encoded_request = encode(pdu)
41
+ log { "sending request..." }
42
+ log(level: 2) { pdu.to_hex }
43
+ encoded_request = pdu.to_der
44
+ log { Hexdump.dump(encoded_request) }
40
45
  encoded_response = @transport.send(encoded_request)
41
- 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
45
54
 
46
- def validate(**options)
47
- proxy = options[:proxy]
55
+ def validate(host: nil, port: 161, proxy: nil, timeout: TIMEOUT, **)
48
56
  if proxy
49
57
  @proxy = true
50
58
  @transport = proxy
51
59
  else
52
- host, port = options.values_at(:host, :port)
53
60
  raise "you must provide an hostname/ip under :host" unless host
54
- port ||= 161 # default snmp port
55
- @transport = Transport.new(host, port.to_i, timeout: options.fetch(:timeout, TIMEOUT))
61
+ @transport = Transport.new(host, port.to_i, timeout: timeout)
56
62
  end
57
63
  @version = case @version
58
64
  when Integer then @version # assume the use know what he's doing
@@ -64,14 +70,6 @@ module NETSNMP
64
70
  end
65
71
  end
66
72
 
67
- def encode(pdu)
68
- pdu.to_der
69
- end
70
-
71
- def decode(stream)
72
- PDU.decode(stream)
73
- end
74
-
75
73
  class Transport
76
74
  MAXPDUSIZE = 0xffff + 1
77
75
 
@@ -4,10 +4,11 @@ module NETSNMP
4
4
  # Abstraction for the v3 semantics.
5
5
  class V3Session < Session
6
6
  # @param [String, Integer] version SNMP version (always 3)
7
- def initialize(version: 3, context: "", **opts)
7
+ def initialize(context: "", **opts)
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}
@@ -19,9 +20,19 @@ module NETSNMP
19
20
  end
20
21
 
21
22
  # @see {NETSNMP::Session#send}
22
- def send(*)
23
- pdu, = super
24
- pdu
23
+ def send(pdu)
24
+ log { "sending request..." }
25
+ encoded_request = encode(pdu)
26
+ encoded_response = @transport.send(encoded_request)
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
25
36
  end
26
37
 
27
38
  private
@@ -58,7 +69,8 @@ module NETSNMP
58
69
  report_sec_params = SecurityParameters.new(security_level: 0,
59
70
  username: @security_parameters.username)
60
71
  pdu = ScopedPDU.build(:get, headers: [])
61
- 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)
62
74
 
63
75
  encoded_response_pdu = @transport.send(encoded_report_pdu)
64
76
 
@@ -67,13 +79,13 @@ module NETSNMP
67
79
  end
68
80
 
69
81
  def encode(pdu)
70
- Message.encode(pdu, security_parameters: @security_parameters,
71
- engine_boots: @engine_boots,
72
- engine_time: @engine_time)
82
+ @message_serializer.encode(pdu, security_parameters: @security_parameters,
83
+ engine_boots: @engine_boots,
84
+ engine_time: @engine_time)
73
85
  end
74
86
 
75
87
  def decode(stream, security_parameters: @security_parameters)
76
- Message.decode(stream, security_parameters: security_parameters)
88
+ @message_serializer.decode(stream, security_parameters: security_parameters)
77
89
  end
78
90
  end
79
91
  end
@@ -4,9 +4,11 @@ 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
- def initialize(oid, value: nil, type: nil, **_opts)
11
+ def initialize(oid, value: nil, type: nil)
10
12
  @oid = OID.build(oid)
11
13
  @type = type
12
14
  @value = convert_val(value) if value
@@ -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
@@ -114,21 +113,27 @@ module NETSNMP
114
113
  IPAddr.new_ntoh(asn.value)
115
114
  when 1, # ASN counter 32
116
115
  2 # gauge
117
- val = asn.value
118
- val.prepend("\x00") while val.bytesize < 4
119
- val.unpack("N*")[0] || 0
116
+ unpack_32bit_integer(asn.value)
120
117
  when 3 # timeticks
121
- val = asn.value
122
- val.prepend("\x00") while val.bytesize < 4
123
- Timetick.new(val.unpack("N*")[0] || 0)
118
+ Timetick.new(unpack_32bit_integer(asn.value))
124
119
  # when 4 # opaque
125
120
  # when 5 # NSAP
126
121
  when 6 # ASN Counter 64
127
- val = asn.value
128
- val.prepend("\x00") while val.bytesize % 16 != 0
129
- val.unpack("NNNN").reduce(0) { |sum, elem| (sum << 32) + elem }
122
+ unpack_64bit_integer(asn.value)
130
123
  # when 7 # ASN UInteger
131
124
  end
132
125
  end
126
+
127
+ private
128
+
129
+ def unpack_32bit_integer(payload)
130
+ payload.prepend("\x00") until (payload.bytesize % 4).zero?
131
+ payload.unpack("N*")[-1] || 0
132
+ end
133
+
134
+ def unpack_64bit_integer(payload)
135
+ payload.prepend("\x00") until (payload.bytesize % 16).zero?
136
+ payload.unpack("NNNN").reduce(0) { |sum, elem| (sum << 32) + elem }
137
+ end
133
138
  end
134
139
  end