1<?php 2namespace SimpleSAML\Utils; 3 4use SimpleSAML\Module; 5use SimpleSAML\Logger; 6 7/** 8 * HTTP-related utility methods. 9 * 10 * @package SimpleSAMLphp 11 */ 12class HTTP 13{ 14 15 /** 16 * Obtain a URL where we can redirect to securely post a form with the given data to a specific destination. 17 * 18 * @param string $destination The destination URL. 19 * @param array $data An associative array containing the data to be posted to $destination. 20 * 21 * @throws \SimpleSAML_Error_Exception If the current session is transient. 22 * @return string A URL which allows to securely post a form to $destination. 23 * 24 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 25 */ 26 private static function getSecurePOSTRedirectURL($destination, $data) 27 { 28 $session = \SimpleSAML_Session::getSessionFromRequest(); 29 $id = self::savePOSTData($session, $destination, $data); 30 31 // get the session ID 32 $session_id = $session->getSessionId(); 33 if (is_null($session_id)) { 34 // this is a transient session, it is pointless to continue 35 throw new \SimpleSAML_Error_Exception('Cannot save POST data to a transient session.'); 36 } 37 38 // encrypt the session ID and the random ID 39 $info = base64_encode(Crypto::aesEncrypt($session_id.':'.$id)); 40 41 $url = Module::getModuleURL('core/postredirect.php', array('RedirInfo' => $info)); 42 return preg_replace('#^https:#', 'http:', $url); 43 } 44 45 46 /** 47 * Retrieve Host value from $_SERVER environment variables. 48 * 49 * @return string The current host name, including the port if needed. It will use localhost when unable to 50 * determine the current host. 51 * 52 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 53 */ 54 private static function getServerHost() 55 { 56 if (array_key_exists('HTTP_HOST', $_SERVER)) { 57 $current = $_SERVER['HTTP_HOST']; 58 } elseif (array_key_exists('SERVER_NAME', $_SERVER)) { 59 $current = $_SERVER['SERVER_NAME']; 60 } else { 61 // almost certainly not what you want, but... 62 $current = 'localhost'; 63 } 64 65 if (strstr($current, ":")) { 66 $decomposed = explode(":", $current); 67 $port = array_pop($decomposed); 68 if (!is_numeric($port)) { 69 array_push($decomposed, $port); 70 } 71 $current = implode(":", $decomposed); 72 } 73 return $current; 74 } 75 76 77 /** 78 * Retrieve HTTPS status from $_SERVER environment variables. 79 * 80 * @return boolean True if the request was performed through HTTPS, false otherwise. 81 * 82 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 83 */ 84 public static function getServerHTTPS() 85 { 86 if (!array_key_exists('HTTPS', $_SERVER)) { 87 // not an https-request 88 return false; 89 } 90 91 if ($_SERVER['HTTPS'] === 'off') { 92 // IIS with HTTPS off 93 return false; 94 } 95 96 // otherwise, HTTPS will be non-empty 97 return !empty($_SERVER['HTTPS']); 98 } 99 100 101 /** 102 * Retrieve the port number from $_SERVER environment variables. 103 * 104 * @return string The port number prepended by a colon, if it is different than the default port for the protocol 105 * (80 for HTTP, 443 for HTTPS), or an empty string otherwise. 106 * 107 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 108 */ 109 public static function getServerPort() 110 { 111 $default_port = self::getServerHTTPS() ? '443' : '80'; 112 $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : $default_port; 113 114 // Take care of edge-case where SERVER_PORT is an integer 115 $port = strval($port); 116 117 if ($port !== $default_port) { 118 return ':'.$port; 119 } 120 return ''; 121 } 122 123 124 /** 125 * This function redirects the user to the specified address. 126 * 127 * This function will use the "HTTP 303 See Other" redirection if the current request used the POST method and the 128 * HTTP version is 1.1. Otherwise, a "HTTP 302 Found" redirection will be used. 129 * 130 * The function will also generate a simple web page with a clickable link to the target page. 131 * 132 * @param string $url The URL we should redirect to. This URL may include query parameters. If this URL is a 133 * relative URL (starting with '/'), then it will be turned into an absolute URL by prefixing it with the 134 * absolute URL to the root of the website. 135 * @param string[] $parameters An array with extra query string parameters which should be appended to the URL. The 136 * name of the parameter is the array index. The value of the parameter is the value stored in the index. Both 137 * the name and the value will be urlencoded. If the value is NULL, then the parameter will be encoded as just 138 * the name, without a value. 139 * 140 * @return void This function never returns. 141 * @throws \InvalidArgumentException If $url is not a string or is empty, or $parameters is not an array. 142 * 143 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 144 * @author Mads Freek Petersen 145 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 146 */ 147 private static function redirect($url, $parameters = array()) 148 { 149 if (!is_string($url) || empty($url) || !is_array($parameters)) { 150 throw new \InvalidArgumentException('Invalid input parameters.'); 151 } 152 if (!empty($parameters)) { 153 $url = self::addURLParameters($url, $parameters); 154 } 155 156 /* Set the HTTP result code. This is either 303 See Other or 157 * 302 Found. HTTP 303 See Other is sent if the HTTP version 158 * is HTTP/1.1 and the request type was a POST request. 159 */ 160 if ($_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.1' && 161 $_SERVER['REQUEST_METHOD'] === 'POST' 162 ) { 163 $code = 303; 164 } else { 165 $code = 302; 166 } 167 168 if (strlen($url) > 2048) { 169 Logger::warning('Redirecting to a URL longer than 2048 bytes.'); 170 } 171 172 if (!headers_sent()) { 173 // set the location header 174 header('Location: '.$url, true, $code); 175 176 // disable caching of this response 177 header('Pragma: no-cache'); 178 header('Cache-Control: no-cache, no-store, must-revalidate'); 179 } 180 181 // show a minimal web page with a clickable link to the URL 182 echo '<?xml version="1.0" encoding="UTF-8"?>'."\n"; 183 echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'; 184 echo ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'."\n"; 185 echo '<html xmlns="http://www.w3.org/1999/xhtml">'."\n"; 186 echo " <head>\n"; 187 echo ' <meta http-equiv="content-type" content="text/html; charset=utf-8">'."\n"; 188 echo ' <meta http-equiv="refresh" content="0;URL=\''.htmlspecialchars($url).'\'">'."\n"; 189 echo " <title>Redirect</title>\n"; 190 echo " </head>\n"; 191 echo " <body>\n"; 192 echo " <h1>Redirect</h1>\n"; 193 echo ' <p>You were redirected to: <a id="redirlink" href="'.htmlspecialchars($url).'">'; 194 echo htmlspecialchars($url)."</a>\n"; 195 echo ' <script type="text/javascript">document.getElementById("redirlink").focus();</script>'."\n"; 196 echo " </p>\n"; 197 echo " </body>\n"; 198 echo '</html>'; 199 200 // end script execution 201 exit; 202 } 203 204 205 /** 206 * Save the given HTTP POST data and the destination where it should be posted to a given session. 207 * 208 * @param \SimpleSAML_Session $session The session where to temporarily store the data. 209 * @param string $destination The destination URL where the form should be posted. 210 * @param array $data An associative array with the data to be posted to $destination. 211 * 212 * @return string A random identifier that can be used to retrieve the data from the current session. 213 * 214 * @author Andjelko Horvat 215 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 216 */ 217 private static function savePOSTData(\SimpleSAML_Session $session, $destination, $data) 218 { 219 // generate a random ID to avoid replay attacks 220 $id = Random::generateID(); 221 $postData = array( 222 'post' => $data, 223 'url' => $destination, 224 ); 225 226 // save the post data to the session, tied to the random ID 227 $session->setData('core_postdatalink', $id, $postData); 228 229 return $id; 230 } 231 232 233 /** 234 * Add one or more query parameters to the given URL. 235 * 236 * @param string $url The URL the query parameters should be added to. 237 * @param array $parameters The query parameters which should be added to the url. This should be an associative 238 * array. 239 * 240 * @return string The URL with the new query parameters. 241 * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array. 242 * 243 * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> 244 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 245 */ 246 public static function addURLParameters($url, $parameters) 247 { 248 if (!is_string($url) || !is_array($parameters)) { 249 throw new \InvalidArgumentException('Invalid input parameters.'); 250 } 251 252 $queryStart = strpos($url, '?'); 253 if ($queryStart === false) { 254 $oldQuery = array(); 255 $url .= '?'; 256 } else { 257 /** @var string|false $oldQuery */ 258 $oldQuery = substr($url, $queryStart + 1); 259 if ($oldQuery === false) { 260 $oldQuery = array(); 261 } else { 262 $oldQuery = self::parseQueryString($oldQuery); 263 } 264 $url = substr($url, 0, $queryStart + 1); 265 } 266 267 /** @var array $oldQuery */ 268 $query = array_merge($oldQuery, $parameters); 269 $url .= http_build_query($query, '', '&'); 270 271 return $url; 272 } 273 274 275 /** 276 * Check for session cookie, and show missing-cookie page if it is missing. 277 * 278 * @param string|null $retryURL The URL the user should access to retry the operation. Defaults to null. 279 * 280 * @return void If there is a session cookie, nothing will be returned. Otherwise, the user will be redirected to a 281 * page telling about the missing cookie. 282 * @throws \InvalidArgumentException If $retryURL is neither a string nor null. 283 * 284 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 285 */ 286 public static function checkSessionCookie($retryURL = null) 287 { 288 if (!is_null($retryURL) && !is_string($retryURL)) { 289 throw new \InvalidArgumentException('Invalid input parameters.'); 290 } 291 292 $session = \SimpleSAML_Session::getSessionFromRequest(); 293 if ($session->hasSessionCookie()) { 294 return; 295 } 296 297 // we didn't have a session cookie. Redirect to the no-cookie page 298 299 $url = Module::getModuleURL('core/no_cookie.php'); 300 if ($retryURL !== null) { 301 $url = self::addURLParameters($url, array('retryURL' => $retryURL)); 302 } 303 self::redirectTrustedURL($url); 304 } 305 306 307 /** 308 * Check if a URL is valid and is in our list of allowed URLs. 309 * 310 * @param string $url The URL to check. 311 * @param array $trustedSites An optional white list of domains. If none specified, the 'trusted.url.domains' 312 * configuration directive will be used. 313 * 314 * @return string The normalized URL itself if it is allowed. An empty string if the $url parameter is empty as 315 * defined by the empty() function. 316 * @throws \InvalidArgumentException If the URL is malformed. 317 * @throws \SimpleSAML_Error_Exception If the URL is not allowed by configuration. 318 * 319 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 320 */ 321 public static function checkURLAllowed($url, array $trustedSites = null) 322 { 323 if (empty($url)) { 324 return ''; 325 } 326 $url = self::normalizeURL($url); 327 328 if (filter_var($url, FILTER_VALIDATE_URL) === false) { 329 throw new \SimpleSAML_Error_Exception('Invalid URL: '.$url); 330 } 331 332 // get the white list of domains 333 if ($trustedSites === null) { 334 $trustedSites = \SimpleSAML_Configuration::getInstance()->getValue('trusted.url.domains', array()); 335 } 336 337 // validates the URL's host is among those allowed 338 if (is_array($trustedSites)) { 339 assert(is_array($trustedSites)); 340 $components = parse_url($url); 341 $hostname = $components['host']; 342 343 // check for userinfo 344 if ((isset($components['user']) && strpos($components['user'], '\\') !== false) || 345 (isset($components['pass']) && strpos($components['pass'], '\\') !== false) 346 ) { 347 throw new \SimpleSAML_Error_Exception('Invalid URL: '.$url); 348 } 349 350 // allow URLs with standard ports specified (non-standard ports must then be allowed explicitly) 351 if (isset($components['port']) && 352 (($components['scheme'] === 'http' && $components['port'] !== 80) || 353 ($components['scheme'] === 'https' && $components['port'] !== 443)) 354 ) { 355 $hostname = $hostname.':'.$components['port']; 356 } 357 358 $self_host = self::getSelfHostWithNonStandardPort(); 359 360 $trustedRegex = \SimpleSAML_Configuration::getInstance()->getValue('trusted.url.regex', false); 361 362 $trusted = false; 363 if ($trustedRegex) { 364 // add self host to the white list 365 $trustedSites[] = preg_quote($self_host); 366 foreach ($trustedSites as $regex) { 367 // Add start and end delimiters. 368 $regex = "@^{$regex}$@"; 369 if (preg_match($regex, $hostname)) { 370 $trusted = true; 371 break; 372 } 373 } 374 } else { 375 // add self host to the white list 376 $trustedSites[] = $self_host; 377 $trusted = in_array($hostname, $trustedSites, true); 378 } 379 380 // throw exception due to redirection to untrusted site 381 if (!$trusted) { 382 throw new \SimpleSAML_Error_Exception('URL not allowed: '.$url); 383 } 384 } 385 return $url; 386 } 387 388 389 /** 390 * Helper function to retrieve a file or URL with proxy support, also 391 * supporting proxy basic authorization.. 392 * 393 * An exception will be thrown if we are unable to retrieve the data. 394 * 395 * @param string $url The path or URL we should fetch. 396 * @param array $context Extra context options. This parameter is optional. 397 * @param boolean $getHeaders Whether to also return response headers. Optional. 398 * 399 * @return string|array An array if $getHeaders is set, containing the data and the headers respectively; string 400 * otherwise. 401 * @throws \InvalidArgumentException If the input parameters are invalid. 402 * @throws \SimpleSAML_Error_Exception If the file or URL cannot be retrieved. 403 * 404 * @author Andjelko Horvat 405 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 406 * @author Marco Ferrante, University of Genova <marco@csita.unige.it> 407 */ 408 public static function fetch($url, $context = array(), $getHeaders = false) 409 { 410 if (!is_string($url)) { 411 throw new \InvalidArgumentException('Invalid input parameters.'); 412 } 413 414 $config = \SimpleSAML_Configuration::getInstance(); 415 416 $proxy = $config->getString('proxy', null); 417 if ($proxy !== null) { 418 if (!isset($context['http']['proxy'])) { 419 $context['http']['proxy'] = $proxy; 420 } 421 $proxy_auth = $config->getString('proxy.auth', false); 422 if ($proxy_auth !== false) { 423 $context['http']['header'] = "Proxy-Authorization: Basic ".base64_encode($proxy_auth); 424 } 425 if (!isset($context['http']['request_fulluri'])) { 426 $context['http']['request_fulluri'] = true; 427 } 428 /* 429 * If the remote endpoint over HTTPS uses the SNI extension (Server Name Indication RFC 4366), the proxy 430 * could introduce a mismatch between the names in the Host: HTTP header and the SNI_server_name in TLS 431 * negotiation (thanks to Cristiano Valli @ GARR-IDEM to have pointed this problem). 432 * See: https://bugs.php.net/bug.php?id=63519 433 * These controls will force the same value for both fields. 434 * Marco Ferrante (marco@csita.unige.it), Nov 2012 435 */ 436 if (preg_match('#^https#i', $url) 437 && defined('OPENSSL_TLSEXT_SERVER_NAME') 438 && OPENSSL_TLSEXT_SERVER_NAME 439 ) { 440 // extract the hostname 441 $hostname = parse_url($url, PHP_URL_HOST); 442 if (!empty($hostname)) { 443 $context['ssl'] = array( 444 'SNI_server_name' => $hostname, 445 'SNI_enabled' => true, 446 ); 447 } else { 448 Logger::warning('Invalid URL format or local URL used through a proxy'); 449 } 450 } 451 } 452 453 $context = stream_context_create($context); 454 $data = @file_get_contents($url, false, $context); 455 if ($data === false) { 456 $error = error_get_last(); 457 throw new \SimpleSAML_Error_Exception('Error fetching '.var_export($url, true).':'. 458 (is_array($error) ? $error['message'] : 'no error available')); 459 } 460 461 // data and headers 462 if ($getHeaders) { 463 if (isset($http_response_header)) { 464 $headers = array(); 465 foreach ($http_response_header as $h) { 466 if (preg_match('@^HTTP/1\.[01]\s+\d{3}\s+@', $h)) { 467 $headers = array(); // reset 468 $headers[0] = $h; 469 continue; 470 } 471 $bits = explode(':', $h, 2); 472 if (count($bits) === 2) { 473 $headers[strtolower($bits[0])] = trim($bits[1]); 474 } 475 } 476 } else { 477 // no HTTP headers, probably a different protocol, e.g. file 478 $headers = null; 479 } 480 return array($data, $headers); 481 } 482 483 return $data; 484 } 485 486 487 /** 488 * This function parses the Accept-Language HTTP header and returns an associative array with each language and the 489 * score for that language. If a language includes a region, then the result will include both the language with 490 * the region and the language without the region. 491 * 492 * The returned array will be in the same order as the input. 493 * 494 * @return array An associative array with each language and the score for that language. 495 * 496 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 497 */ 498 public static function getAcceptLanguage() 499 { 500 if (!array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) { 501 // no Accept-Language header, return an empty set 502 return array(); 503 } 504 505 $languages = explode(',', strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE'])); 506 507 $ret = array(); 508 509 foreach ($languages as $l) { 510 $opts = explode(';', $l); 511 512 $l = trim(array_shift($opts)); // the language is the first element 513 514 $q = 1.0; 515 516 // iterate over all options, and check for the quality option 517 foreach ($opts as $o) { 518 $o = explode('=', $o); 519 if (count($o) < 2) { 520 // skip option with no value 521 continue; 522 } 523 524 $name = trim($o[0]); 525 $value = trim($o[1]); 526 527 if ($name === 'q') { 528 $q = (float) $value; 529 } 530 } 531 532 // remove the old key to ensure that the element is added to the end 533 unset($ret[$l]); 534 535 // set the quality in the result 536 $ret[$l] = $q; 537 538 if (strpos($l, '-')) { 539 // the language includes a region part 540 541 // extract the language without the region 542 $l = explode('-', $l); 543 $l = $l[0]; 544 545 // add this language to the result (unless it is defined already) 546 if (!array_key_exists($l, $ret)) { 547 $ret[$l] = $q; 548 } 549 } 550 } 551 return $ret; 552 } 553 554 555 /** 556 * Try to guess the base SimpleSAMLphp path from the current request. 557 * 558 * This method offers just a guess, so don't rely on it. 559 * 560 * @return string The guessed base path that should correspond to the root installation of SimpleSAMLphp. 561 */ 562 public static function guessBasePath() 563 { 564 if (!array_key_exists('REQUEST_URI', $_SERVER) || !array_key_exists('SCRIPT_FILENAME', $_SERVER)) { 565 return '/'; 566 } 567 // get the name of the current script 568 $path = explode('/', $_SERVER['SCRIPT_FILENAME']); 569 $script = array_pop($path); 570 571 // get the portion of the URI up to the script, i.e.: /simplesaml/some/directory/script.php 572 if (!preg_match('#^/(?:[^/]+/)*'.$script.'#', $_SERVER['REQUEST_URI'], $matches)) { 573 return '/'; 574 } 575 $uri_s = explode('/', $matches[0]); 576 $file_s = explode('/', $_SERVER['SCRIPT_FILENAME']); 577 578 // compare both arrays from the end, popping elements matching out of them 579 while ($uri_s[count($uri_s) - 1] === $file_s[count($file_s) - 1]) { 580 array_pop($uri_s); 581 array_pop($file_s); 582 } 583 // we are now left with the minimum part of the URI that does not match anything in the file system, use it 584 return join('/', $uri_s).'/'; 585 } 586 587 588 /** 589 * Retrieve the base URL of the SimpleSAMLphp installation. The URL will always end with a '/'. For example: 590 * https://idp.example.org/simplesaml/ 591 * 592 * @return string The absolute base URL for the SimpleSAMLphp installation. 593 * @throws \SimpleSAML\Error\CriticalConfigurationError If 'baseurlpath' has an invalid format. 594 * 595 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 596 */ 597 public static function getBaseURL() 598 { 599 $globalConfig = \SimpleSAML_Configuration::getInstance(); 600 $baseURL = $globalConfig->getString('baseurlpath', 'simplesaml/'); 601 602 if (preg_match('#^https?://.*/?$#D', $baseURL, $matches)) { 603 // full URL in baseurlpath, override local server values 604 return rtrim($baseURL, '/').'/'; 605 } elseif ((preg_match('#^/?([^/]?.*/)$#D', $baseURL, $matches)) || 606 (preg_match('#^\*(.*)/$#D', $baseURL, $matches)) || 607 ($baseURL === '') 608 ) { 609 // get server values 610 $protocol = 'http'; 611 $protocol .= (self::getServerHTTPS()) ? 's' : ''; 612 $protocol .= '://'; 613 614 $hostname = self::getServerHost(); 615 $port = self::getServerPort(); 616 $path = $globalConfig->getBasePath(); 617 618 return $protocol.$hostname.$port.$path; 619 } else { 620 /* 621 * Invalid 'baseurlpath'. We cannot recover from this, so throw a critical exception and try to be graceful 622 * with the configuration. Use a guessed base path instead of the one provided. 623 */ 624 $c = $globalConfig->toArray(); 625 $c['baseurlpath'] = self::guessBasePath(); 626 throw new \SimpleSAML\Error\CriticalConfigurationError( 627 'Invalid value for \'baseurlpath\' in config.php. Valid format is in the form: '. 628 '[(http|https)://(hostname|fqdn)[:port]]/[path/to/simplesaml/]. It must end with a \'/\'.', 629 null, 630 $c 631 ); 632 } 633 } 634 635 636 /** 637 * Retrieve the first element of the URL path. 638 * 639 * @param boolean $trailingslash Whether to add a trailing slash to the element or not. Defaults to true. 640 * 641 * @return string The first element of the URL path, with an optional, trailing slash. 642 * 643 * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> 644 */ 645 public static function getFirstPathElement($trailingslash = true) 646 { 647 if (preg_match('|^/(.*?)/|', $_SERVER['SCRIPT_NAME'], $matches)) { 648 return ($trailingslash ? '/' : '').$matches[1]; 649 } 650 return ''; 651 } 652 653 654 /** 655 * Create a link which will POST data. 656 * 657 * @param string $destination The destination URL. 658 * @param array $data The name-value pairs which will be posted to the destination. 659 * 660 * @return string A URL which can be accessed to post the data. 661 * @throws \InvalidArgumentException If $destination is not a string or $data is not an array. 662 * 663 * @author Andjelko Horvat 664 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 665 */ 666 public static function getPOSTRedirectURL($destination, $data) 667 { 668 if (!is_string($destination) || !is_array($data)) { 669 throw new \InvalidArgumentException('Invalid input parameters.'); 670 } 671 672 $config = \SimpleSAML_Configuration::getInstance(); 673 $allowed = $config->getBoolean('enable.http_post', false); 674 675 if ($allowed && preg_match("#^http:#", $destination) && self::isHTTPS()) { 676 // we need to post the data to HTTP 677 $url = self::getSecurePOSTRedirectURL($destination, $data); 678 } else { // post the data directly 679 $session = \SimpleSAML_Session::getSessionFromRequest(); 680 $id = self::savePOSTData($session, $destination, $data); 681 $url = Module::getModuleURL('core/postredirect.php', array('RedirId' => $id)); 682 } 683 684 return $url; 685 } 686 687 688 /** 689 * Retrieve our own host. 690 * 691 * E.g. www.example.com 692 * 693 * @return string The current host. 694 * 695 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 696 */ 697 public static function getSelfHost() 698 { 699 $decomposed = explode(':', self::getSelfHostWithNonStandardPort()); 700 return array_shift($decomposed); 701 } 702 703 /** 704 * Retrieve our own host, including the port in case the it is not standard for the protocol in use. That is port 705 * 80 for HTTP and port 443 for HTTPS. 706 * 707 * E.g. www.example.com:8080 708 * 709 * @return string The current host, followed by a colon and the port number, in case the port is not standard for 710 * the protocol. 711 * 712 * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> 713 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 714 */ 715 public static function getSelfHostWithNonStandardPort() 716 { 717 $url = self::getBaseURL(); 718 719 /** @var int $colon getBaseURL() will allways return a valid URL */ 720 $colon = strpos($url, '://'); 721 $start = $colon + 3; 722 $length = strcspn($url, '/', $start); 723 724 return substr($url, $start, $length); 725 } 726 727 /** 728 * Retrieve our own host together with the URL path. Please note this function will return the base URL for the 729 * current SP, as defined in the global configuration. 730 * 731 * @return string The current host (with non-default ports included) plus the URL path. 732 * 733 * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> 734 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 735 */ 736 public static function getSelfHostWithPath() 737 { 738 $baseurl = explode("/", self::getBaseURL()); 739 $elements = array_slice($baseurl, 3 - count($baseurl), count($baseurl) - 4); 740 $path = implode("/", $elements); 741 return self::getSelfHostWithNonStandardPort()."/".$path; 742 } 743 744 745 /** 746 * Retrieve the current URL using the base URL in the configuration, if possible. 747 * 748 * This method will try to see if the current script is part of SimpleSAMLphp. In that case, it will use the 749 * 'baseurlpath' configuration option to rebuild the current URL based on that. If the current script is NOT 750 * part of SimpleSAMLphp, it will just return the current URL. 751 * 752 * Note that this method does NOT make use of the HTTP X-Forwarded-* set of headers. 753 * 754 * @return string The current URL, including query parameters. 755 * 756 * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> 757 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 758 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 759 */ 760 public static function getSelfURL() 761 { 762 $cfg = \SimpleSAML_Configuration::getInstance(); 763 $baseDir = $cfg->getBaseDir(); 764 $cur_path = realpath($_SERVER['SCRIPT_FILENAME']); 765 // make sure we got a string from realpath() 766 $cur_path = is_string($cur_path) ? $cur_path : ''; 767 // find the path to the current script relative to the www/ directory of SimpleSAMLphp 768 $rel_path = str_replace($baseDir.'www'.DIRECTORY_SEPARATOR, '', $cur_path); 769 // convert that relative path to an HTTP query 770 $url_path = str_replace(DIRECTORY_SEPARATOR, '/', $rel_path); 771 // find where the relative path starts in the current request URI 772 $uri_pos = (!empty($url_path)) ? strpos($_SERVER['REQUEST_URI'], $url_path) : false; 773 774 if ($cur_path == $rel_path || $uri_pos === false) { 775 /* 776 * We were accessed from an external script. This can happen in the following cases: 777 * 778 * - $_SERVER['SCRIPT_FILENAME'] points to a script that doesn't exist. E.g. functional testing. In this 779 * case, realpath() returns false and str_replace an empty string, so we compare them loosely. 780 * 781 * - The URI requested does not belong to a script in the www/ directory of SimpleSAMLphp. In that case, 782 * removing SimpleSAMLphp's base dir from the current path yields the same path, so $cur_path and 783 * $rel_path are equal. 784 * 785 * - The request URI does not match the current script. Even if the current script is located in the www/ 786 * directory of SimpleSAMLphp, the URI does not contain its relative path, and $uri_pos is false. 787 * 788 * It doesn't matter which one of those cases we have. We just know we can't apply our base URL to the 789 * current URI, so we need to build it back from the PHP environment, unless we have a base URL specified 790 * for this case in the configuration. First, check if that's the case. 791 */ 792 793 /** @var \SimpleSAML_Configuration $appcfg */ 794 $appcfg = $cfg->getConfigItem('application', null); 795 $appurl = ($appcfg instanceof \SimpleSAML_Configuration) ? $appcfg->getString('baseURL', '') : ''; 796 if (!empty($appurl)) { 797 $protocol = parse_url($appurl, PHP_URL_SCHEME); 798 $hostname = parse_url($appurl, PHP_URL_HOST); 799 $port = parse_url($appurl, PHP_URL_PORT); 800 $port = !empty($port) ? ':'.$port : ''; 801 } else { // no base URL specified for app, just use the current URL 802 $protocol = 'http'; 803 $protocol .= (self::getServerHTTPS()) ? 's' : ''; 804 $hostname = self::getServerHost(); 805 $port = self::getServerPort(); 806 } 807 return $protocol.'://'.$hostname.$port.$_SERVER['REQUEST_URI']; 808 } 809 810 return self::getBaseURL().$url_path.substr($_SERVER['REQUEST_URI'], $uri_pos + strlen($url_path)); 811 } 812 813 814 /** 815 * Retrieve the current URL using the base URL in the configuration, containing the protocol, the host and 816 * optionally, the port number. 817 * 818 * @return string The current URL without path or query parameters. 819 * 820 * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> 821 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 822 */ 823 public static function getSelfURLHost() 824 { 825 $url = self::getSelfURL(); 826 827 /** @var int $colon getBaseURL() will allways return a valid URL */ 828 $colon = strpos($url, '://'); 829 $start = $colon + 3; 830 $length = strcspn($url, '/', $start) + $start; 831 return substr($url, 0, $length); 832 } 833 834 835 /** 836 * Retrieve the current URL using the base URL in the configuration, without the query parameters. 837 * 838 * @return string The current URL, not including query parameters. 839 * 840 * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> 841 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 842 */ 843 public static function getSelfURLNoQuery() 844 { 845 $url = self::getSelfURL(); 846 $pos = strpos($url, '?'); 847 if (!$pos) { 848 return $url; 849 } 850 return substr($url, 0, $pos); 851 } 852 853 854 /** 855 * This function checks if we are using HTTPS as protocol. 856 * 857 * @return boolean True if the HTTPS is used, false otherwise. 858 * 859 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 860 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 861 */ 862 public static function isHTTPS() 863 { 864 return strpos(self::getSelfURL(), 'https://') === 0; 865 } 866 867 868 /** 869 * Normalizes a URL to an absolute URL and validate it. In addition to resolving the URL, this function makes sure 870 * that it is a link to an http or https site. 871 * 872 * @param string $url The relative URL. 873 * 874 * @return string An absolute URL for the given relative URL. 875 * @throws \InvalidArgumentException If $url is not a string or a valid URL. 876 * 877 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 878 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 879 */ 880 public static function normalizeURL($url) 881 { 882 if (!is_string($url)) { 883 throw new \InvalidArgumentException('Invalid input parameters.'); 884 } 885 886 $url = self::resolveURL($url, self::getSelfURL()); 887 888 // verify that the URL is to a http or https site 889 if (!preg_match('@^https?://@i', $url)) { 890 throw new \InvalidArgumentException('Invalid URL: '.$url); 891 } 892 893 return $url; 894 } 895 896 897 /** 898 * Parse a query string into an array. 899 * 900 * This function parses a query string into an array, similar to the way the builtin 'parse_str' works, except it 901 * doesn't handle arrays, and it doesn't do "magic quotes". 902 * 903 * Query parameters without values will be set to an empty string. 904 * 905 * @param string $query_string The query string which should be parsed. 906 * 907 * @return array The query string as an associative array. 908 * @throws \InvalidArgumentException If $query_string is not a string. 909 * 910 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 911 */ 912 public static function parseQueryString($query_string) 913 { 914 if (!is_string($query_string)) { 915 throw new \InvalidArgumentException('Invalid input parameters.'); 916 } 917 918 $res = array(); 919 if (empty($query_string)) { 920 return $res; 921 } 922 923 foreach (explode('&', $query_string) as $param) { 924 $param = explode('=', $param); 925 $name = urldecode($param[0]); 926 if (count($param) === 1) { 927 $value = ''; 928 } else { 929 $value = urldecode($param[1]); 930 } 931 $res[$name] = $value; 932 } 933 return $res; 934 } 935 936 937 /** 938 * This function redirects to the specified URL without performing any security checks. Please, do NOT use this 939 * function with user supplied URLs. 940 * 941 * This function will use the "HTTP 303 See Other" redirection if the current request used the POST method and the 942 * HTTP version is 1.1. Otherwise, a "HTTP 302 Found" redirection will be used. 943 * 944 * The function will also generate a simple web page with a clickable link to the target URL. 945 * 946 * @param string $url The URL we should redirect to. This URL may include query parameters. If this URL is a 947 * relative URL (starting with '/'), then it will be turned into an absolute URL by prefixing it with the absolute 948 * URL to the root of the website. 949 * @param string[] $parameters An array with extra query string parameters which should be appended to the URL. The 950 * name of the parameter is the array index. The value of the parameter is the value stored in the index. Both the 951 * name and the value will be urlencoded. If the value is NULL, then the parameter will be encoded as just the 952 * name, without a value. 953 * 954 * @return void This function never returns. 955 * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array. 956 * 957 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 958 */ 959 public static function redirectTrustedURL($url, $parameters = array()) 960 { 961 if (!is_string($url) || !is_array($parameters)) { 962 throw new \InvalidArgumentException('Invalid input parameters.'); 963 } 964 965 $url = self::normalizeURL($url); 966 self::redirect($url, $parameters); 967 } 968 969 970 /** 971 * This function redirects to the specified URL after performing the appropriate security checks on it. 972 * Particularly, it will make sure that the provided URL is allowed by the 'trusted.url.domains' directive in the 973 * configuration. 974 * 975 * If the aforementioned option is not set or the URL does correspond to a trusted site, it performs a redirection 976 * to it. If the site is not trusted, an exception will be thrown. 977 * 978 * @param string $url The URL we should redirect to. This URL may include query parameters. If this URL is a 979 * relative URL (starting with '/'), then it will be turned into an absolute URL by prefixing it with the absolute 980 * URL to the root of the website. 981 * @param string[] $parameters An array with extra query string parameters which should be appended to the URL. The 982 * name of the parameter is the array index. The value of the parameter is the value stored in the index. Both the 983 * name and the value will be urlencoded. If the value is NULL, then the parameter will be encoded as just the 984 * name, without a value. 985 * 986 * @return void This function never returns. 987 * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array. 988 * 989 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 990 */ 991 public static function redirectUntrustedURL($url, $parameters = array()) 992 { 993 if (!is_string($url) || !is_array($parameters)) { 994 throw new \InvalidArgumentException('Invalid input parameters.'); 995 } 996 997 $url = self::checkURLAllowed($url); 998 self::redirect($url, $parameters); 999 } 1000 1001 1002 /** 1003 * Resolve a (possibly relative) URL relative to a given base URL. 1004 * 1005 * This function supports these forms of relative URLs: 1006 * - ^\w+: Absolute URL. E.g. "http://www.example.com:port/path?query#fragment". 1007 * - ^// Same protocol. E.g. "//www.example.com:port/path?query#fragment" 1008 * - ^/ Same protocol and host. E.g. "/path?query#fragment". 1009 * - ^? Same protocol, host and path, replace query string & fragment. E.g. "?query#fragment". 1010 * - ^# Same protocol, host, path and query, replace fragment. E.g. "#fragment". 1011 * - The rest: Relative to the base path. 1012 * 1013 * @param string $url The relative URL. 1014 * @param string $base The base URL. Defaults to the base URL of this installation of SimpleSAMLphp. 1015 * 1016 * @return string An absolute URL for the given relative URL. 1017 * @throws \InvalidArgumentException If the base URL cannot be parsed into a valid URL, or the given parameters 1018 * are not strings. 1019 * 1020 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 1021 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 1022 */ 1023 public static function resolveURL($url, $base = null) 1024 { 1025 if ($base === null) { 1026 $base = self::getBaseURL(); 1027 } 1028 1029 if (!is_string($url) || !is_string($base)) { 1030 throw new \InvalidArgumentException('Invalid input parameters.'); 1031 } 1032 1033 if (!preg_match('/^((((\w+:)\/\/[^\/]+)(\/[^?#]*))(?:\?[^#]*)?)(?:#.*)?/', $base, $baseParsed)) { 1034 throw new \InvalidArgumentException('Unable to parse base url: '.$base); 1035 } 1036 1037 $baseDir = dirname($baseParsed[5].'filename'); 1038 $baseScheme = $baseParsed[4]; 1039 $baseHost = $baseParsed[3]; 1040 $basePath = $baseParsed[2]; 1041 $baseQuery = $baseParsed[1]; 1042 1043 if (preg_match('$^\w+:$', $url)) { 1044 return $url; 1045 } 1046 1047 if (substr($url, 0, 2) === '//') { 1048 return $baseScheme.$url; 1049 } 1050 1051 if ($url[0] === '/') { 1052 return $baseHost.$url; 1053 } 1054 if ($url[0] === '?') { 1055 return $basePath.$url; 1056 } 1057 if ($url[0] === '#') { 1058 return $baseQuery.$url; 1059 } 1060 1061 // we have a relative path. Remove query string/fragment and save it as $tail 1062 $queryPos = strpos($url, '?'); 1063 $fragmentPos = strpos($url, '#'); 1064 if ($queryPos !== false || $fragmentPos !== false) { 1065 if ($queryPos === false) { 1066 $tailPos = $fragmentPos; 1067 } elseif ($fragmentPos === false) { 1068 $tailPos = $queryPos; 1069 } elseif ($queryPos < $fragmentPos) { 1070 $tailPos = $queryPos; 1071 } else { 1072 $tailPos = $fragmentPos; 1073 } 1074 1075 $tail = substr($url, $tailPos); 1076 $dir = substr($url, 0, $tailPos); 1077 } else { 1078 $dir = $url; 1079 $tail = ''; 1080 } 1081 1082 $dir = System::resolvePath($dir, $baseDir); 1083 1084 return $baseHost.$dir.$tail; 1085 } 1086 1087 1088 /** 1089 * Set a cookie. 1090 * 1091 * @param string $name The name of the cookie. 1092 * @param string|NULL $value The value of the cookie. Set to NULL to delete the cookie. 1093 * @param array|NULL $params Cookie parameters. 1094 * @param bool $throw Whether to throw exception if setcookie() fails. 1095 * 1096 * @throws \InvalidArgumentException If any parameter has an incorrect type. 1097 * @throws \SimpleSAML\Error\CannotSetCookie If the headers were already sent and the cookie cannot be set. 1098 * 1099 * @return void 1100 * 1101 * @author Andjelko Horvat 1102 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 1103 */ 1104 public static function setCookie($name, $value, $params = null, $throw = true) 1105 { 1106 if (!(is_string($name) && // $name must be a string 1107 (is_string($value) || is_null($value)) && // $value can be a string or null 1108 (is_array($params) || is_null($params)) && // $params can be an array or null 1109 is_bool($throw)) // $throw must be boolean 1110 ) { 1111 throw new \InvalidArgumentException('Invalid input parameters.'); 1112 } 1113 1114 $default_params = array( 1115 'lifetime' => 0, 1116 'expire' => null, 1117 'path' => '/', 1118 'domain' => null, 1119 'secure' => false, 1120 'httponly' => true, 1121 'raw' => false, 1122 ); 1123 1124 if ($params !== null) { 1125 $params = array_merge($default_params, $params); 1126 } else { 1127 $params = $default_params; 1128 } 1129 1130 // Do not set secure cookie if not on HTTPS 1131 if ($params['secure'] && !self::isHTTPS()) { 1132 if ($throw) { 1133 throw new \SimpleSAML\Error\CannotSetCookie( 1134 'Setting secure cookie on plain HTTP is not allowed.', 1135 \SimpleSAML\Error\CannotSetCookie::SECURE_COOKIE 1136 ); 1137 } 1138 Logger::warning('Error setting cookie: setting secure cookie on plain HTTP is not allowed.'); 1139 return; 1140 } 1141 1142 if ($value === null) { 1143 $expire = time() - 365 * 24 * 60 * 60; 1144 } elseif (isset($params['expire'])) { 1145 $expire = $params['expire']; 1146 } elseif ($params['lifetime'] === 0) { 1147 $expire = 0; 1148 } else { 1149 $expire = time() + $params['lifetime']; 1150 } 1151 1152 if ($params['raw']) { 1153 $success = @setrawcookie( 1154 $name, 1155 $value, 1156 $expire, 1157 $params['path'], 1158 $params['domain'], 1159 $params['secure'], 1160 $params['httponly'] 1161 ); 1162 } else { 1163 $success = @setcookie( 1164 $name, 1165 $value, 1166 $expire, 1167 $params['path'], 1168 $params['domain'], 1169 $params['secure'], 1170 $params['httponly'] 1171 ); 1172 } 1173 1174 if (!$success) { 1175 if ($throw) { 1176 throw new \SimpleSAML\Error\CannotSetCookie( 1177 'Headers already sent.', 1178 \SimpleSAML\Error\CannotSetCookie::HEADERS_SENT 1179 ); 1180 } 1181 Logger::warning('Error setting cookie: headers already sent.'); 1182 } 1183 } 1184 1185 1186 /** 1187 * Submit a POST form to a specific destination. 1188 * 1189 * This function never returns. 1190 * 1191 * @param string $destination The destination URL. 1192 * @param array $data An associative array with the data to be posted to $destination. 1193 * 1194 * @throws \InvalidArgumentException If $destination is not a string or $data is not an array. 1195 * 1196 * @return void 1197 * 1198 * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> 1199 * @author Andjelko Horvat 1200 * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> 1201 */ 1202 public static function submitPOSTData($destination, $data) 1203 { 1204 if (!is_string($destination) || !is_array($data)) { 1205 throw new \InvalidArgumentException('Invalid input parameters.'); 1206 } 1207 1208 $config = \SimpleSAML_Configuration::getInstance(); 1209 $allowed = $config->getBoolean('enable.http_post', false); 1210 1211 if ($allowed && preg_match("#^http:#", $destination) && self::isHTTPS()) { 1212 // we need to post the data to HTTP 1213 self::redirect(self::getSecurePOSTRedirectURL($destination, $data)); 1214 } 1215 1216 $p = new \SimpleSAML_XHTML_Template($config, 'post.php'); 1217 $p->data['destination'] = $destination; 1218 $p->data['post'] = $data; 1219 $p->show(); 1220 exit(0); 1221 } 1222} 1223