# -*- mode: perl -*-
# ============================================================================

package Net::SNMP::MessageProcessing;

# $Id: MessageProcessing.pm,v 2.1 2005/07/20 13:53:07 dtown Rel $

# Object that implements the Message Processing module.

# Copyright (c) 2001-2005 David M. Town <dtown@cpan.org>
# All rights reserved.

# This program is free software; you may redistribute it and/or modify it
# under the same terms as Perl itself.

# ============================================================================

use strict;

use Net::SNMP::PDU qw( 
   :types :msgFlags :securityLevels asn1_itoa SNMP_VERSION_3 TRUE FALSE 
); 

## Version of the Net::SNMP::MessageProcessing module

our $VERSION = v2.0.1;

## Package variables

our $INSTANCE;          # Reference to the Singleton object

our $DEBUG = FALSE;     # Debug flag

our $MSG_HANDLES = {};  # Cached request messages

# [public methods] -----------------------------------------------------------

sub instance
{
   $INSTANCE ||= Net::SNMP::MessageProcessing->_new;
}

sub prepare_outgoing_msg
{
   my ($this, $pdu) = @_;

   # Clear any previous errors
   $this->_error_clear;

   if ((@_ != 2) || (!ref($pdu))) {
      return $this->_error('Missing or invalid PDU reference');
   }

   # We must have a Security Model in order to prepare the message. 
   if (!defined($pdu->security)) {
      return $this->_error('No Security Model defined');
   }

   # Create a new Message
   my ($msg, $error) = Net::SNMP::Message->new(
      -callback   => $pdu->callback,
      -leadingdot => $pdu->leading_dot,
      -requestid  => $pdu->request_id,
      -security   => $pdu->security,
      -translate  => $pdu->translate,
      -transport  => $pdu->transport,
      -version    => $pdu->version
   );
   return $this->_error($error) unless defined($msg);

   if ($pdu->version == SNMP_VERSION_3) {
      
      # ScopedPDU::=SEQUENCE

      if (!defined($pdu->prepare_pdu_scope)) {
         return $this->_error($pdu->error);
      }
      
      # We need to copy the contextEngineID and contextName to the 
      # request message so that they are available for comparision 
      # with the response message.
  
      $msg->context_engine_id($pdu->context_engine_id);   
      $msg->context_name($pdu->context_name);

      # msgGlobalData::=SEQUENCE

      if (!defined($this->_prepare_global_data($pdu, $msg))) {
         return $this->_error;
      }

   }

   # Pass off to the Security Model
   if (!defined($pdu->security->generate_request_msg($pdu, $msg))) {
      return $this->_error($pdu->security->error);
   }

   # Cache and return the new Message
   ($pdu->expect_response) ? ($MSG_HANDLES->{$pdu->msg_id} = $msg) : $msg;
}

