<?php
/*
   phpsnmp - a PHP SNMP library
   Copyright (C) 2004 David Eder <david@eder,us>

   Based on snmp - a Python SNMP library
   Copyright (C) 2003 Unicity Pty Ltd <libsnmp@unicity.com.au>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with this library; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/


function hexdump($string)
{
  $ret = '';
  for($i = 0; $i < strlen($string); $i++)
    $ret .= str_pad(dechex(ord($string{$i})), 2, '0', STR_PAD_LEFT) . ' ';
  $ret .= '[';
  for($i = 0; $i < strlen($string); $i++)
  {
    $o = ord($string{$i});
    if($o < 32 || $o > 127) $ret .= '.';
    else $ret .= chr($o);
  }
  return $ret . "]\n";
}

function hexbin($hex) // convert a hex string to raw data
{
  $ret = '';
  foreach(explode(' ', $hex) as $c)
    $ret .= chr(hexdec($c));
  return $ret;
}

define('ASN_TAG_CLASS_UNIVERSAL',	0x00);
define('ASN_TAG_CLASS_APPLICATION',	0x40);
define('ASN_TAG_CLASS_CONTEXT',		0x80);
define('ASN_TAG_CLASS_PRIVATE',		0xC0);

define('ASN_TAG_FORMAT_PRIMITIVE',	0x00);
define('ASN_TAG_FORMAT_CONSTRUCTED',	0x20);

define('ASN_TAG_INTEGER',		0x02);
define('ASN_TAG_OCTETSTRING',		0x04);
define('ASN_TAG_NULL',			0x05);
define('ASN_TAG_OBJECTID',		0x06);
define('ASN_TAG_SEQUENCE',		0x10);

define('ASN_TAG_IPADDRESS',		0x00);
define('ASN_TAG_COUNTER',		0x01);
define('ASN_TAG_GUAGE',			0x02);
define('ASN_TAG_TIMETICKS',		0x03);
define('ASN_TAG_OPAQUE',		0x04);

$ASN_TAG_DICT[0x02] = 'rfc1155_Integer';
$ASN_TAG_DICT[0x04] = 'rfc1155_OctetString';
$ASN_TAG_DICT[0x05] = 'rfc1155_Null';
$ASN_TAG_DICT[0x06] = 'rfc1155_ObjectID';
$ASN_TAG_DICT[0x30] = 'rfc1155_Sequence';
$ASN_TAG_DICT[0x40] = 'rfc1155_IPAddress';
$ASN_TAG_DICT[0x41] = 'rfc1155_Counter';
$ASN_TAG_DICT[0x42] = 'rfc1155_Guage';
$ASN_TAG_DICT[0x43] = 'rfc1155_TimeTicks';
$ASN_TAG_DICT[0x44] = 'rfc1155_Opaque';

class rfc1155_Asn1Object
{
  /* Base class for all Asn1Objects. This is only intended to support a specific subset of ASN1 stuff as defined by the RFCs to
     keep things as simple as possible. */

  var $asnTagClass = ASN_TAG_CLASS_UNIVERSAL;
  var $asnTagFormat= ASN_TAG_FORMAT_PRIMITIVE;
  var $asnTagNumber = NULL;

  var $value = NULL;

  function rfc1155_Asn1Object(){}

 /**
  * encode() this Asn1Object using BER
  */
  function encode()
  {
    $contents = $this->encodeContents();
    return $this->encodeIdentifier() . $this->encodeLength(strlen($contents)) . $contents;
  }

 /**
  * Decode a BER tag field, returning the tag and the remainder of the stream
  */
  function decodeTag($stream)
  {
    $tag = ord($stream{0});
    $n = 1;
    if(($tag & 0x1F) == 0x1F)
    {
      /* A large tag is encoded using concatenated 7-bit values over the following octets, ignoring the initial 5 bits
         in the first octet.  The 8th bit represents a follow-on. */
      $tag = 0;
      do
      {
        $byte = ord($stream{$n});
        $tag = $tag * 128 + ($byte & 0x7F);
        $n += 1;
      } while($byte & 0x80);
    }
    return array($tag, substr($stream, $n));
  }

 /**
  * Decode a BER length field, returing the length and the remainder of the stream
  */
  function decodeLength($stream)
  {
    $length = ord($stream{0});
    $i = 1;
    if($length & 0x80)
    {
      // Multi-Octet length encoding.  The first octet represents the run-length (the number of octets used to build the length)
      $run = $length & 0x7F;
      $length = 0;
      for($i = 1; $i <= $run; $i++)
        $length = $length * 256 + ord($stream{$i});
    }
    return array($length, substr($stream, $i));
  }

 /**
  * decode() an octet stream into a sequence of Asn1Objects
  */
  function decode($stream)
  {
    /* This method should be overridden by subclasses to define how to decode one of themselves from a fixed length stream.  This
       general case method looks at the identifier at the beginning of a stream of octets and uses the appropriate decode() method
       of that known object.  Attempts to decode() an unknown object type result in an error. */
    if(!is_string($stream))
    {
      trigger_error('stream should be of a string, not a ' . gettype($stream), E_USER_WARNING);
      return array();
    }

    $objects = array();
    while(strlen($stream) > 0)
    {
      list($tag, $stream) = $this->decodeTag($stream);
      list($length, $stream) = $this->decodeLength($stream);

      $objectData = substr($stream, 0, $length);
      $stream = substr($stream, $length);

      global $ASN_TAG_DICT;
      if(isset($ASN_TAG_DICT[$tag]))
      {
        $decoder = $ASN_TAG_DICT[$tag];
        $o = new $decoder();
        $objects[] =  $o->decodeContents($objectData);
      }
      else
        trigger_error("Unknown ASN.1 Type 0x" . dechex($tag), E_USER_WARNING);
    }
    return $objects;
  }

 /**
  * encodeContents should be overridden by subclasses to encode the contents of a particular type
  */
  function encodeContents()
  {
    trigger_error('encodeContents not implemented', E_USER_ERROR);
  }

 /**
  * encodeIdentifier() returns encoded identifier octets for this object.  Section 6.3 of ITU-T-X.209
  */
  function encodeIdentifier()
  {
    if($this->asnTagNumber < 0x1F)
    {
      return chr($this->asnTagClass | $this->asnTagFormat | $this->asnTagNumber);
    }
    else
    {
      /* Encode each number of the asnTagNumber from 31 upwards as a sequence of 7-bit numbers with bit 8 set to 1 for
          all but the last octet. Bit 8 set to 0 signifies the last octet of the Identifier octets */

      // encode the first octet
      $resultlist = array();
      $resultlist[] = chr($this->asnTagClass | $this->asnTagFormat | 0x1F);
            
      // encode each subsequent octet
      $integer = $this->asnTagNumber;
      while($integer != -1)
      {
        list($integer, $remainder) = $this->divide_with_remainder($integer, 256);
        $resultlist[] = chr($remainder);
      }
      return join('', $resultlist);
    }
  }

 /**
  * encodeLength() takes the length of the contents and produces the encoding for that length.  Section 6.3 of ITU-T-X.209
  */
  function encodeLength($length)
  {
    if($length < 127)
      return chr($length & 0xff);
    else
    {
      /* Long form - Octet one is the number of octets used to encode the length It has bit 8 set to 1 and the remaining 7 bits are
         used to encode the number of octets used to encode the length Each subsequent octet uses all 8 bits to encode the length */
      $resultlist = array();
      $numOctets = 0;
      while($length > 0)
      {
        list($length, $remainder) = $this->divide_with_remainder($length, 256);
        array_unshift($resultlist, chr($remainder));
        $numOctets++;
      }

      // Add a 1 to the front of the octet
      $numOctets = $numOctets | 0x80;
      array_unshift($resultlist, chr($numOctets & 0xff));

      return join('', $resultlist);
    }
  }

  function encodeEndOfContents()
  {
     return "\000\000";
  }

  function divide_with_remainder($numerator, $denominator)
  {
    $q = intval($numerator / $denominator);
    $r = intval(($numerator / $denominator - $q) * $denominator);
    return array($q, $r);
  }
}

