<?PHP

  /**
   * qcEvents - DNS Recordset
   * Copyright (C) 2015 Bernd Holzmueller <bernd@quarxconnect.de>
   * 
   * This program is free software: you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   * 
   * This program 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 General Public License for more details.
   * 
   * You should have received a copy of the GNU General Public License
   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
   **/
  
  class qcEvents_Stream_DNS_Recordset implements IteratorAggregate, ArrayAccess, Countable {
    /* All records on this set */
    private $Records = array ();
    
    // {{{ getIterator
    /**
     * Retrive a new iterator for this recordset
     * 
     * @access public
     * @return ArrayIterator
     **/
    public function getIterator () {
      return new ArrayIterator ($this->Records);
    }
    // }}}
    
    // {{{ offsetExists
    /**
     * Check if a given index exists on our recordset
     * 
     * @param mixed $Index
     * 
     * @access public
     * @return bool
     **/
    public function offsetExists ($Index) {
      return isset ($this->Records [$Index]);
    }
    // }}}
    
    // {{{ offsetGet
    /**
     * Retrive a single record from this set
     * 
     * @param mixed $Index
     * 
     * @access public
     * @return qcEvents_Stream_DNS_Record
     **/
    public function offsetGet ($Index) {
      if (isset ($this->Records [$Index]))
        return $this->Records [$Index];
    }
    // }}}
    
    // {{{ offsetSet
    /**
     * Store a new record on this recrodset
     * 
     * @param mixed $Index
     * @param qcEvents_Stream_DNS_Record $Record
     * 
     * @access public
     * @return void
     **/
    public function offsetSet ($Index, $Record) {
      if (!($Record instanceof qcEvents_Stream_DNS_Record))
        return;
      
      if ($Index === null)
        $this->Records [] = $Record;
      else
        $this->Records [$Index] = $Record;
    }
    // }}}
    
    // {{{ offsetUnset
    /**
     * Remove a record from this set
     * 
     * @param mixed $Index
     * 
     * @access public
     * @return void
     **/
    public function offsetUnset ($Index) {
      unset ($this->Records [$Index]);
    }
    // }}}
    
    // {{{ count
    /**
     * Count all records on this set
     * 
     * @access public
     * @return int
     **/
    public function count () {
      return count ($this->Records);
    }
    // }}}
    
    // {{{ getRecords
    /**
     * Retrive all records (of a given type) from this set
     * 
     * @param int $Type (optional)
     * 
     * @access public
     * @return qcEvents_Stream_DNS_Recordset
     **/
    public function getRecords ($Type = null) {
      // Check if this is a senseless call
      if ($Type === null)
        return $this;
      
      // Create a cloned result
      $Result = clone $this;
      
      // Filter the result
      foreach ($Result as $ID=>$Record)
        if ($Record->getType () != $Type)
          unset ($Result [$ID]);
      
      return $Result;
    }
    // }}}
    
    // {{{ validate
    /**
     * Try to validate the entire resultset
     * 
     * @param array $Keys Array with available DNS-Keys
     * 
     * @access public
     * @return bool
     **/
    public function validate (array $Keys) {
      // Inline data of IANA DNS Root Signing Public Key Certificate
      static $rootCertificate = <<<EOF
-----BEGIN CERTIFICATE-----
MIIDyjCCArKgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBLMQ4wDAYDVQQKEwVJQ0FO
TjEYMBYGA1UEAxMPSUNBTk4gRE5TU0VDIENBMR8wHQYJKoZIhvcNAQkBExBkbnNz
ZWNAaWNhbm4ub3JnMB4XDTE0MDYxMTE4NDMyMFoXDTE3MDYxMDE4NDMyMFowgbMx
DjAMBgNVBAoTBUlDQU5OMQ0wCwYDVQQLEwRJQU5BMTAwLgYDVQQDEydSb290IFpv
bmUgS1NLIDIwMTAtMDYtMTZUMjE6MTk6MjQrMDA6MDAxYDBeBggrBgEEAYdoNRNS
LiBJTiBEUyAxOTAzNiA4IDIgNDlBQUMxMUQ3QjZGNjQ0NjcwMkU1NEExNjA3Mzcx
NjA3QTFBNDE4NTUyMDBGRDJDRTFDRERFMzJGMjRFOEZCNTCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAKgAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQ
bSEW0O8gcCjFFVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJR
kxoXbfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaDX6RS
6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpzW5hOA2hzCTMj
JPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relSQageu+ipAdTTJ25AsRTA
oub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulqQxA+Uk1ihz0CAwEAAaNQME4wDAYD
VR0TAQH/BAIwADAfBgNVHSMEGDAWgBSPskJpw53kPPoTuf/ywKTv2A/oIjAdBgNV
HQ4EFgQUQRqS+htWdh5iK3HNGv27Q5lfCckwDQYJKoZIhvcNAQELBQADggEBALsL
hwY+hXsV6i6UI1ijqrBDP200JYHYIIJn4tTN9kpG+q7feQ52XhzydV3iPRgwV14L
FqHc26ibnT3Afw+bHnvV7vrDOdvejfaw7cE7OGcJK3twQyvQ/XX2EWVr9qk7BE74
CeqVP0ghSY87juF5MGV2Zdz/+IhMPrjFY2kQea615Qkl/TD1x3PxJNoq3YaXRDZu
skVUjpepBhLLECq8Ewi2w03T8xxj4e6/1AAIBh8POpKVoBS1njSqlAz6zK2ZaWmS
OQQBuGpn7e28dDvCeuagqV3qdwdfkbDuLb5DYUobGu2UsnjJ8Vp6E+evCU95p7lJ
N0M89yQUKiyCrj9/lpg=
-----END CERTIFICATE-----
EOF;

      // Make sure DNSSEC-Support is available
      if (!class_exists ('qcEvents_Stream_DNS_Record_RRSIG')) {
        trigger_error ('DNSSEC-Support unavailable');
        
        return false;
      }
      
      // Isolate all signatures
      $Signatures = $this->getRecords (qcEvents_Stream_DNS_Record_RRSIG::DEFAULT_TYPE);
      
      if (count ($Signatures) == 0)
        return null;
      
      // Validate each signature
      foreach ($Signatures as $Signature) {
        // Retrive all Records for that type from this resultset
        $Records = $this->getRecords ($Type = $Signature->getCoveredType ());
        
        if (count ($Records) == 0) {
          trigger_error ('RRSIG present without records to validate');
          
          continue;
        }
        
        // Find a matching key for this
        $sigKeys = array ();
        
        foreach ($Keys as $Key)
          if (($Key instanceof qcEvents_Stream_DNS_Record_DNSKEY) && ($Key->getKeyTag () == $Signature->getKeyTag ()))
            $sigKeys [] = $Key;
        
        if (count ($sigKeys) == 0) {
          foreach ($Records as $Record)
            if (strlen ($Record->getLabel ()) == 1) {
              require_once ('X509/Certificate.php');
              
              $sigKeys [] = @x509_Certificate::fromPEM ($rootCertificate);
              
              break;
            }
          
          if (count ($sigKeys) == 0) {
            trigger_error ('No matching DNS-Key found');
            
            return false;
          }
        }
        
        // Make sure the records are ordered correctly
        $Records->orderCanonical ();
        
        // Build the verify-buffer
        $Labels = array ();
        $vBuffer =
          substr ($Signature->buildPayload (0, $Labels), 0, 18) .
          qcEvents_Stream_DNS_Message::setLabel ($Signature->getSigner ());
        
        $Class = $Signature->getClass ();
        $TTL = $Signature->getOriginalTTL ();
        $Fixed =
          chr (($Type & 0xFF00) >> 8) . chr ($Type & 0xFF) .
          chr (($Class & 0xFF00) >> 8) . chr ($Class & 0xFF) .
          chr (($TTL & 0xFF000000) >> 24) . chr (($TTL & 0x00FF0000) >> 16) .
          chr (($TTL & 0x0000FF00) >>  8) . chr  ($TTL & 0x000000FF);
        
        foreach ($Records as $Record) {
          $Labels = array ();
          $Length = strlen ($Payload = $Record->getPayload (0, $Labels));
          $vBuffer .=
            qcEvents_Stream_DNS_Message::setLabel ($Record->getLabel ()) .
            $Fixed .
            chr (($Length & 0xFF00) >> 8) . chr ($Length & 0xFF) .
            $Payload;
        }
        
        // Try to verify the buffer with all available keys
        foreach ($sigKeys as $Key)
          if ((($Key instanceof qcEvents_Stream_DNS_Record_DNSKEY) && $Key->verifySignature ($vBuffer, $Signature)) ||
              (($Key instanceof X509_Certificate) && $Key->verifySignature ($vBuffer, $Signature->getSignature (), $Signature->getAlgorithmObjectID ())))
            continue (2);
        
        return false;
      }
      
      return true;
    }
    // }}}
    
    // {{{ orderCanonical
    /**
     * Order records on this set in canonical order
     * 
     * @access public
     * @return void
     **/
    public function orderCanonical () {
      usort ($this->Records, function (qcEvents_Stream_DNS_Record $Left, qcEvents_Stream_DNS_Record $Right) {
        // Compare labels of records
        $lLen = count ($lLabel = $Left->getLabel ());
        $rLen = count ($rLabel = $Right->getLabel ());
        
        if (($lLen == 0) && ($rLen > 0))
          return -1;
        
        elseif (($rLen == 0) && ($lLen > 0))
          return 1;
        
        while (($lLen-- > 0) && ($rLen-- > 0)) {
          if (($r = strcasecmp ($lLabel [$lLen], $rLabel [$rLen])) != 0)
            return $r;
        }
        
        // Compare the type
        if (($d = ($Left->getType () - $Right->getType ())) != 0)
          return $d;
        
        // Compare the class
        if (($d = ($Left->getClass () - $Right->getClass ())) != 0)
          return $d;
        
        return strcmp ($Left->getPayload (), $Right->getPayload ());
      });
    }
    // }}}
  }

?>