netsnmp 0.1.8 → 0.4.1

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