class rfc1155_Integer extends rfc1155_Asn1Object // An ASN.1 Integer type
{
  var $MINVAL = -2147483648;
  var $MAXVAL =  2147483647;

  function rfc1155_Integer($value=0)
  {
    $this->asnTagClass = ASN_TAG_CLASS_UNIVERSAL;
    $this->asnTagNumber = ASN_TAG_INTEGER;
    
    if($value < $this->MINVAL || $value > $this->MAXVAL)
    {
      trigger_error("Integer value of $value is out of bounds", E_USER_WARNING);
    }
    $this->value = $value;
  }

  function toString()
  {
    return $this->value;
  }

  function encodeContents()
  {
    // We handle two special cases otherwise we handle positive and negative numbers independently

    $integer = $this->value;

    if($integer == 0)
      return "\000";
    elseif($integer == -1)
      return "\377";
    elseif($integer > 0)
    {
      $result = array();
      while($integer != 0)
      {
        list($integer, $remainder) = $this->divide_with_remainder($integer, 256);
        array_unshift($result, $remainder);
      }
      if($result[0] & 0x80)
        array_unshift($result, 0);
      return join('', array_map('chr', $result));
    }
    else
    {
      $result = array();
      while($integer != -1)
      {
        list($integer, $remainder) = $this->divide_with_remainder($integer, 256);
        array_unshift($result, $remainder);
      }
      if($result[0] & 0x80 != 0x80)
        array_unshift($result, 0);
      return join('', array_map('chr', result));
    }
  }

