Skip to content
Snippets Groups Projects
Commit 24b764cb authored by Hahn Axel (hahn)'s avatar Hahn Axel (hahn)
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
{
"sub.domain.example.com": "domain.example.com"
}
\ No newline at end of file
{
"comment": "Example redirect configuration file for domain.example.com",
"direct":{
"mail": {"code": 301, "target": "https://www.example.com/mail" }
},
"regex":{
".*": {"code": 307, "target": "https://www.example.com/" }
}
}
# IML Redirect #
Redirect urls of any domain that points here.
Author: Axel Hahn; Institute for Medical Education; University of Bern
License: GNUP GPL 3.0
## Requirements ##
* PHP 7+
* Webserver (docs describe usage for Apache httpd)
## Installation ##
Extract archive 1 level above webroot. The document root of the web must point
to the public_html directory. The config folder is outside the webroot.
Redirect all requests to the index.php. Activate the .htaccess or (better)
add the config of the htaccess file to the vhost config.
```text
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.*$ /index.php [L]
```
In the DNS point all hostnames with redirects only to this server (i.e. with
a CNAME).
If you don't have a single vhost in the webserver then additionally add the
domains to "catch" as ServerAlias.
## Configuration ##
In the config folder are json files. Copy the dist files to the same filename without ".dist"
* redirects_[FQDN].json
* aliases.json
### Redirects ###
There are 2 required sections to define redirects:
* direct
* regex
The section "direct" will be scanned first and has priority.
The json will be read into a hash ... if you define a direct rule twice then
the last rule will win.
If no direct rule matches the regex section will be scanned. Winner is the
first matching regex.
If no rule matches a 404 will be shown.
Hint: if you set regex ".*" as last entry it works as a catchall for unmatched
requests and you can define a default redirect target instead of showing a 404.
Both redirect section contain a redirect definition
* code - http status code for redirection
* target - target url of the redirect
Status codes
* 301 => 'Moved Permanently'; the url is outdated
* 307 => 'Temporary Redirect'; the url is valid
* 308 => 'Permanent Redirect'; the url is outdated
Suggestion for redirection lifecycle:
* an active redirect (i.e. a campaign) use code 307
* if the redirect has finished its life, switch the code to 308 or 301.
* remove the redirect (which results into 404)
Example:
```json
{
"direct":{
"heise": {"code": 307, "target": "https://www.heise.de" }
},
"regex":{
"^$": {"code": 307, "target": "https://www.iml.unibe.ch" },
"^ax.l.*": {"code": 307, "target": "https://www.axel-hahn.de" }
}
}
```
### Server aliases ###
If you have multiple domains with the same rules you can define aliases.
Example:
```json
{
"www.example.com": "example.com",
"zzz-domainname": "existing-host-with-redirect-config"
}
```
The key is the name of the alias. The value is a domain for that was written a
redirect_[hostname].json already.
The existance of a redirect config has higher priority than an entry in the
aliases config.
## Ideas ##
The current functionality is available with rewrite rules. Maybe in future
one of these features could be implmeneted:
* track request before redirecting to generate a statistic
* lifecycle of campaign short urls
* admin interface / just a viewer for all definitions
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^.*$ /index.php [L]
<?php
/**
* ----------------------------------------------------------------------
* _____ _ _ _ _ _
* | __ \ | (_) | | (_) | |
* | |__) |___ __| |_ _ __ ___ ___| |_ ___ ___ _ __ _ _ __ | |_
* | _ // _ \/ _` | | '__/ _ \/ __| __| / __|/ __| '__| | '_ \| __|
* | | \ \ __/ (_| | | | | __/ (__| |_ \__ \ (__| | | | |_) | |_
* |_| \_\___|\__,_|_|_| \___|\___|\__| |___/\___|_| |_| .__/ \__|
* | |
* |_|
* ----------------------------------------------------------------------
* admin: list all defined redirections
* ----------------------------------------------------------------------
*/
require_once 'classes/redirect.admin.class.php';
$oR=new redirectadmin();
$sHtml='';
// echo '<pre>'.print_r($oR->getHosts(), 1).'</pre>';
/*
[aum-biss.iml.unibe.ch] => Array
(
[type] => alias
[target] => aum-biss.unibe.ch
[ip] => 130.92.30.62
)
...
[chirosurf.ch] => Array
(
[type] => config
[file] => /media/sf_htdocs/dev.links.iml.unibe.ch/public_html/classes/../../config/redirects_chirosurf.ch.json
[ip] => 130.92.30.62
[aliases] => Array
(
[0] => www.chirosurf.ch
)
[redirects] => Array
(
[direct] => Array
(
)
[regex] => Array
(
[.*] => Array
(
[code] => 307
[target] => https://chirosurf.elearning.aum.iml.unibe.ch
)
)
)
)
*/
$aHosts=$oR->getHosts();
if(count($aHosts['_errors'])) {
$sHtml.= '<h2>Found errors</h2>'
.'<ol class="error">'
.'<li>' . implode('</li></li>', $aHosts['_errors']).'</li>'
.'</ol>'
;
unset($aHosts['_errors']);
}
function getId($sDomain){
return 'id_'.md5($sDomain);
}
$sHtml.='<h2>Domains and their redirects</h2>'
.'<table class="mydatatable"><thead>
<tr>
<th>Host</th>
<th>Setup</th>
<th>Type</th>
<th>From</th>
<th>Code</th>
<th>Target</th>
</tr>
</thead><tbody>';
foreach($aHosts as $sHost => $aCfg){
$sTdFirst='<tr class="cfgtype-'.$aCfg['type'].'"><td>'.$sHost.'</td><td>'.$aCfg['type'].'</td>';
if (isset($aCfg['redirects'])){
$iCount=0;
foreach(['direct', 'regex'] as $sType){
if (count($aCfg['redirects'][$sType])){
foreach($aCfg['redirects'][$sType] as $sFrom=>$aTo){
$iCount++;
$sHtml.=$sTdFirst
.'<td class="type-'.$sType.'">'.$sType.'</td>'
.'<td class="type-'.$sType.'">'.$sFrom.'</td>'
.'<td class="http-'.$aTo['code'].'">'.$aTo['code'].'</td>'
.'<td><a href="'.$aTo['target'].'" target="_blank">'.$aTo['target'].'</a></td>'
.'</tr>';
}
}
}
} else {
// type = alias
// $sHtml.='<tr>'.$sTdFirst.'<td></td><td></td><td></td><td>'.(isset($aCfg['target']) ? 'see config for <a href="#'.getId($aCfg['target']).'">'.$aCfg['target'].'</a>' : '').'</td></tr>';
$sHtml.=$sTdFirst.'<td></td><td></td><td></td><td>'.(isset($aCfg['target']) ? 'see config for <em>'.$aCfg['target'].'</em>' : '').'</td></tr>';
}
}
$sHtml.='</tbody></table>';
?><!doctype html>
<html>
<head>
<title>Redirects</title>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/dt/dt-1.11.4/datatables.min.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="text/javascript" src="https://cdn.datatables.net/v/dt/dt-1.11.4/datatables.min.js"></script>
<style>
a{color:royalblue;}
body{background: #f8f8f8; color: #234;}
ol.error{background: #fcc;}
pre{background: rgba(0,0,0,0.05);padding: 0.3em; border: 1px solid rgba(0,0,0,0.1)}
.cfgtype-alias{color:#89a; }
.http-301::after{color:#a55; content: ' (Moved Permanently)'}
.http-307::after{color:#488; content: ' (Temporary Redirect)'}
.http-308::after{color:#a95; content: ' (Permanent Redirect)'}
.type-direct{color:#383; }
.type-regex{color:#838; }
</style>
</head>
<body>
<h1>Redirects</h1>
<?php echo $sHtml; ?>
<script>
$(document).ready( function () {
$('.mydatatable').DataTable({
"lengthMenu": [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]],
stateSave: true
});
} );
</script>
</body>
</html>
<?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
*/
/**
* Description of redirect
*
* @author axel
*/
class redirectadmin extends redirect {
public function getHosts(){
$aReturn = array();
$aErrors = array();
foreach(glob($this->sConfigDir . '/redirects_*.json') as $sFilename){
$sMyHost= str_replace(array('redirects_', '.json'), array('',''), basename($sFilename));
$aReturn[$sMyHost]=array(
'type'=>'config',
'file'=>$sFilename,
'ip'=> gethostbyname($sMyHost),
'aliases'=>array(),
'redirects'=>json_decode(file_get_contents($sFilename), 1),
);
}
$aAliases=$this->_getAliases();
foreach($aAliases as $sAlias=>$sConfig){
if(isset($aReturn[$sAlias])){
$aErrors[]="Alias $sAlias was set already - ".print_r($aReturn[$sAlias]);
} else {
if(!isset($aReturn[$sConfig])){
$aErrors[]="Alias $sAlias points to a non existing host $sConfig";
} else {
$aReturn[$sConfig]['aliases'][]=$sAlias;
$aReturn[$sAlias]=array(
'type'=>'alias',
'target'=>$sConfig,
'ip'=> gethostbyname($sMyHost),
);
}
}
}
$aReturn['_errors']=$aErrors;
ksort($aReturn);
return $aReturn;
}
}
<?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
*/
/**
* Description of redirect
*
* @author axel
*/
class redirect {
// ----------------------------------------------------------------------
// CONFIG
// ----------------------------------------------------------------------
protected $bDebug = false;
protected $sConfigDir = __DIR__ . '/../../config';
protected $sAbout = 'IML redirect <small>v1.4</small>';
protected $sHostname = false;
protected $sRequest = false;
protected $sCfgfile = false;
protected $aConfig = false;
protected $aRedirect = false;
// ----------------------------------------------------------------------
// CONSTRUCTOR
// ----------------------------------------------------------------------
public function __constructor($sHostname=false, $sRequest=false) {
if($sHostname){
$this->setHost($sHostname);
}
if($sRequest){
$this->setRequest($sRequest);
}
return true;
}
// ----------------------------------------------------------------------
// DEBUG
// ----------------------------------------------------------------------
/**
* write a debug message into the header
* @global boolean $bDebug flag: show debug infos?
* @staticvar int $i counter
* @param string $sDebugMessage message to show
* @return boolean
*/
protected function _wd($sDebugMessage) {
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() {
return $this->sConfigDir . '/aliases.json';
}
/**
* get a string with full path of a config file based on hostname
* @param string $sHostname
* @return string
*/
protected function _generateCfgfile($sHostname) {
return $this->sConfigDir . '/redirects_' . $sHostname . '.json';
}
protected function _getAliases(){
$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
* @param string $sHostname hostname
* @param boolean $bAbort flag: true = do not scan aliases (used for loop detection)
* @return array
*/
protected function _getConfig() {
$this->aConfig=false;
$this->aRedirect=false;
$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');
}
$this->aConfig=$aConfig;
}
return true;
}
/**
* get an array with redirect config 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 array
*/
protected function _getEffectiveConfigfile($sHostname=false, $bAbort=false) {
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($bEnable){
$this->bDebug=!!$bEnable;
return true;
}
/**
* set hostname; internally it detects the config too
* @param type $sHostname
* @return boolean
*/
public function setHost($sHostname){
$this->sHostname=$sHostname;
$this->_getConfig();
return true;
}
/**
* set the request
* @param type $sRequest
* @return boolean
*/
public function setRequest($sRequest){
$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(){
if(is_array($this->aRedirect)){
return $this->aRedirect;
}
$aRedirect = false;
// 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");
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;
}
public function getRedirectCode($iNone=false){
$aRedirect=$this->getRedirect();
return isset($aRedirect['code']) ? $aRedirect['code'] : $iNone;
}
public function getRedirectTarget(){
$aRedirect=$this->getRedirect();
return isset($aRedirect['target']) ? $aRedirect['target'] : false;
}
// ----------------------------------------------------------------------
// FUNCTIONS - SEND DATA
// ----------------------------------------------------------------------
/**
* make the redirect if it was found ... or a 404
* @return true
*/
public function makeRedirect(){
$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($iCode) {
$aHeaders = array(
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 stringg $sBody message text as html code
*/
public function sendBody($iCode, $sBody) {
$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>&copy; ' . date('Y') . ' '.$this->sHostname.'</footer>'
. '</body></html>'
);
}
}
<?php
/**
* ----------------------------------------------------------------------
* _____ _ _ _ _ _
* | __ \ | (_) | | (_) | |
* | |__) |___ __| |_ _ __ ___ ___| |_ ___ ___ _ __ _ _ __ | |_
* | _ // _ \/ _` | | '__/ _ \/ __| __| / __|/ __| '__| | '_ \| __|
* | | \ \ __/ (_| | | | | __/ (__| |_ \__ \ (__| | | | |_) | |_
* |_| \_\___|\__,_|_|_| \___|\___|\__| |___/\___|_| |_| .__/ \__|
* | |
* |_|
* ----------------------------------------------------------------------
*/
require_once 'classes/redirect.class.php';
$sHostname = $_SERVER['SERVER_NAME'];
// find redirect ... the part between starting "/" to "?" or end of line
$sRequest = preg_replace('#^(.*)\?.*$#', "$1", substr($_SERVER['REQUEST_URI'], 1));
$oR=new redirect();
$oR->setDebug(isset($_GET['debugredirect']) && $_GET['debugredirect'] ? true : false);
$oR->setHost($sHostname);
$oR->setRequest($sRequest);
$oR->makeRedirect();
# IML Redirect #
Redirect urls of any domain that points here.
Author: Axel Hahn; Institute for Medical Education; University of Bern
License: GNUP GPL 3.0
see [docs](./docs)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment