netsnmp 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module NETSNMP
3
4
  # This module encapsulates the public API for encrypting/decrypting and signing/verifying.
4
- #
5
- # It doesn't interact with other layers from the library, rather it is used and passed all
5
+ #
6
+ # It doesn't interact with other layers from the library, rather it is used and passed all
6
7
  # the arguments (consisting mostly of primitive types).
7
8
  # It also provides validation of the security options passed with a client is initialized in v3 mode.
8
9
  class SecurityParameters
@@ -11,8 +12,15 @@ module NETSNMP
11
12
  IPAD = "\x36" * 64
12
13
  OPAD = "\x5c" * 64
13
14
 
15
+ # Timeliness is part of SNMP V3 Security
16
+ # The topic is described very nice here https://www.snmpsharpnet.com/?page_id=28
17
+ # https://www.ietf.org/rfc/rfc2574.txt 1.4.1 Timeliness
18
+ # The probe is outdated after 150 seconds which results in a PDU Error, therefore it should expire before that and be renewed
19
+ # The 150 Seconds is specified in https://www.ietf.org/rfc/rfc2574.txt 2.2.3
20
+ TIMELINESS_THRESHOLD = 150
21
+
14
22
  attr_reader :security_level, :username
15
- attr_accessor :engine_id
23
+ attr_reader :engine_id
16
24
 
17
25
  # @param [String] username the snmp v3 username
18
26
  # @param [String] engine_id the device engine id (initialized to '' for report)
@@ -28,13 +36,14 @@ module NETSNMP
28
36
  # not explicitly set), and :priv_password becomes mandatory.
29
37
  #
30
38
  def initialize(
31
- username: ,
39
+ username:,
32
40
  engine_id: "",
33
- security_level: nil,
34
- auth_protocol: nil,
35
- auth_password: nil,
36
- priv_protocol: nil,
37
- priv_password: nil)
41
+ security_level: nil,
42
+ auth_protocol: nil,
43
+ auth_password: nil,
44
+ priv_protocol: nil,
45
+ priv_password: nil
46
+ )
38
47
  @security_level = security_level
39
48
  @username = username
40
49
  @engine_id = engine_id
@@ -47,21 +56,25 @@ module NETSNMP
47
56
  @priv_pass_key = passkey(@priv_password) unless @priv_password.nil?
48
57
  end
49
58
 
59
+ def engine_id=(id)
60
+ @timeliness = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
61
+ @engine_id = id
62
+ end
50
63
 
51
64
  # @param [#to_asn, #to_der] pdu the pdu to encode (must quack like a asn1 type)
52
65
  # @param [String] salt the salt to use
53
66
  # @param [Integer] engine_time the reported engine time
54
67
  # @param [Integer] engine_boots the reported boots time
55
- #
56
- # @return [Array] a pair, where the first argument in the asn structure with the encoded pdu,
68
+ #
69
+ # @return [Array] a pair, where the first argument in the asn structure with the encoded pdu,
57
70
  # and the second is the calculated salt (if it has been encrypted)
58
- def encode(pdu, salt: , engine_time: , engine_boots: )
71
+ def encode(pdu, salt:, engine_time:, engine_boots:)
59
72
  if encryption