  function decodeContents($stream) // Decode some input octet stream into a signed ASN.1 integer
  {
    $this->value = 0;
    $byte = ord($stream{0});
    if(($byte & 0x80) == 0x80)
    {
      $negbit = 0x80;
      $this->value = $byte & 0x7f;
      for($i = 1; $i < strlen($stream); $i++)
      {
        $negbit *= 256;
        $this->value = $this->value * 256 + ord($stream{$i});
      }
      $this->value = $this->value - $negbit;
    }
    else
    {
      $this->value = $byte;
      for($i = 1; $i < strlen($stream); $i++)
        $this->value = $this->value * 256 + ord($stream{$i});
    }
    return $this;
  }
}

class rfc1155_OctetString extends rfc1155_Asn1Object  // An ASN.1 Octet String type
{
  function rfc1155_OctetString($value='')
  {
    $this->asnTagClass = ASN_TAG_CLASS_UNIVERSAL;
    $this->asnTagNumber = ASN_TAG_OCTETSTRING;
    $this->value = $value;
  }

  function is_printable()
  {
    for($i = strlen($this->value) - 1; $i >= 0; $i--)
      if(ord($this->value{$i}) < 16 || ord($this->value{$i}) > 127)
        return false;
    return true;
  }

  function toHex()
  {
    $ret = '';
    for($i = 0; $i < strlen($this->value); $i++)
      $ret .= str_pad(dechex(ord($this->value{$i})), 2, '0', STR_PAD_LEFT) . ' ';
    return trim($ret);
  }

  function toString()
  {
    if($this->is_printable())
      return $this->value;
    else
      return $this->toHex();
  }

  function encodeContents()
  {
    // An OctetString is already encoded. Whee!
    return $this->value;
  }

  function decodeContents($stream)
  {
    // An OctetString is already decoded. Whee!
    $this->value = $stream;
    return $this;
  }
}

class rfc1155_ObjectID extends rfc1155_Asn1Object // An ASN.1 Object Identifier type
{
  function rfc1155_ObjectID($value=NULL)
  {
    // Create an ObjectID - value is a list of subids as a string or list
    $this->asnTagClass = ASN_TAG_CLASS_UNIVERSAL;
    $this->asnTagFormat = ASN_TAG_FORMAT_PRIMITIVE;
    $this->asnTagNumber = ASN_TAG_OBJECTID;

    if(is_string($value))
    {
      $this->value = array();
      foreach(explode('.', ltrim($value, '.')) as $subid)
      {
        if($subid < 0 || $subid > 0x7FFFFFFF)
          trigger_error('SubID our of range', E_USER_WARNING);
        $this->value[] = $subid;
      }
    }
    elseif(is_array($value))
      $this->value = $value;
    elseif(is_null($value))
      $this->value = array();
    else
      trigger_error('unknown type passed as OID', E_USER_WARNING);
  }

  function toString()
  {
    return '.' . join('.', $this->value);
  }

 /**
  * encode() an objectID into an octet stream
  */
  function encodeContents()
  {
    $result = array();
    $idlist = $this->value;

    // Do the bit with the first 2 subids; section 22.4 of X.209
    $idlist = array_reverse($idlist);
    $subid1 = (array_pop($idlist) * 40) + array_pop($idlist);
    $idlist = array_reverse($idlist);
    array_unshift($idlist, $subid1);
    foreach($idlist as $subid)
    {
      if($subid < 128)
        $result[] = chr($subid & 0x7f);
      else
      {
        list($subid, $remainder) = $this->divide_with_remainder($subid, 128);
        $r = array(chr($remainder));
        while($subid > 0)
        {
          list($subid, $remainder) = $this->divide_with_remainder($subid, 128);
          $r[] = chr(0x80 | $remainder);
        }
        for($i = count($r) - 1; $i >=0; $i--) $result[] = $r[$i];
      }
    }
    return join('', $result);
  }

