netsnmp 0.0.2 → 0.1.0

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