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);