60
- encrypted_pdu, salt = encryption.encrypt(pdu.to_der, engine_boots: engine_boots,
73
+ encrypted_pdu, salt = encryption.encrypt(pdu.to_der, engine_boots: engine_boots,
61
74
  engine_time: engine_time)
62
- [OpenSSL::ASN1::OctetString.new(encrypted_pdu), OpenSSL::ASN1::OctetString.new(salt) ]
75
+ [OpenSSL::ASN1::OctetString.new(encrypted_pdu), OpenSSL::ASN1::OctetString.new(salt)]
63
76
  else
64
- [ pdu.to_asn, salt ]
77
+ [pdu.to_asn, salt]
65
78
  end
66
79
  end
67
80
 
@@ -69,12 +82,12 @@ module NETSNMP
69
82
  # @param [String] salt the salt from the incoming der
70
83
  # @param [Integer] engine_time the reported engine time
71
84
  # @param [Integer] engine_boots the reported engine boots
72
- def decode(der, salt: , engine_time: , engine_boots: )
85
+ def decode(der, salt:, engine_time:, engine_boots:)
73
86
  asn = OpenSSL::ASN1.decode(der)
74
87
  if encryption
75
88
  encrypted_pdu = asn.value
76
89
  pdu_der = encryption.decrypt(encrypted_pdu, salt: salt, engine_time: engine_time, engine_boots: engine_boots)
77
- OpenSSL::ASN1.decode(pdu_der)
90
+ OpenSSL::ASN1.decode(pdu_der)
78
91
  else
79
92
  asn
80
93
  end
@@ -86,7 +99,7 @@ module NETSNMP
86
99
  # @note this method is used in the process of authenticating a message
87
100
  def sign(message)
88
101
  # don't sign unless you have to
89
- return nil if not @auth_protocol
102
+ return nil unless @auth_protocol
90
103
 
91
104
  key = auth_key.dup
92
105
 
@@ -95,24 +108,30 @@ module NETSNMP
95
108
  k2 = key.xor(OPAD)
96
109
 
97
110
  digest.reset
98
- digest << ( k1 + message )
111
+ digest << (k1 + message)
99
112
  d1 = digest.digest
100
113
 
101
114
  digest.reset
102
- digest << ( k2 + d1 )
103
- digest.digest[0,12]
115
+ digest << (k2 + d1)
116
+ digest.digest[0, 12]
104
117
  end
105
118
 
106
119
  # @param [String] stream the encoded incoming payload
107
120
  # @param [String] salt the incoming payload''s salt
108
121
  #
109
- # @raise [NETSNMP::Error] if the message's integration has been violated
122
+ # @raise [NETSNMP::Error] if the message's integration has been violated
110
123
  def verify(stream, salt)
111
124
  return if @security_level < 1
112
125
  verisalt = sign(stream)
113
126
  raise Error, "invalid message authentication salt" unless verisalt == salt
114
127
  end
115
128
 
129
+ def must_revalidate?
130
+ return @engine_id.empty? unless authorizable?
131
+ return true if @engine_id.empty? || @timeliness.nil?
132
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) - @timeliness) >= TIMELINESS_THRESHOLD
133
+ end
134
+
116
135
  private
117
136
 
118
137
  def auth_key
@@ -125,46 +144,43 @@ module NETSNMP
125
144
 
126
145
  def check_parameters
127
146
  @security_level = case @security_level
128
- when Integer then @security_level
129
- when /no_?auth/ then 0
130
- when /auth_?no_?priv/ then 1
131
- when /auth_?priv/, nil then 3
132
- else
133
- raise Error, "security level not supported: #{@security_level}"
134
- end
135
-
136
- if @security_level > 0
147
+ when Integer then @security_level
148
+ when /no_?auth/ then 0
149
+ when /auth_?no_?priv/ then 1
150
+ when /auth_?priv/, nil then 3
151
+ else
152
+ raise Error, "security level not supported: #{@security_level}"
153
+ end
154
+
155
+ if @security_level.positive?
137
156
  @auth_protocol ||= :md5 # this is the default
138
157
  raise "security level requires an auth password" if @auth_password.nil?
139
- raise "auth password must have between 8 to 32 characters" if not (8..32).include?(@auth_password.length)
140
- end
141
- if @security_level > 1
142
- @priv_protocol ||= :des
143
- raise "security level requires a priv password" if @priv_password.nil?
144
- raise "priv password must have between 8 to 32 characters" if not (8..32).include?(@priv_password.length)
158
+ raise "auth password must have between 8 to 32 characters" unless (8..32).cover?(@auth_password.length)
145
159
  end
160
+ return unless @security_level > 1
161
+ @priv_protocol ||= :des
162
+ raise "security level requires a priv password" if @priv_password.nil?
163
+ raise "priv password must have between 8 to 32 characters" unless (8..32).cover?(@priv_password.length)
146
164
  end
147
165
 
148
166
  def localize_key(key)
149
-
150
167
  digest.reset
151
168
  digest << key
152
- digest << @engine_id
169
+ digest << @engine_id
153
170
  digest << key
