netsnmp 0.0.2 → 0.1.0

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +1 -0
  3. data/.travis.yml +4 -4
  4. data/Gemfile +5 -1
  5. data/README.md +124 -63
  6. data/lib/netsnmp.rb +66 -10
  7. data/lib/netsnmp/client.rb +93 -75
  8. data/lib/netsnmp/encryption/aes.rb +84 -0
  9. data/lib/netsnmp/encryption/des.rb +80 -0
  10. data/lib/netsnmp/encryption/none.rb +17 -0
  11. data/lib/netsnmp/errors.rb +1 -3
  12. data/lib/netsnmp/message.rb +81 -0
  13. data/lib/netsnmp/oid.rb +18 -137
  14. data/lib/netsnmp/pdu.rb +106 -64
  15. data/lib/netsnmp/scoped_pdu.rb +23 -0
  16. data/lib/netsnmp/security_parameters.rb +198 -0
  17. data/lib/netsnmp/session.rb +84 -275
  18. data/lib/netsnmp/v3_session.rb +81 -0
  19. data/lib/netsnmp/varbind.rb +65 -156
  20. data/lib/netsnmp/version.rb +2 -1
  21. data/netsnmp.gemspec +2 -8
  22. data/spec/client_spec.rb +147 -99
  23. data/spec/handlers/celluloid_spec.rb +33 -20
  24. data/spec/oid_spec.rb +11 -5
  25. data/spec/pdu_spec.rb +22 -22
  26. data/spec/security_parameters_spec.rb +40 -0
  27. data/spec/session_spec.rb +0 -23
  28. data/spec/support/celluloid.rb +24 -0
  29. data/spec/support/request_examples.rb +36 -0
  30. data/spec/support/start_docker.sh +15 -1
  31. data/spec/v3_session_spec.rb +21 -0
  32. data/spec/varbind_spec.rb +2 -51
  33. metadata +30 -76
  34. data/lib/netsnmp/core.rb +0 -12
  35. data/lib/netsnmp/core/client.rb +0 -15
  36. data/lib/netsnmp/core/constants.rb +0 -153
  37. data/lib/netsnmp/core/inline.rb +0 -20
  38. data/lib/netsnmp/core/libc.rb +0 -48
  39. data/lib/netsnmp/core/libsnmp.rb +0 -44
  40. data/lib/netsnmp/core/structures.rb +0 -167
  41. data/lib/netsnmp/core/utilities.rb +0 -13
  42. data/lib/netsnmp/handlers/celluloid.rb +0 -27
  43. data/lib/netsnmp/handlers/em.rb +0 -56
  44. data/spec/core/libc_spec.rb +0 -2
  45. data/spec/core/libsnmp_spec.rb +0 -32
  46. data/spec/core/structures_spec.rb +0 -54
  47. data/spec/handlers/em_client_spec.rb +0 -34
data/lib/netsnmp/pdu.rb CHANGED
@@ -1,50 +1,87 @@
1
+ # frozen_string_literal: true
1
2
  require 'forwardable'
2
3
  module NETSNMP
3
4
  # Abstracts the PDU base structure into a ruby object. It gives access to its varbinds.
4
5
  #
5
6
  class PDU
6
- extend Forwardable
7
-
8
- Error = Class.new(Error)
7
+ MAXREQUESTID=2147483647
9
8
  class << self
9
+
10
+ def decode(der)
11
+ asn_tree = case der
12
+ when String
13
+ OpenSSL::ASN1.decode(der)
14
+ when OpenSSL::ASN1::ASN1Data
15
+ der
16
+ else
17
+ raise "#{der}: unexpected data"
18
+ end
19
+
20
+ *headers, request = asn_tree.value
21
+
22
+ version, community = headers.map(&:value)
23
+
24
+ type = request.tag
25
+
26
+ *request_headers, varbinds = request.value
27
+
28
+ request_id, error_status, error_index = request_headers.map(&:value).map(&:to_i)
29
+
30
+ varbs = varbinds.value.map do |varbind|
31
+ oid_asn, val_asn = varbind.value
32
+ oid = oid_asn.value
33
+ { oid: oid, value: val_asn }
34
+ end
35
+
36
+ new(type: type, headers: [version, community],
37
+ error_status: error_status,
38
+ error_index: error_index,
39
+ request_id: request_id,
40
+ varbinds: varbs)
41
+ end
42
+
10
43
  # factory method that abstracts initialization of the pdu types that the library supports.
