ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
MetaLoader.php
Go to the documentation of this file.
1 <?php
7 {
8  private $expire;
9  private $metadata;
10  private $oldMetadataSrc;
11  private $stateFile;
12  private $changed;
13  private $state;
14  private $types = array(
15  'saml20-idp-remote',
16  'saml20-sp-remote',
17  'shib13-idp-remote',
18  'shib13-sp-remote',
19  'attributeauthority-remote'
20  );
21 
22 
30  public function __construct($expire = null, $stateFile = null, $oldMetadataSrc = null)
31  {
32  $this->expire = $expire;
33  $this->metadata = array();
34  $this->oldMetadataSrc = $oldMetadataSrc;
35  $this->stateFile = $stateFile;
36  $this->changed = false;
37 
38  // Read file containing $state from disk
39  if (is_readable($stateFile)) {
40  include $stateFile;
41  }
42 
43  $this->state = array();
44 
45  }
46 
47 
53  public function getTypes()
54  {
55  return $this->types;
56  }
57 
58 
65  public function setTypes($types)
66  {
67  if (!is_array($types)) {
68  $types = array($types);
69  }
70  $this->types = $types;
71  }
72 
73 
79  public function loadSource($source)
80  {
81  if (preg_match('@^https?://@i', $source['src'])) {
82  // Build new HTTP context
83  $context = $this->createContext($source);
84 
85  // GET!
86  try {
87  list($data, $responseHeaders) = \SimpleSAML\Utils\HTTP::fetch($source['src'], $context, true);
88  } catch(Exception $e) {
89  SimpleSAML\Logger::warning('metarefresh: ' . $e->getMessage());
90  }
91 
92  // We have response headers, so the request succeeded
93  if (!isset($responseHeaders)) {
94  // No response headers, this means the request failed in some way, so re-use old data
95  SimpleSAML\Logger::debug('No response from ' . $source['src'] . ' - attempting to re-use cached metadata');
96  $this->addCachedMetadata($source);
97  return;
98  } elseif (preg_match('@^HTTP/1\.[01]\s304\s@', $responseHeaders[0])) {
99  // 304 response
100  SimpleSAML\Logger::debug('Received HTTP 304 (Not Modified) - attempting to re-use cached metadata');
101  $this->addCachedMetadata($source);
102  return;
103  } elseif (!preg_match('@^HTTP/1\.[01]\s200\s@', $responseHeaders[0])) {
104  // Other error
105  SimpleSAML\Logger::debug('Error from ' . $source['src'] . ' - attempting to re-use cached metadata');
106  $this->addCachedMetadata($source);
107  return;
108  }
109  } else {
110  // Local file.
111  $data = file_get_contents($source['src']);
112  $responseHeaders = null;
113  }
114 
115  // Everything OK. Proceed.
116  if (isset($source['conditionalGET']) && $source['conditionalGET']) {
117  // Stale or no metadata, so a fresh copy
118  SimpleSAML\Logger::debug('Downloaded fresh copy');
119  }
120 
121  try {
122  $entities = $this->loadXML($data, $source);
123  } catch(Exception $e) {
124  SimpleSAML\Logger::debug('XML parser error when parsing ' . $source['src'] . ' - attempting to re-use cached metadata');
125  SimpleSAML\Logger::debug('XML parser returned: ' . $e->getMessage());
126  $this->addCachedMetadata($source);
127  return;
128  }
129 
130  foreach ($entities as $entity) {
131 
132  if (isset($source['blacklist'])) {
133  if (!empty($source['blacklist']) && in_array($entity->getEntityID(), $source['blacklist'], true)) {
134  SimpleSAML\Logger::info('Skipping "' . $entity->getEntityID() . '" - blacklisted.' . "\n");
135  continue;
136  }
137  }
138 
139  if (isset($source['whitelist'])) {
140  if (!empty($source['whitelist']) && !in_array($entity->getEntityID(), $source['whitelist'], true)) {
141  SimpleSAML\Logger::info('Skipping "' . $entity->getEntityID() . '" - not in the whitelist.' . "\n");
142  continue;
143  }
144  }
145 
146  if (array_key_exists('certificates', $source) && $source['certificates'] !== null) {
147  if (!$entity->validateSignature($source['certificates'])) {
148  SimpleSAML\Logger::info('Skipping "' . $entity->getEntityId() . '" - could not verify signature using certificate.' . "\n");
149  continue;
150  }
151  }
152 
153  if (array_key_exists('validateFingerprint', $source) && $source['validateFingerprint'] !== null) {
154  if (!array_key_exists('certificates', $source) || $source['certificates'] == null) {
155  if (!$entity->validateFingerprint($source['validateFingerprint'])) {
156  SimpleSAML\Logger::info('Skipping "' . $entity->getEntityId() . '" - could not verify signature using fingerprint.' . "\n");
157  continue;
158  }
159  } else {
160  SimpleSAML\Logger::info('Skipping validation with fingerprint since option certificate is set.' . "\n");
161  }
162  }
163 
164  $template = null;
165  if (array_key_exists('template', $source)) {
166  $template = $source['template'];
167  }
168 
169  if (in_array('shib13-sp-remote', $this->types, true)) {
170  $this->addMetadata($source['src'], $entity->getMetadata1xSP(), 'shib13-sp-remote', $template);
171  }
172  if (in_array('shib13-idp-remote', $this->types, true)) {
173  $this->addMetadata($source['src'], $entity->getMetadata1xIdP(), 'shib13-idp-remote', $template);
174  }
175  if (in_array('saml20-sp-remote', $this->types, true)) {
176  $this->addMetadata($source['src'], $entity->getMetadata20SP(), 'saml20-sp-remote', $template);
177  }
178  if (in_array('saml20-idp-remote', $this->types, true)) {
179  $this->addMetadata($source['src'], $entity->getMetadata20IdP(), 'saml20-idp-remote', $template);
180  }
181  if (in_array('attributeauthority-remote', $this->types, true)) {
182  $attributeAuthorities = $entity->getAttributeAuthorities();
183  if (!empty($attributeAuthorities)) {
184  $this->addMetadata($source['src'], $attributeAuthorities[0], 'attributeauthority-remote', $template);
185  }
186  }
187  }
188 
189  $this->saveState($source, $responseHeaders);
190  }
191 
195  private function createContext($source)
196  {
198  $name = $config->getString('technicalcontact_name', null);
199  $mail = $config->getString('technicalcontact_email', null);
200 
201  $rawheader = "User-Agent: SimpleSAMLphp metarefresh, run by $name <$mail>\r\n";
202 
203  if (isset($source['conditionalGET']) && $source['conditionalGET']) {
204  if (array_key_exists($source['src'], $this->state)) {
205  $sourceState = $this->state[$source['src']];
206 
207  if (isset($sourceState['last-modified'])) {
208  $rawheader .= 'If-Modified-Since: ' . $sourceState['last-modified'] . "\r\n";
209  }
210 
211  if (isset($sourceState['etag'])) {
212  $rawheader .= 'If-None-Match: ' . $sourceState['etag'] . "\r\n";
213  }
214  }
215  }
216 
217  return array('http' => array('header' => $rawheader));
218  }
219 
220 
221  private function addCachedMetadata($source)
222  {
223  if (isset($this->oldMetadataSrc)) {
224  foreach ($this->types as $type) {
225  foreach ($this->oldMetadataSrc->getMetadataSet($type) as $entity) {
226  if (array_key_exists('metarefresh:src', $entity)) {
227  if ($entity['metarefresh:src'] == $source['src']) {
228  $this->addMetadata($source['src'], $entity, $type);
229  }
230  }
231  }
232  }
233  }
234  }
235 
236 
240  private function saveState($source, $responseHeaders)
241  {
242  if (isset($source['conditionalGET']) && $source['conditionalGET']) {
243  // Headers section
244  if ($responseHeaders !== null) {
245  $candidates = array('last-modified', 'etag');
246 
247  foreach ($candidates as $candidate) {
248  if (array_key_exists($candidate, $responseHeaders)) {
249  $this->state[$source['src']][$candidate] = $responseHeaders[$candidate];
250  }
251  }
252  }
253 
254  if (!empty($this->state[$source['src']])) {
255  // Timestamp when this src was requested.
256  $this->state[$source['src']]['requested_at'] = $this->getTime();
257  $this->changed = true;
258  }
259  }
260  }
261 
265  private function loadXML($data, $source)
266  {
267  try {
269  } catch (Exception $e) {
270  throw new Exception('Failed to read XML from ' . $source['src']);
271  }
272  if ($doc->documentElement === null) {
273  throw new Exception('Opened file is not an XML document: ' . $source['src']);
274  }
275  return SimpleSAML_Metadata_SAMLParser::parseDescriptorsElement($doc->documentElement);
276  }
277 
278 
282  public function writeState()
283  {
284  if ($this->changed) {
285  SimpleSAML\Logger::debug('Writing: ' . $this->stateFile);
287  $this->stateFile,
288  "<?php\n/* This file was generated by the metarefresh module at ".$this->getTime() . ".\n".
289  " Do not update it manually as it will get overwritten. */\n".
290  '$state = ' . var_export($this->state, true) . ";\n?>\n",
291  0644
292  );
293  }
294  }
295 
296 
300  public function dumpMetadataStdOut()
301  {
302  foreach ($this->metadata as $category => $elements) {
303 
304  echo '/* The following data should be added to metadata/' . $category . '.php. */' . "\n";
305 
306  foreach ($elements as $m) {
307  $filename = $m['filename'];
308  $entityID = $m['metadata']['entityid'];
309 
310  echo "\n";
311  echo '/* The following metadata was generated from ' . $filename . ' on ' . $this->getTime() . '. */' . "\n";
312  echo '$metadata[\'' . addslashes($entityID) . '\'] = ' . var_export($m['metadata'], true) . ';' . "\n";
313  }
314 
315  echo "\n";
316  echo '/* End of data which should be added to metadata/' . $category . '.php. */' . "\n";
317  echo "\n";
318  }
319  }
320 
321 
330  private function addMetadata($filename, $metadata, $type, $template = null)
331  {
332  if ($metadata === null) {
333  return;
334  }
335 
336  if (isset($template)) {
337  $metadata = array_merge($metadata, $template);
338  }
339 
340  $metadata['metarefresh:src'] = $filename;
341  if (!array_key_exists($type, $this->metadata)) {
342  $this->metadata[$type] = array();
343  }
344 
345  // If expire is defined in constructor...
346  if (!empty($this->expire)) {
347  // If expire is already in metadata
348  if (array_key_exists('expire', $metadata)) {
349  // Override metadata expire with more restrictive global config-
350  if ($this->expire < $metadata['expire']) {
351  $metadata['expire'] = $this->expire;
352  }
353 
354  // If expire is not already in metadata use global config
355  } else {
356  $metadata['expire'] = $this->expire;
357  }
358  }
359  $this->metadata[$type][] = array('filename' => $filename, 'metadata' => $metadata);
360  }
361 
362 
366  public function writeARPfile($config)
367  {
368  assert($config instanceof SimpleSAML_Configuration);
369 
370  $arpfile = $config->getValue('arpfile');
371  $types = array('saml20-sp-remote');
372 
373  $md = array();
374  foreach ($this->metadata as $category => $elements) {
375  if (!in_array($category, $types, true)) {
376  continue;
377  }
378  $md = array_merge($md, $elements);
379  }
380 
381  // $metadata, $attributemap, $prefix, $suffix
382  $arp = new sspmod_metarefresh_ARP($md,
383  $config->getValue('attributemap', ''),
384  $config->getValue('prefix', ''),
385  $config->getValue('suffix', '')
386  );
387 
388 
389  $arpxml = $arp->getXML();
390 
391  SimpleSAML\Logger::info('Writing ARP file: ' . $arpfile . "\n");
392  file_put_contents($arpfile, $arpxml);
393  }
394 
395 
399  public function writeMetadataFiles($outputDir)
400  {
401  while (strlen($outputDir) > 0 && $outputDir[strlen($outputDir) - 1] === '/') {
402  $outputDir = substr($outputDir, 0, strlen($outputDir) - 1);
403  }
404 
405  if (!file_exists($outputDir)) {
406  SimpleSAML\Logger::info('Creating directory: ' . $outputDir . "\n");
407  $res = @mkdir($outputDir, 0777, true);
408  if ($res === false) {
409  throw new Exception('Error creating directory: ' . $outputDir);
410  }
411  }
412 
413  foreach ($this->types as $type) {
414  $filename = $outputDir . '/' . $type . '.php';
415 
416  if (array_key_exists($type, $this->metadata)) {
417  $elements = $this->metadata[$type];
418  SimpleSAML\Logger::debug('Writing: ' . $filename);
419 
420  $content = '<?php' . "\n" . '/* This file was generated by the metarefresh module at '. $this->getTime() . "\n";
421  $content .= ' Do not update it manually as it will get overwritten' . "\n" . '*/' . "\n";
422 
423  foreach ($elements as $m) {
424  $entityID = $m['metadata']['entityid'];
425  $content .= "\n";
426  $content .= '$metadata[\'' . addslashes($entityID) . '\'] = ' . var_export($m['metadata'], true) . ';' . "\n";
427  }
428 
429  $content .= "\n" . '?>';
430 
431  SimpleSAML\Utils\System::writeFile($filename, $content, 0644);
432  } elseif (is_file($filename)) {
433  if (unlink($filename)) {
434  SimpleSAML\Logger::debug('Deleting stale metadata file: ' . $filename);
435  } else {
436  SimpleSAML\Logger::warning('Could not delete stale metadata file: ' . $filename);
437  }
438  }
439  }
440  }
441 
442 
448  public function writeMetadataSerialize($outputDir)
449  {
450  assert(is_string($outputDir));
451 
452  $metaHandler = new SimpleSAML_Metadata_MetaDataStorageHandlerSerialize(array('directory' => $outputDir));
453 
454  /* First we add all the metadata entries to the metadata handler. */
455  foreach ($this->metadata as $set => $elements) {
456  foreach ($elements as $m) {
457  $entityId = $m['metadata']['entityid'];
458 
459  SimpleSAML\Logger::debug(
460  'metarefresh: Add metadata entry ' .
461  var_export($entityId, true) . ' in set ' . var_export($set, true) . '.'
462  );
463  $metaHandler->saveMetadata($entityId, $set, $m['metadata']);
464  }
465  }
466 
467  /* Then we delete old entries which should no longer exist. */
468  $ct = time();
469  foreach ($metaHandler->getMetadataSets() as $set) {
470  foreach ($metaHandler->getMetadataSet($set) as $entityId => $metadata) {
471  if (!array_key_exists('expire', $metadata)) {
472  SimpleSAML\Logger::warning(
473  'metarefresh: Metadata entry without expire timestamp: ' . var_export($entityId, true) .
474  ' in set ' . var_export($set, true) . '.'
475  );
476  continue;
477  }
478  if ($metadata['expire'] > $ct) {
479  continue;
480  }
481  SimpleSAML\Logger::debug('metarefresh: ' . $entityId . ' expired ' . date('l jS \of F Y h:i:s A', $metadata['expire']));
482  SimpleSAML\Logger::debug(
483  'metarefresh: Delete expired metadata entry ' .
484  var_export($entityId, true) . ' in set ' . var_export($set, true) . '. (' . ($ct - $metadata['expire']) . ' sec)'
485  );
486  $metaHandler->deleteMetadata($entityId, $set);
487  }
488  }
489  }
490 
491 
492  private function getTime()
493  {
494  /* The current date, as a string. */
495  date_default_timezone_set('UTC');
496  return date('Y-m-d\\TH:i:s\\Z');
497  }
498 }
static writeFile($filename, $data, $mode=0600)
Atomically write a file.
Definition: System.php:183
writeState()
This function writes the state array back to disk.
Definition: MetaLoader.php:282
$context
Definition: webdav.php:25
$config
Definition: bootstrap.php:15
$template
$type
__construct($expire=null, $stateFile=null, $oldMetadataSrc=null)
Constructor.
Definition: MetaLoader.php:30
static debug($string)
Definition: Logger.php:211
dumpMetadataStdOut()
This function writes the metadata to stdout.
Definition: MetaLoader.php:300
loadXML($data, $source)
Parse XML metadata and return entities.
Definition: MetaLoader.php:265
addMetadata($filename, $metadata, $type, $template=null)
This function adds metadata from the specified file to the list of metadata.
Definition: MetaLoader.php:330
static info($string)
Definition: Logger.php:199
createContext($source)
Create HTTP context, with any available caches taken into account.
Definition: MetaLoader.php:195
static warning($string)
Definition: Logger.php:177
loadSource($source)
This function processes a SAML metadata file.
Definition: MetaLoader.php:79
static fetch($url, $context=array(), $getHeaders=false)
Helper function to retrieve a file or URL with proxy support, also supporting proxy basic authorizati...
Definition: HTTP.php:408
$filename
Definition: buildRTE.php:89
getTypes()
Get the types of entities that will be loaded.
Definition: MetaLoader.php:53
setTypes($types)
Set the types of entities that will be loaded.
Definition: MetaLoader.php:65
saveState($source, $responseHeaders)
Store caching state data for a source.
Definition: MetaLoader.php:240
$source
Definition: linkback.php:22
static parseDescriptorsElement(DOMElement $element=null)
This function parses a DOMElement which represents either an EntityDescriptor element or an EntitiesD...
Definition: SAMLParser.php:341
static getInstance($instancename='simplesaml')
Get a configuration file by its instance name.
$data
Definition: bench.php:6