#!/usr/bin/php
<?php

/******************************************************************************
*
* ptmon (pia transmission monitor)
*
* Based on Python script by Scott Hansen
* https://github.com/firecat53/pia_transmission_monitor
*
* Copyright (c) 2015-2024 Ryan Coe <bluemrp9@gmail.com>
*
* The MIT License (MIT)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
******************************************************************************/

/**
 * enforce running from the command line
 *
 * @return void
 */
function check_sapi_cli() : void
{
	error_reporting(E_ALL);

	set_time_limit(0);

	if (php_sapi_name() !== "cli") {
		fwrite(STDERR, "This script can only be ran from the command line\n");
		exit(1);
	}
}

/**
 * enforce running as root user
 *
 * @return void
 */
function check_euid_root() : void
{
	if (posix_geteuid() != 0) {
		fwrite(STDERR, "This script needs to be ran as root!\n");
		exit(1);
	}
}

/**
 * checks required extensions and dies if they are loaded
 *
 * @return void
 */
function check_extensions() : void
{
	$php_extensions = get_loaded_extensions();
	$req_extensions = array('curl', 'json', 'pcntl', 'posix');
	$abort = false;
	foreach ($req_extensions as $ext) {
		if (!in_array($ext, $php_extensions)) {
			$abort = true;
			fwrite(STDERR, "Missing php-".$ext." extension\n");
		}
	}
	if ($abort) {
		exit(1);
	}
}

/**
 * shutdown function to close and delete pid file
 *
 * @return void
 */
function close_pid_file() : void
{
	global $pid_file;
	global $pid_handle;

	flock($pid_handle, LOCK_UN);
	fclose($pid_handle);
	@unlink($pid_file);
}

/**
 * create pid file and register shutdown function
 *
 * @return void
 */
function create_pid_file() : void
{
	global $pid_file;
	global $pid_handle;

	// Create pid file
	$pid_handle = @fopen($pid_file, "a+");
	if ($pid_handle === false) {
		fwrite(STDERR, "Unable to create pid file\n");
		exit(1);
	}

	// Lock file and write pid
	if (flock($pid_handle, LOCK_EX | LOCK_NB, $wb)) {
		ftruncate($pid_handle, 0);
		fprintf($pid_handle, "%d\n", posix_getpid());
		fflush($pid_handle);
	} else {
		fclose($pid_handle);
		if ($wb) {
			fwrite(STDERR, "Another instance is already running\n");
		} else {
			fwrite(STDERR, "Failed to obtain lock on pid file\n");
		}
		exit(1);
	}

	// Register shutdown function
	register_shutdown_function('close_pid_file');
}

/**
 * execute external command to retrieve tunnel address
 *
 * @return string
 */
function get_tunnel_ip() : string
{
	global $tun_addr_cmd;

	$tun_ip = "";
	$output = array();
	exec($tun_addr_cmd, $output, $retval);
	if ($retval == 0 && isset($output[0])) {
		$tun_ip = strval($output[0]);
	}
	return $tun_ip;
}

/**
 * execute external ping command to check tunnel
 *
 * @return bool
 */
function send_ping() : bool
{
	global $ping_cmd;

	$output = array();
	exec($ping_cmd, $output, $retval);
	return ($retval == 0 ? true : false);
}

/**
 * execute vpn stop command
 * @param int $retry
 * @return bool
 */
function vpn_stop(int $retry = 0) : bool
{
	global $vpn_stop;

	do {
		echo "Stopping vpn ...\n";
		$output = array();
		exec($vpn_stop, $output, $retval);
		echo "Return value ".$retval." ".($retval == 0 ? "(SUCCESS)" : "(FAIL)")."\n";
		if ($retval != 0) {
			sleep(2);
			$retry--;
		}
	} while ($retry > 0 && $retval != 0);
	return ($retval == 0 ? true : false);
}

/**
 * execute vpn start command
 *
 * @return bool
 */
function vpn_start() : bool
{
	global $vpn_start;

	echo "Starting vpn ...\n";
	$output = array();
	exec($vpn_start, $output, $retval);
	echo "Return value ".$retval."\n";
	return ($retval == 0 ? true : false);
}

/**
 * execute vpn check command
 * returns true if service is running
 * @return bool
 */
function vpn_check() : bool
{
	global $vpn_check;

	$output = array();
	exec($vpn_check, $output, $retval);
	return ($retval == 0 ? true : false);
}

/**
 * execute transmission stop command
 * @param int $retry
 * @return bool
 */
function transmission_stop(int $retry = 0) : bool
{
	global $transmission_stop;

	do {
		$output = array();
		echo "Stopping transmission ...\n";
		exec($transmission_stop, $output, $retval);
		echo "Return value ".$retval." ".($retval == 0 ? "(SUCCESS)" : "(FAIL)")."\n";
		if ($retval != 0) {
			sleep(2);
			$retry--;
		}
	} while ($retry > 0 && $retval != 0);
	return ($retval == 0 ? true : false);
}