 /**
  * decode() a stream into an ObjectID
  */
  function decodeContents($stream)
  {
    $this->value = array();

    if(strlen($stream) == 0)
    {
      trigger_error('Stream of zero length in ' . get_class($this), E_USER_WARNING);
      return $this;
    }
        
    // Do the funky decode of the first octet
    if(ord($stream{0}) < 128)
    {
      $this->value[] = intval(ord($stream{0}) / 40);
      $this->value[] = ord($stream{0}) % 40;
    }
    else
    {
       /* I haven't bothered putting in the convoluted logic here because the highest likely assignment for the first
          octet is 83 according to Annex B of X.208 Those X.209 does give as an example 2.100.3, which is kinda stupid.
          Actually, a lot of the space-saving encodings, like this first octet, are a real PITA later on.  So yeah,
          stuff it, we'll just raise an exception. */

      trigger_error('NotImplementedError: First octet is > 128! Unsupported oid detected', E_USER_ERROR);
      return $this;
    }

    // Decode the rest of the octets
    $n = 1;        
    while($n < strlen($stream))
    {
      $subid = ord($stream{$n});
      $n += 1;

      // If bit 8 is not set, this is the last octet of this subid
      // If bit 8 is set, the subid spans this octet and the ones afterwards, up until bit 8 isn't set.
      if(($subid & 0x80) == 0x80)
      {
        $val = $subid & 0x7f;
        while(($subid & 0x80) == 0x80)
        {
          $subid = ord($stream{$n});
          $n += 1;
          $val = $val * 128 + ($subid & 0x7f);
        }
        $this->value[] = $val;
      }
      else
      {
        $this->value[] = $subid;
      }
    }
    return $this;
  }
}

class rfc1155_Null extends rfc1155_Asn1Object // An ASN.1 Object Identifier type
{
  function rfc1155_Null()
  {
    $this->asnTagClass = ASN_TAG_CLASS_UNIVERSAL;
    $this->asnTagFormat = ASN_TAG_FORMAT_PRIMITIVE;
    $this->asnTagNumber = ASN_TAG_NULL;
  }

  function toString()
  {
    return '';
  }

  function encodeContents()
  {
    return '';
  }

  function decodeContents($stream)
  {
    if(strlen($stream) != 0)
      trigger_error('Input stream too long for ' . get_class($this), E_USER_WARNING);
    return $this;
  }
}

class rfc1155_Sequence extends rfc1155_Asn1Object
{
  /* A Sequence is basically a list of name, value pairs with the name being an object Type and the value being an instance of an
     Asn1Object of that Type. */

  function rfc1155_Sequence($value=array())
  {
    $this->asnTagClass = ASN_TAG_CLASS_UNIVERSAL;
    $this->asnTagFormat = ASN_TAG_FORMAT_CONSTRUCTED;
    $this->asnTagNumber = ASN_TAG_SEQUENCE;
    $this->value = $value;
  }

  function encodeContents()
  {
    // To encode a Sequence, we simply encode() each sub-object in turn.
    $resultlist = array();
    foreach($this->value as $elem)
      $resultlist[] = $elem->encode();
    return join('', $resultlist);
  }

 /**
  * decode a sequence of objects
  */
  function decodeContents($stream)
  {
    $objectList = $this->decode($stream);
    $this->value = $objectList;
    //return Sequence(objectList)
    return $this;
  }
}

class rfc1155_SequenceOf extends rfc1155_Sequence
{
  // A SequenceOf is a special kind of sequence that places a constraint on the kind of objects it can contain.  It is variable in length.

  var $componentType;

  function rfc1155_SequenceOf($componentType='asn1Object', $value=array())
  {
    parent::rfc1155_Sequence($value);
    $this->asnTagClass = ASN_TAG_CLASS_UNIVERSAL;
    $this->asnTagFormat = ASN_TAG_FORMAT_CONSTRUCTED;
    $this->asnTagNumber = ASN_TAG_SEQUENCE;

    $this->componentType = $componentType;

    // Add each item in the list to ourselves, which automatically checks each one to ensure it is of the correct type.
        
    $this->value = array();
    foreach($value as $item)
      $this->append($item);
  }