154
171
 
155
172
  digest.digest
156
173
  end
157
174
 
158
175
  def passkey(password)
159
-
160
176
  digest.reset
161
177
  password_index = 0
162
178
 
163
- buffer = String.new
179
+ # buffer = +""
164
180
  password_length = password.length
165
181
  while password_index < 1048576
166
182
  initial = password_index % password_length
167
- rotated = password[initial..-1] + password[0,initial]
183
+ rotated = password[initial..-1] + password[0, initial]
168
184
  buffer = rotated * (64 / rotated.length) + rotated[0, 64 % rotated.length]
169
185
  password_index += 64
170
186
  digest << buffer
@@ -172,27 +188,30 @@ module NETSNMP
172
188
  end
173
189
 
174
190
  dig = digest.digest
175
- dig = dig[0,16] if @auth_protocol == :md5
191
+ dig = dig[0, 16] if @auth_protocol == :md5
176
192
  dig
177
193
  end
178
194
 
179
- def digest
195
+ def digest
180
196
  @digest ||= case @auth_protocol
181
- when :md5 then OpenSSL::Digest::MD5.new
182
- when :sha then OpenSSL::Digest::SHA1.new
183
- else
184
- raise Error, "unsupported auth protocol: #{@auth_protocol}"
185
- end
197
+ when :md5 then OpenSSL::Digest::MD5.new
198
+ when :sha then OpenSSL::Digest::SHA1.new
199
+ else
200
+ raise Error, "unsupported auth protocol: #{@auth_protocol}"
201
+ end
186
202
  end
187
203
 
188
204
  def encryption
189
205
  @encryption ||= case @priv_protocol
190
- when :des
191
- Encryption::DES.new(priv_key)
192
- when :aes
193
- Encryption::AES.new(priv_key)
194
- end
206
+ when :des
207
+ Encryption::DES.new(priv_key)
208
+ when :aes
209
+ Encryption::AES.new(priv_key)
210
+ end
195
211
  end
196
212
 
213
+ def authorizable?
214
+ @auth_protocol && @auth_protocol != :none
215
+ end
197
216
  end
198
217
  end
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module NETSNMP
3
- # Let's just remind that there is no session in snmp, this is just an abstraction.
4
- #
4
+ # Let's just remind that there is no session in snmp, this is just an abstraction.
5
+ #
5
6
  class Session
6
7
  TIMEOUT = 2
7
8
 
8
- # @param [Hash] opts the options set
9
+ # @param [Hash] opts the options set
9
10
  def initialize(version: 1, community: "public", **options)
10
- @version = 1
11
+ @version = version
11
12
  @community = community
12
13
  validate(options)
13
14
  end
@@ -25,7 +26,7 @@ module NETSNMP
25
26
  # @return [NETSNMP::PDU] a pdu
26
27
  #
27
28
  def build_pdu(type, *vars)
28
- PDU.build(type, headers: [ @version, @community ], varbinds: vars)
29
+ PDU.build(type, headers: [@version, @community], varbinds: vars)
29
30
  end
30
31
 
31
32
  # send a pdu, receives a pdu
@@ -35,7 +36,7 @@ module NETSNMP
35
36
  # @return [NETSNMP::PDU] the response pdu
36
37
  #
37
38
  def send(pdu)
38
- encoded_request = encode(pdu)
39
+ encoded_request = encode(pdu)
39
40
  encoded_response = @transport.send(encoded_request)
40
41
  decode(encoded_response)
41
42
  end
@@ -46,7 +47,7 @@ module NETSNMP
46
47
  proxy = options[:proxy]
47
48
  if proxy
48
49
  @proxy = true
49
- @transport = proxy
50
+ @transport = proxy
50
51
  else
51
52
  host, port = options.values_at(:host, :port)
52
53
  raise "you must provide an hostname/ip under :host" unless host
@@ -54,16 +55,15 @@ module NETSNMP
54
55
  @transport = Transport.new(host, port.to_i, timeout: options.fetch(:timeout, TIMEOUT))
55
56
  end
56
57
  @version = case @version