/**
 * execute transmission start command
 *
 * @return bool
 */
function transmission_start() : bool
{
	global $transmission_start;

	echo "Starting transmission ...\n";
	$output = array();
	exec($transmission_start, $output, $retval);
	echo "Return value ".$retval."\n";
	return ($retval == 0 ? true : false);
}

/**
 * execute transmission check command
 * returns true if service is running
 * @return bool
 */
function transmission_check() : bool
{
	global $transmission_check;

	$output = array();
	exec($transmission_check, $output, $retval);
	return ($retval == 0 ? true : false);
}

/**
 * configure transmission settings json file
 *
 * @return bool
 */
function settings_json() : bool
{
	global $transmission_settings;
	global $tun_ip;
	global $port_forward;

	if (strlen($tun_ip) == 0) {
		echo "Invalid tunnel ip\n";
		return false;
	}
	if ($port_forward <= 1024 || $port_forward > 65535) {
		echo "Invalid forwarding port\n";
		return false;
	}
	$conf = @file_get_contents($transmission_settings);
	if ($conf === false || strlen($conf) == 0) {
		echo "Failed to read ".$transmission_settings."\n";
		return false;
	}
	$json = json_decode($conf, true);
	if ($json == null) {
		echo "Failed to decode json data\n";
		return false;
	}
	if (!array_key_exists("bind-address-ipv4", $json) || !array_key_exists("peer-port", $json) || !array_key_exists("port-forwarding-enabled", $json)) {
		echo "Missing array key\n";
		return false;
	}

	// bind address
	$json['bind-address-ipv4'] = $tun_ip;

	// port forward
	$json['peer-port'] = $port_forward;
	$json['port-forwarding-enabled'] = false;

	$conf = json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n";
	if ($conf === false) {
		echo "Json encode failed\n";
		return false;
	}
	if (@file_put_contents($transmission_settings, $conf) === false) {
		echo "Failed to write ".$transmission_settings."\n";
		return false;
	}
	return true;
}

/**
 * execute transmission check port forward command
 *
 * @return bool
 */
function check_port_forward() : bool
{
	global $transmission_check;

	$output = array();
	exec($transmission_check, $output, $retval);
	return ($retval == 0 ? true : false);
}

/**
 * signal handler function
 *
 * @param int $signo
 * @param mixed $siginfo
 * @return void
 */
function sig_handler(int $signo, mixed $siginfo) : void
{
	global $shutdown;
	global $restart;

	switch ($signo) {
	case SIGTERM:
		echo "Caught SIGTERM\n";
		$shutdown = true;
	case SIGINT:
		echo "Caught SIGINT\n";
		$shutdown = true;
		break;
	case SIGHUP:
		echo "Caught SIGHUP\n";
		$restart = true;
		break;
	default:  // handle all other signals
		break;
	}
}

// program name & version
$program = "ptmon";
$version = "4.4.1";

// pid file
$pid_file = "/run/{$program}.pid";

// empty config
$vpn_conf = "";
$port_forward = 0;
$port_forward_check = true;
$no_restart = false;
$usage = false;

// command line only
check_sapi_cli();

// Check for required extensions
check_extensions();

// get command line options
$opts = getopt("hf", array("conf:", "help", "no-check", "no-restart", "port:", "tun:"));
if (array_key_exists("h", $opts) || array_key_exists("help", $opts)) {
	$usage = true;
}
$port_forward = (array_key_exists("port", $opts) ? intval($opts['port']) : 0);
$vpn_conf = (array_key_exists("conf", $opts) ? strval($opts['conf']) : "");
$tun_if = (array_key_exists("tun", $opts) ? strval($opts['tun']) : "tun0");
if (array_key_exists("no-check", $opts)) {
	$port_forward_check = false;
}
if (array_key_exists("no-restart", $opts)) {
	$no_restart = true;
}

// check for valid vpn configuration
if (strlen($vpn_conf) == 0) {
	fwrite(STDERR, "Invalid vpn configuration\n");
	$usage = true;
}

// check port forward
if ($port_forward <= 1024 || $port_forward > 65535) {
	fwrite(STDERR, "Invalid forwarding port\n");
	$usage = true;
}

// check tunnel interface
if (strlen($tun_if) == 0) {
	fwrite(STDERR, "Invalid tunnel interface\n");
	$usage = true;
}

// show usage
if ($usage) {
	fprintf(STDERR, "Usage: %s --conf --port num --tun tun0 [--no-check] [--no-restart]\nrun this script as root\n", $argv[0]);
	exit(1);
}

// only run as root
check_euid_root();

