Skip to content
Snippets Groups Projects
MetaLoader.php 14.8 KiB
Newer Older
<?php
/*
 * @author Andreas Åkre Solberg <andreas.solberg@uninett.no>
 * @package SimpleSAMLphp
Tim van Dijen's avatar
Tim van Dijen committed
class sspmod_metarefresh_MetaLoader
{
	private $metadata;
	private $oldMetadataSrc;
	private $stateFile;
	private $changed;
Tim van Dijen's avatar
Tim van Dijen committed
    private $state;
	private $types = array(
		'saml20-idp-remote',
		'saml20-sp-remote',
		'shib13-idp-remote',
		'shib13-sp-remote',
		'attributeauthority-remote'
	);
Tim van Dijen's avatar
Tim van Dijen committed
	public function __construct($expire = null, $stateFile = null, $oldMetadataSrc = null)
    {
		$this->expire = $expire;
		$this->oldMetadataSrc = $oldMetadataSrc;
		$this->stateFile = $stateFile;
Tim van Dijen's avatar
Tim van Dijen committed
		$this->changed = false;

		// Read file containing $state from disk
		if(is_readable($stateFile)) {
			require($stateFile);
		}

Tim van Dijen's avatar
Tim van Dijen committed
		$this->state = array();

	/**
	 * Get the types of entities that will be loaded.
	 *
	 * @return array The entity types allowed.
	 */
	public function getTypes()
	{
		return $this->types;
	}


	/**
	 * Set the types of entities that will be loaded.
	 *
	 * @param string|array $types Either a string with the name of one single type allowed, or an array with a list of
	 * types. Pass an empty array to reset to all types of entities.
	 */
	public function setTypes($types)
	{
		if (!is_array($types)) {
			$types = array($types);
		}
		$this->types = $types;
	}


	/**
	 * This function processes a SAML metadata file.
	 *
Tim van Dijen's avatar
Tim van Dijen committed
	public function loadSource($source)
    {
		if (preg_match('@^https?://@i', $source['src'])) {
			// Build new HTTP context
			$context = $this->createContext($source);
Tim van Dijen's avatar
Tim van Dijen committed
				list($data, $responseHeaders) = \SimpleSAML\Utils\HTTP::fetch($source['src'], $context, true);
				SimpleSAML\Logger::warning('metarefresh: ' . $e->getMessage());
			// We have response headers, so the request succeeded
Tim van Dijen's avatar
Tim van Dijen committed
			if (!isset($responseHeaders)) {
				// No response headers, this means the request failed in some way, so re-use old data
				SimpleSAML\Logger::debug('No response from ' . $source['src'] . ' - attempting to re-use cached metadata');
				$this->addCachedMetadata($source);
				return;
Tim van Dijen's avatar
Tim van Dijen committed
			} elseif (preg_match('@^HTTP/1\.[01]\s304\s@', $responseHeaders[0])) {
				SimpleSAML\Logger::debug('Received HTTP 304 (Not Modified) - attempting to re-use cached metadata');
				$this->addCachedMetadata($source);
				return;
Tim van Dijen's avatar
Tim van Dijen committed
			} elseif (!preg_match('@^HTTP/1\.[01]\s200\s@', $responseHeaders[0])) {
				// Other error
				SimpleSAML\Logger::debug('Error from ' . $source['src'] . ' - attempting to re-use cached metadata');
				$this->addCachedMetadata($source);
				return;
			}
		} else {
			// Local file.
			$data = file_get_contents($source['src']);
Tim van Dijen's avatar
Tim van Dijen committed
			$responseHeaders = null;
		// Everything OK. Proceed.
		if (isset($source['conditionalGET']) && $source['conditionalGET']) {
			// Stale or no metadata, so a fresh copy
			SimpleSAML\Logger::debug('Downloaded fresh copy');
		try {
			$entities = $this->loadXML($data, $source);
		} catch(Exception $e) {
			SimpleSAML\Logger::debug('XML parser error when parsing ' . $source['src'] . ' - attempting to re-use cached metadata');
Tim van Dijen's avatar
Tim van Dijen committed
		foreach ($entities as $entity) {
Tim van Dijen's avatar
Tim van Dijen committed
			if (isset($source['blacklist'])) {
				if (!empty($source['blacklist']) && in_array($entity->getEntityID(), $source['blacklist'], true)) {
					SimpleSAML\Logger::info('Skipping "' .  $entity->getEntityID() . '" - blacklisted.' . "\n");
Tim van Dijen's avatar
Tim van Dijen committed
			if (isset($source['whitelist'])) {
				if (!empty($source['whitelist']) && !in_array($entity->getEntityID(), $source['whitelist'], true)) {
					SimpleSAML\Logger::info('Skipping "' .  $entity->getEntityID() . '" - not in the whitelist.' . "\n");
Tim van Dijen's avatar
Tim van Dijen committed
			if (array_key_exists('certificates', $source) && $source['certificates'] !== null) {
				if (!$entity->validateSignature($source['certificates'])) {
					SimpleSAML\Logger::info('Skipping "' . $entity->getEntityId() . '" - could not verify signature using certificate.' . "\n");
Tim van Dijen's avatar
Tim van Dijen committed
			if (array_key_exists('validateFingerprint', $source) && $source['validateFingerprint'] !== null) {
				if (!array_key_exists('certificates', $source) || $source['certificates'] == null) {
					if (!$entity->validateFingerprint($source['validateFingerprint'])) {
						SimpleSAML\Logger::info('Skipping "' . $entity->getEntityId() . '" - could not verify signature using fingerprint.' . "\n");
					SimpleSAML\Logger::info('Skipping validation with fingerprint since option certificate is set.' . "\n");
Tim van Dijen's avatar
Tim van Dijen committed
			$template = null;
			if (array_key_exists('template', $source)) $template = $source['template'];

m0ark's avatar
m0ark committed
			if (in_array('shib13-sp-remote', $this->types)) {
                    $this->addMetadata($source['src'], $entity->getMetadata1xSP(), 'shib13-sp-remote', $template);
            }
            if (in_array('shib13-idp-remote', $this->types)) {
                    $this->addMetadata($source['src'], $entity->getMetadata1xIdP(), 'shib13-idp-remote', $template);
            }
            if (in_array('saml20-sp-remote', $this->types)) {
                    $this->addMetadata($source['src'], $entity->getMetadata20SP(), 'saml20-sp-remote', $template);
            }
            if (in_array('saml20-idp-remote', $this->types)) {
                    $this->addMetadata($source['src'], $entity->getMetadata20IdP(), 'saml20-idp-remote', $template);
            }
            if (in_array('attributeauthority-remote', $this->types)) {
                    $attributeAuthorities = $entity->getAttributeAuthorities();
                    if (!empty($attributeAuthorities)) {
                            $this->addMetadata($source['src'], $attributeAuthorities[0], 'attributeauthority-remote', $template);
                    }
            }

		$this->saveState($source, $responseHeaders);
	}

	/**
	 * Create HTTP context, with any available caches taken into account
	 */