57
- when Integer then @version # assume the use know what he's doing
58
- when /v?1/ then 0
59
- when /v?2c?/ then 1
60
- when /v?3/ then 3
61
- else
62
- raise "unsupported snmp version (#{@version})"
63
- end
58
+ when Integer then @version # assume the use know what he's doing
59
+ when /v?1/ then 0
60
+ when /v?2c?/ then 1
61
+ when /v?3/ then 3
62
+ else
63
+ raise "unsupported snmp version (#{@version})"
64
+ end
64
65
  end
65
66
 
66
-
67
67
  def encode(pdu)
68
68
  pdu.to_der
69
69
  end
@@ -75,13 +75,13 @@ module NETSNMP
75
75
  class Transport
76
76
  MAXPDUSIZE = 0xffff + 1
77
77
 
78
- def initialize(host, port, timeout: )
78
+ def initialize(host, port, timeout:)
79
79
  @socket = UDPSocket.new
80
- @socket.connect( host, port )
80
+ @socket.connect(host, port)
81
81
  @timeout = timeout
82
82
  end
83
83
 
84
- def close
84
+ def close
85
85
  @socket.close
86
86
  end
87
87
 
@@ -96,9 +96,9 @@ module NETSNMP
96
96
  end
97
97
  end
98
98
 
99
- def recv(bytesize=MAXPDUSIZE)
99
+ def recv(bytesize = MAXPDUSIZE)
100
100
  perform_io do
101
- datagram, _ = @socket.recvfrom_nonblock(bytesize)
101
+ datagram, = @socket.recvfrom_nonblock(bytesize)
102
102
  datagram
103
103
  end
104
104
  end
@@ -118,11 +118,9 @@ module NETSNMP
118
118
  end
119
119
 
120
120
  def wait(mode)
121
- unless @socket.__send__(mode, @timeout)
122
- raise Timeout::Error, "Timeout after #{@timeout} seconds"
123
- end
121
+ return if @socket.__send__(mode, @timeout)
122
+ raise Timeout::Error, "Timeout after #{@timeout} seconds"
124
123
  end
125
-
126
124
  end
127
125
  end
128
126
  end
@@ -1,12 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NETSNMP
2
4
  class Timetick < Numeric
3
-
4
5
  # @param [Integer] ticks number of microseconds since the time it was read
5
6
  def initialize(ticks)
6
7
  @ticks = ticks
7
8
  end
8
9
 
9
-
10
10
  def to_s
11
11
  days = days_since
12
12
  hours = hours_since(days)
@@ -24,7 +24,7 @@ module NETSNMP
24
24
  end
25
25
 
26
26
  def coerce(other)
27
- [ Timetick.new(other), self]
27
+ [Timetick.new(other), self]
28
28
  end
29
29
 
30
30
  def <=>(other)
@@ -32,24 +32,23 @@ module NETSNMP
32
32
  end
33
33
 
34
34
  def +(other)
35
- Timetick.new( (to_i + other.to_i))
35
+ Timetick.new((to_i + other.to_i))
36
36
  end
37
37
 
38
38
  def -(other)
39
- Timetick.new( (to_i - other.to_i))
39
+ Timetick.new((to_i - other.to_i))
40
40
  end
41
41
 
42
42
  def *(other)
43
- Timetick.new( (to_i * other.to_i))
43
+ Timetick.new((to_i * other.to_i))
44
44
  end
45
45
 
46
46
  def /(other)
47
- Timetick.new( (to_i / other.to_i))
47
+ Timetick.new((to_i / other.to_i))
48
48
  end
49
49
 
50
50
  private
51
51
 
52
-
53
52
  def days_since
54
53
  Rational(@ticks, 8_640_000)
55
54
  end
@@ -61,10 +60,9 @@ module NETSNMP
61
60
  def minutes_since(hours)
62
61
  Rational((hours.to_f - hours.to_i) * 60)
63
62
  end
64
-
63
+
65
64
  def milliseconds_since(minutes)
66
65
  Rational((minutes.to_f - minutes.to_i) * 60)
67
66
  end
68
-
69
67
  end
70
68
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module NETSNMP
3
4
  # Abstraction for the v3 semantics.
