<?php namespace SimpleSAML\Utils; use SimpleSAML\Module; use SimpleSAML\Logger; /** * HTTP-related utility methods. * * @package SimpleSAMLphp */ class HTTP { /** * Obtain a URL where we can redirect to securely post a form with the given data to a specific destination. * * @param string $destination The destination URL. * @param array $data An associative array containing the data to be posted to $destination. * * @throws \SimpleSAML_Error_Exception If the current session is transient. * @return string A URL which allows to securely post a form to $destination. * * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ private static function getSecurePOSTRedirectURL($destination, $data) { $session = \SimpleSAML_Session::getSessionFromRequest(); $id = self::savePOSTData($session, $destination, $data); // get the session ID $session_id = $session->getSessionId(); if (is_null($session_id)) { // this is a transient session, it is pointless to continue throw new \SimpleSAML_Error_Exception('Cannot save POST data to a transient session.'); } // encrypt the session ID and the random ID $info = base64_encode(Crypto::aesEncrypt($session_id.':'.$id)); $url = Module::getModuleURL('core/postredirect.php', array('RedirInfo' => $info)); return preg_replace('#^https:#', 'http:', $url); } /** * Retrieve Host value from $_SERVER environment variables. * * @return string The current host name, including the port if needed. It will use localhost when unable to * determine the current host. * * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> */ private static function getServerHost() { if (array_key_exists('HTTP_HOST', $_SERVER)) { $current = $_SERVER['HTTP_HOST']; } elseif (array_key_exists('SERVER_NAME', $_SERVER)) { $current = $_SERVER['SERVER_NAME']; } else { // almost certainly not what you want, but... $current = 'localhost'; } if (strstr($current, ":")) { $decomposed = explode(":", $current); $port = array_pop($decomposed); if (!is_numeric($port)) { array_push($decomposed, $port); } $current = implode($decomposed, ":"); } return $current; } /** * Retrieve HTTPS status from $_SERVER environment variables. * * @return boolean True if the request was performed through HTTPS, false otherwise. * * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> */ public static function getServerHTTPS() { if (!array_key_exists('HTTPS', $_SERVER)) { // not an https-request return false; } if ($_SERVER['HTTPS'] === 'off') { // IIS with HTTPS off return false; } // otherwise, HTTPS will be non-empty return !empty($_SERVER['HTTPS']); } /** * Retrieve the port number from $_SERVER environment variables. * * @return string The port number prepended by a colon, if it is different than the default port for the protocol * (80 for HTTP, 443 for HTTPS), or an empty string otherwise. * * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> */ public static function getServerPort() { $default_port = self::getServerHTTPS() ? '443' : '80'; $port = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : $default_port; if ($port !== $default_port) { return ':'.$port; } return ''; } /** * This function redirects the user to the specified address. * * This function will use the "HTTP 303 See Other" redirection if the current request used the POST method and the * HTTP version is 1.1. Otherwise, a "HTTP 302 Found" redirection will be used. * * The function will also generate a simple web page with a clickable link to the target page. * * @param string $url The URL we should redirect to. This URL may include query parameters. If this URL is a * relative URL (starting with '/'), then it will be turned into an absolute URL by prefixing it with the * absolute URL to the root of the website. * @param string[] $parameters An array with extra query string parameters which should be appended to the URL. The * name of the parameter is the array index. The value of the parameter is the value stored in the index. Both * the name and the value will be urlencoded. If the value is NULL, then the parameter will be encoded as just * the name, without a value. * * @return void This function never returns. * @throws \InvalidArgumentException If $url is not a string or is empty, or $parameters is not an array. * * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> * @author Mads Freek Petersen * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ private static function redirect($url, $parameters = array()) { if (!is_string($url) || empty($url) || !is_array($parameters)) { throw new \InvalidArgumentException('Invalid input parameters.'); } if (!empty($parameters)) { $url = self::addURLParameters($url, $parameters); } /* Set the HTTP result code. This is either 303 See Other or * 302 Found. HTTP 303 See Other is sent if the HTTP version * is HTTP/1.1 and the request type was a POST request. */ if ($_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.1' && $_SERVER['REQUEST_METHOD'] === 'POST' ) { $code = 303; } else { $code = 302; } if (strlen($url) > 2048) { Logger::warning('Redirecting to a URL longer than 2048 bytes.'); } if (!headers_sent()) { // set the location header header('Location: '.$url, true, $code); // disable caching of this response header('Pragma: no-cache'); header('Cache-Control: no-cache, no-store, must-revalidate'); } // show a minimal web page with a clickable link to the URL echo '<?xml version="1.0" encoding="UTF-8"?>'."\n"; echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"'; echo ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'."\n"; echo '<html xmlns="http://www.w3.org/1999/xhtml">'."\n"; echo " <head>\n"; echo ' <meta http-equiv="content-type" content="text/html; charset=utf-8">'."\n"; echo ' <meta http-equiv="refresh" content="0;URL=\''.htmlspecialchars($url).'\'">'."\n"; echo " <title>Redirect</title>\n"; echo " </head>\n"; echo " <body>\n"; echo " <h1>Redirect</h1>\n"; echo ' <p>You were redirected to: <a id="redirlink" href="'.htmlspecialchars($url).'">'; echo htmlspecialchars($url)."</a>\n"; echo ' <script type="text/javascript">document.getElementById("redirlink").focus();</script>'."\n"; echo " </p>\n"; echo " </body>\n"; echo '</html>'; // end script execution exit; } /** * Save the given HTTP POST data and the destination where it should be posted to a given session. * * @param \SimpleSAML_Session $session The session where to temporarily store the data. * @param string $destination The destination URL where the form should be posted. * @param array $data An associative array with the data to be posted to $destination. * * @return string A random identifier that can be used to retrieve the data from the current session. * * @author Andjelko Horvat * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ private static function savePOSTData(\SimpleSAML_Session $session, $destination, $data) { // generate a random ID to avoid replay attacks $id = Random::generateID(); $postData = array( 'post' => $data, 'url' => $destination, ); // save the post data to the session, tied to the random ID $session->setData('core_postdatalink', $id, $postData); return $id; } /** * Add one or more query parameters to the given URL. * * @param string $url The URL the query parameters should be added to. * @param array $parameters The query parameters which should be added to the url. This should be an associative * array. * * @return string The URL with the new query parameters. * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array. * * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> */ public static function addURLParameters($url, $parameters) { if (!is_string($url) || !is_array($parameters)) { throw new \InvalidArgumentException('Invalid input parameters.'); } $queryStart = strpos($url, '?'); if ($queryStart === false) { $oldQuery = array(); $url .= '?'; } else { /** @var string|false $oldQuery */ $oldQuery = substr($url, $queryStart + 1); if ($oldQuery === false) { $oldQuery = array(); } else { $oldQuery = self::parseQueryString($oldQuery); } $url = substr($url, 0, $queryStart + 1); } /** @var array $oldQuery */ $query = array_merge($oldQuery, $parameters); $url .= http_build_query($query, '', '&'); return $url; } /** * Check for session cookie, and show missing-cookie page if it is missing. * * @param string|null $retryURL The URL the user should access to retry the operation. Defaults to null. * * @return void If there is a session cookie, nothing will be returned. Otherwise, the user will be redirected to a * page telling about the missing cookie. * @throws \InvalidArgumentException If $retryURL is neither a string nor null. * * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> */ public static function checkSessionCookie($retryURL = null) { if (!is_null($retryURL) && !is_string($retryURL)) { throw new \InvalidArgumentException('Invalid input parameters.'); } $session = \SimpleSAML_Session::getSessionFromRequest(); if ($session->hasSessionCookie()) { return; } // we didn't have a session cookie. Redirect to the no-cookie page $url = Module::getModuleURL('core/no_cookie.php'); if ($retryURL !== null) { $url = self::addURLParameters($url, array('retryURL' => $retryURL)); } self::redirectTrustedURL($url); } /** * Check if a URL is valid and is in our list of allowed URLs. * * @param string $url The URL to check. * @param array $trustedSites An optional white list of domains. If none specified, the 'trusted.url.domains' * configuration directive will be used. * * @return string The normalized URL itself if it is allowed. An empty string if the $url parameter is empty as * defined by the empty() function. * @throws \InvalidArgumentException If the URL is malformed. * @throws \SimpleSAML_Error_Exception If the URL is not allowed by configuration. * * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ public static function checkURLAllowed($url, array $trustedSites = null) { if (empty($url)) { return ''; } $url = self::normalizeURL($url); if (filter_var($url, FILTER_VALIDATE_URL) === false) { throw new \SimpleSAML_Error_Exception('Invalid URL: '.$url); } // get the white list of domains if ($trustedSites === null) { $trustedSites = \SimpleSAML_Configuration::getInstance()->getValue('trusted.url.domains', array()); } // validates the URL's host is among those allowed if (is_array($trustedSites)) { assert(is_array($trustedSites)); $components = parse_url($url); $hostname = $components['host']; // check for userinfo if ((isset($components['user']) && strpos($components['user'], '\\') !== false) || (isset($components['pass']) && strpos($components['pass'], '\\') !== false) ) { throw new \SimpleSAML_Error_Exception('Invalid URL: '.$url); } // allow URLs with standard ports specified (non-standard ports must then be allowed explicitly) if (isset($components['port']) && (($components['scheme'] === 'http' && $components['port'] !== 80) || ($components['scheme'] === 'https' && $components['port'] !== 443)) ) { $hostname = $hostname.':'.$components['port']; } $self_host = self::getSelfHostWithNonStandardPort(); $trustedRegex = \SimpleSAML_Configuration::getInstance()->getValue('trusted.url.regex', false); $trusted = false; if ($trustedRegex) { // add self host to the white list $trustedSites[] = preg_quote($self_host); foreach ($trustedSites as $regex) { // Add start and end delimiters. $regex = "@^{$regex}$@"; if (preg_match($regex, $hostname)) { $trusted = true; break; } } } else { // add self host to the white list $trustedSites[] = $self_host; $trusted = in_array($hostname, $trustedSites, true); } // throw exception due to redirection to untrusted site if (!$trusted) { throw new \SimpleSAML_Error_Exception('URL not allowed: '.$url); } } return $url; } /** * Helper function to retrieve a file or URL with proxy support, also * supporting proxy basic authorization.. * * An exception will be thrown if we are unable to retrieve the data. * * @param string $url The path or URL we should fetch. * @param array $context Extra context options. This parameter is optional. * @param boolean $getHeaders Whether to also return response headers. Optional. * * @return string|array An array if $getHeaders is set, containing the data and the headers respectively; string * otherwise. * @throws \InvalidArgumentException If the input parameters are invalid. * @throws \SimpleSAML_Error_Exception If the file or URL cannot be retrieved. * * @author Andjelko Horvat * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> * @author Marco Ferrante, University of Genova <marco@csita.unige.it> */ public static function fetch($url, $context = array(), $getHeaders = false) { if (!is_string($url)) { throw new \InvalidArgumentException('Invalid input parameters.'); } $config = \SimpleSAML_Configuration::getInstance(); $proxy = $config->getString('proxy', null); if ($proxy !== null) { if (!isset($context['http']['proxy'])) { $context['http']['proxy'] = $proxy; } $proxy_auth = $config->getString('proxy.auth', false); if ($proxy_auth !== false) { $context['http']['header'] = "Proxy-Authorization: Basic".base64_encode($proxy_auth); } if (!isset($context['http']['request_fulluri'])) { $context['http']['request_fulluri'] = true; } /* * If the remote endpoint over HTTPS uses the SNI extension (Server Name Indication RFC 4366), the proxy * could introduce a mismatch between the names in the Host: HTTP header and the SNI_server_name in TLS * negotiation (thanks to Cristiano Valli @ GARR-IDEM to have pointed this problem). * See: https://bugs.php.net/bug.php?id=63519 * These controls will force the same value for both fields. * Marco Ferrante (marco@csita.unige.it), Nov 2012 */ if (preg_match('#^https#i', $url) && defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME ) { // extract the hostname $hostname = parse_url($url, PHP_URL_HOST); if (!empty($hostname)) { $context['ssl'] = array( 'SNI_server_name' => $hostname, 'SNI_enabled' => true, ); } else { Logger::warning('Invalid URL format or local URL used through a proxy'); } } } $context = stream_context_create($context); $data = @file_get_contents($url, false, $context); if ($data === false) { $error = error_get_last(); throw new \SimpleSAML_Error_Exception('Error fetching '.var_export($url, true).':'. (is_array($error) ? $error['message'] : 'no error available')); } // data and headers if ($getHeaders) { if (isset($http_response_header)) { $headers = array(); foreach ($http_response_header as $h) { if (preg_match('@^HTTP/1\.[01]\s+\d{3}\s+@', $h)) { $headers = array(); // reset $headers[0] = $h; continue; } $bits = explode(':', $h, 2); if (count($bits) === 2) { $headers[strtolower($bits[0])] = trim($bits[1]); } } } else { // no HTTP headers, probably a different protocol, e.g. file $headers = null; } return array($data, $headers); } return $data; } /** * This function parses the Accept-Language HTTP header and returns an associative array with each language and the * score for that language. If a language includes a region, then the result will include both the language with * the region and the language without the region. * * The returned array will be in the same order as the input. * * @return array An associative array with each language and the score for that language. * * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> */ public static function getAcceptLanguage() { if (!array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) { // no Accept-Language header, return an empty set return array(); } $languages = explode(',', strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE'])); $ret = array(); foreach ($languages as $l) { $opts = explode(';', $l); $l = trim(array_shift($opts)); // the language is the first element $q = 1.0; // iterate over all options, and check for the quality option foreach ($opts as $o) { $o = explode('=', $o); if (count($o) < 2) { // skip option with no value continue; } $name = trim($o[0]); $value = trim($o[1]); if ($name === 'q') { $q = (float) $value; } } // remove the old key to ensure that the element is added to the end unset($ret[$l]); // set the quality in the result $ret[$l] = $q; if (strpos($l, '-')) { // the language includes a region part // extract the language without the region $l = explode('-', $l); $l = $l[0]; // add this language to the result (unless it is defined already) if (!array_key_exists($l, $ret)) { $ret[$l] = $q; } } } return $ret; } /** * Try to guess the base SimpleSAMLphp path from the current request. * * This method offers just a guess, so don't rely on it. * * @return string The guessed base path that should correspond to the root installation of SimpleSAMLphp. */ public static function guessBasePath() { if (!array_key_exists('REQUEST_URI', $_SERVER) || !array_key_exists('SCRIPT_FILENAME', $_SERVER)) { return '/'; } // get the name of the current script $path = explode('/', $_SERVER['SCRIPT_FILENAME']); $script = array_pop($path); // get the portion of the URI up to the script, i.e.: /simplesaml/some/directory/script.php if (!preg_match('#^/(?:[^/]+/)*'.$script.'#', $_SERVER['REQUEST_URI'], $matches)) { return '/'; } $uri_s = explode('/', $matches[0]); $file_s = explode('/', $_SERVER['SCRIPT_FILENAME']); // compare both arrays from the end, popping elements matching out of them while ($uri_s[count($uri_s) - 1] === $file_s[count($file_s) - 1]) { array_pop($uri_s); array_pop($file_s); } // we are now left with the minimum part of the URI that does not match anything in the file system, use it return join('/', $uri_s).'/'; } /** * Retrieve the base URL of the SimpleSAMLphp installation. The URL will always end with a '/'. For example: * https://idp.example.org/simplesaml/ * * @return string The absolute base URL for the SimpleSAMLphp installation. * @throws \SimpleSAML\Error\CriticalConfigurationError If 'baseurlpath' has an invalid format. * * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> */ public static function getBaseURL() { $globalConfig = \SimpleSAML_Configuration::getInstance(); $baseURL = $globalConfig->getString('baseurlpath', 'simplesaml/'); if (preg_match('#^https?://.*/?$#D', $baseURL, $matches)) { // full URL in baseurlpath, override local server values return rtrim($baseURL, '/').'/'; } elseif ( (preg_match('#^/?([^/]?.*/)$#D', $baseURL, $matches)) || (preg_match('#^\*(.*)/$#D', $baseURL, $matches)) || ($baseURL === '') ) { // get server values $protocol = 'http'; $protocol .= (self::getServerHTTPS()) ? 's' : ''; $protocol .= '://'; $hostname = self::getServerHost(); $port = self::getServerPort(); $path = $globalConfig->getBasePath(); return $protocol.$hostname.$port.$path; } else { /* * Invalid 'baseurlpath'. We cannot recover from this, so throw a critical exception and try to be graceful * with the configuration. Use a guessed base path instead of the one provided. */ $c = $globalConfig->toArray(); $c['baseurlpath'] = self::guessBasePath(); throw new \SimpleSAML\Error\CriticalConfigurationError( 'Invalid value for \'baseurlpath\' in config.php. Valid format is in the form: '. '[(http|https)://(hostname|fqdn)[:port]]/[path/to/simplesaml/]. It must end with a \'/\'.', null, $c ); } } /** * Retrieve the first element of the URL path. * * @param boolean $trailingslash Whether to add a trailing slash to the element or not. Defaults to true. * * @return string The first element of the URL path, with an optional, trailing slash. * * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> */ public static function getFirstPathElement($trailingslash = true) { if (preg_match('|^/(.*?)/|', $_SERVER['SCRIPT_NAME'], $matches)) { return ($trailingslash ? '/' : '').$matches[1]; } return ''; } /** * Create a link which will POST data. * * @param string $destination The destination URL. * @param array $data The name-value pairs which will be posted to the destination. * * @return string A URL which can be accessed to post the data. * @throws \InvalidArgumentException If $destination is not a string or $data is not an array. * * @author Andjelko Horvat * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ public static function getPOSTRedirectURL($destination, $data) { if (!is_string($destination) || !is_array($data)) { throw new \InvalidArgumentException('Invalid input parameters.'); } $config = \SimpleSAML_Configuration::getInstance(); $allowed = $config->getBoolean('enable.http_post', false); if ($allowed && preg_match("#^http:#", $destination) && self::isHTTPS()) { // we need to post the data to HTTP $url = self::getSecurePOSTRedirectURL($destination, $data); } else { // post the data directly $session = \SimpleSAML_Session::getSessionFromRequest(); $id = self::savePOSTData($session, $destination, $data); $url = Module::getModuleURL('core/postredirect.php', array('RedirId' => $id)); } return $url; } /** * Retrieve our own host. * * E.g. www.example.com * * @return string The current host. * * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ public static function getSelfHost() { $decomposed = explode(':', self::getSelfHostWithNonStandardPort()); return array_shift($decomposed); } /** * Retrieve our own host, including the port in case the it is not standard for the protocol in use. That is port * 80 for HTTP and port 443 for HTTPS. * * E.g. www.example.com:8080 * * @return string The current host, followed by a colon and the port number, in case the port is not standard for * the protocol. * * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> */ public static function getSelfHostWithNonStandardPort() { $url = self::getBaseURL(); /** @var int $colon getBaseURL() will allways return a valid URL */ $colon = strpos($url, '://'); $start = $colon + 3; $length = strcspn($url, '/', $start); return substr($url, $start, $length); } /** * Retrieve our own host together with the URL path. Please note this function will return the base URL for the * current SP, as defined in the global configuration. * * @return string The current host (with non-default ports included) plus the URL path. * * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> */ public static function getSelfHostWithPath() { $baseurl = explode("/", self::getBaseURL()); $elements = array_slice($baseurl, 3 - count($baseurl), count($baseurl) - 4); $path = implode("/", $elements); return self::getSelfHostWithNonStandardPort()."/".$path; } /** * Retrieve the current URL using the base URL in the configuration, if possible. * * This method will try to see if the current script is part of SimpleSAMLphp. In that case, it will use the * 'baseurlpath' configuration option to rebuild the current URL based on that. If the current script is NOT * part of SimpleSAMLphp, it will just return the current URL. * * Note that this method does NOT make use of the HTTP X-Forwarded-* set of headers. * * @return string The current URL, including query parameters. * * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ public static function getSelfURL() { $cfg = \SimpleSAML_Configuration::getInstance(); $baseDir = $cfg->getBaseDir(); $cur_path = realpath($_SERVER['SCRIPT_FILENAME']); // make sure we got a string from realpath() $cur_path = is_string($cur_path) ? $cur_path : ''; // find the path to the current script relative to the www/ directory of SimpleSAMLphp $rel_path = str_replace($baseDir.'www'.DIRECTORY_SEPARATOR, '', $cur_path); // convert that relative path to an HTTP query $url_path = str_replace(DIRECTORY_SEPARATOR, '/', $rel_path); // find where the relative path starts in the current request URI $uri_pos = (!empty($url_path)) ? strpos($_SERVER['REQUEST_URI'], $url_path) : false; if ($cur_path == $rel_path || $uri_pos === false) { /* * We were accessed from an external script. This can happen in the following cases: * * - $_SERVER['SCRIPT_FILENAME'] points to a script that doesn't exist. E.g. functional testing. In this * case, realpath() returns false and str_replace an empty string, so we compare them loosely. * * - The URI requested does not belong to a script in the www/ directory of SimpleSAMLphp. In that case, * removing SimpleSAMLphp's base dir from the current path yields the same path, so $cur_path and * $rel_path are equal. * * - The request URI does not match the current script. Even if the current script is located in the www/ * directory of SimpleSAMLphp, the URI does not contain its relative path, and $uri_pos is false. * * It doesn't matter which one of those cases we have. We just know we can't apply our base URL to the * current URI, so we need to build it back from the PHP environment, unless we have a base URL specified * for this case in the configuration. First, check if that's the case. */ /** @var \SimpleSAML_Configuration $appcfg */ $appcfg = $cfg->getConfigItem('application', null); $appurl = ($appcfg instanceof \SimpleSAML_Configuration) ? $appcfg->getString('baseURL', '') : ''; if (!empty($appurl)) { $protocol = parse_url($appurl, PHP_URL_SCHEME); $hostname = parse_url($appurl, PHP_URL_HOST); $port = parse_url($appurl, PHP_URL_PORT); $port = !empty($port) ? ':'.$port : ''; } else { // no base URL specified for app, just use the current URL $protocol = 'http'; $protocol .= (self::getServerHTTPS()) ? 's' : ''; $hostname = self::getServerHost(); $port = self::getServerPort(); } return $protocol.'://'.$hostname.$port.$_SERVER['REQUEST_URI']; } return self::getBaseURL().$rel_path.substr($_SERVER['REQUEST_URI'], $uri_pos + strlen($url_path)); } /** * Retrieve the current URL using the base URL in the configuration, containing the protocol, the host and * optionally, the port number. * * @return string The current URL without path or query parameters. * * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> */ public static function getSelfURLHost() { $url = self::getSelfURL(); /** @var int $colon getBaseURL() will allways return a valid URL */ $colon = strpos($url, '://'); $start = $colon + 3; $length = strcspn($url, '/', $start) + $start; return substr($url, 0, $length); } /** * Retrieve the current URL using the base URL in the configuration, without the query parameters. * * @return string The current URL, not including query parameters. * * @author Andreas Solberg, UNINETT AS <andreas.solberg@uninett.no> * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ public static function getSelfURLNoQuery() { $url = self::getSelfURL(); $pos = strpos($url, '?'); if (!$pos) { return $url; } return substr($url, 0, $pos); } /** * This function checks if we are using HTTPS as protocol. * * @return boolean True if the HTTPS is used, false otherwise. * * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ public static function isHTTPS() { return strpos(self::getSelfURL(), 'https://') === 0; } /** * Normalizes a URL to an absolute URL and validate it. In addition to resolving the URL, this function makes sure * that it is a link to an http or https site. * * @param string $url The relative URL. * * @return string An absolute URL for the given relative URL. * @throws \InvalidArgumentException If $url is not a string or a valid URL. * * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ public static function normalizeURL($url) { if (!is_string($url)) { throw new \InvalidArgumentException('Invalid input parameters.'); } $url = self::resolveURL($url, self::getSelfURL()); // verify that the URL is to a http or https site if (!preg_match('@^https?://@i', $url)) { throw new \InvalidArgumentException('Invalid URL: '.$url); } return $url; } /** * Parse a query string into an array. * * This function parses a query string into an array, similar to the way the builtin 'parse_str' works, except it * doesn't handle arrays, and it doesn't do "magic quotes". * * Query parameters without values will be set to an empty string. * * @param string $query_string The query string which should be parsed. * * @return array The query string as an associative array. * @throws \InvalidArgumentException If $query_string is not a string. * * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> */ public static function parseQueryString($query_string) { if (!is_string($query_string)) { throw new \InvalidArgumentException('Invalid input parameters.'); } $res = array(); if (empty($query_string)) { return $res; } foreach (explode('&', $query_string) as $param) { $param = explode('=', $param); $name = urldecode($param[0]); if (count($param) === 1) { $value = ''; } else { $value = urldecode($param[1]); } $res[$name] = $value; } return $res; } /** * This function redirects to the specified URL without performing any security checks. Please, do NOT use this * function with user supplied URLs. * * This function will use the "HTTP 303 See Other" redirection if the current request used the POST method and the * HTTP version is 1.1. Otherwise, a "HTTP 302 Found" redirection will be used. * * The function will also generate a simple web page with a clickable link to the target URL. * * @param string $url The URL we should redirect to. This URL may include query parameters. If this URL is a * relative URL (starting with '/'), then it will be turned into an absolute URL by prefixing it with the absolute * URL to the root of the website. * @param string[] $parameters An array with extra query string parameters which should be appended to the URL. The * name of the parameter is the array index. The value of the parameter is the value stored in the index. Both the * name and the value will be urlencoded. If the value is NULL, then the parameter will be encoded as just the * name, without a value. * * @return void This function never returns. * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array. * * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ public static function redirectTrustedURL($url, $parameters = array()) { if (!is_string($url) || !is_array($parameters)) { throw new \InvalidArgumentException('Invalid input parameters.'); } $url = self::normalizeURL($url); self::redirect($url, $parameters); } /** * This function redirects to the specified URL after performing the appropriate security checks on it. * Particularly, it will make sure that the provided URL is allowed by the 'redirect.trustedsites' directive in the * configuration. * * If the aforementioned option is not set or the URL does correspond to a trusted site, it performs a redirection * to it. If the site is not trusted, an exception will be thrown. * * @param string $url The URL we should redirect to. This URL may include query parameters. If this URL is a * relative URL (starting with '/'), then it will be turned into an absolute URL by prefixing it with the absolute * URL to the root of the website. * @param string[] $parameters An array with extra query string parameters which should be appended to the URL. The * name of the parameter is the array index. The value of the parameter is the value stored in the index. Both the * name and the value will be urlencoded. If the value is NULL, then the parameter will be encoded as just the * name, without a value. * * @return void This function never returns. * @throws \InvalidArgumentException If $url is not a string or $parameters is not an array. * * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ public static function redirectUntrustedURL($url, $parameters = array()) { if (!is_string($url) || !is_array($parameters)) { throw new \InvalidArgumentException('Invalid input parameters.'); } $url = self::checkURLAllowed($url); self::redirect($url, $parameters); } /** * Resolve a (possibly relative) URL relative to a given base URL. * * This function supports these forms of relative URLs: * - ^\w+: Absolute URL. E.g. "http://www.example.com:port/path?query#fragment". * - ^// Same protocol. E.g. "//www.example.com:port/path?query#fragment" * - ^/ Same protocol and host. E.g. "/path?query#fragment". * - ^? Same protocol, host and path, replace query string & fragment. E.g. "?query#fragment". * - ^# Same protocol, host, path and query, replace fragment. E.g. "#fragment". * - The rest: Relative to the base path. * * @param string $url The relative URL. * @param string $base The base URL. Defaults to the base URL of this installation of SimpleSAMLphp. * * @return string An absolute URL for the given relative URL. * @throws \InvalidArgumentException If the base URL cannot be parsed into a valid URL, or the given parameters * are not strings. * * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ public static function resolveURL($url, $base = null) { if ($base === null) { $base = self::getBaseURL(); } if (!is_string($url) || !is_string($base)) { throw new \InvalidArgumentException('Invalid input parameters.'); } if (!preg_match('/^((((\w+:)\/\/[^\/]+)(\/[^?#]*))(?:\?[^#]*)?)(?:#.*)?/', $base, $baseParsed)) { throw new \InvalidArgumentException('Unable to parse base url: '.$base); } $baseDir = dirname($baseParsed[5].'filename'); $baseScheme = $baseParsed[4]; $baseHost = $baseParsed[3]; $basePath = $baseParsed[2]; $baseQuery = $baseParsed[1]; if (preg_match('$^\w+:$', $url)) { return $url; } if (substr($url, 0, 2) === '//') { return $baseScheme.$url; } if ($url[0] === '/') { return $baseHost.$url; } if ($url[0] === '?') { return $basePath.$url; } if ($url[0] === '#') { return $baseQuery.$url; } // we have a relative path. Remove query string/fragment and save it as $tail $queryPos = strpos($url, '?'); $fragmentPos = strpos($url, '#'); if ($queryPos !== false || $fragmentPos !== false) { if ($queryPos === false) { $tailPos = $fragmentPos; } elseif ($fragmentPos === false) { $tailPos = $queryPos; } elseif ($queryPos < $fragmentPos) { $tailPos = $queryPos; } else { $tailPos = $fragmentPos; } $tail = substr($url, $tailPos); $dir = substr($url, 0, $tailPos); } else { $dir = $url; $tail = ''; } $dir = System::resolvePath($dir, $baseDir); return $baseHost.$dir.$tail; } /** * Set a cookie. * * @param string $name The name of the cookie. * @param string|NULL $value The value of the cookie. Set to NULL to delete the cookie. * @param array|NULL $params Cookie parameters. * @param bool $throw Whether to throw exception if setcookie() fails. * * @throws \InvalidArgumentException If any parameter has an incorrect type. * @throws \SimpleSAML\Error\CannotSetCookie If the headers were already sent and the cookie cannot be set. * * @return void * * @author Andjelko Horvat * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ public static function setCookie($name, $value, $params = null, $throw = true) { if (!(is_string($name) && // $name must be a string (is_string($value) || is_null($value)) && // $value can be a string or null (is_array($params) || is_null($params)) && // $params can be an array or null is_bool($throw)) // $throw must be boolean ) { throw new \InvalidArgumentException('Invalid input parameters.'); } $default_params = array( 'lifetime' => 0, 'expire' => null, 'path' => '/', 'domain' => null, 'secure' => false, 'httponly' => true, 'raw' => false, ); if ($params !== null) { $params = array_merge($default_params, $params); } else { $params = $default_params; } // Do not set secure cookie if not on HTTPS if ($params['secure'] && !self::isHTTPS()) { if ($throw) { throw new \SimpleSAML\Error\CannotSetCookie( 'Setting secure cookie on plain HTTP is not allowed.', \SimpleSAML\Error\CannotSetCookie::SECURE_COOKIE ); } Logger::warning('Error setting cookie: setting secure cookie on plain HTTP is not allowed.'); return; } if ($value === null) { $expire = time() - 365 * 24 * 60 * 60; } elseif (isset($params['expire'])) { $expire = $params['expire']; } elseif ($params['lifetime'] === 0) { $expire = 0; } else { $expire = time() + $params['lifetime']; } if ($params['raw']) { $success = @setrawcookie( $name, $value, $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly'] ); } else { $success = @setcookie( $name, $value, $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly'] ); } if (!$success) { if ($throw) { throw new \SimpleSAML\Error\CannotSetCookie( 'Headers already sent.', \SimpleSAML\Error\CannotSetCookie::HEADERS_SENT ); } Logger::warning('Error setting cookie: headers already sent.'); } } /** * Submit a POST form to a specific destination. * * This function never returns. * * @param string $destination The destination URL. * @param array $data An associative array with the data to be posted to $destination. * * @throws \InvalidArgumentException If $destination is not a string or $data is not an array. * * @return void * * @author Olav Morken, UNINETT AS <olav.morken@uninett.no> * @author Andjelko Horvat * @author Jaime Perez, UNINETT AS <jaime.perez@uninett.no> */ public static function submitPOSTData($destination, $data) { if (!is_string($destination) || !is_array($data)) { throw new \InvalidArgumentException('Invalid input parameters.'); } $config = \SimpleSAML_Configuration::getInstance(); $allowed = $config->getBoolean('enable.http_post', false); if ($allowed && preg_match("#^http:#", $destination) && self::isHTTPS()) { // we need to post the data to HTTP self::redirect(self::getSecurePOSTRedirectURL($destination, $data)); } $p = new \SimpleSAML_XHTML_Template($config, 'post.php'); $p->data['destination'] = $destination; $p->data['post'] = $data; $p->show(); exit(0); } }