sub prepare_data_elements
{
   my ($this, $msg) = @_;

   # Clear any previous errors
   $this->_error_clear;

   if ((@_ != 2) || (!ref($msg))) {
      return $this->_error('Missing or invalid Message reference');
   }

   # message::=SEQUENCE
   return $this->_error($msg->error) unless defined($msg->process(SEQUENCE));

   # version::=INTEGER
   if (!defined($msg->version($msg->process(INTEGER)))) {
      return $this->_error($msg->error); 
   }

   # Find the request message in the cache.  We are assuming this 
   # message is a response to an outstanding request.
 
   my $request;

   if ($msg->version == SNMP_VERSION_3) {

      # msgGlobalData::=SEQUENCE
      if (!defined($this->_process_global_data($msg))) {
         return $this->_error; 
      }

      if (!exists($MSG_HANDLES->{$msg->msg_id})) {
         return $this->_error('Unknown msgID [%d]', $msg->msg_id);
      }

      $request = delete($MSG_HANDLES->{$msg->msg_id}); 

   } else {

      # community::=OCTET STRING
      if (!defined($msg->security_name($msg->process(OCTET_STRING)))) {
         return $this->_error($msg->error); 
      }

      # Cast the Message to a PDU
      if (!defined($msg = Net::SNMP::PDU->new($msg))) {
         return $this->_error('Failed to allocate new PDU');
      }

      # PDU::=SEQUENCE
      if (!defined($msg->process_pdu_sequence)) {
         return $this->_error($msg->error);
      }

      if ($msg->pdu_type != GET_RESPONSE) {
          return $this->_error(
             'Expected %s, but found %s', 
             asn1_itoa(GET_RESPONSE), asn1_itoa($msg->pdu_type)
          ); 
      }
      
      if (!exists($MSG_HANDLES->{$msg->request_id})) {
         return $this->_error('Unknown request-id [%d]', $msg->request_id);
      }

      $request = delete($MSG_HANDLES->{$msg->request_id});

   }

   # Add the callback
   $msg->callback($request->callback);

   # Copy the timeout_id
   $msg->timeout_id($request->timeout_id);

   # Now that we have found the matching request for this response
   # we return a FALSE error instead of undefined so that the error
   # gets propagated back to the user.

   # Compare the Security Models
   if ($msg->msg_security_model != $request->msg_security_model) {
      $this->_error(
         'Unknown incoming msgSecurityModel [%d]', $msg->msg_security_model
      );
      return FALSE;
   }

   # Pass off to the Security Model
   if (!defined($request->security->process_incoming_msg($msg))) {
      $this->_error($request->security->error);
      return FALSE;
   } 

   if ($msg->version == SNMP_VERSION_3) {

      # Adjust our maxMsgSize if necessary
      if ($msg->msg_max_size < $request->max_msg_size) {
         DEBUG_INFO('new maxMsgSize = %d', $msg->msg_max_size);
         if (!defined($request->max_msg_size($msg->msg_max_size))) {
            $this->_error($request->error);
            return FALSE;
         }
      }

      # Cast the Message to a PDU
      if (!defined($msg = Net::SNMP::PDU->new($msg))) {
         $this->_error('Failed to allocate new PDU');
         return FALSE;
      }

      # ScopedPDU::=SEQUENCE
      if (!defined($msg->process_pdu_scope)) {
         $this->_error($msg->error);
         return FALSE;
      }

      # PDU::=SEQUENCE
      if (!defined($msg->process_pdu_sequence)) {
         $this->_error($msg->error);
         return FALSE;
      }

      if ($msg->pdu_type != REPORT) {

         if ($msg->pdu_type != GET_RESPONSE) {
            $this->_error(
               'Expected %s, but found %s',
               asn1_itoa(GET_RESPONSE), asn1_itoa($msg->pdu_type)
            );
            return FALSE;
         }

         # Compare the contextEngineID
         if ($msg->context_engine_id ne $request->context_engine_id) {
            $this->_error(
               'Unknown incoming contextEngineID [%s]',
               unpack('H*', $msg->context_engine_id)
            );
            return FALSE;
         }

         # Compare the contextName
         if ($msg->context_name ne $request->context_name) {
            $this->_error(
               'Unknown incoming contextName [%s]', $msg->context_name
            );
            return FALSE;
         }

         # Check the request-id
         if ($msg->request_id != $request->request_id) {
            $this->_error('Invalid incoming request-id [%d]', $msg->request_id);
            return FALSE;
         }
      }

   }

   # Adjust the "leading dot" and "translate" parameters
   $msg->leading_dot($request->leading_dot);
   $msg->translate($request->translate);

   # VarBindList::=SEQUENCE OF VarBind

   if (!defined($msg->process_var_bind_list)) {
      $this->_error($msg->error);
      return FALSE;
   }

   # Return the PDU
   $msg;
}

sub msg_handle_delete
{
   my ($this, $handle) = @_;

   # Clear any previous errors
   $this->_error_clear;

   return $this->_error('No msgHandle specified') unless (@_ == 2);

   if (!exists($MSG_HANDLES->{$handle})) {
      return $this->_error('Unknown msgHandle [%d]', $handle);
   }

   delete($MSG_HANDLES->{$handle});
}

sub error
{
   $_[0]->[0] || '';
}

sub debug
{
   (@_ == 2) ? $DEBUG = ($_[1]) ? TRUE : FALSE : $DEBUG;
}

# [private methods] ----------------------------------------------------------

sub _new
{
   my ($class) = @_;

   # The constructor is private since we only want one
   # MessageProcessing object.

   bless [ undef ], $class; 
}