  function append($value)
  {
    if(!is_a($value, $this->componentType))
      trigger_error(get_class($this) . ' cannot contain components of type: ' . get_class($value), E_USER_WARNING);
    $this->value[] = $value;
  }
}

class rfc1155_IPAddress extends rfc1155_OctetString
{
  // An IpAddress is a special type of OctetString.  It represents a 32-bit internet address as an OctetString of length 4, in network byte order.

  function rfc1155_IPAddress($value=NULL)
  {
    parent::rfc1155_OctetString($value);
    $this->asnTagClass = ASN_TAG_CLASS_APPLICATION;
    $this->asnTagFormat = ASN_TAG_FORMAT_PRIMITIVE;
    $this->asnTagNumber = ASN_TAG_IPADDRESS;

    if(is_numeric($value))
      $value = long2ip($value);

    if(is_string($value))
      $value = explode('.', $value);

    if(is_array($value))
    {
      if(count($value) != 4)
        trigger_error('IPAddress must be of length 4', E_USER_WARNING);
      foreach($value as $item)
        $this->value += chr(int($item));
    }
    else
    {
      $this->value = '';
    }
  }

  function toString()
  {
    return ord($this->value{0}) . '.' . ord($this->value{1}) . '.' . ord($this->value{2}) . '.' . ord($this->value{3});
  }

  function decodeContents($stream)
  {
    // An IPAddress is already decoded. Whee!

    $this->value = $stream;
    return $this;
  }
}

class rfc1155_NetworkAddress extends rfc1155_IPAddress
{
  var $name;

  // A Network Address is a CHOICE with only one possible value: internet
  function rfc1155_NetworkAddress($value)
  {
    parent::rfc1155_IPAddress($value);
    $this->name = 'internet';
  }
}

class rfc1155_Counter extends rfc1155_Integer
{
  // A counter starts at zero and keeps going to a maximum integer value of 2^32-1 where it wraps back to zero.

  function rfc1155_Counter($value=0)
  {    
    $this->MINVAL = 0;
    $this->MAXVAL = 4294967295;
    parent::rfc1155_Integer($value);
    $this->asnTagClass = ASN_TAG_CLASS_APPLICATION;
    $this->asnTagFormat = ASN_TAG_FORMAT_PRIMITIVE;
    $this->asnTagNumber = ASN_TAG_COUNTER;
  }

  function decodeContents($stream)
  {
    parent::decodeContents($stream);

    /* Some agents encode Counters incorrectly (hello Solaris) as a negative number.  I'm assuming most SNMP libraries don't
       notice the problem because the are written in C and cast the result to an unsigned int - problem solved (if
       accidentally).  This ugly hack on their behalf flips the value over to the positive world. */

    $this->value = abs($this->value);
    return $this;
  }
}

class rfc1155_Guage extends rfc1155_Integer
{
  // A Guage is a non negative integer.  It may increase or decrease. It latches at a maximum value.

  function rfc1155_Guage($value=0)
  {
    $this->MINVAL = 0;
    $this->MAXVAL = 4294967295;
    parent::rfc1155_Integer($value);
    $this->asnTagClass = ASN_TAG_CLASS_APPLICATION;
    $this->asnTagFormat = ASN_TAG_FORMAT_PRIMITIVE;
    $this->asnTagNumber = ASN_TAG_GUAGE;
  }
}    

class rfc1155_TimeTicks extends rfc1155_Integer
{
  // TimeTicks is the number of hundredths of a second since an epoch, specified at object creation time

  var $epoch;

  function rfc1155_TimeTicks($value=0, $epoch=NULL)
  {
    $this->MINVAL = 0;
    $this->MAXVAL = 4294967295;
    parent::rfc1155_Integer($value);
    $this->asnTagClass = ASN_TAG_CLASS_APPLICATION;
    $this->asnTagFormat = ASN_TAG_FORMAT_PRIMITIVE;
    $this->asnTagNumber = ASN_TAG_TIMETICKS;
    $this->epoch = $epoch;
  }
}

class rfc1155_Opaque extends rfc1155_OctetString
{
  /* Opaque is a fun type that allows you to pass arbitrary ASN.1 encoded stuff in an object. The value is some ASN.1 syntax encoded
     using BER which this object encodes as an OctetString.  We don't do any decoding of this object because we don't have to, and that
     makes this all much quicker. */
  function rfc1155_Opaque($value)
  {
    parent::rfc1155_OctetString($value);
  }
}
?>
