Revision: 2264
Author: olavmrk
Date: Wed Apr 21 05:56:46 2010
Log: aggregator2-module
http://code.google.com/p/simplesamlphp/source/detail?r=2264

Added:
 /trunk/modules/aggregator2
 /trunk/modules/aggregator2/bin
 /trunk/modules/aggregator2/bin/update.php
 /trunk/modules/aggregator2/config-templates
 /trunk/modules/aggregator2/config-templates/module_aggregator2.php
 /trunk/modules/aggregator2/default-disable
 /trunk/modules/aggregator2/docs
 /trunk/modules/aggregator2/docs/aggregator2.txt
 /trunk/modules/aggregator2/hooks
 /trunk/modules/aggregator2/hooks/hook_cron.php
 /trunk/modules/aggregator2/lib
 /trunk/modules/aggregator2/lib/Aggregator.php
 /trunk/modules/aggregator2/lib/EntitySource.php
 /trunk/modules/aggregator2/www
 /trunk/modules/aggregator2/www/get.php

=======================================
--- /dev/null
+++ /trunk/modules/aggregator2/bin/update.php   Wed Apr 21 05:56:46 2010
@@ -0,0 +1,16 @@
+#!/usr/bin/env php
+<?php
+
+require_once(dirname(dirname(dirname(dirname(__FILE__)))) . 
'/lib/_autoload.php');
+
+$name = basename($argv[0]);
+
+if ($argc < 2) {
+       fprintf(STDERR, "$name: Missing id of aggregator.\n");
+       exit(1);
+}
+
+$id = $argv[1];
+
+$aggregator = sspmod_aggregator2_Aggregator::getAggregator($id);
+$aggregator->updateCache();
=======================================
--- /dev/null
+++ /trunk/modules/aggregator2/config-templates/module_aggregator2.php Wed Apr 21 05:56:46 2010
@@ -0,0 +1,113 @@
+<?php
+
+/* This is the configuration file for the aggregator2-module. */
+$config = array(
+
+       /*
+        * 'example' will be one set of aggregated metadata.
+        * The aggregated metadata can be retrieved from:
+        *   https://.../simplesaml/module.php/aggregator2/get.php?id=example
+        */
+       'example' => array(
+
+ /* 'sources' is an array with the places we want to fetch metadata from. */
+               'sources' => array(
+                       /* Metadata validated by the https-certificate of the 
server. */
+                       array(
+                               /* The URL we should fetch the metadata from. */
+                               'url' => 'https://sp.example.org/metadata.xml',
+
+                               /*
+                                * To enable validation of the 
https-certificate, we must
+                                * specify a file with valid CA certificates.
+                                *
+                                * This can be an absolute path, or a path 
relative to the
+                                * cert-directory.
+                                */
+                               'ssl.cafile' => 
'/etc/ssl/certs/ca-certificates.crt',
+                       ),
+
+                       /* Metadata validated by its signature. */
+                       array(
+                               /* The URL we should fetch the metadata from. */
+                               'url' => 'http://idp.example.org/metadata.xml',
+
+                               /*
+                                * To verify the signature in the metadata, we 
must specify
+                                * a certificate that should be used. Note: 
This cannot
+                                * be a CA certificate.
+                                *
+                                * This can be an absolute path, or a path 
relative to the
+                                * cert-directory.
+                                */
+                               'cert' => 'idp.example.org.crt',
+                       ),
+
+                       /* Metadata from a file. */
+                       array(
+                               'url' => '/var/simplesaml/somemetadata.xml',
+                       ),
+
+               ),
+
+               /*
+                * Update this metadata during this cron tag.
+                *
+                * For this option to work, you must configure the cron-module,
+                * and also add a cache directory.
+                *
+                * This option is optional. If cron is not configured, the 
metadata
+                * caches will be updated when receiving requests for metadata.
+                */
+               'cron.tag' => 'hourly',
+
+               /*
+                * The directory we will store downloaded and generated 
metadata.
+                * This directory must be writeable by the web-server.
+                *
+                * This option is optional, but if unspecified, every request 
for the
+                * aggregated metadata will result in the aggregator fetching 
and
+                * parsing all metadata sources.
+                */
+               'cache.directory' => '/var/cache/simplesaml-aggregator2',
+
+               /*
+ * This is the number of seconds we will cache the metadata file we generate. + * This should be a longer time than the interval between each time the cron
+                * job is executed.
+                *
+ * This option is optional. If unspecified, the metadata will be generated
+                * on every request.
+                */
+               'cache.generated' => 24*60*60,
+
+               /*
+ * The generated metadata will have a validUntil set to the time it is generated
+                * plus this number of seconds.
+                */
+               'valid.length' => 7*24*60*60,
+
+               /*
+                * The private key we should use to sign the metadata, in 
pem-format.
+                *
+ * This is optional. If it is not specified, the metadata will not be signed.
+                */
+               'sign.privatekey' => 'metadata.pem',
+
+               /*
+                * The password for the private key.
+                *
+                * Optional, the private key is assumed to be unencrypted if 
this option
+                * isn't set.
+                */
+               'sign.privatekey_pass' => 'secret',
+
+               /*
+                * The certificate that corresponds to the private key.
+                *
+ * If specified, the certificate will be included in the signature in the metadata.
+                */
+               'sign.certificate' => 'metadata.crt',
+       ),
+
+);
=======================================
--- /dev/null
+++ /trunk/modules/aggregator2/default-disable  Wed Apr 21 05:56:46 2010
@@ -0,0 +1,3 @@
+This file indicates that the default state of this module
+is disabled. To enable, create a file named enable in the
+same directory as this file.
=======================================
--- /dev/null
+++ /trunk/modules/aggregator2/docs/aggregator2.txt     Wed Apr 21 05:56:46 2010
@@ -0,0 +1,113 @@
+aggregator2 Module
+==================
+
+This is an experimental module for aggregating metadata.
+It is designed to preserve most of the common metadata items, and also attempt to preserve unknown elements.
+
+*Note*: This aggregator only works on XML metadata, and does its work independently of the of other parts of simpleSAMLphp, such as the `metarefresh` module.
+
+
+Configuration
+-------------
+
+This module is configured through the `config/module_aggregator2.php` configuration file.
+An example file is available in `modules/aggregator2/config-templates/`:
+
+    cd /var/simplesaml
+    cp modules/aggregator2/config-templates/module_aggregator2.php config/
+
+The configuration file contains one or more aggregators in the configuration array. +The index in the configuration array gives the identifier of the aggregator.
+
+
+### Aggregator entry configuration
+
+The aggregator can be configured with the following options:
+
+`sources`
+:   Array which describes which metadata we should download.
+
+`cron.tag`
+:   Can be used to periodically run an update.
+    Only useful when you have enabled caching of metadata.
+
+`cache.directory`
+: A path to a directory where the aggregator will cache downloaded and generated metadata.
+    This directory must be writeable by the webserver.
+
+`cache.generated`
+:   The number of seconds generated metadata should be cached.
+    If this option is unset, the generated metadata will not be cached.
+
+`valid.length`
+:   The number of seconds the generated metadata should be valid.
+    This is used to set the validUntil attribute on the generated metadata.
+    The default is one week.
+
+: *Note*: The `cache.generated` option must be smaller than this option, otherwise you will end up returning outdated metadata.
+
+`ssl.cafile`
+: This option enables validation of the server certificate when fetching metadata over https. + It must be set to a path to a PEM-file which contains one or more valid CA certificates. + The path can be absolute, or it can be relative to the `cert`-directory.
+
+:   *Note*: This option can be overridden for each metadata source.
+
+`sign.privatekey`
+: The private key that should be used to sign the metadata, in PEM format. + The path to the private key can be absolute, or it can be relative to the `cert`-directory.
+
+`sign.privatekey_pass`
+:   The password for the private key.
+    If this option is unset, the private key is assumed to be unencrypted.
+
+`sign.certificate`
+: The certificate which contains the public key corresponding to the private key, in PEM format.
+    This certificate is included in the generated metadata.
+ The path to the certificate can be absolute, or it can be relative to the `cert`-directory.
+
+
+### Aggregator source configuration
+
+`url`
+:   The URL the metadata should be fetched from.
+
+`ssl.cafile`
+: This option enables validation of the server certificate when fetching metadata over https. + It must be the path to a PEM-file which contains one or more valid CA certificates. + The path can be absolute, or it can be relative to the `cert`-directory.
+
+:   *Note*: This option overrides the aggregator option.
+
+`cert`
+:   Check the signature on the metadata against the specified certificate.
+ The path to the certificate can be absolute, or it can be relative to the `cert`-directory.
+
+:   *Note*: This can not be a CA certificate.
+    Validation against a CA certificate is not supported.
+
+
+Retrieving aggregated metadata
+------------------------------
+
+The metadata can be downloaded from the following location:
+
+ http://<server>/simplesaml/modules.php/aggregator2/get.php?id=<aggregator id>
+
+
+Asynchronous metadata updates
+-----------------------------
+
+By default, the `aggregator2` module will update the metadata when receiving a request. +For performance reasons, it is recommended to run the updates asynchronously.
+By doing this, the aggregated metadata will be generated in the background.
+
+To enable this, you must configure a cache directory with the `cache.directory` option.
+This directory must be writeable by the web server.
+You can then enable caching of generated metadata by setting the `cache.generated` option to the number of seconds the metadata can be cached.
+
+You will now have a configuration that caches both downloaded and generated metadata. +It will however still update the metadata when the user accesses the aggregator endpoint +To update the generated metadata in the background, you must add a `cron.tag` option. +This option must reference a cron tag entry configured in `module_cron.php`. +Once this is done, your aggregated metadata will be updated everytime that cron entry is executed.
=======================================
--- /dev/null
+++ /trunk/modules/aggregator2/hooks/hook_cron.php      Wed Apr 21 05:56:46 2010
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * cron hook to update aggregator2 metadata.
+ *
+ * @param array &$croninfo  Output
+ */
+function aggregator2_hook_cron(&$croninfo) {
+       assert('is_array($croninfo)');
+       assert('array_key_exists("summary", $croninfo)');
+       assert('array_key_exists("tag", $croninfo)');
+
+       $cronTag = $croninfo['tag'];
+
+       $config = SimpleSAML_Configuration::getConfig('module_aggregator2.php');
+       $config = $config->toArray();
+
+       foreach ($config as $id => $c) {
+               if (!isset($c['cron.tag'])) {
+                       continue;
+               }
+               if ($c['cron.tag'] !== $cronTag) {
+                       continue;
+               }
+
+               try {
+                       $a = sspmod_aggregator2_Aggregator::getAggregator($id);
+                       $a->updateCache();
+               } catch (Exception $e) {
+ $croninfo['summary'][] = 'Error during aggregator2 cacheupdate: ' . $e->getMessage();
+               }
+       }
+}
=======================================
--- /dev/null
+++ /trunk/modules/aggregator2/lib/Aggregator.php       Wed Apr 21 05:56:46 2010
@@ -0,0 +1,453 @@
+<?php
+
+/**
+ * Class which implements a basic metadata aggregator.
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_aggregator2_Aggregator {
+
+       /**
+        * The ID of this aggregator.
+        *
+        * @var string
+        */
+       protected $id;
+
+
+       /**
+        * Our log "location".
+        *
+        * @var string
+        */
+       protected $logLoc;
+
+
+       /**
+        * Which cron-tag this should be updated in.
+        *
+        * @var string|NULL
+        */
+       protected $cronTag;
+
+
+       /**
+        * Absolute path to a cache directory.
+        *
+        * @var string|NULL
+        */
+       protected $cacheDirectory;
+
+
+       /**
+        * The entity sources.
+        *
+        * Array of sspmod_aggregator2_EntitySource objects.
+        *
+        * @var array
+        */
+       protected $sources = array();
+
+
+       /**
+ * How long the generated metadata should be valid, as a number of seconds.
+        *
+ * This is used to set the validUntil attribute on the generated EntityDescriptor.
+        *
+        * @var int
+        */
+       protected $validLength;
+
+
+       /**
+        * Duration we should cache generated metadata.
+        *
+        * @var int
+        */
+       protected $cacheGenerated;
+
+
+       /**
+        * The key we should use to sign the metadata.
+        *
+        * @var string|NULL
+        */
+       protected $signKey;
+
+
+       /**
+        * The password for the private key.
+        *
+        * @var string|NULL
+        */
+       protected $signKeyPass;
+
+
+       /**
+        * The certificate of the key we sign the metadata with.
+        *
+        * @var string|NULL
+        */
+       protected $signCert;
+
+
+       /**
+ * The CA certificate file that should be used to validate https-connections.
+        *
+        * @var string|NULL
+        */
+       protected $sslCAFile;
+
+
+       /**
+        * The cache ID for our generated metadata.
+        *
+        * @var string
+        */
+       protected $cacheId;
+
+
+       /**
+        * The cache tag for our generated metadata.
+        *
+        * This tag is used to make sure that a config change
+        * invalidates our cached metadata.
+        *
+        * @var string
+        */
+       protected $cacheTag;
+
+
+       /**
+        * Initialize this aggregator.
+        *
+        * @param string $id  The id of this aggregator.
+ * @param SimpleSAML_Configuration $config The configuration for this aggregator.
+        */
+       protected function __construct($id, SimpleSAML_Configuration $config) {
+               assert('is_string($id)');
+
+               $this->id = $id;
+               $this->logLoc = 'aggregator2:' . $this->id . ': ';
+
+               $this->cronTag = $config->getString('cron.tag', NULL);
+
+               $this->cacheDirectory = $config->getString('cache.directory', 
NULL);
+               if ($this->cacheDirectory !== NULL) {
+ $this->cacheDirectory = SimpleSAML_Utilities::resolvePath($this->cacheDirectory);
+               }
+
+               $this->cacheGenerated = $config->getInteger('cache.generated', 
NULL);
+               if ($this->cacheGenerated !== NULL) {
+                       $this->cacheId = sha1($this->id);
+                       $this->cacheTag = sha1(serialize($config));
+               }
+
+               $this->validLength = $config->getInteger('valid.length', 
7*24*60*60);
+
+               $globalConfig = SimpleSAML_Configuration::getInstance();
+               $certDir = $globalConfig->getPathValue('certdir', 'cert/');
+
+               $signKey = $config->getString('sign.privatekey', NULL);
+               if ($signKey !== NULL) {
+                       $signKey = SimpleSAML_Utilities::resolvePath($signKey, 
$certDir);
+                       $this->signKey = @file_get_contents($signKey);
+                       if ($this->signKey === NULL) {
+ throw new SimpleSAML_Error_Exception('Unable to load private key from ' . var_export($signKey, TRUE));
+                       }
+               }
+
+               $this->signKeyPass = $config->getString('sign.privatekey_pass', 
NULL);
+
+               $signCert = $config->getString('sign.certificate', NULL);
+               if ($signCert !== NULL) {
+                       $signCert = 
SimpleSAML_Utilities::resolvePath($signCert, $certDir);
+                       $this->signCert = @file_get_contents($signCert);
+                       if ($this->signCert === NULL) {
+ throw new SimpleSAML_Error_Exception('Unable to load certificate file from ' . var_export($signCert, TRUE));
+                       }
+               }
+
+
+               $this->sslCAFile = $config->getString('ssl.cafile', NULL);
+
+               $this->initSources($config->getConfigList('sources'));
+       }
+
+
+       /**
+        * Populate the sources array.
+        *
+ * This is called from the constructor, and can be overridden in subclasses.
+        *
+ * @param array $sources The sources as an array of SimpleSAML_Configuration objects.
+        */
+       protected function initSources(array $sources) {
+
+               foreach ($sources as $source) {
+                       $this->sources[] = new 
sspmod_aggregator2_EntitySource($this, $source);
+               }
+       }
+
+
+       /**
+        * Return an instance of the aggregator with the given id.
+        *
+        * @param string $id  The id of the aggregator.
+        */
+       public static function getAggregator($id) {
+               assert('is_string($id)');
+
+               $config = 
SimpleSAML_Configuration::getConfig('module_aggregator2.php');
+ return new sspmod_aggregator2_Aggregator($id, $config->getConfigItem($id));
+       }
+
+
+       /**
+        * Retrieve the ID of the aggregator.
+        *
+        * @return string  The ID of this aggregator.
+        */
+       public function getId() {
+               return $this->id;
+       }
+
+
+       /**
+        * Add an item to the cache.
+        *
+        * @param string $id  The identifier of this data.
+        * @param string $data  The data.
+        * @param int $expires  The timestamp the data expires.
+ * @param string|NULL $tag An extra tag that can be used to verify the validity of the cached data.
+        */
+       public function addCacheItem($id, $data, $expires, $tag = NULL) {
+               assert('is_string($id)');
+               assert('is_string($data)');
+               assert('is_int($expires)');
+               assert('is_null($tag) || is_string($tag)');
+
+               $cacheFile = $this->cacheDirectory . '/' . $id;
+               try {
+                       SimpleSAML_Utilities::writeFile($cacheFile, $data);
+               } catch (Exception $e) {
+ SimpleSAML_Logger::warning($this->logLoc . 'Unable to write to cache file ' . var_export($cacheFile, TRUE));
+                       return;
+               }
+
+               $expireInfo = (string)$expires;
+               if ($tag !== NULL) {
+                       $expireInfo .= ':' . $tag;
+               }
+
+               $expireFile = $cacheFile . '.expire';
+               try {
+                       SimpleSAML_Utilities::writeFile($expireFile, 
$expireInfo);
+               } catch (Exception $e) {
+ SimpleSAML_Logger::warning($this->logLoc . 'Unable to write expiration info to ' . var_export($expireFile, TRUE));
+                       return $metadata;
+               }
+
+       }
+
+
+       /**
+        * Check validity of cached data.
+        *
+        * @param string $id  The identifier of this data.
+        * @param string $tag  The tag that was passed to addCacheItem.
+        * @return bool  TRUE if the data is valid, FALSE if not.
+        */
+       public function isCacheValid($id, $tag = NULL) {
+               assert('is_string($id)');
+               assert('is_null($tag) || is_string($tag)');
+
+               $cacheFile = $this->cacheDirectory . '/' . $id;
+               if (!file_exists($cacheFile)) {
+                       return FALSE;
+               }
+
+               $expireFile = $cacheFile . '.expire';
+               if (!file_exists($expireFile)) {
+                       return FALSE;
+               }
+
+               $expireData = @file_get_contents($expireFile);
+               if ($expireData === FALSE) {
+                       return FALSE;
+               }
+
+               $expireData = explode(':', $expireData, 2);
+
+               $expireTime = (int)$expireData[0];
+               if ($expireTime <= time()) {
+                       return FALSE;
+               }
+
+               if (count($expireData) === 1) {
+                       $expireTag = NULL;
+               } else {
+                       $expireTag = $expireData[1];
+               }
+               if ($expireTag !== $tag) {
+                       return FALSE;
+               }
+
+               return TRUE;
+       }
+
+
+       /**
+        * Get the cache item.
+        *
+        * @param string $id  The identifier of this data.
+        * @param string $tag  The tag that was passed to addCacheItem.
+ * @return string|NULL The cache item, or NULL if it isn't cached or if it is expired.
+        */
+       public function getCacheItem($id, $tag = NULL) {
+               assert('is_string($id)');
+               assert('is_null($tag) || is_string($tag)');
+
+               if (!$this->isCacheValid($id, $tag)) {
+                       return NULL;
+               }
+
+               $cacheFile = $this->cacheDirectory . '/' . $id;
+               return @file_get_contents($cacheFile);
+       }
+
+
+       /**
+        * Get the cache filename for the specific id.
+        *
+        * @param string $id  The identifier of the cached data.
+ * @return string|NULL The filename, or NULL if the cache file doesn't exist.
+        */
+       public function getCacheFile($id) {
+               assert('is_string($id)');
+
+               $cacheFile = $this->cacheDirectory . '/' . $id;
+               if (!file_exists($cacheFile)) {
+                       return NULL;
+               }
+
+               return $cacheFile;
+       }
+
+
+       /**
+        * Retrieve the SSL CA file path, if it is set.
+        *
+        * @return string|NULL  The SSL CA file path.
+        */
+       public function getCAFile() {
+
+               return $this->sslCAFile;
+       }
+
+
+       /**
+        * Sign the generated EntitiesDescriptor.
+        */
+       protected function addSignature(SAML2_SignedElement $element) {
+
+               if ($this->signKey === NULL) {
+                       return;
+               }
+
+ $privateKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type' => 'private'));
+               if ($this->signKeyPass !== NULL) {
+                       $privateKey->passphrase = $this->signKeyPass;
+               }
+               $privateKey->loadKey($this->signKey, FALSE);
+
+
+               $element->setSignatureKey($privateKey);
+
+               if ($this->signCert !== NULL) {
+                       $element->setCertificates(array($this->signCert));
+               }
+       }
+
+
+       /**
+        * Retrieve all entities as an EntitiesDescriptor.
+        *
+        * @return SAML2_XML_md_EntitiesDescriptor  The entities.
+        */
+       protected function getEntitiesDescriptor() {
+
+               $ret = new SAML2_XML_md_EntitiesDescriptor();
+               foreach ($this->sources as $source) {
+                       $m = $source->getMetadata();
+                       if ($m === NULL) {
+                               continue;
+                       }
+                       $ret->children[] = $m;
+               }
+
+               $ret->validUntil = time() + $this->validLength;
+
+               return $ret;
+       }
+
+
+       /**
+        * Retrieve the complete, signed metadata as text.
+        *
+ * This function will write the new metadata to the cache file, but will not return
+        * the cached metadata.
+        *
+        * @return string  The metadata, as text.
+        */
+       public function updateCachedMetadata() {
+
+               $ed = $this->getEntitiesDescriptor();
+               $this->addSignature($ed);
+
+               $xml = $ed->toXML();
+               $xml = $xml->ownerDocument->saveXML($xml);
+
+               if ($this->cacheGenerated !== NULL) {
+ SimpleSAML_Logger::debug($this->logLoc . 'Saving generated metadata to cache.'); + $this->addCacheItem($this->cacheId, $xml, time() + $this->cacheGenerated, $this->cacheTag);
+               }
+
+               return $xml;
+
+       }
+
+
+       /**
+        * Retrieve the complete, signed metadata as text.
+        *
+        * @return string  The metadata, as text.
+        */
+       public function getMetadata() {
+
+               if ($this->cacheGenerated !== NULL) {
+                       $xml = $this->getCacheItem($this->cacheId, 
$this->cacheTag);
+                       if ($xml !== NULL) {
+ SimpleSAML_Logger::debug($this->logLoc . 'Loaded generated metadata from cache.');
+                               return $xml;
+                       }
+               }
+
+               return $this->updateCachedMetadata();
+       }
+
+
+       /**
+        * Update the cached copy of our metadata.
+        */
+       public function updateCache() {
+
+               foreach ($this->sources as $source) {
+                       $source->updateCache();
+               }
+
+               $this->updateCachedMetadata();
+       }
+
+}
=======================================
--- /dev/null
+++ /trunk/modules/aggregator2/lib/EntitySource.php     Wed Apr 21 05:56:46 2010
@@ -0,0 +1,274 @@
+<?php
+
+/**
+ * Class for loading metadata from files and URLs.
+ *
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_aggregator2_EntitySource {
+
+       /**
+        * Our log "location".
+        *
+        * @var string
+        */
+       protected $logLoc;
+
+
+       /**
+        * The aggregator we belong to.
+        *
+        * @var sspmod_aggregator2_Aggregator
+        */
+       protected $aggregator;
+
+
+       /**
+        * The URL we should fetch it from.
+        *
+        * @var string
+        */
+       protected $url;
+
+
+       /**
+        * The SSL CA file that should be used to validate the connection.
+        *
+        * @var string|NULL
+        */
+       protected $sslCAFile;
+
+
+       /**
+        * The certificate we should use to validate downloaded metadata.
+        *
+        * @var string|NULL
+        */
+       protected $certificate;
+
+
+       /**
+        * The parsed metadata.
+        *
+        * @var 
SAML2_XML_md_EntitiesDescriptor|SAML2_XML_md_EntityDescriptor|NULL
+        */
+       protected $metadata;
+
+
+       /**
+        * The cache ID.
+        *
+        * @var string
+        */
+       protected $cacheId;
+
+
+       /**
+        * The cache tag.
+        *
+        * @var string
+        */
+       protected $cacheTag;
+
+
+       /**
+        * Whether we have attempted to update the cache already.
+        *
+        * @var bool
+        */
+       protected $updateAttempted;
+
+
+       /**
+        * Initialize this EntitySource.
+        *
+        * @param SimpleSAML_Configuration $config  The configuration.
+        */
+ public function __construct(sspmod_aggregator2_Aggregator $aggregator, SimpleSAML_Configuration $config) {
+
+               $this->logLoc = 'aggregator2:' . $aggregator->getId() . ': ';
+               $this->aggregator = $aggregator;
+
+               $this->url = $config->getString('url');
+               $this->sslCAFile = $config->getString('ssl.cafile', NULL);
+               if ($this->sslCAFile === NULL) {
+                       $this->sslCAFile = $aggregator->getCAFile();
+               }
+
+               $this->certificate = $config->getString('cert', NULL);
+
+               $this->cacheId = sha1($this->url);
+               $this->cacheTag = sha1(serialize($config));
+       }
+
+
+       /**
+        * Retrieve and parse the metadata.
+        *
+ * @return SAML2_XML_md_EntitiesDescriptor|SAML2_XML_md_EntityDescriptor| NULL + * The downloaded metadata or NULL if we were unable to download or parse it.
+        */
+       private function downloadMetadata() {
+
+               SimpleSAML_Logger::debug($this->logLoc . 'Downloading metadata 
from ' .
+                       var_export($this->url, TRUE));
+
+               $context = array('ssl' => array());
+               if ($this->sslCAFile !== NULL) {
+ $context['ssl']['cafile'] = SimpleSAML_Utilities::resolveCert($this->sslCAFile); + SimpleSAML_Logger::debug($this->logLoc . 'Validating https connection against CA certificate(s) found in ' .
+                               var_export($context['ssl']['cafile'], TRUE));
+                       $context['ssl']['verify_peer'] = TRUE;
+                       $context['ssl']['CN_match'] = parse_url($this->url, 
PHP_URL_HOST);
+               }
+
+               $context = stream_context_create($context);
+
+               $data = file_get_contents($this->url, 0, $context);
+               if ($data === FALSE || $data === NULL) {
+ SimpleSAML_Logger::error($this->logLoc . 'Unable to load metadata from ' .
+                               var_export($this->url, TRUE));
+                       return NULL;
+               }
+
+               $doc = new DOMDocument();
+               $res = $doc->loadXML($data);
+               if (!$res) {
+                       SimpleSAML_Logger::error($this->logLoc . 'Error parsing 
XML from ' .
+                               var_export($this->url, TRUE));
+                       return NULL;
+               }
+
+ $root = SAML2_Utils::xpQuery($doc->firstChild, '/saml_metadata:EntityDescriptor| /saml_metadata:EntitiesDescriptor');
+               if (count($root) === 0) {
+ SimpleSAML_Logger::error($this->logLoc . 'No <EntityDescriptor> or <EntitiesDescriptor> in metadata from ' .
+                               var_export($this->url, TRUE));
+                       return NULL;
+               }
+
+               if (count($root) > 1) {
+ SimpleSAML_Logger::error($this->logLoc . 'More than one <EntityDescriptor> or <EntitiesDescriptor> in metadata from ' .
+                               var_export($this->url, TRUE));
+                       return NULL;
+               }
+
+               $root = $root[0];
+               try {
+                       if ($root->localName === 'EntityDescriptor') {
+                               $md = new SAML2_XML_md_EntityDescriptor($root);
+                       } else {
+                               $md = new 
SAML2_XML_md_EntitiesDescriptor($root);
+                       }
+               } catch (Exception $e) {
+ SimpleSAML_Logger::error($this->logLoc . 'Unable to parse metadata from ' .
+                               var_export($this->url, TRUE) . ': ' . 
$e->getMessage());
+                       return NULL;
+               }
+
+
+               if ($this->certificate !== NULL) {
+                       $file = 
SimpleSAML_Utilities::resolveCert($this->certificate);
+                       $certData = file_get_contents($file);
+                       if ($certData === FALSE) {
+ throw new SimpleSAML_Error_Exception('Error loading certificate from ' . var_export($file, TRUE));
+                       }
+
+                       /* Extract the public key from the certificate for 
validation. */
+ $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type'=>'public'));
+                       $key->loadKey($file, TRUE);
+
+                       if (!$md->validate($key)) {
+ SimpleSAML_Logger::error($this->logLoc . 'Error validating signature on metadata.');
+                               return NULL;
+                       }
+ SimpleSAML_Logger::debug($this->logLoc . 'Validated signature on metadata from ' . var_export($this->url, TRUE));
+               }
+
+               return $md;
+       }
+
+
+       /**
+        * Attempt to update our cache file.
+        */
+       public function updateCache() {
+
+               if ($this->updateAttempted) {
+                       return;
+               }
+               $this->updateAttempted = TRUE;
+
+               $this->metadata = $this->downloadMetadata();
+               if ($this->metadata === NULL) {
+                       return;
+               }
+
+               $expires = time() + 24*60*60; /* Default expires in one day. */
+
+ if ($this->metadata->validUntil !== NULL && $this->metadata->validUntil < $expires) {
+                       $expires = $this->metadata->validUntil;
+               }
+
+               if ($this->metadata->cacheDuration !== NULL) {
+                       try {
+ $durationTo = SimpleSAML_Utilities::parseDuration($this->metadata->cacheDuration);
+                       } catch (Exception $e) {
+ SimpleSAML_Logger::warning($this->logLoc . 'Invalid cacheDuration in metadata from ' . + var_export($this->url, TRUE) . ': ' . var_export($this->metadata->cacheDuration, TRUE));
+                               return;
+                       }
+                       if ($durationTo < $expires) {
+                               $expires = $durationTo;
+                       }
+               }
+
+               $metadataSerialized = serialize($this->metadata);
+
+ $this->aggregator->addCacheItem($this->cacheId, $metadataSerialized, $expires, $this->cacheTag);
+       }
+
+
+       /**
+        * Retrieve the metadata file.
+        *
+ * This function will check its cached copy, to see whether it can be used.
+        *
+ * @return SAML2_XML_md_EntityDescriptor|SAML2_XML_md_EntitiesDescriptor| NULL The downloaded metadata.
+        */
+       public function getMetadata() {
+
+               if ($this->metadata !== NULL) {
+                       /* We have already downloaded the metdata. */
+                       return $this->metadata;
+               }
+
+               if (!$this->aggregator->isCacheValid($this->cacheId, 
$this->cacheTag)) {
+                       $this->updateCache();
+                       if ($this->metadata !== NULL) {
+                               return $this->metadata;
+                       }
+                       /* We were unable to update the cache - use cached 
metadata. */
+               }
+
+
+               $cacheFile = $this->aggregator->getCacheFile($this->cacheId);
+
+               if (!file_exists($cacheFile)) {
+ SimpleSAML_Logger::error($this->logLoc . 'No cached metadata available.');
+                       return NULL;
+               }
+
+               SimpleSAML_Logger::debug($this->logLoc . 'Using cached metadata 
from ' .
+                       var_export($cacheFile, TRUE));
+
+               $metadata = file_get_contents($cacheFile);
+               if ($metadata !== NULL) {
+                       $this->metadata = unserialize($metadata);
+                       return $this->metadata;
+               }
+
+               return NULL;
+       }
+
+}
=======================================
--- /dev/null
+++ /trunk/modules/aggregator2/www/get.php      Wed Apr 21 05:56:46 2010
@@ -0,0 +1,14 @@
+<?php
+
+if (!isset($_REQUEST['id'])) {
+       throw new SimpleSAML_Error_BadRequest('Missing required id-parameter.');
+}
+
+$id = (string)$_REQUEST['id'];
+
+$aggregator = sspmod_aggregator2_Aggregator::getAggregator($id);
+$xml = $aggregator->getMetadata();
+
+header('Content-Type: application/samlmetadata+xml');
+header('Content-Length: ' . strlen($xml));
+echo($xml);

--
You received this message because you are subscribed to the Google Groups 
"simpleSAMLphp commits" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/simplesamlphp-commits?hl=en.

Reply via email to