// config
$vpn_service = "openvpn-client@".$vpn_conf.".service";
$vpn_start = "systemctl start ".$vpn_service;
$vpn_stop = "systemctl stop ".$vpn_service;
$vpn_check = "systemctl is-active ".$vpn_service." --quiet";
$tun_addr_cmd = "ifconfig ".$tun_if." 2>/dev/null | grep -m 1 'inet' | awk '{print $2}' | sed -e 's/.*://'";
$ping_dest = "8.8.8.8";
$ping_cmd = "ping -c 1 -I ".$tun_if." ".$ping_dest;
$transmission_confdir = "/var/lib/transmission/info";
$transmission_service = "transmission.service";
$transmission_start = "systemctl start ".$transmission_service;
$transmission_stop = "systemctl stop ".$transmission_service;
$transmission_check = "systemctl is-active ".$transmission_service." --quiet";
$transmission_settings = $transmission_confdir."/settings.json";
$transmission_user_pass = "transmission:transmission";
$transmission_remote = "/usr/bin/transmission-remote";
$transmission_check = $transmission_remote." -n ".$transmission_user_pass." -pt | grep -q 'Yes'";
$reboot_cmd = "reboot";
$check_minutes = 5;
$max_errors = 5;
$service_retry = 2;

// check for settings files existance
if (!@file_exists($transmission_settings)) {
	fwrite(STDERR, "Missing file ".$transmission_settings."\n");
	exit(1);
}

// create pid file
create_pid_file();

// install some signal handlers for clean shutdown
$shutdown = false;
$restart = false;
$reboot = false;
pcntl_async_signals(true);
pcntl_signal(SIGTERM, 'sig_handler');
pcntl_signal(SIGINT, 'sig_handler');
pcntl_signal(SIGHUP, 'sig_handler');

echo "Starting ".$program." version ".$version."\n";

$state = 0;
$tun_ip = "";
$sleep = 0;
$retry = 0;
$minutes = 0;
$error = 0;

// main loop
while ($shutdown == false) {
	if ($restart) {
		$restart = false;
		if ($state >= 8) {
			$state = 0;
			$error = 0;
			$sleep = 0;
		}
	}
	if ($sleep > 0) {
		sleep(1);
		$sleep--;
		continue;
	}
	switch ($state) {
	default:
		echo "Invalid state - aborting\n";
		$shutdown = true;
		break;
	case -1:
		echo "Too many errors - rebooting system\n";
		$shutdown = true;
		$reboot = true;
		break;
	case 0:
		if ($error > $max_errors && !$no_restart) {
			$state = -1;
		} else {
			if (transmission_check()) {
				transmission_stop($service_retry);
			}
			if (vpn_check()) {
				vpn_stop($service_retry);
			}
			$state = 1;
			$sleep = 2;
			$retry = 0;
		}
		break;
	case 1:
		clearstatcache();
		if (!vpn_check() && !transmission_check()) {
			$state = 2;
		} else {
			if ($retry < 15) {
				$retry++;
			} else {
				echo "Failed to stop processes\n";
				$state = 0;
				$error++;
			}
		}
		break;
	case 2:
		if (!vpn_start()) {
			$error++;
			$state = 0;
		} else {
			echo "Detecting tunnel address ...\n";
			$state = 3;
			$sleep = 3;
			$retry = 0;
		}
		break;
	case 3:
		$tun_ip = get_tunnel_ip();
		if (strlen($tun_ip) == 0) {
			if ($retry < 30) {
				$retry++;
			} else {
				echo "Failed to detect tunnel address\n";
				$state = 0;
				$error++;
			}
		} else {
			echo "Tunnel interface address is ".$tun_ip."\n";
			$state = 4;
		}
		break;
	case 4:
		$state = 5;
		break;
	case 5:
		echo "Configuring settings.json ...\n";
		if (!settings_json()) {
			$error++;
			$state = 0;
		} else {
			$state = 7;
		}
		break;
	case 6:
		transmission_stop();
		$state = 7;
		$sleep = 5;
		break;
	case 7:
		if (!transmission_start()) {
			$state = 0;
			$error++;
		} else {
			$state = 8;
			$error = 0;
		}
		break;
	case 8:
		$state = 9;
		$sleep = 60;
		$minutes = 0;
		break;
	case 9:
		$minutes++;
		if ($minutes >= $check_minutes) {
			$state = 10;
			$retry = 0;
		} else {
			$sleep = 60;
		}
		break;
	case 10:
		if ($retry < 5) {
			if (!send_ping()) {
				echo "Ping on tunnel interface failed\n";
				$retry++;
			} else {
				$state = 11;
			}
		} else {
			$state = 0;
		}
		break;
	case 11:
		if ($port_forward_check && !check_port_forward()) {
			echo "Forwarding port is not open\n";
			$state = 0;
		} else {
			$state = 8;
		}
		break;
	}
	if ($sleep == 0 && !$shutdown) {
		$sleep = 1;
	}
}

echo "Shutdown initiated\n";

// stop transmission
transmission_stop($service_retry);

// stop vpn
vpn_stop($service_retry);

echo "Shutdown finished\n";

// reboot
if ($reboot) {
	exec($reboot_cmd);
}

exit(0);