11
44
  #
12
45
  # @param [Symbol] type the type of pdu structure to build
13
- # @return [RequestPDU] a fully-formed request pdu
14
46
  #
15
- def build(type, *args)
16
- case type
17
- when :get then RequestPDU.new(Core::Constants::SNMP_MSG_GET, *args)
18
- when :getnext then RequestPDU.new(Core::Constants::SNMP_MSG_GETNEXT, *args)
19
- when :getbulk then RequestPDU.new(Core::Constants::SNMP_MSG_GETBULK, *args)
20
- when :set then RequestPDU.new(Core::Constants::SNMP_MSG_SET, *args)
21
- when :response then ResponsePDU.new(Core::Constants::SNMP_MSG_RESPONSE, *args)
47
+ def build(type, **args)
48
+ typ = case type
49
+ when :get then 0
50
+ when :getnext then 1
51
+ # when :getbulk then 5
52
+ when :set then 3
53
+ when :response then 2
22
54
  else raise Error, "#{type} is not supported as type"
23
55
  end
56
+ new(args.merge(type: typ))
24
57
  end
25
58
  end
26
59
 
27
- attr_reader :struct, :varbinds
60
+ attr_reader :varbinds, :type
28
61
 
29
- def_delegators :@struct, :[], :[]=, :pointer
62
+ attr_reader :version, :community, :request_id
30
63
 
31
- # @param [FFI::Pointer] the pointer to the initialized structure
32
- #
33
- def initialize(pointer)
34
- @struct = Core::Structures::PDU.new(pointer)
64
+ def initialize(type: , headers: ,
65
+ request_id: nil,
66
+ error_status: 0,
67
+ error_index: 0,
68
+ varbinds: [])
69
+ @version, @community = headers
70
+ @version = @version.to_i
71
+ @error_status = error_status
72
+ @error_index = error_index
73
+ @type = type
35
74
  @varbinds = []
75
+ varbinds.each do |varbind|
76
+ add_varbind(varbind)
77
+ end
78
+ @request_id = request_id || SecureRandom.random_number(MAXREQUESTID)
79
+ check_error_status(@error_status)
36
80
  end
37
81
 
38
82
 
39
- end
40
-
41
- # Abstracts the request PDU
42
- # Main characteristic is that it has a write-only API, in that you can add varbinds to it.
43
- #
44
- class RequestPDU < PDU
45
- def initialize(type)
46
- pointer = Core::LibSNMP.snmp_pdu_create(type)
47
- super(pointer)
83
+ def to_der
84
+ to_asn.to_der
48
85
  end
49
86
 
50
87
  # Adds a request varbind to the pdu
@@ -52,54 +89,59 @@ module NETSNMP
52
89
  # @param [OID] oid a valid oid
53
90
  # @param [Hash] options additional request varbind options
54
91
  # @option options [Object] :value the value for the oid
55
- def add_varbind(oid, **options)
56
- @varbinds << RequestVarbind.new(self, oid, options[:value], options)
92
+ def add_varbind(oid: , **options)
93
+ @varbinds << Varbind.new(oid, **options)
57
94
  end
58
95
  alias_method :<<, :add_varbind
59
- end
60
96
 
61
- # Abstracts the response PDU
62
- # Main characteristic is: it reads the values on initialization (because the response structure
63
- # is at some point free'd). It is therefore a read-only entity
64
- #
65
- class ResponsePDU < PDU
66
97
 
67
- # @param [FFI::Pointer] the pointer to the response pdu structure
68
- #
69
- # @note it loads the variable as well.
70
- #
71
- def initialize(pointer)
72
- super
73
- load_variables
74
- end
98
+ 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 )
75
102
 
76
- # @return [String] the concatenation of the varbind values (usually, it's only one)
77
- #
78
- def value
79
- case @varbinds.size
80
- when 0 then nil
81
- when 1 then @varbinds.first.value
82
- else
83
- # assume that they're all strings
84
- @varbinds.map(&:value).join(' ')
85
- end
103
+ varbind_asns = OpenSSL::ASN1::Sequence.new( @varbinds.map(&:to_asn) )
104
+
105
+ request_asn = OpenSSL::ASN1::ASN1Data.new( [request_id_asn,
106
+ error_asn, error_index_asn,
107
+ varbind_asns], @type,
108
+ :CONTEXT_SPECIFIC )
109
+
110
+ OpenSSL::ASN1::Sequence.new( [ *encode_headers_asn, request_asn ] )
86
111
  end
