Select Git revision
redirect.admin.class.php
Hahn Axel (hahn) authored
redirect.admin.class.php 11.08 KiB
<?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>';
}
}