diff --git a/modules/aggregator2/bin/update.php b/modules/aggregator2/bin/update.php new file mode 100755 index 0000000000000000000000000000000000000000..790f4fd99fc525ac65f6f886a010e39e66536d32 --- /dev/null +++ b/modules/aggregator2/bin/update.php @@ -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(); diff --git a/modules/aggregator2/config-templates/module_aggregator2.php b/modules/aggregator2/config-templates/module_aggregator2.php new file mode 100644 index 0000000000000000000000000000000000000000..da6aae3b295d878397b48ef9ab080fd69dc12021 --- /dev/null +++ b/modules/aggregator2/config-templates/module_aggregator2.php @@ -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', + ), + +); diff --git a/modules/aggregator2/default-disable b/modules/aggregator2/default-disable new file mode 100644 index 0000000000000000000000000000000000000000..fa0bd82e2df7bd79d57593d35bc53c1f9d3ef71f --- /dev/null +++ b/modules/aggregator2/default-disable @@ -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. diff --git a/modules/aggregator2/docs/aggregator2.txt b/modules/aggregator2/docs/aggregator2.txt new file mode 100644 index 0000000000000000000000000000000000000000..43483323d4d1d6ef059f706c7316071e10423e58 --- /dev/null +++ b/modules/aggregator2/docs/aggregator2.txt @@ -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. diff --git a/modules/aggregator2/hooks/hook_cron.php b/modules/aggregator2/hooks/hook_cron.php new file mode 100644 index 0000000000000000000000000000000000000000..c052b43e22829b4326cf0e530d0d53292101bc62 --- /dev/null +++ b/modules/aggregator2/hooks/hook_cron.php @@ -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(); + } + } +} diff --git a/modules/aggregator2/lib/Aggregator.php b/modules/aggregator2/lib/Aggregator.php new file mode 100644 index 0000000000000000000000000000000000000000..caf283a72f5ca58723854dcfd719faa91574d896 --- /dev/null +++ b/modules/aggregator2/lib/Aggregator.php @@ -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(); + } + +} diff --git a/modules/aggregator2/lib/EntitySource.php b/modules/aggregator2/lib/EntitySource.php new file mode 100644 index 0000000000000000000000000000000000000000..f6e329290c9ea8486eb4f1999bb9c990207425a7 --- /dev/null +++ b/modules/aggregator2/lib/EntitySource.php @@ -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; + } + +} diff --git a/modules/aggregator2/www/get.php b/modules/aggregator2/www/get.php new file mode 100644 index 0000000000000000000000000000000000000000..bb47f8d696e296dcae02785031105f1d8f68a234 --- /dev/null +++ b/modules/aggregator2/www/get.php @@ -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);