87
112
 
88
113
  private
89
114
 
90
- # loads the C-level structure variables into ruby ResponseVarbind objects,
91
- # and store them as state in {{@varbinds}}
92
- def load_variables
93
- variable = @struct[:variables]
94
- unless variable.null?
95
- @varbinds << ResponseVarbind.new(variable)
96
- variable = Core::Structures::VariableList.new(variable)
97
- while( !(variable = variable[:next_variable]).null? )
98
- variable = Core::Structures::VariableList.new(variable)
99
- @varbinds << ResponseVarbind.new(variable.pointer)
100
- end
101
- end
115
+ def encode_headers_asn
116
+ [ OpenSSL::ASN1::Integer.new( @version ),
117
+ OpenSSL::ASN1::OctetString.new( @community ) ]
102
118
  end
103
119
 
120
+
121
+ # http://www.tcpipguide.com/free/t_SNMPVersion2SNMPv2MessageFormats-5.htm#Table_219
122
+ def check_error_status(status)
123
+ return if status == 0
124
+ message = case status
125
+ when 1 then "Response-PDU too big"
126
+ when 2 then "No such name"
127
+ when 3 then "Bad value"
128
+ when 4 then "Read Only"
129
+ when 5 then "General Error"
130
+ when 6 then "Access denied"
131
+ when 7 then "Wrong type"
132
+ when 8 then "Wrong length"
133
+ when 9 then "Wrong encoding"
134
+ when 10 then "Wrong value"
135
+ when 11 then "No creation"
136
+ when 12 then "Inconsistent value"
137
+ when 13 then "Resource unavailable"
138
+ when 14 then "Commit failed"
139
+ when 15 then "Undo Failed"
140
+ when 16 then "Authorization Error"
141
+ when 17 then "Not Writable"
142
+ when 18 then "Inconsistent Name"
143
+ end
144
+ raise Error, message
145
+ end
104
146
  end
