N°3671 - app_root_url: handle reverse proxies during the setup and preserve existing configuration during an upgrade.

This commit is contained in:
bruno-ds
2021-03-03 11:53:50 +01:00
parent d4607ee815
commit 7b093a6bba
5 changed files with 274 additions and 41 deletions

View File

@@ -782,14 +782,29 @@ class utils
}
}
public static function IsProxyTrusted()
{
if (empty($_SERVER['REMOTE_ADDR'])) {
return false;
}
$bTrustProxies = (bool) self::GetConfig()->Get('trust_proxies');
return $bTrustProxies;
}
/**
* Returns the absolute URL to the application root path
*
* @param bool $bForceTrustProxy
*
* @return string The absolute URL to the application root, without the first slash
*
* @throws \Exception
*
* @since 2.7.4 $bForceTrustProxy param added
*/
public static function GetAbsoluteUrlAppRoot()
public static function GetAbsoluteUrlAppRoot($bForceTrustProxy = false)
{
static $sUrl = null;
if ($sUrl === null)
@@ -797,7 +812,7 @@ class utils
$sUrl = self::GetConfig()->Get('app_root_url');
if ($sUrl == '')
{
$sUrl = self::GetDefaultUrlAppRoot();
$sUrl = self::GetDefaultUrlAppRoot($bForceTrustProxy);
}
elseif (strpos($sUrl, SERVER_NAME_PLACEHOLDER) > -1)
{
@@ -821,40 +836,31 @@ class utils
* For most usages, when an root url is needed, use utils::GetAbsoluteUrlAppRoot() instead as uses this only as a fallback when the
* app_root_url conf parameter is not defined.
*
* @param bool $bForceTrustProxy
*
* @return string
*
* @throws \Exception
*/
public static function GetDefaultUrlAppRoot()
*
* @since 2.7.4 $bForceTrustProxy param added
*/
public static function GetDefaultUrlAppRoot($bForceTrustProxy = false)
{
// Build an absolute URL to this page on this server/port
$sServerName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '';
$sProtocol = self::IsConnectionSecure() ? 'https' : 'http';
$iPort = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80;
if ($sProtocol == 'http')
{
$sPort = ($iPort == 80) ? '' : ':'.$iPort;
}
else
$sServerName = self::GetServerName($bForceTrustProxy);
$bIsSecure = self::IsConnectionSecure($bForceTrustProxy) ;
$sProtocol = $bIsSecure ? 'https' : 'http';
$iPort = self::GetServerPort($bForceTrustProxy);
if ($bIsSecure)
{
$sPort = ($iPort == 443) ? '' : ':'.$iPort;
}
// $_SERVER['REQUEST_URI'] is empty when running on IIS
// Let's use Ivan Tcholakov's fix (found on www.dokeos.com)
if (!empty($_SERVER['REQUEST_URI']))
{
$sPath = $_SERVER['REQUEST_URI'];
}
else
{
$sPath = $_SERVER['SCRIPT_NAME'];
if (!empty($_SERVER['QUERY_STRING']))
{
$sPath .= '?'.$_SERVER['QUERY_STRING'];
}
$_SERVER['REQUEST_URI'] = $sPath;
$sPort = ($iPort == 80) ? '' : ':'.$iPort;
}
$sPath = $_SERVER['REQUEST_URI'];
$sPath = self::GetRequestUri($bForceTrustProxy);
// remove all the parameters from the query string
$iQuestionMarkPos = strpos($sPath, '?');
@@ -870,6 +876,61 @@ class utils
return self::GetAppRootUrl($sCurrentScript, $sAppRoot, $sAbsoluteUrl);
}
/**
* @param false $bForceTrustProxy
* @since 2.7.4
*/
public static function GetServerName($bForceTrustProxy = false)
{
$bTrustProxy = $bForceTrustProxy || self::IsProxyTrusted();
$sServerName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '';
if ($bTrustProxy) {
$sServerName = isset($_SERVER['X_FORWARDED_HOST']) ? $_SERVER['X_FORWARDED_HOST'] : $sServerName;
}
return $sServerName;
}
/**
* @param false $bForceTrustProxy
* @since 2.7.4
*/
public static function GetServerPort($bForceTrustProxy = false)
{
$bTrustProxy = $bForceTrustProxy || self::IsProxyTrusted();
$sServerPort = isset($_SERVER['SERVER_PORT']) ? $_SERVER['SERVER_PORT'] : 80;
if ($bTrustProxy) {
$sServerPort = isset($_SERVER['X_FORWARDED_PORT']) ? $_SERVER['X_FORWARDED_PORT'] : $sServerPort;
}
return $sServerPort;
}
/**
* @since 2.7.4
*/
public static function GetRequestUri()
{
// $_SERVER['REQUEST_URI'] is empty when running on IIS
// Let's use Ivan Tcholakov's fix (found on www.dokeos.com)
if (empty($_SERVER['REQUEST_URI']))
{
$sPath = $_SERVER['SCRIPT_NAME'];
if (!empty($_SERVER['QUERY_STRING']))
{
$sPath .= '?'.$_SERVER['QUERY_STRING'];
}
$_SERVER['REQUEST_URI'] = $sPath;
}
$sPath = $_SERVER['REQUEST_URI'];
return $sPath;
}
/**
* @param $sCurrentScript
* @param $sAppRoot
@@ -914,16 +975,33 @@ class utils
*
* Though the official specs says 'a non empty string', some servers like IIS do set it to 'off' !
* nginx set it to an empty string
* Others might leave it unset (no array entry)
* Others might leave it unset (no array entry)
*
* @param bool $bForceTrustProxy
*
* @return bool
*
* @since 2.7.4 reverse proxies handling
*/
public static function IsConnectionSecure()
public static function IsConnectionSecure($bForceTrustProxy = false)
{
$bSecured = false;
if (!empty($_SERVER['HTTPS']) && (strtolower($_SERVER['HTTPS']) != 'off'))
$bTrustProxy = $bForceTrustProxy || self::IsProxyTrusted();
if ($bTrustProxy && !empty($_SERVER['HTTP_X_FORWARDED_PROTO']))
{
$bSecured = ($_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
}
elseif ($bTrustProxy && !empty($_SERVER['HTTP_X_FORWARDED_PROTOCOL']))
{
$bSecured = ($_SERVER['HTTP_X_FORWARDED_PROTOCOL'] === 'https');
}
elseif ((!empty($_SERVER['HTTPS'])) && (strtolower($_SERVER['HTTPS']) != 'off'))
{
$bSecured = true;
}
return $bSecured;
}

View File

@@ -1265,6 +1265,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'trust_proxies' => [
'type' => 'bool',
'description' => 'If true, then proxies custom header (X-Forwarded-*) are taken into account. Use only if the webserver is not publicly accessible (reachable only by the reverse proxy)',
'default' => false,
'value' => false,
'source_of_value' => '',
'show_in_conf_sample' => false,
],
);

View File

@@ -144,7 +144,7 @@ class SetupPage extends NiceWebPage
public function output()
{
$sLogo = utils::GetAbsoluteUrlAppRoot().'/images/itop-logo.png';
$sLogo = utils::GetAbsoluteUrlAppRoot(true).'/images/itop-logo.png';
$this->s_content = "<div id=\"header\"><h1><a href=\"http://www.combodo.com/itop\" target=\"_blank\"><img title=\"iTop by Combodo\" alt=\" \" src=\"{$sLogo}?t=".utils::GetCacheBusterTimestamp()."\"></a>&nbsp;".htmlentities($this->s_title,
ENT_QUOTES, self::PAGES_CHARSET)."</h1>\n</div><div id=\"setup\">{$this->s_content}\n</div>\n";

View File

@@ -978,7 +978,7 @@ class WizStepMiscParams extends WizardStep
public function Display(WebPage $oPage)
{
$sDefaultLanguage = $this->oWizard->GetParameter('default_language', $this->oWizard->GetParameter('admin_language'));
$sApplicationURL = $this->oWizard->GetParameter('application_url', utils::GetDefaultUrlAppRoot());
$sApplicationURL = $this->oWizard->GetParameter('application_url', utils::GetDefaultUrlAppRoot(true));
$sDefaultGraphvizPath = (strtolower(substr(PHP_OS, 0, 3)) === 'win') ? 'C:\\Program Files\\Graphviz\\bin\\dot.exe' : '/usr/bin/dot';
$sGraphvizPath = $this->oWizard->GetParameter('graphviz_path', $sDefaultGraphvizPath);
$sSampleData = $this->oWizard->GetParameter('sample_data', 'yes');
@@ -996,14 +996,14 @@ class WizStepMiscParams extends WizardStep
$oPage->add('<fieldset>');
$oPage->add('<legend>Application URL</legend>');
$oPage->add('<table>');
$oPage->add('<tr><td>URL: </td><td><input id="application_url" name="application_url" type="text" size="35" maxlength="1024" value="'.htmlentities($sApplicationURL, ENT_QUOTES, 'UTF-8').'"><span id="v_application_url"/></td><tr>');
$oPage->add('<tr><td colspan="2">Change the value above if the end-users will be accessing the application by another path due to a specific configuration of the web server.</td><tr>');
$oPage->add('<tr><td>URL: </td><td><input id="application_url" name="application_url" type="text" size="35" maxlength="1024" value="'.htmlentities($sApplicationURL, ENT_QUOTES, 'UTF-8').'" style="width: 100%;box-sizing: border-box;"><span id="v_application_url"/></td><tr>');
$oPage->add('<tr><td colspan="2"><div class="message message-warning">Change the value above if the end-users will be accessing the application by another path due to a specific configuration of the web server.</div></td><tr>');
$oPage->add('</table>');
$oPage->add('</fieldset>');
$oPage->add('<fieldset>');
$oPage->add('<legend>Path to Graphviz\' dot application</legend>');
$oPage->add('<table style="width: 100%;">');
$oPage->add('<tr><td>Path: </td><td><input id="graphviz_path" name="graphviz_path" type="text" size="35" maxlength="1024" value="'.htmlentities($sGraphvizPath, ENT_QUOTES, 'UTF-8').'"><span id="v_graphviz_path"/></td><tr>');
$oPage->add('<tr><td>Path: </td><td><input id="graphviz_path" name="graphviz_path" type="text" size="35" maxlength="1024" value="'.htmlentities($sGraphvizPath, ENT_QUOTES, 'UTF-8').'" style="width: 100%;box-sizing: border-box;"><span id="v_graphviz_path"/></td><tr>');
$oPage->add('<tr><td colspan="2"><a href="http://www.graphviz.org" target="_blank">Graphviz</a> is required to display the impact analysis graph (i.e. impacts / depends on).</td><tr>');
$oPage->add('<tr><td colspan="2"><span id="graphviz_status"></span></td><tr>');
$oPage->add('</table>');
@@ -1125,21 +1125,21 @@ class WizStepUpgradeMiscParams extends WizardStep
public function Display(WebPage $oPage)
{
$sApplicationURL = $this->oWizard->GetParameter('application_url', utils::GetDefaultUrlAppRoot());
$sApplicationURL = $this->oWizard->GetParameter('application_url', utils::GetAbsoluteUrlAppRoot(true)); //Preserve existing configuration.
$sDefaultGraphvizPath = (strtolower(substr(PHP_OS, 0, 3)) === 'win') ? 'C:\\Program Files\\Graphviz\\bin\\dot.exe' : '/usr/bin/dot';
$sGraphvizPath = $this->oWizard->GetParameter('graphviz_path', $sDefaultGraphvizPath);
$oPage->add('<h2>Additional parameters</h2>');
$oPage->add('<fieldset>');
$oPage->add('<legend>Application URL</legend>');
$oPage->add('<table>');
$oPage->add('<tr><td>URL: </td><td><input id="application_url" name="application_url" type="text" size="35" maxlength="1024" value="'.htmlentities($sApplicationURL, ENT_QUOTES, 'UTF-8').'"><span id="v_application_url"/></td><tr>');
$oPage->add('<tr><td colspan="2">Change the value above if the end-users will be accessing the application by another path due to a specific configuration of the web server.</td><tr>');
$oPage->add('<tr><td>URL: </td><td><input id="application_url" name="application_url" type="text" size="35" maxlength="1024" value="'.htmlentities($sApplicationURL, ENT_QUOTES, 'UTF-8').'" style="width: 100%;box-sizing: border-box;"><span id="v_application_url"/></td><tr>');
$oPage->add('<tr><td colspan="2"><div class="message message-warning">Change the value above if the end-users will be accessing the application by another path due to a specific configuration of the web server.</div></td><tr>');
$oPage->add('</table>');
$oPage->add('</fieldset>');
$oPage->add('<fieldset>');
$oPage->add('<legend>Path to Graphviz\' dot application</legend>');
$oPage->add('<table style="width: 100%;">');
$oPage->add('<tr><td>Path: </td><td><input id="graphviz_path" name="graphviz_path" type="text" size="35" maxlength="1024" value="'.htmlentities($sGraphvizPath, ENT_QUOTES, 'UTF-8').'"><span id="v_graphviz_path"/></td><tr>');
$oPage->add('<tr><td>Path: </td><td><input id="graphviz_path" name="graphviz_path" type="text" size="35" maxlength="1024" value="'.htmlentities($sGraphvizPath, ENT_QUOTES, 'UTF-8').'" style="width: 100%;box-sizing: border-box;"><span id="v_graphviz_path"/></td><tr>');
$oPage->add('<tr><td colspan="2"><a href="http://www.graphviz.org" target="_blank">Graphviz</a> is required to display the impact analysis graph (i.e. impacts / depends on).</td><tr>');
$oPage->add('<tr><td colspan="2"><span id="graphviz_status"></span></td><tr>');
$oPage->add('</table>');
@@ -1376,14 +1376,14 @@ class WizStepModulesChoice extends WizardStep
if (substr($sBannerPath, 0, 1) == '/')
{
// absolute path, means relative to APPROOT
$sBannerUrl = utils::GetDefaultUrlAppRoot().$sBannerPath;
$sBannerUrl = utils::GetDefaultUrlAppRoot(true).$sBannerPath;
}
else
{
// relative path: i.e. relative to the directory containing the XML file
$sFullPath = dirname($this->GetSourceFilePath()).'/'.$sBannerPath;
$sRealPath = realpath($sFullPath);
$sBannerUrl = utils::GetDefaultUrlAppRoot().str_replace(realpath(APPROOT), '', $sRealPath);
$sBannerUrl = utils::GetDefaultUrlAppRoot(true).str_replace(realpath(APPROOT), '', $sRealPath);
}
$oPage->add('<td><img src="'.$sBannerUrl.'"/><td>');
}
@@ -2515,7 +2515,7 @@ class WizStepDone extends WizardStep
$aManualSteps = array();
$aAvailableModules = SetupUtils::AnalyzeInstallation($this->oWizard);
$sRootUrl = utils::GetAbsoluteUrlAppRoot();
$sRootUrl = utils::GetAbsoluteUrlAppRoot(true);
$aSelectedModules = json_decode($this->oWizard->GetParameter('selected_modules'), true);
foreach($aSelectedModules as $sModuleId)
{
@@ -2547,7 +2547,7 @@ class WizStepDone extends WizardStep
{
// To mitigate security risks: pass only the filename without the extension, the download will add the extension itself
$oPage->p('Your backup is ready');
$oPage->p('<a style="background:transparent;" href="'.utils::GetAbsoluteUrlAppRoot().'setup/ajax.dataloader.php?operation=async_action&step_class=WizStepDone&params[backup]='.urlencode($sBackupDestination).'&authent='.$this->oWizard->GetParameter('authent','').'" target="_blank"><img src="../images/tar.png" style="border:0;vertical-align:middle;">&nbsp;Download '.basename($sBackupDestination).'</a>');
$oPage->p('<a style="background:transparent;" href="'.utils::GetAbsoluteUrlAppRoot(true).'setup/ajax.dataloader.php?operation=async_action&step_class=WizStepDone&params[backup]='.urlencode($sBackupDestination).'&authent='.$this->oWizard->GetParameter('authent','').'" target="_blank"><img src="../images/tar.png" style="border:0;vertical-align:middle;">&nbsp;Download '.basename($sBackupDestination).'</a>');
}
else
{

View File

@@ -168,4 +168,151 @@ class UtilsTest extends \Combodo\iTop\Test\UnitTest\ItopTestCase
);
}
/**
* @dataProvider GetDefaultUrlAppRootProvider
*/
public function testGetDefaultUrlAppRoot($bForceTrustProxy, $bConfTrustProxy, $aServerVars, $sExpectedAppRootUrl)
{
$_SERVER = $aServerVars;
utils::GetConfig()->Set('trust_proxies', $bConfTrustProxy);
$sAppRootUrl = utils::GetDefaultUrlAppRoot($bForceTrustProxy);
$this->assertEquals($sExpectedAppRootUrl, $sAppRootUrl);
}
public function GetDefaultUrlAppRootProvider()
{
$this->setUp();
$baseServerVar = [
'REMOTE_ADDR' => '127.0.0.1', //is not set, disable IsProxyTrusted
'SERVER_NAME' => 'example.com',
'X_FORWARDED_HOST' => null,
'SERVER_PORT' => '80',
'X_FORWARDED_PORT' => null,
'REQUEST_URI' => '/index.php?baz=1',
'SCRIPT_NAME' => '/index.php',
'SCRIPT_FILENAME' => APPROOT.'index.php',
'QUERY_STRING' => 'baz=1',
'HTTP_X_FORWARDED_PROTO' => null,
'HTTP_X_FORWARDED_PROTOCOL' => null,
'HTTPS' => null,
];
return [
'no proxy, http' => [
'bForceTrustProxy' => false,
'bConfTrustProxy' => false,
'aServerVars' => array_merge($baseServerVar, []),
'sExpectedAppRootUrl' => 'http://example.com/',
],
'no proxy, subPath, http' => [
'bForceTrustProxy' => false,
'bConfTrustProxy' => false,
'aServerVars' => array_merge($baseServerVar, [
'REQUEST_URI' => '/foo/index.php?baz=1',
]),
'sExpectedAppRootUrl' => 'http://example.com/foo/',
],
'IIS lack REQUEST_URI' => [
'bForceTrustProxy' => false,
'bConfTrustProxy' => false,
'aServerVars' => array_merge($baseServerVar, [
'REQUEST_URI' => null,
'SCRIPT_NAME' => '/foo/index.php',
]),
'sExpectedAppRootUrl' => 'http://example.com/foo/',
],
'no proxy, https' => [
'bForceTrustProxy' => false,
'bConfTrustProxy' => false,
'aServerVars' => array_merge($baseServerVar, [
'SERVER_PORT' => '443',
'HTTPS' => 'on',
]),
'sExpectedAppRootUrl' => 'https://example.com/',
],
'no proxy, https on 4443' => [
'bForceTrustProxy' => false,
'bConfTrustProxy' => false,
'aServerVars' => array_merge($baseServerVar, [
'SERVER_PORT' => '4443',
'HTTPS' => 'on',
]),
'sExpectedAppRootUrl' => 'https://example.com:4443/',
],
'with proxy, not enabled' => [
'bForceTrustProxy' => false,
'bConfTrustProxy' => false,
'aServerVars' => array_merge($baseServerVar, [
'X_FORWARDED_HOST' => 'proxy.com',
'X_FORWARDED_PORT' => '4443',
'HTTP_X_FORWARDED_PROTO' => 'https',
]),
'sExpectedAppRootUrl' => 'http://example.com/',
],
'with proxy, enabled' => [
'bForceTrustProxy' => false,
'bConfTrustProxy' => true,
'aServerVars' => array_merge($baseServerVar, [
'X_FORWARDED_HOST' => 'proxy.com',
'X_FORWARDED_PORT' => '4443',
'HTTP_X_FORWARDED_PROTO' => 'https',
]),
'sExpectedAppRootUrl' => 'https://proxy.com:4443/',
],
'with proxy, enabled - alt' => [
'bForceTrustProxy' => false,
'bConfTrustProxy' => true,
'aServerVars' => array_merge($baseServerVar, [
'X_FORWARDED_HOST' => 'proxy.com',
'X_FORWARDED_PORT' => '4443',
'HTTP_X_FORWARDED_PROTOCOL' => 'https',
]),
'sExpectedAppRootUrl' => 'https://proxy.com:4443/',
],
'with proxy, disabled, forced' => [
'bForceTrustProxy' => true,
'bConfTrustProxy' => false,
'aServerVars' => array_merge($baseServerVar, [
'X_FORWARDED_HOST' => 'proxy.com',
'X_FORWARDED_PORT' => '4443',
'HTTP_X_FORWARDED_PROTO' => 'https',
]),
'sExpectedAppRootUrl' => 'https://proxy.com:4443/',
],
'with proxy, enabled, forced' => [
'bForceTrustProxy' => true,
'bConfTrustProxy' => true,
'aServerVars' => array_merge($baseServerVar, [
'X_FORWARDED_HOST' => 'proxy.com',
'X_FORWARDED_PORT' => '4443',
'HTTP_X_FORWARDED_PROTO' => 'https',
]),
'sExpectedAppRootUrl' => 'https://proxy.com:4443/',
],
'with proxy, disabled, forced, no remote addr' => [
'bForceTrustProxy' => true,
'bConfTrustProxy' => false,
'aServerVars' => array_merge($baseServerVar, [
'REMOTE_ADDR' => null,
'X_FORWARDED_HOST' => 'proxy.com',
'X_FORWARDED_PORT' => '4443',
'HTTP_X_FORWARDED_PROTO' => 'https',
]),
'sExpectedAppRootUrl' => 'https://proxy.com:4443/',
],
'with proxy, enabled, no remote addr' => [
'bForceTrustProxy' => false,
'bConfTrustProxy' => true,
'aServerVars' => array_merge($baseServerVar, [
'REMOTE_ADDR' => null,
'X_FORWARDED_HOST' => 'proxy.com',
'X_FORWARDED_PORT' => '4443',
'HTTP_X_FORWARDED_PROTO' => 'https',
]),
'sExpectedAppRootUrl' => 'http://example.com/',
],
];
}
}