4
5
  class V3Session < Session
5
-
6
6
  # @param [String, Integer] version SNMP version (always 3)
7
7
  def initialize(version: 3, context: "", **opts)
8
8
  @context = context
9
- @security_parameters = opts.delete(:security_parameters)
9
+ @security_parameters = opts.delete(:security_parameters)
10
10
  super
11
11
  end
12
12
 
@@ -15,12 +15,12 @@ module NETSNMP
15
15
  # @return [NETSNMP::ScopedPDU] a pdu
16
16
  def build_pdu(type, *vars)
17
17
  engine_id = security_parameters.engine_id
18
- pdu = ScopedPDU.build(type, headers: [engine_id, @context], varbinds: vars)
18
+ ScopedPDU.build(type, headers: [engine_id, @context], varbinds: vars)
19
19
  end
20
20
 
21
21
  # @see {NETSNMP::Session#send}
22
22
  def send(*)
23
- pdu, _ = super
23
+ pdu, = super
24
24
  pdu
25
25
  end
26
26
 
@@ -28,29 +28,27 @@ module NETSNMP
28
28
 
29
29
  def validate(**options)
30
30
  super
31
- if s = @security_parameters
31
+ if (s = @security_parameters)
32
32
  # inspect public API
33
33
  unless s.respond_to?(:encode) &&
34
34
  s.respond_to?(:decode) &&
35
35
  s.respond_to?(:sign) &&
36
36
  s.respond_to?(:verify)
37
- raise Error, "#{s} doesn't respect the sec params public API (#encode, #decode, #sign)"
38
- end
37
+ raise Error, "#{s} doesn't respect the sec params public API (#encode, #decode, #sign)"
38
+ end
39
39
  else