105
147
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module NETSNMP
3
+ class ScopedPDU < PDU
4
+
5
+
6
+ attr_reader :engine_id
7
+
8
+ def initialize(type: , headers:,
9
+ request_id: nil,
10
+ error_status: 0,
11
+ error_index: 0,
12
+ varbinds: [])
13
+ @engine_id, @context = headers
14
+ super(type: type, headers: [3, nil], request_id: request_id, varbinds: varbinds)
15
+ end
16
+
17
+ def encode_headers_asn
18
+ [ OpenSSL::ASN1::OctetString.new(@engine_id || ""),
19
+ OpenSSL::ASN1::OctetString.new(@context || "") ]
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+ module NETSNMP
3
+ # 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
6
+ # the arguments (consisting mostly of primitive types).
7
+ # It also provides validation of the security options passed with a client is initialized in v3 mode.
8
+ class SecurityParameters
9
+ using StringExtensions
10
+
11
+ IPAD = "\x36" * 64
12
+ OPAD = "\x5c" * 64
13
+
14
+ attr_reader :security_level, :username
15
+ attr_accessor :engine_id
16
+
17
+ # @param [String] username the snmp v3 username
18
+ # @param [String] engine_id the device engine id (initialized to '' for report)
19
+ # @param [Symbol, integer] security_level allowed snmp v3 security level (:auth_priv, :auh_no_priv, etc)
20
+ # @param [Symbol, nil] auth_protocol a supported authentication protocol (currently supported: :md5, :sha)
21
+ # @param [Symbol, nil] priv_protocol a supported privacy protocol (currently supported: :des, :aes)
22
+ # @param [String, nil] auth_password the authentication password
23
+ # @param [String, nil] priv_password the privacy password
24
+ #
25
+ # @note if security level is set to :no_auth_no_priv, all other parameters are optional; if
26
+ # :auth_no_priv, :auth_protocol will be coerced to :md5 (if not explicitly set), and :auth_password is
27
+ # mandatory; if :auth_priv, the sentence before applies, and :priv_protocol will be coerced to :des (if
28
+ # not explicitly set), and :priv_password becomes mandatory.
29
+ #
30
+ def initialize(
31
+ username: ,
32
+ engine_id: "",
33
+ security_level: nil,
34
+ auth_protocol: nil,
35
+ auth_password: nil,
36
+ priv_protocol: nil,
37
+ priv_password: nil)
38
+ @security_level = security_level
39
+ @username = username
40
+ @engine_id = engine_id
41
+ @auth_protocol = auth_protocol.to_sym unless auth_protocol.nil?
42
+ @priv_protocol = priv_protocol.to_sym unless priv_protocol.nil?
43
+ @auth_password = auth_password
44
+ @priv_password = priv_password
45
+ check_parameters
46
+ @auth_pass_key = passkey(@auth_password) unless @auth_password.nil?
47
+ @priv_pass_key = passkey(@priv_password) unless @priv_password.nil?
48
+ end
49
+
50
+
51
+ # @param [#to_asn, #to_der] pdu the pdu to encode (must quack like a asn1 type)
52
+ # @param [String] salt the salt to use
53
+ # @param [Integer] engine_time the reported engine time
54
+ # @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,
57
+ # and the second is the calculated salt (if it has been encrypted)
58
+ def encode(pdu, salt: , engine_time: , engine_boots: )
59
+ if encryption
60
+ encrypted_pdu, salt = encryption.encrypt(pdu.to_der, engine_boots: engine_boots,
61
+ engine_time: engine_time)
62
+ [OpenSSL::ASN1::OctetString.new(encrypted_pdu), OpenSSL::ASN1::OctetString.new(salt) ]
63
+ else
64
+ [ pdu.to_asn, salt ]
65
+ end
66
+ end
67
+
68
+ # @param [String] der the encoded der to be decoded
69
+ # @param [String] salt the salt from the incoming der
70
+ # @param [Integer] engine_time the reported engine time
71
+ # @param [Integer] engine_boots the reported engine boots
72
+ def decode(der, salt: , engine_time: , engine_boots: )
73
+ asn = OpenSSL::ASN1.decode(der)
74
+ if encryption
75
+ encrypted_pdu = asn.value
76
+ pdu_der = encryption.decrypt(encrypted_pdu, salt: salt, engine_time: engine_time, engine_boots: engine_boots)
77
+ OpenSSL::ASN1.decode(pdu_der)
78
+ else
79
+ asn
80
+ end
81
+ end
82
+
83
+ # @param [String] message the already encoded snmp v3 message
84
+ # @return [String] the digest signature of the message payload
85
+ #
86
+ # @note this method is used in the process of authenticating a message
87
+ def sign(message)
88
+ # don't sign unless you have to
89
+ return nil if not @auth_protocol
90
+
91
+ key = auth_key.dup
92
+
93
+ key << "\x00" * (@auth_protocol == :md5 ? 48 : 44)
94
+ k1 = key.xor(IPAD)
95
+ k2 = key.xor(OPAD)
96
+
97
+ digest.reset
98
+ digest << ( k1 + message )
99
+ d1 = digest.digest
100
+
101
+ digest.reset
102
+ digest << ( k2 + d1 )
103
+ digest.digest[0,12]
104
+ end
105
+
106
+ # @param [String] stream the encoded incoming payload
107
+ # @param [String] salt the incoming payload''s salt
108
+ #
109
+ # @raise [NETSMP::Error] if the message's integration has been violated
110
+ def verify(stream, salt)
111
+ return if @security_level < 1
112
+ verisalt = sign(stream)
113
+ raise Error, "invalid message authentication salt" unless verisalt == salt
114
+ end
115
+
116
+ private
117
+
118
+ def auth_key
119
+ @auth_key ||= localize_key(@auth_pass_key)
120
+ end
121
+
122
+ def priv_key
123
+ @priv_key ||= localize_key(@priv_pass_key)
124
+ end
125
+
126
+ def check_parameters
127
+ @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
137
+ @auth_protocol ||= :md5 # this is the default
138
+ 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)
145
+ end
146
+ end
147
+
148
+ def localize_key(key)
149
+
150
+ digest.reset
151
+ digest << key
152
+ digest << @engine_id
153
+ digest << key
154
+
155
+ digest.digest
156
+ end
157
+
158
+ def passkey(password)
159
+
160
+ digest.reset
161
+ password_index = 0
162
+
163
+ buffer = String.new
164
+ password_length = password.length
165
+ while password_index < 1048576
166
+ initial = password_index % password_length
167
+ rotated = password[initial..-1] + password[0,initial]
168
+ buffer = rotated * (64 / rotated.length) + rotated[0, 64 % rotated.length]
169
+ password_index += 64
170
+ digest << buffer
171
+ buffer.clear
172
+ end
173
+
174
+ dig = digest.digest
175
+ dig = dig[0,16] if @auth_protocol == :md5
176
+ dig
177
+ end
178
+
179
+ def digest
180
+ @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
186
+ end
187
+
188
+ def encryption
189
+ @encryption ||= case @priv_protocol
190
+ when :des
191
+ Encryption::DES.new(priv_key)
192
+ when :aes
193
+ Encryption::AES.new(priv_key)
194
+ end
195
+ end
196
+
197
+ end
198
+ end
@@ -1,317 +1,126 @@
1
+ # frozen_string_literal: true
1
2
  module NETSNMP
