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