<?php /** * ---------------------------------------------------------------------- * _____ __ __ _ _____ _ _ _ * |_ _| \/ | | | __ \ | (_) | | * | | | \ / | | | |__) |___ __| |_ _ __ ___ ___| |_ * | | | |\/| | | | _ // _ \/ _` | | '__/ _ \/ __| __| * _| |_| | | | |____ | | \ \ __/ (_| | | | | __/ (__| |_ * |_____|_| |_|______| |_| \_\___|\__,_|_|_| \___|\___|\__| * * ---------------------------------------------------------------------- * Loads a config json from outside webroot and makes a 3xx redirect * if the definition exists * ---------------------------------------------------------------------- * errorcodes: * - code 500 - no config or statuscode is not supported * - code 404 - the redirect does not exist in the json file or is * incomplete (no target, no http status code) * ---------------------------------------------------------------------- * ah=<axel.hahn@iml.unibe.ch> * 2019-04-23 v1.0 ah first version (without tracking) * 2019-04-25 v1.1 ah added regex handling; added GET param "debugredirect" * 2019-04-25 v1.2 ah use REQUEST_URI (works on Win and Linux) * 2020-05-06 v1.3 ah added aliases for multiple domains with the same config * 2020-05-06 v1.4 ah rewrite as class * 2023-08-28 v1.5 ah fix loop over config with missing regex section. * 2024-10-03 v1.6 ah php8 only: typed variables */ /** * Description of redirect * * @author axel */ class redirect { // ---------------------------------------------------------------------- // CONFIG // ---------------------------------------------------------------------- /** * Flag: debug is enabled? * @var bool */ protected bool $bDebug = false; /** * configuration dir * @var string */ protected string $sConfigDir = __DIR__ . '/../../config'; /** * About message * @var string */ protected string $sAbout = 'IML redirect <small>v1.4</small>'; /** * Hostname * @var string */ protected string $sHostname = ''; /** * Request to handle and to redirect to aconfigured target url * @var string */ protected string $sRequest = ''; /** * Config file * @var */ protected string $sCfgfile = ''; /** * Config data for the current hostname * @var */ protected array $aConfig = []; /** * Redirect data for the current request * @var array */ protected array $aRedirect = []; /** * URL to the repository of this project * @var string */ public string $urlRepo = 'https://git-repo.iml.unibe.ch/iml-open-source/redirect-handler'; /** * Url to the docs * @var string */ public string $urlDocs = 'https://os-docs.iml.unibe.ch/redirect-handler/'; // ---------------------------------------------------------------------- // CONSTRUCTOR // ---------------------------------------------------------------------- /** * Constructor * @param string $sHostname hostname * @param string $sRequest request to proces * @return void */ public function __constructor(string $sHostname = '', string $sRequest = '') { if ($sHostname) { $this->setHost($sHostname); } if ($sRequest) { $this->setRequest($sRequest); } } // ---------------------------------------------------------------------- // DEBUG // ---------------------------------------------------------------------- /** * Write a debug message into the http response header * * @global boolean $bDebug flag: show debug infos? * @staticvar int $i counter * * @param string $sDebugMessage message to show * @return boolean */ protected function _wd(string $sDebugMessage): bool { if (!$this->bDebug) { return false; } static $i; $i++; header("X-DEBUG-$i: $sDebugMessage"); return true; } // ---------------------------------------------------------------------- // SET INTERNAL VARS // ---------------------------------------------------------------------- /** * Get a string with full path of a config file with aliases * @return string */ protected function _generateAliasfile(): string { return $this->sConfigDir . '/aliases.json'; } /** * Get a string with full path of a config file based on hostname * * @param string $sHostname hostname/ fqdn * @return string */ protected function _generateCfgfile(string $sHostname): string { return $this->sConfigDir . '/redirects_' . $sHostname . '.json'; } /** * Get Aliases from aliases.json * It returns false if no alias was found * * @return bool|array */ protected function _getAliases(): bool|array { $sAliasfile = $this->_generateAliasfile(); $this->_wd('check alias file ' . $sAliasfile); if (!file_exists($sAliasfile)) { $this->_wd('alias do not exist'); // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 01'); return false; } $aAliases = json_decode(file_get_contents($sAliasfile), 1); return $aAliases; } /** * Get an array with redirect config based on a given hostname. * @return bool */ protected function _getConfig(): bool { $this->aConfig = []; $this->aRedirect = []; $this->_getEffectiveConfigfile(); if ($this->sCfgfile) { $aConfig = json_decode(file_get_contents($this->sCfgfile), 1); if (!is_array($aConfig) || !count($aConfig)) { $this->_wd('no config available'); // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 02'); } else { $this->aConfig = $aConfig; } } return true; } /** * Get filename of config file based on a given hostname * or detection in the alias config * * @param string $sHostname hostname * @param boolean $bAbort flag: true = do not scan aliases (used for loop detection) * @return bool|string */ protected function _getEffectiveConfigfile(string $sHostname = '', bool $bAbort = false): bool|string { if (!$sHostname) { $sHostname = $this->sHostname; } $this->sCfgfile = false; $sCfgfile = $this->_generateCfgfile($sHostname); $this->_wd('check config ' . $sCfgfile); if (!file_exists($sCfgfile)) { if ($bAbort) { // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 01'); return false; } $aAliases = $this->_getAliases(); $this->_wd('checking aliases ' . print_r($aAliases, 1)); if (!isset($aAliases[$sHostname]) || !file_exists($this->_generateCfgfile($aAliases[$sHostname]))) { $this->_wd('sorry no valid alias for ' . $sHostname); // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 01'); return false; } // remark: with abort flag return $this->_getEffectiveConfigfile($aAliases[$this->sHostname], 1); } $this->sCfgfile = $sCfgfile; return $sCfgfile; } /** * Enable/ disable debug * @param boolean $bEnable */ public function setDebug(bool $bEnable): bool { $this->bDebug = !!$bEnable; return true; } /** * Set hostname; internally it detects the config too * @param string $sHostname * @return boolean */ public function setHost(string $sHostname): bool { $this->sHostname = $sHostname; $this->_getConfig(); return true; } /** * Set the request * @param string $sRequest * @return boolean */ public function setRequest(string $sRequest): bool { $this->sRequest = $sRequest; $this->aRedirect = false; return true; } // ---------------------------------------------------------------------- // FUNCTIONS - GET REDIRECT // ---------------------------------------------------------------------- /** * Get an array with the matching redirect; it returns false if none was * detected * * @return array */ public function getRedirect(): array { if (is_array($this->aRedirect)) { return $this->aRedirect; } $aRedirect = []; // remark: // $this->aConfig is set in setHost() with $this->_getConfig(); if (isset($this->aConfig['direct'][$this->sRequest])) { $this->_wd("DIRECT MATCH"); $aRedirect = $this->aConfig['direct'][$this->sRequest]; } else { $this->_wd("no direct match ... scanning regex"); if (isset($this->aConfig['regex']) && is_array($this->aConfig['regex'])) { foreach (array_keys($this->aConfig['regex']) as $sRegex) { $this->_wd("check if regex [$sRegex] matches $this->sRequest"); if (preg_match('#' . $sRegex . '#', $this->sRequest)) { $this->_wd("REGEX MATCH! aborting tests"); $aRedirect = $this->aConfig['regex'][$sRegex]; break; } } } } $this->aRedirect = $aRedirect; return $aRedirect; } /** * Get 30x redirect code * @param integer $iNone fallback value if no redirect status code was found * @return integer */ public function getRedirectCode($iNone = 307): int { $aRedirect = $this->getRedirect(); return isset($aRedirect['code']) ? $aRedirect['code'] : $iNone; } /** * Get Target url of redirect * @return string */ public function getRedirectTarget(): string { $aRedirect = $this->getRedirect(); return $aRedirect['target'] ?? ''; } // ---------------------------------------------------------------------- // FUNCTIONS - SEND DATA // ---------------------------------------------------------------------- /** * make the redirect if it was found ... or a 404 * @return true */ public function makeRedirect(): bool { $sTarget = $this->getRedirectTarget(); if (!$sTarget) { $this->_wd('send a not found'); $this->sendBody(404, '<h1>404 Not found</h1>'); } else { $iCode = $this->getRedirectCode(); $this->_wd("Redirect with http status [$iCode] to new Location: $sTarget"); $this->sendHttpStatusheader($iCode); header("Location: " . $sTarget); } return true; } /** * send http status header * @param integer $iCode http code * @return boolean */ public function sendHttpStatusheader(int $iCode): bool { $aHeaders = [ 301 => 'Moved Permanently', 302 => 'Found', // (Moved Temporarily) 303 => 'See other', // redirect with GET 307 => 'Temporary Redirect', 308 => 'Permanent Redirect', 404 => 'Not found', 410 => 'Gone', 500 => 'Internal Server Error', ]; $iCode = (int) $iCode; if (!isset($aHeaders[$iCode])) { $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 03: ' . $iCode); } header("HTTP/1.0 $iCode " . $aHeaders[$iCode]); return true; } /** * send http header for given status code, show document and exit * @see sendHttpStatusheader() * * @param integer $iCode http status code * @param string $sBody message text as html code * @return void */ public function sendBody(int $iCode, string $sBody): void { $this->sendHttpStatusheader($iCode); die('<!doctype html><html><head>' . '<title>Redirect</title>' . '<style>' . 'body{background:#eee; background: linear-gradient(-10deg,#ccc,#eee,#ddd) fixed; color:#444; font-family: verdana,arial;}' . 'h1{color:#a44;font-size: 300%;border-bottom: 1px solid #fff;}' . 'h2{color:#ccc; color: rgba(0,0,0,0.1); font-size: 300%; position: absolute; right: 1em; bottom: 1em; text-align: right;}' . 'h2 small{font-size: 50%;}' . 'footer{background:#ccc; bottom: 1em; color:#666; position: absolute; padding: 1em; right: 1em; }' . '</style>' . '</head>' . '<body>' . $sBody . '<h2>' . $this->sAbout . '</h2>' . '<footer>© ' . date('Y') . ' ' . $this->sHostname . '</footer>' . '</body></html>' ); } }