2
- # The Entity abstracts the C net-snmp session, and the lifecycle steps.
3
+ # Let's just remind that there is no session in snmp, this is just an abstraction.
3
4
  #
4
- # For example, a session must be initialized (memory allocated) and opened
5
- # (authentication, encryption, signature)
6
- #
7
- # The session uses the signature to send and receive PDUs. They are built somewhere else.
8
- #
9
- # After the session is established, a socket handle is read from the structure. This will
10
- # be later used for non-blocking behaviour. It's important to notice, there is no
11
- # usage of the C net-snmp sync API, we always do async send/response, even if the
12
- # ruby API "feels" blocking. This was done so that the GIL can be released between
13
- # sends and receives, and the load can be shared through different threads possibly.
14
- # As we use the session abstraction, this means we ONLY use the thread-safe API.
15
- #
16
5
  class Session
6
+ TIMEOUT = 2
17
7
 
18
- attr_reader :host, :signature
19
-
20
- # @param [String] host the host IP/hostname
21
8
  # @param [Hash] opts the options set
22
- #
23
- def initialize(host, opts)
24
- @host = host
25
- # this is because other evented clients might discover IP first, but hostnames
26
- # give you better trackability of errors. Give the opportunity to the users to
27
- # pass it, by setting the hostname explicitly. If not, fallback to the host.
28
- @hostname = opts.fetch(:hostname, @host)
29
- @options = opts
30
- @logged_at = nil
31
- @request = nil
32
- # For now, let's eager load the signature
33
- @signature = build_signature(@options)
34
- if @signature.null?
35
- raise ConnectionFailed, "could not build signature for #@hostname"
36
- end
37
- @requests ||= {}
38
- end
39
-
40
- # TODO: do we need this?
41
- def reachable?
42
- !!transport
9
+ def initialize(version: 1, community: "public", **options)
10
+ @version = 1
11
+ @community = community
12
+ validate(options)
43
13
  end
44
14
 
45
15
  # Closes the session
46
16
  def close
47
- return unless @signature
48
- if @transport
49
- transport.close rescue nil
50
- end
51
- if Core::LibSNMP.snmp_sess_close(@signature) == 0
52
- raise Error, "#@hostname: Couldn't clean up session properly"
53
- end
54
- end
55
-
56
- # sends a request PDU and waits for the response
57
- #
58
- # @param [RequestPDU] pdu a request pdu
59
- # @param [Hash] opts additional options
60
- # @option opts [true, false] :async if true, it doesn't wait for response (defaults to false)
61
- def send(pdu, **opts)
62
- write(pdu)
63
- read
64
- end
65
-
66
- private
67
-
68
- LoggedInTimeout = Class.new(Timeout::Error)
69
- def try_login
70
- return yield unless @logged_at.nil?
71
-
72
- begin
73
- # problem for snmp is, there is no login process.
74
- # signature is sent on first packet. As we are not using the synch interface, this will not
75
- # be handled by the core library. Which sucks. But even core library would just retry and timeout
76
- # at some point.
77
- # we do something similar: before any PDU succeeds, after first PDU is async-sent, we wait for reads, but we can't block. If
78
- # the socket hasn't anything to read, we can assume it was the wrong USERNAME (?).
79
- # TODO: research if this is true.
80
- Timeout.timeout(@timeout, LoggedInTimeout) do
81
- yield
82
- end
83
- rescue LoggedInTimeout
84
- raise ConnectionFailed, "failed to login to #@hostname"
85
- end
17
+ # if the transport came as an argument,
18
+ # then let the outer realm care for its lifecycle
19
+ @transport.close unless @proxy
86
20
  end
