<?php require_once 'redirect.class.php'; /** * ---------------------------------------------------------------------- * _____ __ __ _ _____ _ _ _ * |_ _| \/ | | | __ \ | (_) | | * | | | \ / | | | |__) |___ __| |_ _ __ ___ ___| |_ * | | | |\/| | | | _ // _ \/ _` | | '__/ _ \/ __| __| * _| |_| | | | |____ | | \ \ __/ (_| | | | | __/ (__| |_ * |_____|_| |_|______| |_| \_\___|\__,_|_|_| \___|\___|\__| * * ---------------------------------------------------------------------- * Loads a config json from outside webroot and makes a 3xx redirect * if the definition exists * ---------------------------------------------------------------------- * 2020-05-11 v1.4 ah rewrite as class * 2022-02-03 v1.5 ah add method isEnabled * 2022-05-23 v1.6 ah add http head check+render output; * 2022-05-31 v1.7 ah optical changes * 2023-08-28 v1.8 ah remove php warning if there is no config yet * 2024-10-04 v1.9 ah php8 only: typed variables * 2025-01-13 v1.10 ah fetch curl error * 2025-01-20 v1.11 ah Update infoblock for found redirects; handle and show cookies */ /** * Description of redirect * * @author axel */ class redirectadmin extends redirect { /** * Filename to collect cookies of a request * @var string */ protected string $_sCookiefile=''; /** * Get default curl options * @return array */ protected function _getCurlOptions(): array { $aReturn = [ CURLOPT_HEADER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_USERAGENT => strip_tags($this->_app . ' ' . $this->_version), CURLOPT_VERBOSE => false, CURLOPT_ENCODING => 'gzip, deflate', // to fetch encoding CURLOPT_HTTPHEADER => [ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language: en', 'DNT: 1', ], CURLOPT_TIMEOUT => 5, ]; return $aReturn; } /** * Make a single http(s) get request and return an array with response header, body, curl error info * @param string $url url to fetch * @param boolean $bHeaderOnly optional: true=make HEAD request; default: false (=GET) * @return array */ public function httpGet(string $url, bool $bHeaderOnly = false): array { $aResult = []; $ch = curl_init($url); foreach ($this->_getCurlOptions() as $sCurlOption => $sCurlValue) { curl_setopt($ch, $sCurlOption, $sCurlValue); } // handle cookies $this->_sCookiefile = sys_get_temp_dir() . '/redirect_admin__found_cookies__' . md5($url) . '.txt'; if(file_exists($this->_sCookiefile)){ unlink($this->_sCookiefile); } curl_setopt($ch, CURLOPT_COOKIEFILE, $this->_sCookiefile); curl_setopt($ch, CURLOPT_COOKIEJAR, $this->_sCookiefile); if ($bHeaderOnly) { curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_NOBODY, 1); } $res = curl_exec($ch); $sHeader = ''; $sBody = ''; if($bHeaderOnly){ $sHeader = $res; $sBody = ''; } else { $aResponse = explode("\r\n\r\n", $res, 2); list($sHeader, $sBody) = count($aResponse) > 1 ? $aResponse : [$aResponse[0], '']; } $aResult = [ 'url' => $url, 'response_header' => $sHeader, 'response_body' => $sBody, // 'curlinfo' => curl_getinfo($ch), 'curlerrorcode' => curl_errno($ch), 'curlerrormsg' => curl_error($ch), ]; curl_close($ch); return $aResult; } /** * Get html code to show found cookies. * The cookie file will be deleted too. * * @return string */ public function renderCookies(): string{ $sReturn = ''; $iCounter=0; if(file_exists($this->_sCookiefile)) { $lines = explode(PHP_EOL, file_get_contents($this->_sCookiefile)); foreach ($lines as $line) { if (substr_count($line, "\t") == 6) { $iCounter++; $sReturn.= $line . '<br>'; } } unlink($this->_sCookiefile); } return $sReturn ? "Found cookies: $iCounter<pre>$sReturn</pre>" : ''; } /** * Get html code for a response header of a request * * @param array $aResponse response array from httpGet() method * @return string */ public function renderHttpResponseHeader(array $aResponse): string { $sHeader=$aResponse['response_header']."\r\n\r\n".$aResponse['response_body']; // $sReturn.="<pre>".print_r($aResponse, 1)."</pre>"; $iJump = 0; $sBox = ''; $sReturn = ''; $aHosts = []; $aWebs = []; if ($aResponse['curlerrorcode']) { $sReturn .= '<br>' .'<span class="status status-error">Request failed.</span>' .'<pre>' .'Curl error #'.$aResponse['curlerrorcode'] .':<br>' . '<strong>'.$aResponse['curlerrormsg'].'</strong><br><br>' .'</pre>' .'🌐 <a href="https://curl.se/libcurl/c/libcurl-errors.html" target="_blank">Curl error codes</a>' ; } $sUrl=$aResponse['url']; foreach(explode("\r\n\r\n", $aResponse['response_header']."\r\n\r\n".$aResponse['response_body'] ) as $sBlock){ if(strlen($sBlock)){ $iJump++; // find http status preg_match('/HTTP\/.* ([0-9]*) /', $sBlock, $aTmp); // $sReturn.='<pre>'.print_r($aTmp, 1).'</pre>'; $iStatus=$aTmp[1] ?? '0'; $sStatus=''; if($iStatus>=200 && $iStatus<300){ $sStatus='status-ok'; } elseif($iStatus>=300 && $iStatus<400){ $sStatus='status-redirect'; } elseif($iStatus>=400 && $iStatus<500){ $sStatus='status-error'; } elseif($iStatus>=500 && $iStatus<600){ $sStatus='status-error'; } // find location preg_match('/Location: (.*)/i', $sBlock, $aTmp); $sNextUrl=$aTmp[1] ?? ''; // modify lines in block $sBlock = preg_replace('/(x-debug-.*)\\r/i', '<span class="debug">$1</span>', $sBlock); $sBlock = preg_replace('/(location:.*)\\r/i', '<span class="location">$1</span>', $sBlock); $sReturn.="<span class=\"status $sStatus\">$sUrl ... $iStatus</span><pre>$sBlock</pre>"; // $sReturn .= '<strong>'.$iJump.') HTTP status: '.$iStatus.' - '.$sUrl.'</strong><pre>'.$sBlock.'</pre>'; $sWebhost=preg_replace('/_$/', '', parse_url($sUrl, PHP_URL_HOST)); $sIp=$this->_getIp($sWebhost); $aHosts[$sIp]=1; $aWebs[$sWebhost]=1; $sBox.="<div class=\"box $sStatus\">" // .$sUrl.'<br>' .$sWebhost.'<br>' . $sIp .'</div>'; if($sNextUrl){ $sBox.= "<div class=\"redirectstatus\"><nobr> --- $iStatus ---> </nobr></div>"; $sUrl=$sNextUrl; } } } $iHops = $iJump-1; $sReturn = '<br>'.($iHops > 0 ? 'Found hops: <strong>' . $iHops . '</strong> ' . ($iHops > 1 ? '<span class="warning"> ⚠️ Verify your redirect to skip unneeded hops.</span><br>' .'The configured redirect is not the final url - it continues redirecting from there.' : '' ) . '<br>' .sprintf('Required webs to be online to reach the final url: <strong>%s</strong>; required number of hosts: <strong>%s</strong>', count($aWebs), count($aHosts)) . '<br><br>' . '<div class="allJumps">'.$sBox.'</div>' : '' ) . $this->renderCookies() . $sReturn; return $sReturn; } /** * Check if admin is enabled * * @return bool */ public function isEnabled(): bool { $sFile2Enable = __DIR__ . '/' . basename(__FILE__) . '_enabled.txt'; return file_exists($sFile2Enable); } /** * Get ip address of a given hostname as ip (string) or false * @return string|bool */ protected function _getIp(string $sHostname): string { $sIp = gethostbyname($sHostname); return $sIp === $sHostname ? '' : $sIp; } /** * Get an array with all config entries in all json files * including some error checking * * @return array */ public function getHosts(): array { $aReturn = []; $aErrors = []; foreach (glob($this->sConfigDir . '/redirects_*.json') as $sFilename) { $sMyHost = str_replace(['redirects_', '.json'], ['', ''], basename($sFilename)); $aReturn[$sMyHost] = [ 'type' => 'config', 'file' => $sFilename, 'ip' => $this->_getIp($sMyHost), 'aliases' => [], 'redirects' => json_decode(file_get_contents($sFilename), 1), ]; if (!$aReturn[$sMyHost]['ip']) { $aErrors[] = basename($sFilename) . ': The hostname was not found in DNS: ' . $sMyHost; } } $aAliases = $this->_getAliases(); if (is_array($aAliases) && count($aAliases)) { foreach ($aAliases as $sAlias => $sConfig) { if (isset($aReturn[$sAlias])) { $aErrors[] = "alias.json: A configuration for alias [$sAlias] is useless. There exists a file redirects_{$sAlias}.json (which has priority)."; } else { if (!isset($aReturn[$sConfig])) { $aErrors[] = "alias.json: [$sAlias] points to a non existing host [$sConfig] - a file redirects_$sConfig.yml does not exist."; } else { $aReturn[$sConfig]['aliases'][] = $sAlias; $aReturn[$sAlias] = [ 'type' => 'alias', 'target' => $sConfig, 'ip' => $this->_getIp($sAlias), ]; if (!$aReturn[$sAlias]['ip']) { $aErrors[] = 'alias.json: The hostname was not found in DNS: ' . $sAlias; } } } } } $aReturn['_errors'] = $aErrors; ksort($aReturn); return $aReturn; } function getTitle(): string { return 'Redirect :: admin <small>'.$this->_version.'</small>'; } }