Tim van Dijen's avatar
Tim van Dijen committed
	private function createContext($source)
    {
		$config = SimpleSAML_Configuration::getInstance();
Tim van Dijen's avatar
Tim van Dijen committed
		$name = $config->getString('technicalcontact_name', null);
		$mail = $config->getString('technicalcontact_email', null);
		$rawheader = "User-Agent: SimpleSAMLphp metarefresh, run by $name <$mail>\r\n";

		if (isset($source['conditionalGET']) && $source['conditionalGET']) {
Tim van Dijen's avatar
Tim van Dijen committed
			if (array_key_exists($source['src'], $this->state)) {

				$sourceState = $this->state[$source['src']];

Tim van Dijen's avatar
Tim van Dijen committed
				if (isset($sourceState['last-modified'])) {
					$rawheader .= 'If-Modified-Since: ' . $sourceState['last-modified'] . "\r\n";
				}

Tim van Dijen's avatar
Tim van Dijen committed
				if (isset($sourceState['etag'])) {
					$rawheader .= 'If-None-Match: ' . $sourceState['etag'] . "\r\n";
				}
			}
		}

		return array('http' => array('header' => $rawheader));
	}
Tim van Dijen's avatar
Tim van Dijen committed
	private function addCachedMetadata($source)
    {
		if (isset($this->oldMetadataSrc)) {
			foreach ($this->types as $type) {
				foreach ($this->oldMetadataSrc->getMetadataSet($type) as $entity) {
					if (array_key_exists('metarefresh:src', $entity)) {
						if ($entity['metarefresh:src'] == $source['src']) {
							$this->addMetadata($source['src'], $entity, $type);
						}
					}
	/**
	 * Store caching state data for a source
	 */
Tim van Dijen's avatar
Tim van Dijen committed
	private function saveState($source, $responseHeaders)
    {
		if (isset($source['conditionalGET']) && $source['conditionalGET']) {
			// Headers section
            if ($responseHeaders !== null) {
			    $candidates = array('last-modified', 'etag');

			    foreach ($candidates as $candidate) {
				    if (array_key_exists($candidate, $responseHeaders)) {
					    $this->state[$source['src']][$candidate] = $responseHeaders[$candidate];
				    }
                }    
Tim van Dijen's avatar
Tim van Dijen committed
			if (!empty($this->state[$source['src']])) {
				// Timestamp when this src was requested.
				$this->state[$source['src']]['requested_at'] = $this->getTime();

Tim van Dijen's avatar
Tim van Dijen committed
				$this->changed = true;
	 * Parse XML metadata and return entities
Tim van Dijen's avatar
Tim van Dijen committed
	private function loadXML($data, $source)
    {
			$doc = \SAML2\DOMDocumentFactory::fromString($data);
			throw new Exception('Failed to read XML from ' . $source['src']);
		}
Tim van Dijen's avatar
Tim van Dijen committed
		if ($doc->documentElement === null) {
			throw new Exception('Opened file is not an XML document: ' . $source['src']);
		}
		return SimpleSAML_Metadata_SAMLParser::parseDescriptorsElement($doc->documentElement);
	}


	/**
	 * This function writes the state array back to disk
	 */
Tim van Dijen's avatar
Tim van Dijen committed
	public function writeState()
    {
		if ($this->changed) {
			SimpleSAML\Logger::debug('Writing: ' . $this->stateFile);
            SimpleSAML\Utils\System::writeFile(
				$this->stateFile,
				"<?php\n/* This file was generated by the metarefresh module at ".$this->getTime() . ".\n".
				" Do not update it manually as it will get overwritten. */\n".
Tim van Dijen's avatar
Tim van Dijen committed
				'$state = ' . var_export($this->state, true) . ";\n?>\n",
	/**
	 * This function writes the metadata to stdout.
	 */
Tim van Dijen's avatar
Tim van Dijen committed
	public function dumpMetadataStdOut()
    {
		foreach ($this->metadata as $category => $elements) {
Tim van Dijen's avatar
Tim van Dijen committed
			echo '/* The following data should be added to metadata/' . $category . '.php. */' . "\n";
Tim van Dijen's avatar
Tim van Dijen committed
			foreach ($elements as $m) {
				$filename = $m['filename'];
				$entityID = $m['metadata']['entityid'];
	
Tim van Dijen's avatar
Tim van Dijen committed
				echo "\n";
				echo '/* The following metadata was generated from ' . $filename . ' on ' . $this->getTime() . '. */' . "\n";
				echo '$metadata[\'' . addslashes($entityID) . '\'] = ' . var_export($m['metadata'], true) . ';' . "\n";
Tim van Dijen's avatar
Tim van Dijen committed
			echo "\n";
			echo '/* End of data which should be added to metadata/' . $category . '.php. */' . "\n";
			echo "\n";
		}
	}

	
	/**
	 * This function adds metadata from the specified file to the list of metadata.
	 * This function will return without making any changes if $metadata is NULL.
	 *
	 * @param $filename The filename the metadata comes from.
	 * @param $metadata The metadata.
	 * @param $type The metadata type.
	 */
Tim van Dijen's avatar
Tim van Dijen committed
	private function addMetadata($filename, $metadata, $type, $template = null)
    {
		if ($metadata === null) {
		if (isset($template)) {
			$metadata = array_merge($metadata, $template);
		}
	
		$metadata['metarefresh:src'] = $filename;
Tim van Dijen's avatar
Tim van Dijen committed
		if (!array_key_exists($type, $this->metadata)) {
		// If expire is defined in constructor...
		if (!empty($this->expire)) {
			
			// If expire is already in metadata
			if (array_key_exists('expire', $metadata)) {
			
				// Override metadata expire with more restrictive global config-
				if ($this->expire < $metadata['expire'])
					$metadata['expire'] = $this->expire;
					
			// If expire is not already in metadata use global config
			} else {
				$metadata['expire'] = $this->expire;
	
		$this->metadata[$type][] = array('filename' => $filename, 'metadata' => $metadata);
	}


	/**
	 * This function writes the metadata to an ARP file
	 */
Tim van Dijen's avatar
Tim van Dijen committed
	public function writeARPfile($config)
    {
		assert($config instanceof SimpleSAML_Configuration);
		
		$arpfile = $config->getValue('arpfile');
		$types = array('saml20-sp-remote');
		
		$md = array();
Tim van Dijen's avatar
Tim van Dijen committed
		foreach ($this->metadata as $category => $elements) {
			if (!in_array($category, $types, true)) continue;
			$md = array_merge($md, $elements);
		}
		
		// $metadata, $attributemap, $prefix, $suffix
		$arp = new sspmod_metarefresh_ARP($md, 
			$config->getValue('attributemap', ''),  
			$config->getValue('prefix', ''),  
			$config->getValue('suffix', '')
		);
		
		
		$arpxml = $arp->getXML();

		SimpleSAML\Logger::info('Writing ARP file: ' . $arpfile . "\n");
		file_put_contents($arpfile, $arpxml);
	}
	
	
	/**
	 * This function writes the metadata to to separate files in the output directory.
	 */
Tim van Dijen's avatar
Tim van Dijen committed
	public function writeMetadataFiles($outputDir)
    {
		while (strlen($outputDir) > 0 && $outputDir[strlen($outputDir) - 1] === '/') {
			$outputDir = substr($outputDir, 0, strlen($outputDir) - 1);
		}
	
Tim van Dijen's avatar
Tim van Dijen committed
		if (!file_exists($outputDir)) {
			SimpleSAML\Logger::info('Creating directory: ' . $outputDir . "\n");
Tim van Dijen's avatar
Tim van Dijen committed
			$res = @mkdir($outputDir, 0777, true);
			if ($res === false) {
				throw new Exception('Error creating directory: ' . $outputDir);
			}
Tim van Dijen's avatar
Tim van Dijen committed
		foreach ($this->types as $type) {
			$filename = $outputDir . '/' . $type . '.php';

Tim van Dijen's avatar
Tim van Dijen committed
			if (array_key_exists($type, $this->metadata)) {
				$elements = $this->metadata[$type];
				SimpleSAML\Logger::debug('Writing: ' . $filename);

				$content  = '<?php' . "\n" . '/* This file was generated by the metarefresh module at '. $this->getTime() . "\n";
				$content .= ' Do not update it manually as it will get overwritten' . "\n" . '*/' . "\n";

Tim van Dijen's avatar
Tim van Dijen committed
				foreach ($elements as $m) {
					$entityID = $m['metadata']['entityid'];
					$content .= "\n";
					$content .= '$metadata[\'' . addslashes($entityID) . '\'] = ' . var_export($m['metadata'], TRUE) . ';' . "\n";
				}

				$content .= "\n" . '?>';

                SimpleSAML\Utils\System::writeFile($filename, $content, 0644);
Tim van Dijen's avatar
Tim van Dijen committed
			} elseif (is_file($filename)) {
				if (unlink($filename)) {
					SimpleSAML\Logger::debug('Deleting stale metadata file: ' . $filename);
					SimpleSAML\Logger::warning('Could not delete stale metadata file: ' . $filename);

	/**
	 * Save metadata for loading with the 'serialize' metadata loader.
	 *
	 * @param string $outputDir  The directory we should save the metadata to.
	 */
Tim van Dijen's avatar
Tim van Dijen committed
	public function writeMetadataSerialize($outputDir)
    {
		assert(is_string($outputDir));

		$metaHandler = new SimpleSAML_Metadata_MetaDataStorageHandlerSerialize(array('directory' => $outputDir));

		/* First we add all the metadata entries to the metadata handler. */
		foreach ($this->metadata as $set => $elements) {
			foreach ($elements as $m) {
				$entityId = $m['metadata']['entityid'];

				SimpleSAML\Logger::debug('metarefresh: Add metadata entry ' .
Tim van Dijen's avatar
Tim van Dijen committed
					var_export($entityId, true) . ' in set ' . var_export($set, true) . '.');
				$metaHandler->saveMetadata($entityId, $set, $m['metadata']);
			}
		}

		/* Then we delete old entries which should no longer exist. */
		$ct = time();
		foreach ($metaHandler->getMetadataSets() as $set) {
			foreach ($metaHandler->getMetadataSet($set) as $entityId => $metadata) {
				if (!array_key_exists('expire', $metadata)) {
Tim van Dijen's avatar
Tim van Dijen committed
					SimpleSAML\Logger::warning('metarefresh: Metadata entry without expire timestamp: ' . var_export($entityId, true) .
						' in set ' . var_export($set, true) . '.');
				}
				if ($metadata['expire'] > $ct) {
					continue;
				}
				SimpleSAML\Logger::debug('metarefresh: ' . $entityId . ' expired ' . date('l jS \of F Y h:i:s A', $metadata['expire']) );
				SimpleSAML\Logger::debug('metarefresh: Delete expired metadata entry ' .
Tim van Dijen's avatar
Tim van Dijen committed
					var_export($entityId, true) . ' in set ' . var_export($set, true) . '. (' . ($ct - $metadata['expire']) . ' sec)');
				$metaHandler->deleteMetadata($entityId, $set);
			}
		}
	}


Tim van Dijen's avatar
Tim van Dijen committed
	private function getTime()
    {
		/* The current date, as a string. */
		date_default_timezone_set('UTC');
Tim van Dijen's avatar
Tim van Dijen committed
		return date('Y-m-d\\TH:i:s\\Z');