87
21
 
88
- def transport
89
- @transport ||= fetch_transport
22
+ # @param [Symbol] type the type of PDU (:get, :set, :getnext)
23
+ # @param [Array<Hashes>] vars collection of options to generate varbinds (see {NETSMP::Varbind.new} for all the possible options)
24
+ #
25
+ # @return [NETSNMP::PDU] a pdu
26
+ #
27
+ def build_pdu(type, *vars)
28
+ PDU.build(type, headers: [ @version, @community ], varbinds: vars)
90
29
  end
91
30
 
92
- def write(pdu)
93
- wait_writable
94
- async_send(pdu)
31
+ # send a pdu, receives a pdu
32
+ #
33
+ # @param [NETSNMP::PDU, #to_der] an encodable request pdu
34
+ #
35
+ # @return [NETSNMP::PDU] the response pdu
36
+ #
37
+ def send(pdu)
38
+ encoded_request = encode(pdu)
39
+ encoded_response = @transport.send(encoded_request)
40
+ decode(encoded_response)
95
41
  end
96
42
 
97
- def async_send(pdu)
98
- if ( @reqid = Core::LibSNMP.snmp_sess_async_send(@signature, pdu.pointer, session_callback, nil) ) == 0
99
- # it's interesting, pdu's are only fred if the async send is successful... netsnmp 1 - me 0
100
- Core::LibSNMP.snmp_free_pdu(pdu.pointer)
101
- # if it's the first time we're passing here and send fails, we can (?) assume that
102
- # AUTH_PASSWORD is wrong
103
- if @logged_at.nil?
104
- raise ConnectionFailed, "failed to login to #@hostname"
105
- else
106
- raise SendError, "#@hostname: Failed to send pdu"
107
- end
108
- end
109
- end
110
-
111
- def read
112
- receive # trigger callback ahead of time and wait for it
113
- # Sounds a bit unreasonable, but only after we arrived here we know for sure that the credentials are proper.
114
- # So we can set this variable, so further errors can be safely ignored.
115
- @logged_at ||= Time.now
116
- handle_response
117
- end
118
-
119
- def handle_response
120
- operation, response_pdu = @requests.delete(@reqid)
121
- case operation
122
- when :success
123
- response_pdu
124
- when :send_failed
125
- raise ReceiveError, "#@hostname: Failed to receive pdu"
126
- when :timeout
127
- raise Timeout::Error, "#@hostname: timed out while waiting for pdu response"
128
- else
129
- raise Error, "#@hostname: unrecognized operation for request #{@reqid}: #{operation} for #{response_pdu}"
130
- end
131
- end
43
+ private
132
44
 
133
- def receive
134
- readers, _ = try_login { wait_readable }
135
- case readers.size
136
- when 1..Float::INFINITY
137
- # triggers callback
138
- async_read
139
- when 0
140
- Core::LibSNMP.snmp_sess_timeout(@signature)
141
- else
142
- raise ReceiveError, "#@hostname: error receiving data"
45
+ def validate(**options)
46
+ proxy = options[:proxy]
47
+ if proxy
48
+ @proxy = true
49
+ @transport = proxy
50
+ else
51
+ host, port = options.values_at(:host, :port)
52
+ raise "you must provide an hostname/ip under :host" unless host
53
+ port ||= 161 # default snmp port
54
+ @transport = Transport.new(host, port.to_i, timeout: options.fetch(:timeout, TIMEOUT))
143
55
  end
144
- end
145
-
146
- def async_read
147
- if Core::LibSNMP.snmp_sess_read(@signature, get_selectable_sockets.pointer) != 0
148
- # if it's the first time we're passing here and send fails, we can (?) assume that
149
- # PRIV_PASSWORD is wrong
150
- if @logged_at.nil?
151
- raise ConnectionFailed, "failed to login to #@hostname"
56
+ @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
152
61
  else
153
- raise ReceiveError, "#@hostname: Failed to receive pdu response"
154
- end
62
+ raise "unsupported snmp version (#{@version})"
155
63
  end
156
64
  end
157
65
 