sub _prepare_global_data
{
   my ($this, $pdu, $msg) = @_;

   # msgSecurityModel::=INTEGER

   if (!defined(
         $msg->prepare(
            INTEGER, $msg->msg_security_model($pdu->msg_security_model)
         )
      )) 
   {
      return $this->_error($msg->error);
   }

   # msgFlags::=OCTET STRING

   my $security_level = $pdu->security_level;
   my $msg_flags      = MSG_FLAGS_NOAUTHNOPRIV | MSG_FLAGS_REPORTABLE;

   if ($security_level > SECURITY_LEVEL_NOAUTHNOPRIV) {
      $msg_flags |= MSG_FLAGS_AUTH;
      if ($security_level > SECURITY_LEVEL_AUTHNOPRIV) {
         $msg_flags |= MSG_FLAGS_PRIV;
      }
   }

   if (!$pdu->expect_response) {
      $msg_flags &= ~MSG_FLAGS_REPORTABLE;
   }

   if (!defined($msg->prepare(OCTET_STRING, pack('C', $msg_flags)))) {
      $this->_error($msg->error);
   } else {
      $msg->msg_flags($msg_flags);
   }

   # msgMaxSize::=INTEGER

   if (!defined(
         $msg->prepare(INTEGER, $msg->msg_max_size($pdu->max_msg_size))
      ))
   {
      return $this->_error($msg->error);
   }

   # msgID::=INTEGER
   if (!defined($msg->prepare(INTEGER, $msg->msg_id($pdu->msg_id)))) {
      return $this->_error($msg->error);
   }

   # msgGlobalData::=SEQUENCE
   if (!defined($msg->prepare(SEQUENCE))) {
      return $this->_error($msg->error);
   }

   TRUE;
}

sub _process_global_data
{
   my ($this, $msg) = @_;

   # msgGlobalData::=SEQUENCE
   return $this->_error($msg->error) unless defined($msg->process(SEQUENCE));

   # msgID::=INTEGER
   if (!defined($msg->msg_id($msg->process(INTEGER)))) {
      return $this->_error($msg->error);
   }

   # msgMaxSize::=INTEGER
   if (!defined($msg->msg_max_size($msg->process(INTEGER)))) {
      return $this->_error($msg->error);
   }

   # msgFlags::=OCTET STRING

   if (defined(my $msg_flags = $msg->process(OCTET_STRING))) {

      if (CORE::length($msg_flags) != 1) {
         return $this->_error(
            'Invalid msgFlags length [%d octets]', CORE::length($msg_flags)
         );
      }

      $msg->msg_flags($msg_flags = unpack('C', $msg_flags));

      # Validate the msgFlags and derive the securityLevel. 

      my $security_level = SECURITY_LEVEL_NOAUTHNOPRIV;

      if ($msg_flags & MSG_FLAGS_AUTH) {
         $security_level = SECURITY_LEVEL_AUTHNOPRIV;
         if ($msg_flags & MSG_FLAGS_PRIV) {
            $security_level = SECURITY_LEVEL_AUTHPRIV;
         }
      } elsif ($msg_flags & MSG_FLAGS_PRIV) {

         # RFC 3412 - Section 7.2 1d: "If the authFlag is not set
         # and privFlag is set... ...the message is discarded..."
     
         return $this->_error(
            'Invalid incoming msgFlags [0x%02x]', $msg_flags
         );
      }

      # RFC 3412 - Section 7.2 1e: "Any other bits... ...are ignored."
      if ($msg_flags & ~MSG_FLAGS_MASK) {
         DEBUG_INFO('questionable msgFlags [0x%02x]', $msg_flags);
      }

      $msg->security_level($security_level);     

   } else {
      return $this->_error($msg->error);   
   }

   # msgSecurityModel::=INTEGER
   if (!defined($msg->msg_security_model($msg->process(INTEGER)))) {
      return $this->_error($msg->error);
   }

   TRUE;
}

sub _error
{
   my $this = shift;

   if (!defined($this->[0])) {
      $this->[0] = (@_ > 1) ? sprintf(shift(@_), @_) : $_[0];
      if ($this->debug) {
         printf("error: [%d] %s(): %s\n",
            (caller(0))[2], (caller(1))[3], $this->[0]
         );
      }
   }

   return;
}

sub _error_clear
{
   $_[0]->[0] = undef;
}

sub DEBUG_INFO
{
   return unless $DEBUG;

   printf(
      sprintf('debug: [%d] %s(): ', (caller(0))[2], (caller(1))[3]) .
      ((@_ > 1) ? shift(@_) : '%s') . 
      "\n",
      @_
   );

   $DEBUG;
}

# ============================================================================
1; # [end Net::SNMP::MessageProcessing]
