#!/usr/bin/php -q
<?php
if (PHP_VERSION_ID < 60000) {
	mb_internal_encoding("UTF-8");
	set_time_limit(0);
	ob_implicit_flush();
	declare(ticks = 10);
	date_default_timezone_set("Europe/Paris");
}
error_reporting(-1);
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('display_errors', true);
ini_set('display_startup_errors', true);
ini_set('log_errors', true);
ini_set('error_log', '/var/log/syslog');
//------ INIT ------------------------------------------------------------------
openlog ( "GATEWAY-SWITCHER" , LOG_PID , LOG_SYSLOG );
$config=array();
$config['debug']=FALSE;
$proc=FALSE;
//----FUNCTION------------------------------------------------------------------
class Thread {
    const FUNCTION_NOT_CALLABLE     = 10;
    const COULD_NOT_FORK            = 15;
    private $errors = array(
        Thread::FUNCTION_NOT_CALLABLE   => 'ERROR : No valid function given to THREAD function...',
        Thread::COULD_NOT_FORK          => 'ERROR : Fork process failed...',
    );
    protected $runnable;
    private $pid;
	//--------------------------------------------
    public static function available() {
        $required_functions = array(
            'pcntl_fork',
        );
        
        foreach( $required_functions as $function ) {
            if ( !function_exists( $function ) ) {
                return false;
            }
        }
        
        return true;
    }
	//-------------------------------------------------
    public function __construct( $_runnable = null ) {
    	if( $_runnable !== null ) {
        	$this->setRunnable( $_runnable );
    	}
    }
    //-------------------------------------------------
    public function setRunnable( $_runnable ) {
        if( self::runnableOk( $_runnable ) ) {
            $this->runnable = $_runnable;
        }
        else {
            throw new Exception( $this->getError( Thread::FUNCTION_NOT_CALLABLE ), Thread::FUNCTION_NOT_CALLABLE );
        }
    }
	//-------------------------------------------------
    public function getRunnable() {
        return $this->runnable;
    }
	//--------------------------------------------------
    public static function runnableOk( $_runnable ) {
        return ( function_exists( $_runnable ) && is_callable( $_runnable ) );
    }
	//--------------------------------------------------
    public function getPid() {
        return $this->pid;
    }
	//---------------------------------------------------
    public function isAlive() {
        $pid = pcntl_waitpid( $this->pid, $status, WNOHANG );
        return ( $pid === 0 );
        
    }
	//---------------------------------------------------------
    public function start() {
        $pid = @ pcntl_fork();
        if( $pid == -1 ) {
            throw new Exception( $this->getError( Thread::COULD_NOT_FORK ), Thread::COULD_NOT_FORK );
        }
        if( $pid ) {
            $this->pid = $pid;
        }
        else {
            pcntl_signal( SIGTERM, array( $this, 'signalHandler' ) );
            $arguments = func_get_args();
            if ( !empty( $arguments ) ) {
                call_user_func_array( $this->runnable, $arguments );
            }
            else {
                call_user_func( $this->runnable );
            }
            
            exit( 0 );
        }
    }
	//------------------------------------------------------------------------
    public function stop( $_signal = SIGTERM, $_wait = false ) {
        if( $this->isAlive() ) {
            posix_kill( $this->pid, $_signal );
            if( $_wait ) {
                pcntl_waitpid( $this->pid, $status = 0 );
            }
        }
    }
	//------------------------------------------------------------------------
    public function kill( $_signal = SIGKILL, $_wait = false ) {
        posix_kill( $this->pid, $_signal );
    }
    //------------------------------------------------------------------------
    public function getError( $_code ) {
        if ( isset( $this->errors[$_code] ) ) {
            return $this->errors[$_code];
        }
        else {
            return "error " . $_code;
        }
    }
	//--------------------------------------------------------------------------
    protected function signalHandler( $_signal ) {
        switch( $_signal ) {
            case SIGTERM:
                echo "SIGTERM via TREAD";
				exit( 0 );
            break;
        }
    }
	//---------------------------------------------------------------------------
}
//-----------------------------------------------------------------------------
function is_ip_match_cdir($ip, $cdir) {
    list ($subnet, $bits) = explode('/', $cdir);
    $ip = ip2long($ip);
    $subnet = ip2long($subnet);
    $mask = -1 << (32 - $bits);
    $subnet &= $mask;
    return ($ip & $mask) == $subnet;
}
//------------------------------------------------------------------------------
function myexec($commande) {
	global $config;
	global $connbdd;
	//if ($config['debug']) logme("MYEXEC -> ".$commande);
	exec ($commande, $result, $return);
	if ($return > 0) logme ("EXEC : ".$commande." -> ".$return);
	return $result;
}
//------------------------------------------------------------------------------
function logme ($data) {
	syslog ( LOG_INFO , $data );
}
//------------------------------------------------------------------------------
function become_daemon() { 

	$pid = pcntl_fork(); 
	if ($pid == -1) { 
		exit(); 
    }
	elseif ($pid) { 
		exit(); 
    }
	else { 
		posix_setsid(); 
		chdir('/'); 
		umask(0); 
		return posix_getpid(); 
    } 
}
//------------------------------------------------------------------------------
function sig_handler($sig) { 
	global $proc;
	global $sock;
    switch($sig) { 
        case SIGTERM:
			exit(0);
		break;
        case SIGINT:
			exit(0);
        break;	 		
    } 
}
//------------------------------------------------------------------------------
function sig_handler_thread($sig) { 
	global $proc;
    switch($sig) { 
        case SIGTERM:
			logme ("thread received SIGTERM");
			if (is_resource($proc)) {
				proc_terminate($proc, 15);
			}
			exit(0);
		break;
        case SIGINT:
			logme ("thread received SIGINT");
			if (is_resource($proc)) {
				proc_terminate($proc, 15);
			}
			exit(0);
        break; 
    } 
}
//------------------------------------------------------------------------------
function get_mac_from_ip($ip) {
	myexec("ping -c 1 -W 2 ".$ip);
	$e=myexec("ip neighbor show ".$ip);
	if (isset($e[0])) {
		preg_match("/^.*\s+(?P<mac>([0-9a-f\:]){17})\s+.*$/", $e[0], $match);
		if (isset($match['mac'])) {
			return strtoupper($match['mac']);
		}
		else return FALSE;
	}
	else return FALSE;
}
//------------------------------------------------------------------------------
function get_gateway_list() {
	global $config;
	$gw=array();
	$current_iface="";
	$current_sip="";
	$current_metric=-1;
	$current_gw_ip="";
	$id=-1;
	$a=myexec ("cat /etc/network/interfaces");
	foreach ($a AS $b=>$c) {
		preg_match("/^iface\s(?P<iface>([a-zA-Z0-9\:])+)\sinet\sstati.*$/", $c, $match);
		if (isset($match['iface'])) { // if find new iface reset all current
			$tmp=explode(":", $match['iface']);
			$current_iface=$tmp[0];
			$current_sip="";
			$current_metric=-1;
			$current_gw_ip="";
			$id=-1;
		}
		preg_match("/^.*address\s+(?P<sip>([0-9\.\/])+)$/", $c, $match);
		if (isset($match['sip'])) {
			$tmp=explode("/", $match['sip']);
			$current_sip=$tmp[0];
		}
		preg_match("/^.*gateway\s+(?P<gw_ip>([0-9\.])+)$/", $c, $match);
		if (isset($match['gw_ip'])) {
			$current_gw_ip=$match['gw_ip'];
			$current_metric=0;
			$id=1;
		}
		preg_match("/^.*route\sadd\sdefault\s.*\s(?P<gw_ip>([0-9\.])+)\smetric\s(?P<metric>([0-9])+)$/", $c, $match);
		if ((isset($match['gw_ip'])) AND (isset($match['metric']))) {
			$current_gw_ip=$match['gw_ip'];
			$current_metric=$match['metric'];
			$id=$current_metric-9;
		}
		if (($id > -1) AND ($current_iface!="") AND ($current_sip!="") AND ($current_metric > -1) AND ($current_gw_ip!="")) { // if gw if found with all params
			$gw[$id]['metric']=$current_metric;
			$gw[$id]['ip']=$current_gw_ip;
			$gw[$id]['iface']=$current_iface;
			$gw[$id]['mac']="";
			$gw[$id]['sip']=$current_sip;
			$gw[$id]['active']=FALSE;
			$gw[$id]['status']=FALSE;
			apc_store("metric_".$id, $current_metric);
		}
	}
	foreach ($gw AS $id=>$v) { // get gateway mac
		$a=get_mac_from_ip($v['ip']);
		if ($a!==FALSE) {
			$gw[$id]['mac']=$a;
		}
	}
	$z=get_active_gw($gw); // try to find what gateway is active
	if ($z > 0) {
		$gw[$z]['active']=TRUE;
	}
	else {
		logme("GET CONFIG ERROR : impossible to know what gateway is active !!!");
	}
	foreach ($gw AS $id=>$v) {
		if ($v['active']) $active="ACTIVE !";
		else $active="";
		logme(" -> GET CONFIG : ".$id." ".$active." : IP ".$gw[$id]['ip']." MAC ".$gw[$id]['mac']." FROM ".$gw[$id]['sip']." IFACE ".$gw[$id]['iface']." metric ".$gw[$id]['metric']);
	}
	return $gw;
}
//------------------------------------------------------------------------------
function get_active_gw($actual_gw_config) { // return ID active gateway
	$a=myexec ("ip route");
	foreach ($a AS $b=>$c) {
		preg_match("/^default\svia\s(?P<gw_active_ip>([0-9\.])+)\sdev\s.*\sonlink$/", $c, $match);
		if (isset($match['gw_active_ip'])) {
			foreach ($actual_gw_config AS $k=>$v) {
				if ($v['ip']==$match['gw_active_ip']) {
					return $k;
				}
			}
		}
		preg_match("/^default\svia\s(?P<gw_active_ip>([0-9\.])+)\sdev\s.*\smetric\s(?P<metric>([0-9])+)$/", $c, $match);
		if (isset($match['gw_active_ip']) AND isset($match['metric'])) {
			return 0;
		}
		preg_match("/^default\svia\s(?P<gw_active_ip>([0-9\.])+)\sdev\s.*$/", $c, $match);
		if (isset($match['gw_active_ip'])) {
			foreach ($actual_gw_config AS $k=>$v) {
				if ($v['ip']==$match['gw_active_ip']) {
					return $k;
				}
			}
		}
	}
	return 0;
}
//------------------------------------------------------------------------------
function is_gateway_alive($sip, $dmac, $iface) {
	if ($dmac=="") return FALSE;
	$r=myexec("nping -c 2 -S ".$sip." -e ".$iface." --icmp --dest-mac ".$dmac." 8.8.8.8");
	if (isset($r[0])) {
		foreach ($r AS $k=>$v) {
			preg_match("/^.*RCVD\s.*type=(?P<type>([0-9])+)\/code.*$/", $v, $match);
			if ((isset($match['type'])) AND $match['type']==0) {
				return TRUE;
			}
		}
	}
	$r=myexec("nping -c 2 -S ".$sip." -e ".$iface." --icmp --dest-mac ".$dmac." 193.252.148.192");
	if (isset($r[0])) {
		foreach ($r AS $k=>$v) {
			preg_match("/^.*RCVD\s.*type=(?P<type>([0-9])+)\/code.*$/", $v, $match);
			if ((isset($match['type'])) AND $match['type']==0) {
				return TRUE;
			}
		}
	}
	$r=myexec("nping -c 2 -S ".$sip." -e ".$iface." --icmp --dest-mac ".$dmac." 1.1.1.1");
	if (isset($r[0])) {
		foreach ($r AS $k=>$v) {
			preg_match("/^.*RCVD\s.*type=(?P<type>([0-9])+)\/code.*$/", $v, $match);
			if ((isset($match['type'])) AND $match['type']==0) {
				return TRUE;
			}
		}
	}
	else return FALSE;
}
//------------------------------------------------------------------------------
function is_gateway_connected($sip, $dmac, $iface, $dip) {
	$r=myexec("nping -c 2 -S ".$sip." -e ".$iface." --icmp --dest-mac ".$dmac." ".$dip);
	if (isset($r[0])) {
		foreach ($r AS $k=>$v) {
			preg_match("/^.*RCVD\s.*type=(?P<type>([0-9])+)\/code.*$/", $v, $match);
			if ((isset($match['type'])) AND $match['type']==0) {
				return TRUE;
			}
		}
	}
	else return FALSE;
}
//------------------------------------------------------------------------------
function swap_gateway($to) { // swap to gateway given ID
	logme("SWAP gateway to ".$to);
	$config=apc_fetch("config");
	myexec("route del default && route del default && route del default && route del default && route del default && route del default && route del default && route del default && route del default && route del default");
	myexec("ip route add default via ".$config['gw'][$to]['ip']." dev ".$config['gw'][$to]['iface']);
	myexec("/etc/init.d/openvpn restart & /etc/init.d/minion restart");
}
//------------------------------------------------------------------------------
function thread_check_gw() {
	pcntl_signal(SIGTERM, 'sig_handler_thread'); 
	pcntl_signal(SIGINT, 'sig_handler_thread');
	pcntl_signal(SIGCHLD, SIG_IGN);
	proc_nice(19);
	$notify_old_status=array();
	while(TRUE) {
		$status=array();
		$config=apc_fetch("config");
		$old_status=apc_fetch("status");
		foreach($config['gw'] AS $id=>$value) {
			if ($value['mac']=="") {
				$status[$id]=FALSE;
				if (((isset($notify_old_status[$id])) && ($notify_old_status[$id]!="nomac")) || (!isset($notify_old_status[$id]))) {
					logme("GW ".$value['ip']." Routeur is not connected no MAC address resolv");
				}
				$notify_old_status[$id]="nomac";
				if ($config['debug']) logme("GW ".$value['ip']." Routeur is not connected no MAC address resolv");
			}
			else if ($value['mac']) {
				if (is_gateway_alive($value['sip'], $value['mac'], $value['iface'])) {
					$status[$id]=TRUE;
					if (((isset($notify_old_status[$id])) && ($notify_old_status[$id]!="alive")) || (!isset($notify_old_status[$id]))) {
						logme("GW ".$value['ip']." is ALIVE");
					}
					$notify_old_status[$id]="alive";
					if ($config['debug']) logme("GW ".$value['ip']." is ALIVE");
				}
				else {
					$status[$id]=FALSE;
					if (!is_gateway_connected($value['sip'], $value['mac'], $value['iface'], $value['ip'])) {
						if (((isset($notify_old_status[$id])) && ($notify_old_status[$id]!="norouteur")) || (!isset($notify_old_status[$id]))) {
							logme("GW ".$value['ip']." Routeur is not connected no ping");
						}
						$notify_old_status[$id]="norouteur";
						if ($config['debug']) logme("GW ".$value['ip']." Routeur is not connected no ping");
					}
					else {
						if (((isset($notify_old_status[$id])) && ($notify_old_status[$id]!="hs")) || (!isset($notify_old_status[$id]))) {
							logme("GW ".$value['ip']." is HS");
						}
						$notify_old_status[$id]="hs";
					}
				}
			}
			usleep(500000);
		}
		apc_store('status', $status);
		sleep(10);
	}
}
//------------------------------------------------------------------------------
function thread_decide() {
	pcntl_signal(SIGTERM, 'sig_handler_thread'); 
	pcntl_signal(SIGINT, 'sig_handler_thread');
	pcntl_signal(SIGCHLD, SIG_IGN);
	proc_nice(19);
	$config=apc_fetch("config");
	while(TRUE) {
		$must_active=-1;
		$config=apc_fetch("config");
		$status=apc_fetch("status");
		$active=get_active_gw($config['gw']);
		foreach($config['gw'] AS $id=>$v) {
			if (isset($status[$id])) {
				if (($status[$id]===TRUE) AND $must_active<0) {
					$must_active=$id;
					if ($config['debug']) logme("MUST ACTIVE ".$must_active." IS ACTIVE ".$active);
				}
			}
			else {
				if ($config['debug']) logme("DECIDE -> no status");
				break;
			}
		}
		if (($must_active > -1) AND $must_active!=$active) { //need to switch
			swap_gateway($must_active);
			sleep(30);
		}
		sleep(5);
	}
}
//-----BECOME DAEMON------------------------------------------------------------
logme("start ...");
if (!file_exists("/usr/bin/nping")) {
	echo("ERROR -> NPING n\'est pas disponible sur ce système -> exit\n");
	logme("ERROR -> NPING n\'est pas disponible sur ce système -> exit");
	exit();
}
if (function_exists ( "apc_store" )) {
	apc_store("test", "test");
	$t=apc_fetch("test");
	if ($t!=="test") {
		echo("ERROR -> PHP APC n\'est pas disponible sur ce système -> exit\n");
		logme("ERROR -> PHP APC n\'est pas disponible sur ce système -> exit");
		exit();
	}
}
else {
	echo("ERROR -> PHP APC n\'est pas disponible sur ce système -> exit\n");
	logme("ERROR -> PHP APC n\'est pas disponible sur ce système -> exit");
	exit();
}
if (!file_exists("/etc/gateway-switcher")) {
	myexec("mkdir /etc/gateway-switcher; chmod 750 /etc/gateway-switcher");
}
$__parent_pid = become_daemon();
passthru("echo '".getmypid()."' > /var/run/gateway-switcher");
pcntl_signal(SIGTERM, 'sig_handler'); 
pcntl_signal(SIGINT, 'sig_handler');
pcntl_signal(SIGCHLD, SIG_IGN);
//---MAIN PROGRAM---------------------------------------------------------------
$config['gw']=get_gateway_list();
$status=array();
apc_store("config", $config);
apc_store("status", $status);
//---THREADS---------------------------------------------------------------------
$a=0;
$thread_task_arg = array();
$a++;$thread_task[$a] = new Thread( 'thread_check_gw' );$thread_task[$a]->start();apc_store("thread_check_gw", $thread_task[$a]->getPid());
sleep(10);
$a++;$thread_task[$a] = new Thread( 'thread_decide' );$thread_task[$a]->start();apc_store("thread_decide", $thread_task[$a]->getPid());
//---MAIN thread become WATCHDOG-------------------------------------------------------
proc_nice(19);
while (TRUE) {
	sleep (2);
	foreach ($thread_task AS $key => $value) {
		if (!$thread_task[$key]->isAlive()) {
			if (isset($thread_task_arg[$key])) {
				$thread_task[$key]->start($thread_task_arg[$key]);
			}
			else {
				$thread_task[$key]->start();
			}
		}
		sleep(1);
	}
}
?>