158
- def timeout
159
- Core::LibSNMP.snmp_sess_timeout(@signature)
160
- end
161
-
162
- def wait_writable
163
- IO.select([],[transport])
164
- end
165
66
 
166
- def wait_readable
167
- IO.select([transport])
67
+ def encode(pdu)
68
+ pdu.to_der
168
69
  end
169
70
 
170
- def get_selectable_sockets
171
- fdset = Core::C::FDSet.new
172
- fdset.clear
173
- num_fds = FFI::MemoryPointer.new(:int)
174
- tv_sec = 0
175
- tv_usec = 0
176
- tval = Core::C::Timeval.new
177
- tval[:tv_sec] = tv_sec
178
- tval[:tv_usec] = tv_usec
179
- block = FFI::MemoryPointer.new(:int)
180
- block.write_int(0)
181
- Core::LibSNMP.snmp_sess_select_info(@signature, num_fds, fdset.pointer, tval.pointer, block )
182
- fdset
71
+ def decode(stream)
72
+ PDU.decode(stream)
183
73
  end
184
74
 
75
+ class Transport
76
+ MAXPDUSIZE = 0xffff + 1
185
77
 
186
- # @param [Core::Structures::Session] session the snmp session structure
187
- # @param [Hash] options session options with authorization parameters
188
- # @option options [String] :version the snmp protocol version (if < 3, forget the rest)
189
- # @option options [Integer, nil] :security_level the SNMP security level (defaults to authPriv)
190
- # @option options [Symbol, nil] :auth_protocol the authorization protocol (ex: :md5, :sha1)
191
- # @option options [Symbol, nil] :priv_protocol the privacy protocol (ex: :aes, :des)
192
- # @option options [String, nil] :context the authoritative context
193
- # @option options [String] :version the snmp protocol version (defaults to 3, if not 3, you actually don't need the rest)
194
- # @option options [String] :username the username to login with
195
- # @option options [String] :auth_password the authorization password
196
- # @option options [String] :priv_password the privacy password
197
- def session_authorization(session, options)
198
- # we support version 3 by default
199
- session[:version] = case options[:version]
200
- when /v?1/ then Core::Constants::SNMP_VERSION_1
201
- when /v?2c?/ then Core::Constants::SNMP_VERSION_2c
202
- when /v?3/, nil then Core::Constants::SNMP_VERSION_3
78
+ def initialize(host, port, timeout: )
79
+ @socket = UDPSocket.new
80
+ @socket.connect( host, port )
81
+ @timeout = timeout
203
82
  end
204
- return unless session[:version] == Core::Constants::SNMP_VERSION_3
205
83
 
206
-
207
- session[:securityAuthProtoLen] = 10
208
- session[:securityAuthKeyLen] = Core::Constants::USM_AUTH_KU_LEN
209
- session[:securityPrivProtoLen] = 10
210
- session[:securityPrivKeyLen] = Core::Constants::USM_PRIV_KU_LEN
211
-
212
- # Security Authorization
213
- session[:securityLevel] = case options[:security_level]
214
- when /noauth/ then Core::Constants::SNMP_SEC_LEVEL_NOAUTH
215
- when /auth_?no_?priv/ then Core::Constants::SNMP_SEC_LEVEL_AUTHNOPRIV
216
- when /auth_?priv/ then Core::Constants::SNMP_SEC_LEVEL_AUTHPRIV
217
- when Integer
218
- options[:security_level]
219
- else Core::Constants::SNMP_SEC_LEVEL_AUTHPRIV
220
- end
221
-
222
- auth_protocol_oid = case options[:auth_protocol]
223
- when :md5 then MD5OID.new
224
- when :sha1 then SHA1OID.new
225
- when nil then NoAuthOID.new
226
- else raise Error, "#@hostname: #{options[:auth_protocol]} is an unsupported authorization protocol"
84
+ def close
85
+ @socket.close
227
86
  end
228
87
 
229
- # Priv Protocol
230
- priv_protocol_oid = case options[:priv_protocol]
231
- when :aes then AESOID.new
232
- when :des then DESOID.new
233
- when nil then NoPrivOID.new
234
- else raise Error, "#@hostname: #{options[:priv_protocol]} is an unsupported privacy protocol"
88
+ def send(payload)
89
+ write(payload)
90
+ recv
235
91
  end