40
- @security_parameters = SecurityParameters.new(security_level: options[:security_level],
40
+ @security_parameters = SecurityParameters.new(security_level: options[:security_level],
41
41
  username: options[:username],
42
42
  auth_protocol: options[:auth_protocol],
43
43
  priv_protocol: options[:priv_protocol],
44
44
  auth_password: options[:auth_password],
45
45
  priv_password: options[:priv_password])
46
-
46
+
47
47
  end
48
48
  end
49
49
 
50
50
  def security_parameters
51
- if @security_parameters.engine_id.empty?
52
- @security_parameters.engine_id = probe_for_engine
53
- end
51
+ @security_parameters.engine_id = probe_for_engine if @security_parameters.must_revalidate?
54
52
  @security_parameters
55
53
  end
56
54
 
@@ -69,7 +67,7 @@ module NETSNMP
69
67
  end
70
68
 
71
69
  def encode(pdu)
72
- Message.encode(pdu, security_parameters: @security_parameters,
70
+ Message.encode(pdu, security_parameters: @security_parameters,
73
71
  engine_boots: @engine_boots,
74
72
  engine_time: @engine_time)
75
73
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module NETSNMP
3
4
  # Abstracts the PDU variable structure into a ruby object
4
5
  #
5
6
  class Varbind
6
-
7
7
  attr_reader :oid, :value
8
8
 
9
- def initialize(oid , value: nil, type: nil, **opts)
9
+ def initialize(oid, value: nil, type: nil, **_opts)
10
10
  @oid = OID.build(oid)
11
11
  @type = type
12
12
  @value = convert_val(value) if value
@@ -23,29 +23,28 @@ module NETSNMP
23
23
  def to_asn
24
24
  asn_oid = OID.to_asn(@oid)
25
25
  asn_val = if @type
26
- convert_to_asn(@type, @value)
27
- else
28
- case @value
29
- when String
30
- OpenSSL::ASN1::OctetString.new(@value)
31
- when Integer
32
- OpenSSL::ASN1::Integer.new(@value)
33
- when true, false
34
- OpenSSL::ASN1::Boolean.new(@value)
35
- when nil
36
- OpenSSL::ASN1::Null.new(nil)
37
- when IPAddr
38
- OpenSSL::ASN1::ASN1Data.new(@value.hton, 0, :APPLICATION)
39
- when Timetick
40
- @value.to_asn
41
- else
42
- raise Error, "#{@value}: unsupported varbind type"
43
- end
44
- end
45
- OpenSSL::ASN1::Sequence.new( [asn_oid, asn_val] )
26
+ convert_to_asn(@type, @value)
27
+ else
28
+ case @value
29
+ when String
30
+ OpenSSL::ASN1::OctetString.new(@value)
31
+ when Integer
32
+ OpenSSL::ASN1::Integer.new(@value)
33
+ when true, false
34
+ OpenSSL::ASN1::Boolean.new(@value)
35
+ when nil
36
+ OpenSSL::ASN1::Null.new(nil)
37
+ when IPAddr
38
+ OpenSSL::ASN1::ASN1Data.new(@value.hton, 0, :APPLICATION)
39
+ when Timetick
40
+ @value.to_asn
41
+ else
42
+ raise Error, "#{@value}: unsupported varbind type"
43
+ end
44
+ end
45
+ OpenSSL::ASN1::Sequence.new([asn_oid, asn_val])
46
46
  end
47
47
 
48
-
49
48
  def convert_val(asn_value)
50
49
  case asn_value
51
50
  when OpenSSL::ASN1::OctetString
@@ -68,10 +67,10 @@ module NETSNMP
68
67
  when OpenSSL::ASN1::ASN1Data
69
68
  # application data
70
69
  convert_application_asn(asn_value)
71
- when OpenSSL::BN
70
+ # when OpenSSL::BN
72
71
  else
73
- asn_value # assume it's already primitive
74
- end
72
+ asn_value # assume it's already primitive
73
+ end
75
74
  end
76
75
 
77
76
  def convert_to_asn(typ, value)
@@ -79,35 +78,40 @@ module NETSNMP
79
78
  asn_val = value
80
79
  if typ.is_a?(Symbol)
81
80
  asn_type = case typ
82
- when :ipaddress then 0
83
- when :counter32 then 1
84
- when :gauge then 2
85
- when :timetick
86
- return Timetick.new(value).to_asn
87
- when :opaque then 4
88
- when :nsap then 5
89
- when :counter64 then 6
90
- when :uinteger then 7
91
- else
92
- raise Error, "#{typ}: unsupported application type"
93
- end
81
+ when :ipaddress then 0
82
+ when :counter32
83
+ asn_val = [value].pack("n*")
84
+ 1
85
+ when :gauge
86
+ asn_val = [value].pack("n*")
87
+ 2
88
+ when :timetick
89
+ return Timetick.new(value).to_asn
90
+ when :opaque then 4
91
+ when :nsap then 5
92
+ when :counter64 then 6
93
+ when :uinteger then 7
94
+ else
95
+ raise Error, "#{typ}: unsupported application type"
96
+ end
94
97
  end
95
98
  OpenSSL::ASN1::ASN1Data.new(asn_val, asn_type, :APPLICATION)
96
99
  end
97
100
 
98
101
  def convert_application_asn(asn)
99
102
  case asn.tag
100
- when 0 # IP Address
101
- IPAddr.new_ntoh(asn.value)
102
- when 1 # ASN counter 32
103
- asn.value.unpack("n*")[0] || 0
104
- when 2 # gauge
105
- when 3 # timeticks
106
- Timetick.new(asn.value.unpack("N*")[0] || 0)
107
- when 4 # opaque
108
- when 5 # NSAP
109
- when 6 # ASN Counter 64
110
- when 7 # ASN UInteger
103
+ when 0 # IP Address
104
+ IPAddr.new_ntoh(asn.value)
105
+ when 1 # ASN counter 32
106
+ asn.value.unpack("n*")[0] || 0
107
+ when 2 # gauge
108
+ asn.value.unpack("n*")[0] || 0
109
+ when 3 # timeticks
110
+ Timetick.new(asn.value.unpack("N*")[0] || 0)
111
+ # when 4 # opaque
112
+ # when 5 # NSAP
113
+ # when 6 # ASN Counter 64
114
+ # when 7 # ASN UInteger
111
115
  end
112
116
  end
113
117
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module NETSNMP
3
- VERSION = '0.1.3'
4
+ VERSION = "0.1.4"
4
5
  end