netsnmp 0.1.3 → 0.1.4

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.
@@ -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