236
-
237
- user, auth_pass, priv_pass = options.values_at(:username, :auth_password, :priv_password)
238
- auth_protocol_oid.generate_key(session, user, auth_pass)
239
- priv_protocol_oid.generate_key(session, user, priv_pass )
240
92
 
241
- if options[:context]
242
- session[:contextName] = FFI::MemoryPointer.from_string(options[:context])
243
- session[:contextNameLen] = options[:context].length
93
+ def write(payload)
94
+ perform_io do
95
+ @socket.send(payload, 0)
96
+ end
244
97
  end
245
98
 
246
-
247
- end
248
-
249
-
250
- # @param [Hash] options options to open the net-snmp session
251
- # @option options [String] :community the snmp community string (defaults to public)
252
- # @option options [Integer] :timeout number of sec until first timeout
253
- # @option options [Integer] :retries number of retries before timeout
254
- # @return [FFI::Pointer] a pointer to the validated session signature, which will therefore be used in all _sess_ methods from libnetsnmp
255
- def build_signature(options)
256
- # allocate new session
257
- session = Core::Structures::Session.new(nil)
258
- Core::LibSNMP.snmp_sess_init(session.pointer)
259
-
260
- # initialize session
261
- if options[:community]
262
- community = options[:community]
263
- session[:community] = FFI::MemoryPointer.from_string(community)
264
- session[:community_len] = community.length
99
+ def recv(bytesize=MAXPDUSIZE)
100
+ perform_io do
101
+ datagram, _ = @socket.recvfrom_nonblock(bytesize)
102
+ datagram
103
+ end
265
104
  end
266
-
267
- peername = host
268
- unless peername[':']
269
- port = options[:port] || '161'.freeze
270
- peername = "#{peername}:#{port}"
271
- end
272
-
273
- session[:peername] = FFI::MemoryPointer.from_string(peername)
274
-
275
- @timeout = options[:timeout] || 10
276
- session[:timeout] = @timeout * 1000000
277
- session[:retries] = options[:retries] || 5
278
- session_authorization(session, options)
279
- Core::LibSNMP.snmp_sess_open(session.pointer)
280
- end
281
105
 
282
- def fetch_transport
283
- return unless @signature
284
- list = Core::Structures::SessionList.new @signature
285
- return if not list or list.pointer.null?
286
- t = Core::Structures::Transport.new list[:transport]
287
- IO.new(t[:sock])
288
- end
289
-
290
- # @param [Core::Structures::Session] session the snmp session structure
291
- def session_callback
292
- @callback ||= FFI::Function.new(:int, [:int, :pointer, :int, :pointer, :pointer]) do |operation, session, reqid, pdu_ptr, magic|
293
- op = case operation
294
- when Core::Constants::NETSNMP_CALLBACK_OP_RECEIVED_MESSAGE then :success
295
- when Core::Constants::NETSNMP_CALLBACK_OP_TIMED_OUT then :timeout
296
- when Core::Constants::NETSNMP_CALLBACK_OP_SEND_FAILED then :send_failed
297
- when Core::Constants::NETSNMP_CALLBACK_OP_CONNECT then :connect
298
- when Core::Constants::NETSNMP_CALLBACK_OP_DISCONNECT then :disconnect
299
- else :unrecognized_operation
106
+ private
107
+
108
+ def perform_io
109
+ loop do
110
+ begin
111
+ return yield
112
+ rescue IO::WaitReadable
113
+ wait(:wait_readable)
114
+ rescue IO::WaitWritable
115
+ wait(:wait_writable)
116
+ end
300
117
  end
118
+ end
301
119
 
302
-
303
- # TODO: pass exception in case of failure
304
-
305
- response_pdu = ResponsePDU.new(pdu_ptr)
306
- @requests[@reqid] = [op, response_pdu]
307
- if reqid == @reqid
308
- # probably pass the result as a yield from a fiber
309
- op.eql?(:unrecognized_operation) ? 0 : 1
310
- else
311
- # this is happening when user is unknown(????)
312
- #puts "wow, unexpected #{op}.... #{reqid} different than #{@reqid}"
313
- 0
314
- end
120
+ def wait(mode)
121
+ unless @socket.__send__(mode, @timeout)
122
+ raise Timeout::Error, "Timeout after #{@timeout} seconds"
123
+ end
315
124
  end
316
125
 
317
126
  end